simpleserver.pas 17 KB

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