simpleserver.pas 16 KB

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