simpleserver.pas 17 KB

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