simpleserver.pas 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. {
  2. This file is part of the Free Pascal run time library.
  3. Copyright (c) 2019 by the Free Pascal development team
  4. Sample HTTP server application
  5. See the file COPYING.FPC, included in this distribution,
  6. for details about the copyright.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  10. **********************************************************************}
  11. {$mode objfpc}
  12. {$h+}
  13. { $DEFINE USEGNUTLS}
  14. { $DEFINE USEMICROHTTP}
  15. program simpleserver;
  16. {$IFDEF USEMICROHTTP}
  17. {$UNDEF USEGNUTLS}
  18. {$ENDIF}
  19. uses
  20. {$IFDEF UNIX}
  21. cwstring,
  22. cthreads,
  23. {$ENDIF}
  24. {$IFNDEF USEMICROHTTP}
  25. {$ifdef USEGNUTLS}
  26. gnutlssockets,
  27. {$else}
  28. opensslsockets,
  29. {$endif}
  30. custhttpapp,
  31. {$ELSE}
  32. custmicrohttpapp,
  33. {$ENDIF}
  34. {$ifdef unix}
  35. baseunix,
  36. {$endif}
  37. sysutils, Classes, jsonparser, fpjson, strutils, inifiles, sslbase, httproute, httpdefs, fpmimetypes, fpwebfile, fpwebproxy,
  38. webutil, fpdebugcapturesvc;
  39. Const
  40. ServerVersion = '1.0';
  41. Type
  42. { THTTPApplication }
  43. {$IFDEF USEMICROHTTP}
  44. TParentApp = TCustomMicroHTTPApplication;
  45. {$ELSE}
  46. TParentApp = TCustomHTTPApplication;
  47. {$ENDIF}
  48. { TMySimpleFileModule }
  49. TMySimpleFileModule = class(TSimpleFileModule)
  50. Public
  51. Procedure SendFile(const AFileName: String; AResponse: TResponse); override;
  52. end;
  53. THTTPApplication = Class(TParentApp)
  54. private
  55. FAPISecret : String;
  56. FBaseDir: string;
  57. FIndexPageName: String;
  58. FInterfaceAddress: String;
  59. FMimeFile: String;
  60. FNoIndexPage: Boolean;
  61. FQuiet: Boolean;
  62. FBackground : Boolean;
  63. FPassword : string;
  64. FEcho : Boolean;
  65. FMaxAge : Integer;
  66. FCrossOriginIsolation : Boolean;
  67. procedure AddProxy(const aProxyDef: String);
  68. procedure DoEcho(ARequest: TRequest; AResponse: TResponse);
  69. procedure DoProxyLog(Sender: TObject; const aMethod, aLocation, aFromURL, aToURL: String);
  70. procedure DoQuit(ARequest: TRequest; AResponse: TResponse);
  71. procedure LoadMimeTypes;
  72. procedure ProcessOptions;
  73. procedure ReadConfigFile(const ConfigFile: string);
  74. procedure SetupCapture;
  75. procedure Usage(Msg: String);
  76. procedure Writeinfo;
  77. Public
  78. Destructor Destroy; override;
  79. published
  80. procedure DoLog(EventType: TEventType; const Msg: String); override;
  81. Procedure DoRun; override;
  82. property Quiet : Boolean read FQuiet Write FQuiet;
  83. Property MimeFile : String Read FMimeFile Write FMimeFile;
  84. Property BaseDir : string Read FBaseDir Write FBaseDir;
  85. Property NoIndexPage : Boolean Read FNoIndexPage Write FNoIndexPage;
  86. Property IndexPageName : String Read FIndexPageName Write FIndexPageName;
  87. Property InterfaceAddress : String Read FInterfaceAddress Write FInterfaceAddress;
  88. end;
  89. Var
  90. Application : THTTPApplication;
  91. { TMySimpleFileModule }
  92. procedure TMySimpleFileModule.SendFile(const AFileName: String; AResponse: TResponse);
  93. begin
  94. AResponse.SetCustomHeader('Cross-Origin-Embedder-Policy','require-corp');
  95. AResponse.SetCustomHeader('Cross-Origin-Opener-Policy','same-origin');
  96. inherited SendFile(AFileName, AResponse);
  97. end;
  98. { THTTPApplication }
  99. procedure THTTPApplication.DoEcho(ARequest: TRequest; AResponse: TResponse);
  100. Var
  101. L : TStrings;
  102. begin
  103. L:=TStringList.Create;
  104. try
  105. L.AddStrings(['<!doctype html>',
  106. '<html>',
  107. '<head>',
  108. '<title>Echo request</title>',
  109. '</head>',
  110. '<body>'
  111. ]);
  112. DumpRequest(aRequest,L);
  113. L.AddStrings(['</body>','</html>']);
  114. AResponse.Content:=L.Text;
  115. AResponse.ContentLength:=Length(AResponse.Content);
  116. AResponse.SendResponse;
  117. finally
  118. L.Free;
  119. end;
  120. end;
  121. procedure THTTPApplication.Doquit(ARequest: TRequest; AResponse: TResponse);
  122. Var
  123. PWD : String;
  124. begin
  125. PWD:=ARequest.QueryFields.Values['password'];
  126. if PWD='' then
  127. ARequest.ContentFields.Values['password'];
  128. if PWD=FPassword then
  129. begin
  130. AResponse.Content:='OK';
  131. AResponse.SendContent;
  132. Terminate;
  133. end
  134. else
  135. begin
  136. AResponse.Code:=403;
  137. AResponse.CodeText:='Forbidden';
  138. AResponse.SendContent;
  139. end;
  140. end;
  141. procedure THTTPApplication.DoLog(EventType: TEventType; const Msg: String);
  142. begin
  143. if Quiet then
  144. exit;
  145. if IsConsole then
  146. Writeln(FormatDateTime('yyyy-mm-dd hh:nn:ss.zzz',Now),' [',EventType,'] ',Msg)
  147. else
  148. inherited DoLog(EventType, Msg);
  149. end;
  150. procedure THTTPApplication.DoProxyLog(Sender: TObject; const aMethod, aLocation, aFromURL, aToURL: String);
  151. Var
  152. Msg : String;
  153. begin
  154. if Quiet then
  155. exit;
  156. Msg:=Format('(Proxy redirect) location: %s, Method: %s, From: %s, to: %s.',[aLocation,aMethod,aFromURl,atoURL]);
  157. if IsConsole then
  158. Writeln(FormatDateTime('yyyy-mm-dd hh:nn:ss.zzz',Now),' [',etInfo,'] ',Msg)
  159. else
  160. inherited DoLog(etInfo, Msg);
  161. end;
  162. procedure THTTPApplication.Usage(Msg : String);
  163. begin
  164. if (Msg<>'') then
  165. Writeln('Error: ',Msg);
  166. Writeln('Usage ',ExtractFileName(ParamStr(0)),' [options] ');
  167. Writeln('Version : ',ServerVersion);
  168. Writeln('Where options is one or more of : ');
  169. Writeln('-A --api=path,secret Activate location API on path, using secret as accepted bearer token.');
  170. Writeln('-a --max-age=age Set max-age expiry header on returned file requests.');
  171. Writeln('-c --config=file Ini configuration file (default: simpleserver.ini).');
  172. {$ifdef unix}
  173. Writeln('-b --background fork to background.');
  174. {$endif}
  175. Writeln('-d --directory=dir Base directory from which to serve files.');
  176. Writeln(' Default is current working directory: ',GetCurrentDir);
  177. Writeln('-e --echo Activate /echo URL.');
  178. Writeln('-h --help This help text.');
  179. Writeln('-H --hostname=NAME Set hostname for self-signed SSL certificate.');
  180. Writeln('-i --indexpage=name Directory index page to use (default: index.html)/');
  181. Writeln('-I --interface=IP Listen on this interface address only.');
  182. Writeln('-m --mimetypes=file Path of mime.types. Loaded in addition to OS known types.');
  183. Writeln('-n --noindexpage Do not allow index page.');
  184. Writeln('-o --coi Enable Cross-Origin Isolation headers.');
  185. Writeln('-p --port=NNNN TCP/IP port to listen on (default is 3000).');
  186. Writeln('-q --quiet Do not write diagnostic messages.');
  187. Writeln('-Q --quit=PWD Register /quit URL. Send request with password variable equal to PWD to stop.');
  188. Writeln('-s --ssl Use SSL.');
  189. Writeln('-u --capture[=FILE] Set up /debugcapture route to capture output sent by browser.');
  190. Writeln(' If FILE is specified, write to file. If not specified, writes to STDOUT.');
  191. Writeln('-V --version Display server version and exit.');
  192. Writeln('-x --proxy=proxydef Add proxy definition. Definition is of form:');
  193. Writeln(' name:BaseURL');
  194. Writeln('');
  195. Writeln('Config file is ini file, section [Server]. Key names are long option names.');
  196. Writeln('Proxies are defined in section [Proxy], Key is name, value is URL.');
  197. Writeln('Locations are defined in section [Locations], Key is location name, value is path.');
  198. Halt(Ord(Msg<>''));
  199. end;
  200. procedure THTTPApplication.LoadMimeTypes;
  201. begin
  202. MimeTypes.LoadKnownTypes;
  203. if MimeFile<>'' then
  204. begin
  205. MimeTypesFile:=MimeFile;
  206. if (MimeTypesFile<>'') and not FileExists(MimeTypesFile) then
  207. begin
  208. Log(etWarning,'mimetypes file not found: '+MimeTypesFile);
  209. MimeTypesFile:='';
  210. end;
  211. end;
  212. If MimeTypesFile<>'' then
  213. MimeTypes.LoadFromFile(MimeTypesFile);
  214. end;
  215. procedure THTTPApplication.AddProxy(const aProxyDef: String);
  216. Var
  217. P : Integer;
  218. N,URL : String;
  219. begin
  220. P:=Pos(':',aProxyDef);
  221. If P=0 then Raise
  222. EHTTP.CreateFmt('Invalid proxy definition: %s',[aProxyDef]);
  223. N:=Copy(aProxyDef,1,P-1);
  224. URL:=Copy(aProxyDef,P+1,Length(aProxyDef));
  225. ProxyManager.RegisterLocation(N,URL).AppendPathInfo:=True;
  226. end;
  227. Const
  228. SCaptureRoute = '/debugcapture';
  229. Const
  230. SConfig = 'Server';
  231. SProxy = 'Proxy';
  232. SLocations = 'Locations';
  233. KeyPort = 'Port';
  234. KeyInterface = 'Interface';
  235. KeyDir = 'Directory';
  236. KeyIndexPage = 'IndexPage';
  237. KeyHostName = 'hostname';
  238. keyMimetypes = 'mimetypes';
  239. KeySSL = 'SSL';
  240. KeyQuiet = 'quiet';
  241. KeyQuit = 'quit';
  242. KeyEcho = 'echo';
  243. KeyNoIndexPage = 'noindexpage';
  244. KeyBackground = 'background';
  245. KeyMaxAge = 'MaxAge';
  246. KeyAPI = 'API';
  247. KeyCOI = 'CrossOriginIsolation';
  248. KeyCapture = 'DebugCapture';
  249. procedure THTTPApplication.ReadConfigFile(const ConfigFile: string);
  250. Var
  251. L : TStringList;
  252. P,U : String;
  253. I : Integer;
  254. begin
  255. if (ConfigFile='') or Not FileExists(ConfigFile) then exit;
  256. L:=Nil;
  257. With TMemIniFile.Create(ConfigFile) do
  258. try
  259. BaseDir:=ReadString(SConfig,KeyDir,BaseDir);
  260. Port:=ReadInteger(SConfig,KeyPort,Port);
  261. InterfaceAddress:=ReadString(SConfig,KeyInterface,InterfaceAddress);
  262. Quiet:=ReadBool(SConfig,KeyQuiet,Quiet);
  263. MimeFile:=ReadString(SConfig,keyMimetypes,MimeFile);
  264. NoIndexPage:=ReadBool(SConfig,KeyNoIndexPage,NoIndexPage);
  265. IndexPageName:=ReadString(SConfig,KeyIndexPage,IndexPageName);
  266. HostName:=ReadString(SConfig,KeyHostName,HostName);
  267. UseSSL:=ReadBool(SConfig,KeySSL,UseSSL);
  268. FBackground:=ReadBool(SConfig,Keybackground,FBackGround);
  269. FPassword:=ReadString(SConfig,KeyQuit,FPassword);
  270. FEcho:=ReadBool(SConfig,KeyEcho,FEcho);
  271. FMaxAge:=ReadInteger(SConfig,KeyMaxAge,FMaxAge);
  272. FAPISecret:=ReadString(SConfig,KeyAPI,'');
  273. FCrossOriginIsolation:=ReadBool(SConfig,KeyCOI,FCrossOriginIsolation);
  274. if ValueExists(SConfig,KeyCapture) then
  275. begin
  276. TDebugCaptureService.Instance.LogFileName:=ReadString(SConfig,keyCapture,'');
  277. end;
  278. L:=TstringList.Create;
  279. ReadSectionValues(SProxy,L,[]);
  280. For I:=0 to L.Count-1 do
  281. begin
  282. L.GetNameValue(I,P,U);
  283. if (P<>'') and (U<>'') then
  284. ProxyManager.RegisterLocation(P,U).AppendPathInfo:=true;
  285. end;
  286. L.Clear;
  287. ReadSectionValues(SLocations,L,[]);
  288. For I:=0 to L.Count-1 do
  289. begin
  290. L.GetNameValue(I,P,U);
  291. if (P<>'') and (U<>'') then
  292. RegisterFileLocation(P,U);
  293. end;
  294. finally
  295. L.Free;
  296. Free;
  297. end;
  298. end;
  299. procedure THTTPApplication.ProcessOptions;
  300. procedure HasGetOptionValue(var aValue: string; Const C: Char; Const S : String);
  301. begin
  302. if HasOption(C,S) then
  303. aValue:=GetOptionValue(C,S);
  304. end;
  305. Var
  306. S : String;
  307. begin
  308. for S in GetOptionValues('x','proxy') do
  309. AddProxy(S);
  310. HasGetOptionValue(FAPISecret,'A','api');
  311. if HasOption('e','echo') then
  312. FEcho:=true;
  313. if HasOption('q','quiet') then
  314. Quiet:=true;
  315. HasGetOptionValue(FPassword,'Q','quit');
  316. if HasOption('p','port') then
  317. Port:=StrToIntDef(GetOptionValue('p','port'),Port);
  318. LoadMimeTypes;
  319. if HasOption('d','directory') then
  320. BaseDir:=GetOptionValue('d','directory');
  321. UseSSL:=HasOption('s','ssl');
  322. if HasOption('H','hostname') then
  323. HostName:=GetOptionValue('H','hostname');
  324. if HasOption('n','noindexpage') then
  325. NoIndexPage:=True
  326. else
  327. IndexPageName:=GetOptionValue('i','indexpage');
  328. if HasOption('I','interface') then
  329. InterfaceAddress:=GetOptionValue('I','interface');
  330. if HasOption('a','max-age') then
  331. FMaxAge:=StrToIntDef(GetOptionValue('a','max-age'),FMaxAge);
  332. if HasOption('b','background') then
  333. FBackground:=true;
  334. if hasOption('o','coi') then
  335. FCrossOriginIsolation:=true;
  336. if HasOption('u','capture') then
  337. begin
  338. S:=GetOptionValue('u','capture');
  339. if S='' then
  340. TDebugCaptureService.Instance.LogToConsole:=True
  341. else
  342. TDebugCaptureService.Instance.LogFileName:=S;
  343. end;
  344. end;
  345. procedure THTTPApplication.Writeinfo;
  346. Var
  347. I : Integer;
  348. begin
  349. Log(etInfo,'Listening on port %d, serving files from directory: %s (using SSL: %s).',[Port,BaseDir,BoolToStr(UseSSL,'true','false')]);
  350. For I:=0 to ProxyManager.LocationCount-1 do
  351. with ProxyManager.Locations[i] do
  352. Log(etInfo,'Proxy location /proxy/%s redirects to: %s',[Path,URL]);
  353. if not NoIndexPage then
  354. Log(etInfo,'Using index page: %s',[IndexPageName]);
  355. if (Self.FPassword<>'') then
  356. DoLog(etInfo,'/quit route set up.');
  357. if FEcho then
  358. DoLog(etInfo,'Setting up /echo route.');
  359. Log(etInfo,'Location REST API '+IfThen(FAPISecret<>'','','NOT ')+'activated.');
  360. Log(etInfo,'Navigate to: http'+IfThen(UseSSL,'s','')+'://localhost:'+IntToStr(Port)+'/');
  361. end;
  362. destructor THTTPApplication.Destroy;
  363. begin
  364. inherited Destroy;
  365. end;
  366. procedure THTTPApplication.SetupCapture;
  367. Var
  368. Dest : String;
  369. Svc : TDebugCaptureService;
  370. begin
  371. Svc:=TDebugCaptureService.Instance;
  372. Dest:=Svc.LogFileName;
  373. if (Dest='') and Svc.LogToConsole then
  374. Dest:='Console';
  375. if Dest<>'' then
  376. begin
  377. DoLog(etInfo,Format('Setting up capture on route "%s", writing to: %s',[SCaptureRoute,Dest]));
  378. HTTPRouter.RegisterRoute(SCaptureRoute,rmPost,@Svc.HandleRequest,False);
  379. end;
  380. end;
  381. procedure THTTPApplication.DoRun;
  382. Var
  383. S,ConfigFile : String;
  384. begin
  385. FMaxAge:=31557600;
  386. S:=Checkoptions('hqd:ni:p:sH:m:x:c:beQ:a:A:ou::VI',['help','quiet','noindexpage','directory:','port:','indexpage:','ssl','hostname:','mimetypes:','proxy:','config:','background','echo','quit:','max-age:','api:','coi','capture','version','interface']);
  387. if (S<>'') or HasOption('h','help') then
  388. usage(S);
  389. if HasOption('V','version') then
  390. begin
  391. Terminate;
  392. Writeln(ServerVersion);
  393. Exit;
  394. end;
  395. if HasOption('c','config') then
  396. ConfigFile:=GetOptionValue('c','config')
  397. else
  398. ConfigFile:='simpleserver.ini';
  399. ReadConfigFile(ConfigFile);
  400. ProcessOptions;
  401. if FBackground then
  402. begin
  403. {$ifdef unix}
  404. if FPFork>0 then Halt(0);
  405. {$else}
  406. Log(etError,'Background option not supported.');
  407. {$endif}
  408. end;
  409. SetupCapture;
  410. if FPassword<>'' then
  411. begin
  412. HTTPRouter.RegisterRoute('/quit',rmAll,@Doquit,False);
  413. end;
  414. if FEcho then
  415. HTTPRouter.RegisterRoute('/echo',rmAll,@DoEcho,False);
  416. if ProxyManager.LocationCount>0 then
  417. begin
  418. TProxyWebModule.RegisterModule('Proxy',True);
  419. ProxyManager.OnLog:=@DoProxyLog;
  420. end;
  421. DefaultCacheControlMaxAge:=FMaxAge; // one year by default
  422. if BaseDir='' then
  423. BaseDir:=GetCurrentDir;
  424. if (BaseDir<>'') then
  425. BaseDir:=IncludeTrailingPathDelimiter(BaseDir);
  426. if FAPISecret<>'' then
  427. TFPWebFileLocationAPIModule.RegisterFileLocationAPI(ExtractWord(1,FAPISecret,[',']),ExtractWord(2,FAPISecret,[',']));
  428. if FCrossOriginIsolation then
  429. TSimpleFileModule.DefaultSimpleFileModuleClass:=TMySimpleFileModule;
  430. TSimpleFileModule.RegisterDefaultRoute;
  431. TSimpleFileModule.BaseDir:=BaseDir;
  432. TSimpleFileModule.OnLog:=@Log;
  433. If not NoIndexPage then
  434. begin
  435. if (IndexPageName='') then
  436. IndexPageName:='index.html';
  437. TSimpleFileModule.IndexPageName:=IndexPageName;
  438. end;
  439. if not Quiet then
  440. WriteInfo;
  441. if InterfaceAddress<>'' then
  442. HTTPHandler.Address:=InterfaceAddress;
  443. inherited;
  444. end;
  445. begin
  446. Application:=THTTPApplication.Create(Nil);
  447. Application.Initialize;
  448. Application.Run;
  449. Application.Free;
  450. end.