simpleserver.pas 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579
  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. Var
  373. S : String;
  374. begin
  375. for S in GetOptionValues('x','proxy') do
  376. AddProxy(S);
  377. FAPISecret:=GetOptionValue('A','api');
  378. FEcho:=HasOption('e','echo');
  379. Quiet:=HasOption('q','quiet');
  380. FPassword:=GetOptionValue('Q','quit');
  381. Port:=StrToIntDef(GetOptionValue('p','port'),Port);
  382. LoadMimeTypes;
  383. if HasOption('d','directory') then
  384. BaseDir:=GetOptionValue('d','directory');
  385. UseSSL:=HasOption('s','ssl');
  386. if HasOption('H','hostname') then
  387. HostName:=GetOptionValue('H','hostname');
  388. if HasOption('n','noindexpage') then
  389. NoIndexPage:=True
  390. else
  391. IndexPageName:=GetOptionValue('i','indexpage');
  392. if HasOption('I','interface') then
  393. InterfaceAddress:=GetOptionValue('I','interface');
  394. FMaxAge:=StrToIntDef(GetOptionValue('a','max-age'),FMaxAge);
  395. FBackground:=HasOption('b','background');
  396. FCrossOriginIsolation:=hasOption('o','coi');
  397. if HasOption('u','capture') then
  398. begin
  399. FCaptureFileName:=GetOptionValue('u','capture');
  400. if FCaptureFileName='' then
  401. FCaptureFileName:='-';
  402. end;
  403. end;
  404. procedure THTTPApplication.Writeinfo;
  405. Var
  406. I : Integer;
  407. begin
  408. Log(etInfo,'Listening on port %d, serving files from directory: %s (using SSL: %s)',[Port,BaseDir,BoolToStr(UseSSL,'true','false')]);
  409. For I:=0 to ProxyManager.LocationCount-1 do
  410. with ProxyManager.Locations[i] do
  411. Log(etInfo,'Proxy location /proxy/%s redirects to %s',[Path,URL]);
  412. if not NoIndexPage then
  413. Log(etInfo,'Using index page %s',[IndexPageName]);
  414. if (Self.FPassword<>'') then
  415. DoLog(etInfo,'/quit route set up');
  416. if FEcho then
  417. DoLog(etInfo,'Setting up /echo route');
  418. Log(etInfo,'Location REST API '+IfThen(FAPISecret<>'','','NOT ')+'activated.');
  419. end;
  420. destructor THTTPApplication.Destroy;
  421. begin
  422. FreeAndNil(FCaptureStream);
  423. inherited Destroy;
  424. end;
  425. procedure THTTPApplication.SetupCapture(Const aFileName : string);
  426. Var
  427. Dest : String;
  428. begin
  429. if (aFileName<>'') and (aFileName<>'-') then
  430. begin
  431. FCaptureStream:=TFileStream.Create(aFileName,fmCreate);
  432. Dest:='file: '+aFileName
  433. end
  434. else
  435. Dest:='console';
  436. DoLog(etInfo,Format('Setting up capture on route "%s", writing to %s',[SCaptureRoute,Dest]));
  437. HTTPRouter.RegisterRoute(SCaptureRoute,rmPost,@DoCapture,False);
  438. end;
  439. procedure THTTPApplication.DoRun;
  440. Var
  441. S,ConfigFile : String;
  442. begin
  443. FMaxAge:=31557600;
  444. 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']);
  445. if (S<>'') or HasOption('h','help') then
  446. usage(S);
  447. if HasOption('V','version') then
  448. begin
  449. Terminate;
  450. Writeln(ServerVersion);
  451. Exit;
  452. end;
  453. if HasOption('c','config') then
  454. ConfigFile:=GetOptionValue('c','config')
  455. else
  456. ConfigFile:='simpleserver.ini';
  457. ReadConfigFile(ConfigFile);
  458. ProcessOptions;
  459. if FBackground then
  460. begin
  461. {$ifdef unix}
  462. if FPFork>0 then Halt(0);
  463. {$else}
  464. Log(etError,'Background option not supported');
  465. {$endif}
  466. end;
  467. if FCaptureFileName<>'' then
  468. SetupCapture(FCaptureFileName);
  469. if FPassword<>'' then
  470. begin
  471. HTTPRouter.RegisterRoute('/quit',rmAll,@Doquit,False);
  472. end;
  473. if FEcho then
  474. HTTPRouter.RegisterRoute('/echo',rmAll,@DoEcho,False);
  475. if ProxyManager.LocationCount>0 then
  476. begin
  477. TProxyWebModule.RegisterModule('Proxy',True);
  478. ProxyManager.OnLog:=@DoProxyLog;
  479. end;
  480. DefaultCacheControlMaxAge:=FMaxAge; // one year by default
  481. if BaseDir='' then
  482. BaseDir:=GetCurrentDir;
  483. if (BaseDir<>'') then
  484. BaseDir:=IncludeTrailingPathDelimiter(BaseDir);
  485. if FAPISecret<>'' then
  486. TFPWebFileLocationAPIModule.RegisterFileLocationAPI(ExtractWord(1,FAPISecret,[',']),ExtractWord(2,FAPISecret,[',']));
  487. if FCrossOriginIsolation then
  488. TSimpleFileModule.DefaultSimpleFileModuleClass:=TMySimpleFileModule;
  489. TSimpleFileModule.RegisterDefaultRoute;
  490. TSimpleFileModule.BaseDir:=BaseDir;
  491. TSimpleFileModule.OnLog:=@Log;
  492. If not NoIndexPage then
  493. begin
  494. if (IndexPageName='') then
  495. IndexPageName:='index.html';
  496. TSimpleFileModule.IndexPageName:=IndexPageName;
  497. end;
  498. if not Quiet then
  499. WriteInfo;
  500. if InterfaceAddress<>'' then
  501. HTTPHandler.Address:=InterfaceAddress;
  502. inherited;
  503. end;
  504. begin
  505. Application:=THTTPApplication.Create(Nil);
  506. Application.Initialize;
  507. Application.Run;
  508. Application.Free;
  509. end.