2
0

simpleserver.pas 16 KB

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