httpcompiler.pp 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983
  1. unit httpcompiler;
  2. {$mode objfpc}
  3. {$H+}
  4. interface
  5. uses
  6. {$ifdef unix}baseunix,{$endif}
  7. sysutils, classes, fpjson, contnrs, syncobjs, fpmimetypes, custhttpapp, inifiles,
  8. fpwebproxy, webutil, fpwebfile, httproute, httpdefs, dirwatch, Pas2JSFSCompiler,
  9. Pas2JSCompilerCfg, ssockets;
  10. Const
  11. HTTPCompilerVersion = '1.0';
  12. nErrTooManyThreads = -1;
  13. nExitCodeSocketError = 1;
  14. Type
  15. TDirWatcher = Class;
  16. THTTPCompilerApplication = Class;
  17. { TCompileItem }
  18. TCompileItem = Class(TCollectionItem)
  19. private
  20. FBaseDir: string;
  21. FConfigFile: String;
  22. FFileName: string;
  23. FOutput : TStrings;
  24. FOptions : TStrings;
  25. FSuccess: Boolean;
  26. FThread: TThread;
  27. function GetOptions: TStrings;
  28. function GetOutput: TStrings;
  29. Public
  30. Destructor Destroy; override;
  31. Property BaseDir : string Read FBaseDir Write FBaseDir;
  32. Property FileName : string Read FFileName Write FFileName;
  33. Property ConfigFile: String Read FConfigFile Write FConfigFile;
  34. Property Options : TStrings Read GetOptions;
  35. Property Output : TStrings Read GetOutput;
  36. Property Thread : TThread Read FThread;
  37. Property Success : Boolean Read FSuccess;
  38. end;
  39. { TCompiles }
  40. TCompiles = Class(TCollection)
  41. private
  42. function GetC(AIndex : Integer): TCompileItem;
  43. Public
  44. Property Compiles[AIndex : Integer] : TCompileItem Read GetC; default;
  45. end;
  46. { TCompileThread }
  47. TCompileThread = class(TThread)
  48. private
  49. FApp : THTTPCompilerApplication;
  50. FItem: TCompileItem;
  51. procedure DoCompilerLog(Sender: TObject; const Msg: String);
  52. procedure SetItem(AValue: TCompileItem);
  53. Public
  54. Constructor create(App : THTTPCompilerApplication; aItem : TCompileItem);
  55. Procedure Execute; override;
  56. Property Item : TCompileItem read FItem write SetItem;
  57. end;
  58. { TDirWatcher }
  59. TDirWatcher = Class(TComponent)
  60. Private
  61. FApp : THTTPCompilerApplication;
  62. FDW : TDirWatch;
  63. procedure DoChange(Sender: TObject; aEntry: TDirectoryEntry; AEvents: TFileEvents);
  64. Public
  65. Constructor Create(App : THTTPCompilerApplication; ADir : String);overload;
  66. Destructor Destroy; override;
  67. end;
  68. { TMySimpleFileModule }
  69. TMySimpleFileModule = class(TSimpleFileModule)
  70. Public
  71. Procedure SendFile(const AFileName: String; AResponse: TResponse); override;
  72. end;
  73. { THTTPCompilerApplication }
  74. THTTPCompilerApplication = Class(TCustomHTTPApplication)
  75. private
  76. FCaptureFileName : String;
  77. FCaptureStream : TFileStream;
  78. FAPI: String;
  79. FBaseDir: String;
  80. FConfigFile: String;
  81. FIndexPageName: String;
  82. FNoIndexPage: Boolean;
  83. FProjectFile: String;
  84. FStatusLock : TCriticalSection;
  85. FQuiet: Boolean;
  86. FWatch: Boolean;
  87. FDW : TDirWatcher;
  88. FStatusList : TFPObjectList;
  89. FCompiles : TCompiles;
  90. FServeOnly : Boolean;
  91. FMimeFile : String;
  92. FBackground:boolean;
  93. FPassword:String;
  94. FEcho:Boolean;
  95. FMaxAge: integer;
  96. FCrossOriginIsolation : Boolean;
  97. FInterfaceAddress : String;
  98. procedure AddToStatus(O: TJSONObject);
  99. procedure DoEcho(ARequest: TRequest; AResponse: TResponse);
  100. procedure DoProxyLog(Sender: TObject; const aMethod, aLocation, aFromURL, aToURL: String);
  101. procedure Doquit(ARequest: TRequest; AResponse: TResponse);
  102. procedure DoCapture(ARequest: TRequest; AResponse: TResponse);
  103. function GetCaptureJSON(ARequest: TRequest; AResponse: TResponse; var aJSON: TJSONData): TJSONArray;
  104. procedure SetupCapture(const aFileName: string);
  105. procedure ShowCaptureOutput(aJSON: TJSONData);
  106. function HandleCompileOptions(aDir: String): Boolean;
  107. function ProcessOptions: Boolean;
  108. procedure ReadConfigFile(const ConfigFile: string);
  109. Procedure ReportBuilding(AItem : TCompileItem);
  110. Procedure ReportBuilt(AItem : TCompileItem);
  111. Procedure AddToStatus(AEntry : TDirectoryEntry; AEvents : TFileEvents);
  112. procedure DoStatusRequest(ARequest: TRequest; AResponse: TResponse);
  113. procedure DoRecompile(ARequest: TRequest; AResponse: TResponse);
  114. function ScheduleCompile(const aProjectFile: String; Options : TStrings = Nil): Integer;
  115. procedure StartWatch(ADir: String);
  116. protected
  117. procedure Usage(Msg: String); virtual;
  118. function GetDefaultMimeTypesFile: string; virtual;
  119. procedure LoadDefaultMimeTypes; virtual;
  120. public
  121. Constructor Create(AOWner : TComponent); override;
  122. Destructor Destroy; override;
  123. procedure DoLog(EventType: TEventType; const Msg: String); override;
  124. Procedure DoRun; override;
  125. Property API : String Read FAPI Write FAPI;
  126. property Quiet : Boolean read FQuiet Write FQuiet;
  127. Property Watch : Boolean Read FWatch Write FWatch;
  128. Property ProjectFile : String Read FProjectFile Write FProjectFile;
  129. Property ConfigFile : String Read FConfigFile Write FConfigFile;
  130. Property BaseDir : String Read FBaseDir;
  131. Property ServeOnly : Boolean Read FServeOnly;
  132. Property MimeFile : String Read FMimeFile;
  133. Property NoIndexPage : Boolean Read FNoIndexPage Write FNoIndexPage;
  134. Property IndexPageName : String Read FIndexPageName Write FIndexPageName;
  135. Property InterfaceAddress : String Read FInterfaceAddress Write FInterfaceAddress;
  136. end;
  137. Implementation
  138. uses strutils;
  139. { TMySimpleFileModule }
  140. procedure TMySimpleFileModule.SendFile(const AFileName: String; AResponse: TResponse);
  141. begin
  142. AResponse.SetCustomHeader('Cross-Origin-Embedder-Policy','require-corp');
  143. AResponse.SetCustomHeader('Cross-Origin-Opener-Policy','same-origin');
  144. inherited SendFile(AFileName, AResponse);
  145. end;
  146. { TCompileThread }
  147. procedure TCompileThread.SetItem(AValue: TCompileItem);
  148. begin
  149. if FItem=AValue then Exit;
  150. FItem:=AValue;
  151. end;
  152. procedure TCompileThread.DoCompilerLog(Sender: TObject; const Msg: String);
  153. begin
  154. If Assigned(Item) then
  155. Item.Output.Add(Msg);
  156. end;
  157. constructor TCompileThread.create(App: THTTPCompilerApplication; aItem: TCompileItem);
  158. begin
  159. FItem:=aItem;
  160. FApp:=App;
  161. FreeOnTerminate:=True;
  162. inherited create(False);
  163. end;
  164. procedure TCompileThread.Execute;
  165. Var
  166. C : TPas2JSFSCompiler;
  167. L : TStrings;
  168. begin
  169. L:=Nil;
  170. C:=TPas2JSFSCompiler.Create;
  171. Try
  172. C.ConfigSupport:=TPas2JSFileConfigSupport.Create(C);
  173. FApp.ReportBuilding(Item);
  174. L:=TStringList.Create;
  175. L.Assign(Item.Options);
  176. if (Item.ConfigFile<>'') then
  177. L.Add('@'+Item.ConfigFile);
  178. L.Add(Item.FileName);
  179. C.Log.OnLog:=@DoCompilerLog;
  180. try
  181. C.Run(ParamStr(0),Item.BaseDir,L,True);
  182. Item.FSuccess:=True;
  183. except
  184. On E : Exception do
  185. Item.Output.Add(Format('Error %s compiling %s: %s',[E.ClassName,Item.FileName,E.Message]));
  186. end;
  187. FApp.ReportBuilt(Item);
  188. Finally
  189. C.Free;
  190. L.Free;
  191. end;
  192. Item.FThread:=Nil;
  193. end;
  194. { TCompiles }
  195. function TCompiles.GetC(AIndex : Integer): TCompileItem;
  196. begin
  197. Result:=Items[Aindex] as TCompileItem;
  198. end;
  199. { TCompileItem }
  200. function TCompileItem.GetOutput: TStrings;
  201. begin
  202. If (FOutput=Nil) then
  203. FOutput:=TStringList.Create;
  204. Result:=FOutput;
  205. end;
  206. function TCompileItem.GetOptions: TStrings;
  207. begin
  208. If (FOptions=Nil) then
  209. FOptions:=TStringList.Create;
  210. Result:=FOptions;
  211. end;
  212. destructor TCompileItem.Destroy;
  213. begin
  214. FreeAndNil(FOutput);
  215. FreeAndNil(FOptions);
  216. inherited Destroy;
  217. end;
  218. { TDirWatcher }
  219. procedure TDirWatcher.DoChange(Sender: TObject; aEntry: TDirectoryEntry; AEvents: TFileEvents);
  220. begin
  221. if Assigned(FApp) then
  222. FApp.AddToStatus(AEntry,AEvents);
  223. end;
  224. constructor TDirWatcher.Create(App: THTTPCompilerApplication; ADir: String);
  225. begin
  226. Inherited create(APP);
  227. FApp:=App;
  228. FDW:=TDirwatch.Create(Self);
  229. FDW.AddWatch(ADir,allEvents);
  230. FDW.OnChange:=@DoChange;
  231. TThread.ExecuteInThread(@FDW.StartWatch);
  232. end;
  233. destructor TDirWatcher.Destroy;
  234. begin
  235. FApp:=Nil;
  236. FDW.Terminate;
  237. FreeAndNil(FDW);
  238. inherited Destroy;
  239. end;
  240. { THTTPCompilerApplication }
  241. procedure THTTPCompilerApplication.DoLog(EventType: TEventType; const Msg: String);
  242. begin
  243. {AllowWriteln}
  244. if Quiet then
  245. exit;
  246. if IsConsole then
  247. Writeln(FormatDateTime('yyyy-mm-dd hh:nn:ss.zzz',Now),' [',EventType,'] ',Msg)
  248. else
  249. inherited DoLog(EventType, Msg);
  250. {AllowWriteln-}
  251. end;
  252. procedure THTTPCompilerApplication.Usage(Msg : String);
  253. begin
  254. {AllowWriteln}
  255. if (Msg<>'') then
  256. Writeln('Error: ',Msg);
  257. Writeln('Version ',HTTPCompilerVersion);
  258. Writeln('Usage ',ExtractFileName(ParamStr(0)),' [options] ');
  259. Writeln('Where options is one or more of : ');
  260. Writeln('-A --api=location,secret Enable location management API.');
  261. Writeln('-c --compile[=proj] Recompile project if pascal files change. Default project is app.lpr');
  262. Writeln('-d --directory=dir Base directory from which to serve files.');
  263. Writeln(' Default is current working directory: ',GetCurrentDir);
  264. Writeln('-h --help This help text');
  265. Writeln('-i --indexpage=name Directory index page to use (default: index.html)');
  266. Writeln('-I --interface=IP Listen on this interface address only.');
  267. Writeln('-m --mimetypes=file Set Filename for loading mimetypes. Default is ',GetDefaultMimeTypesFile);
  268. Writeln('-n --noindexpage Do not allow index page.');
  269. Writeln('-o --coi Enable Cross-Origin Isolation headers');
  270. Writeln('-p --port=NNNN TCP/IP port to listen on (default is 3000)');
  271. Writeln('-q --quiet Do not write diagnostic messages');
  272. Writeln('-s --simpleserver Only serve files, do not enable compilation.');
  273. Writeln('-u --capture[=FILE] Set up /debugcapture route to capture output sent by browser.');
  274. Writeln(' If FILE is specified, write to file. If not specified, writes to STDOUT.');
  275. Writeln('-w --watch Watch directory for changes');
  276. Halt(Ord(Msg<>''));
  277. {AllowWriteln-}
  278. end;
  279. function THTTPCompilerApplication.GetDefaultMimeTypesFile: string;
  280. begin
  281. {$ifdef unix}
  282. Result:='/etc/mime.types';
  283. {$ifdef darwin}
  284. if not FileExists(Result) then
  285. Result:='/private/etc/apache2/mime.types';
  286. {$endif}
  287. {$else}
  288. Result:=ExtractFilePath(System.ParamStr(0))+'mime.types';
  289. {$endif}
  290. end;
  291. procedure THTTPCompilerApplication.LoadDefaultMimeTypes;
  292. begin
  293. MimeTypes.LoadKnownTypes;
  294. // To be sure
  295. MimeTypes.AddType('application/xhtml+xml','xhtml;xht');
  296. MimeTypes.AddType('text/html','html;htm');
  297. MimeTypes.AddType('text/plain','txt');
  298. MimeTypes.AddType('application/javascript','js');
  299. MimeTypes.AddType('text/plain','map');
  300. MimeTypes.AddType('application/json','json');
  301. MimeTypes.AddType('image/png','png');
  302. MimeTypes.AddType('image/jpeg','jpeg;jpg');
  303. MimeTypes.AddType('image/gif','gif');
  304. MimeTypes.AddType('image/jp2','jp2');
  305. MimeTypes.AddType('image/tiff','tiff;tif');
  306. MimeTypes.AddType('application/pdf','pdf');
  307. MimeTypes.AddType('text/css','css');
  308. end;
  309. constructor THTTPCompilerApplication.Create(AOWner: TComponent);
  310. begin
  311. inherited Create(AOWner);
  312. FStatusLock:=TCriticalSection.Create;
  313. FStatusList:=TFPObjectList.Create(False);
  314. FCompiles:=TCompiles.Create(TCompileItem);
  315. end;
  316. destructor THTTPCompilerApplication.Destroy;
  317. begin
  318. FStatusLock.Enter;
  319. try
  320. FreeAndNil(FCompiles);
  321. FreeAndNil(FStatusList);
  322. finally
  323. FStatusLock.Leave;
  324. end;
  325. FreeAndNil(FStatusLock);
  326. inherited Destroy;
  327. end;
  328. procedure THTTPCompilerApplication.StartWatch(ADir : String);
  329. begin
  330. FDW:=TDirWatcher.Create(Self,ADir);
  331. end;
  332. procedure THTTPCompilerApplication.ReportBuilding(AItem: TCompileItem);
  333. Var
  334. O : TJSONObject;
  335. begin
  336. O:=TJSONObject.Create(['action','building','compileID',AItem.ID,'project',AItem.FileName,'config',AItem.ConfigFile]);
  337. AddToStatus(O);
  338. end;
  339. procedure THTTPCompilerApplication.ReportBuilt(AItem: TCompileItem);
  340. Var
  341. O : TJSONObject;
  342. A : TJSONArray;
  343. I : Integer;
  344. begin
  345. A:=TJSONArray.Create;
  346. For I:=0 to AItem.Output.Count-1 do
  347. A.Add(AItem.Output[i]);
  348. O:=TJSONObject.Create(['action','built','compileID',AItem.ID,'project',AItem.FileName,'config',AItem.ConfigFile,'output',A,'success',AItem.Success]);
  349. AddToStatus(O);
  350. end;
  351. procedure THTTPCompilerApplication.AddToStatus(O : TJSONObject);
  352. begin
  353. FStatusLock.Enter;
  354. try
  355. {$ifdef VerboseHTTPCompiler}
  356. Writeln('Adding to status ',Assigned(O),' : ',O.ClassName);
  357. {$endif}
  358. FStatusList.Add(O);
  359. finally
  360. FStatusLock.Leave;
  361. end;
  362. end;
  363. procedure THTTPCompilerApplication.AddToStatus(AEntry: TDirectoryEntry; AEvents: TFileEvents);
  364. Var
  365. O : TJSONObject;
  366. FN : String;
  367. begin
  368. Log(etDebug,'File change detected: %s (%s)',[AEntry.name,FileEventsToStr(AEvents)]);
  369. O:=TJSONObject.Create(['action','file','name',AEntry.name,'events',FileEventsToStr(AEvents)]);
  370. if Pos(ExtractFileExt(AEntry.Name),'.lpr.pas.pp.inc.dpr')>0 then
  371. FN:=AEntry.Name;
  372. if (FN<>'') then
  373. O.Add('recompile',true);
  374. AddToStatus(O);
  375. if (FN<>'') then
  376. begin
  377. Log(etDebug,'File change forces recompile: %s',[AEntry.name]);
  378. ScheduleCompile('',Nil);
  379. end;
  380. end;
  381. procedure THTTPCompilerApplication.DoStatusRequest(ARequest : TRequest; AResponse : TResponse);
  382. Var
  383. R,O : TJSONObject;
  384. A : TJSONArray;
  385. I : integer;
  386. begin
  387. Log(etDebug,'Status request from: %s',[ARequest.RemoteAddress]);
  388. R:=Nil;
  389. try
  390. FStatusLock.Enter;
  391. try
  392. if (FStatusList.Count=0) then
  393. R:=TJSONObject.Create(['ping',True])
  394. else
  395. begin
  396. {$ifdef VerboseHTTPCompiler}
  397. Writeln(FStatusList[0].ClassName);
  398. {$endif}
  399. O:=FStatusList[0] as TJSONObject;
  400. FStatusList.Delete(0);
  401. if O.Get('action','')<>'file' then
  402. R:=O
  403. else
  404. begin
  405. // If first event is file event, then add and delete all file events in list.
  406. A:=TJSONArray.Create([O]);
  407. O.Delete('action');
  408. R:=TJSONObject.Create(['action','sync','files',A]);
  409. For I:=FStatusList.Count-1 downto 0 do
  410. begin
  411. O:=FStatusList[I] as TJSONObject;
  412. if (O.Get('action','')='file') then
  413. begin
  414. A.Add(O);
  415. O.Delete('action');
  416. FStatusList.Delete(I);
  417. end;
  418. end;
  419. end
  420. end;
  421. finally
  422. FStatusLock.Leave;
  423. end;
  424. AResponse.ContentType:='application/json';
  425. AResponse.Content:=R.AsJSON;
  426. AResponse.SendResponse;
  427. finally
  428. R.Free;
  429. end;
  430. end;
  431. function THTTPCompilerApplication.ScheduleCompile(const aProjectFile: String;
  432. Options: TStrings): Integer;
  433. Var
  434. CI : TCompileItem;
  435. I,TC : Integer;
  436. begin
  437. TC:=0;
  438. For I:=0 to FCompiles.Count-1 do
  439. if Assigned(FCompiles[I].THread) then
  440. Inc(TC);
  441. if TC>10 then
  442. begin
  443. Log(etError,'Refusing compile of file "%s" using config file "%s"',[AProjectFile, ConfigFile]);
  444. Exit(nErrTooManyThreads);
  445. end;
  446. CI:=FCompiles.Add as TCompileItem;
  447. Log(etInfo,'Scheduling compile ID %d of file "%s" using config file "%s"',[CI.ID,AProjectFile, ConfigFile]);
  448. CI.BaseDir:=BaseDir;
  449. CI.FileName:=AProjectFile;
  450. CI.ConfigFile:=ConfigFile;
  451. if Assigned(Options) then
  452. CI.Options.Assign(Options);
  453. TCompileThread.Create(Self,CI);
  454. Result:=CI.ID;
  455. end;
  456. procedure THTTPCompilerApplication.DoRecompile(ARequest: TRequest; AResponse: TResponse);
  457. Var
  458. ID : Integer;
  459. PF,CL : String;
  460. Options: TStrings;
  461. begin
  462. PF:=ARequest.ContentFields.Values['ProjectFile'];
  463. CL:=ARequest.ContentFields.Values['CompileOptions'];
  464. if PF='' then
  465. PF:=ProjectFile;
  466. If (PF='') then
  467. begin
  468. AResponse.ContentType:='application/json';
  469. AResponse.Content:='{ "success" : false, "message": "no project file set or provided" }';
  470. AResponse.Code:=404;
  471. AResponse.CodeText:='No project file';
  472. end
  473. else
  474. begin
  475. Options:=Nil;
  476. try
  477. if CL<>'' then
  478. begin
  479. Options:=TStringList.Create;
  480. Options.Text:=Cl;
  481. end;
  482. ID:=ScheduleCompile(PF,Options);
  483. finally
  484. FreeAndNil(Options);
  485. end;
  486. if ID=nErrTooManyThreads then
  487. begin
  488. AResponse.Code:=403;
  489. AResponse.CodeText:='Too many compiles';
  490. AResponse.ContentType:='application/json';
  491. AResponse.Content:='{ "success" : false, "message": "Too many compiles running" }';
  492. end
  493. else
  494. begin
  495. AResponse.Code:=200;
  496. AResponse.ContentType:='application/json';
  497. AResponse.Content:=Format('{ "success" : true, "file": "%s", "commandLine" : "%s", "compileID": %d }',[StringToJSONString(PF),StringToJSONString(CL),ID]);
  498. end
  499. end;
  500. AResponse.SendResponse;
  501. end;
  502. function THTTPCompilerApplication.HandleCompileOptions(aDir: String): Boolean;
  503. begin
  504. Result:=False;
  505. Watch:=HasOption('w','watch');
  506. if Hasoption('P','project') then
  507. begin
  508. ProjectFile:=GetOptionValue('P','project');
  509. if ProjectFile='' then
  510. ProjectFile:=IncludeTrailingPathDelimiter(aDir)+'app.lpr';
  511. If Not FileExists(ProjectFile) then
  512. begin
  513. Terminate;
  514. Log(etError,'Project file "%s" does not exist, aborting.',[ProjectFile]);
  515. Exit;
  516. end;
  517. ConfigFile:=GetOptionValue('c','config');
  518. if (ConfigFile='') then
  519. ConfigFile:=ChangeFileExt(Projectfile,'.cfg');
  520. if not FileExists(ConfigFile) then
  521. ConfigFile:='';
  522. end;
  523. if Watch then
  524. begin
  525. if (ProjectFile='') then
  526. Log(etWarning,'No project file specified, disabling watch.') ;
  527. StartWatch(aDir);
  528. end;
  529. Result:=True;
  530. end;
  531. procedure THTTPCompilerApplication.DoProxyLog(Sender: TObject; const aMethod, aLocation, aFromURL, aToURL: String);
  532. Var
  533. Msg : String;
  534. begin
  535. if Quiet then
  536. exit;
  537. Msg:=Format('(Proxy redirect) location: %s, Method: %s, From: %s, to: %s',[aLocation,aMethod,aFromURl,atoURL]);
  538. if IsConsole then
  539. {AllowWriteln}
  540. Writeln(FormatDateTime('yyyy-mm-dd hh:nn:ss.zzz',Now),' [',etInfo,'] ',Msg)
  541. {AllowWriteln-}
  542. else
  543. inherited DoLog(etInfo, Msg);
  544. end;
  545. procedure THTTPCompilerApplication.DoEcho(ARequest: TRequest; AResponse: TResponse);
  546. Var
  547. L : TStrings;
  548. begin
  549. L:=TStringList.Create;
  550. try
  551. L.AddStrings(['<!doctype html>',
  552. '<html>',
  553. '<head>',
  554. '<title>Echo request</title>',
  555. '</head>',
  556. '<body>'
  557. ]);
  558. DumpRequest(aRequest,L);
  559. L.AddStrings(['</body>','</html>']);
  560. AResponse.Content:=L.Text;
  561. AResponse.SendResponse;
  562. finally
  563. L.Free;
  564. end;
  565. end;
  566. procedure THTTPCompilerApplication.Doquit(ARequest: TRequest; AResponse: TResponse);
  567. Var
  568. PWD : String;
  569. begin
  570. PWD:=ARequest.QueryFields.Values['password'];
  571. if PWD='' then
  572. ARequest.ContentFields.Values['password'];
  573. if PWD=FPassword then
  574. begin
  575. AResponse.Content:='OK';
  576. AResponse.SendContent;
  577. Terminate;
  578. end
  579. else
  580. begin
  581. AResponse.Code:=403;
  582. AResponse.CodeText:='Forbidden';
  583. AResponse.SendContent;
  584. end;
  585. end;
  586. Const
  587. SCaptureRoute = '/debugcapture';
  588. procedure THTTPCompilerApplication.ReadConfigFile(Const ConfigFile : string);
  589. Const
  590. SConfig = 'Server';
  591. SProxy = 'Proxy';
  592. SLocations = 'Locations';
  593. KeyPort = 'Port';
  594. KeyInterface = 'Interface';
  595. KeyDir = 'Directory';
  596. KeyIndexPage = 'IndexPage';
  597. KeyHostName = 'hostname';
  598. keyMimetypes = 'mimetypes';
  599. KeySSL = 'SSL';
  600. KeyQuiet = 'quiet';
  601. KeyQuit = 'quit';
  602. KeyEcho = 'echo';
  603. KeyNoIndexPage = 'noindexpage';
  604. KeyBackground = 'background';
  605. KeyMaxAge = 'MaxAge';
  606. KeyAPI = 'API';
  607. KeyCOI = 'CrossOriginIsolation';
  608. KeyCapture = 'DebugCapture';
  609. Var
  610. L : TStringList;
  611. P,U : String;
  612. I : Integer;
  613. begin
  614. if (ConfigFile='') or Not FileExists(ConfigFile) then exit;
  615. L:=Nil;
  616. With TMemIniFile.Create(ConfigFile) do
  617. try
  618. FBaseDir:=ReadString(SConfig,KeyDir,BaseDir);
  619. Port:=ReadInteger(SConfig,KeyPort,Port);
  620. InterfaceAddress:=ReadString(SConfig,KeyInterface,InterfaceAddress);
  621. Quiet:=ReadBool(SConfig,KeyQuiet,Quiet);
  622. FMimeFile:=ReadString(SConfig,keyMimetypes,MimeFile);
  623. NoIndexPage:=ReadBool(SConfig,KeyNoIndexPage,NoIndexPage);
  624. IndexPageName:=ReadString(SConfig,KeyIndexPage,IndexPageName);
  625. HostName:=ReadString(SConfig,KeyHostName,HostName);
  626. UseSSL:=ReadBool(SConfig,KeySSL,UseSSL);
  627. FBackground:=ReadBool(SConfig,Keybackground,FBackGround);
  628. FPassword:=ReadString(SConfig,KeyQuit,FPassword);
  629. FEcho:=ReadBool(SConfig,KeyEcho,FEcho);
  630. FMaxAge:=ReadInteger(SConfig,KeyMaxAge,FMaxAge);
  631. FAPI:=ReadString(SConfig,keyAPI,'');
  632. FCrossOriginIsolation:=ReadBool(SConfig,KeyCOI,FCrossOriginIsolation);
  633. if ValueExists(SConfig,KeyCapture) then
  634. begin
  635. FCaptureFileName:=ReadString(SConfig,keyCapture,'');
  636. if FCaptureFileName='' then
  637. FCaptureFileName:='-';
  638. end;
  639. L:=TstringList.Create;
  640. ReadSectionValues(SProxy,L,[]);
  641. For I:=0 to L.Count-1 do
  642. begin
  643. L.GetNameValue(I,P,U);
  644. if (P<>'') and (U<>'') then
  645. ProxyManager.RegisterLocation(P,U).AppendPathInfo:=true;
  646. end;
  647. L.Clear;
  648. ReadSectionValues(SLocations,L,[]);
  649. For I:=0 to L.Count-1 do
  650. begin
  651. L.GetNameValue(I,P,U);
  652. if (P<>'') and (U<>'') then
  653. RegisterFileLocation(P,U);
  654. end;
  655. finally
  656. L.Free;
  657. Free;
  658. end;
  659. end;
  660. function THTTPCompilerApplication.ProcessOptions: Boolean;
  661. Var
  662. IndexPage,D : String;
  663. begin
  664. Result:=False;
  665. if HasOption('A','api') then
  666. FAPI:=GetOptionValue('A','api');
  667. FServeOnly:=FServeOnly or HasOption('s','serve-only');
  668. Quiet:=Quiet or HasOption('q','quiet');
  669. if (Port=0) or HasOption('p','port') then
  670. Port:=StrToIntDef(GetOptionValue('p','port'),3000);
  671. if HasOption('d','directory') then
  672. D:=GetOptionValue('d','directory');
  673. if D='' then
  674. D:=GetCurrentDir;
  675. if HasOption('m','mimetypes') then
  676. MimeTypesFile:=GetOptionValue('m','mimetypes');
  677. if MimeTypesFile='' then
  678. begin
  679. MimeTypesFile:=GetDefaultMimeTypesFile;
  680. if not FileExists(MimeTypesFile) then
  681. begin
  682. MimeTypesFile:='';
  683. LoadDefaultMimeTypes;
  684. end;
  685. end
  686. else if not FileExists(MimeTypesFile) then
  687. Log(etWarning,'mimetypes file not found: '+MimeTypesFile);
  688. FBaseDir:=D;
  689. if not ServeOnly then
  690. if not HandleCompileOptions(D) then
  691. exit(False);
  692. TSimpleFileModule.BaseDir:=IncludeTrailingPathDelimiter(D);
  693. TSimpleFileModule.OnLog:=@Log;
  694. Log(etInfo,'Listening on port %d, serving files from directory: %s',[Port,D]);
  695. if ServeOnly then
  696. Log(etInfo,'Compile requests will be ignored.');
  697. NoIndexPage:=NoIndexPage or HasOption('n','noindexpage');
  698. if HasOption('i','indexpage') then
  699. IndexPage:=GetOptionValue('i','indexpage');
  700. if HasOption('I','interface') then
  701. InterfaceAddress:=GetOptionValue('I','interface');
  702. If not NoIndexPage then
  703. begin
  704. if (IndexPage='') then
  705. IndexPage:='index.html';
  706. Log(etInfo,'Using index page %s',[IndexPage]);
  707. TSimpleFileModule.IndexPageName:=IndexPage;
  708. end;
  709. FCrossOriginIsolation:=hasOption('o','coi');
  710. if HasOption('u','capture') then
  711. begin
  712. FCaptureFileName:=GetOptionValue('u','capture');
  713. if FCaptureFileName='' then
  714. FCaptureFileName:='-';
  715. end;
  716. Result:=True;
  717. end;
  718. procedure THTTPCompilerApplication.DoRun;
  719. Var
  720. S : String;
  721. begin
  722. S:=Checkoptions('shqVd:ni:p:wP::cm:A:I:u::',['help','quiet','version','noindexpage','directory:','port:','indexpage:','watch','project::','config:','simpleserver','mimetypes:','api:','interface:','capture']);
  723. if (S<>'') or HasOption('h','help') then
  724. Usage(S);
  725. if HasOption('V','version') then
  726. begin
  727. {AllowWriteln}
  728. writeln(HTTPCompilerVersion);
  729. {AllowWriteln-}
  730. Terminate;
  731. exit;
  732. end;
  733. if HasOption('c','config') then
  734. ConfigFile:=GetOptionValue('c','config')
  735. else
  736. ConfigFile:='compileserver.ini';
  737. Port:=3000;
  738. ReadConfigFile(ConfigFile);
  739. If not ProcessOptions then
  740. begin
  741. Terminate;
  742. exit;
  743. end;
  744. if FBackground then
  745. begin
  746. {$ifdef unix}
  747. if FPFork>0 then Halt(0);
  748. {$else}
  749. Log(etError,'Background option not supported');
  750. {$endif}
  751. end;
  752. // Handle options
  753. if FCaptureFileName<>'' then
  754. SetupCapture(FCaptureFileName);
  755. if FPassword<>'' then
  756. HTTPRouter.RegisterRoute('/quit',rmAll,@Doquit,False);
  757. if FEcho then
  758. HTTPRouter.RegisterRoute('/echo',rmAll,@DoEcho,False);
  759. if ProxyManager.LocationCount>0 then
  760. begin
  761. TProxyWebModule.RegisterModule('Proxy',True);
  762. ProxyManager.OnLog:=@DoProxyLog;
  763. end;
  764. DefaultCacheControlMaxAge:=FMaxAge; // one year by default
  765. if not ServeOnly then
  766. begin
  767. httprouter.RegisterRoute('$sys/compile',rmPost,@DoRecompile);
  768. httprouter.RegisterRoute('$sys/status',rmGet,@DoStatusRequest);
  769. end;
  770. if FAPI<>'' then
  771. {$IF FPC_FULLVERSION > 30300}
  772. TFPWebFileLocationAPIModule.RegisterFileLocationAPI(ExtractWord(1,FAPI,[',']),ExtractWord(2,FAPI,[',']));
  773. {$ELSE}
  774. Log(etError,'API support missing, Compile with fpc 3.3.1+');
  775. {$ENDIF}
  776. if FCrossOriginIsolation then
  777. TSimpleFileModule.DefaultSimpleFileModuleClass:=TMySimpleFileModule;
  778. TSimpleFileModule.RegisterDefaultRoute;
  779. if InterfaceAddress<>'' then
  780. HTTPHandler.Address:=InterfaceAddress;
  781. try
  782. inherited DoRun;
  783. except
  784. on E: ESocketError do begin
  785. Log(etError,E.ClassName+': '+E.Message);
  786. ExitCode:=nExitCodeSocketError;
  787. Terminate;
  788. end;
  789. end;
  790. end;
  791. function THTTPCompilerApplication.GetCaptureJSON(ARequest: TRequest;
  792. AResponse: TResponse; var aJSON: TJSONData): TJSONArray;
  793. var
  794. aJSONObj : TJSONObject absolute aJSON;
  795. Cont : String;
  796. begin
  797. Result:=Nil;
  798. aJSON:=Nil;
  799. try
  800. Cont:=aRequest.Content;
  801. aJSON:=GetJSON(Cont);
  802. if aJSON.JSONType<>jtObject then
  803. Raise EHTTP.Create('No JSON object in capture JSON');
  804. Result:=aJSONObj.Get('lines',TJSONArray(Nil));
  805. if Result=Nil then
  806. begin
  807. FreeAndNil(aJSON);
  808. Raise EHTTP.Create('No lines element in capture JSON');
  809. end;
  810. except
  811. On E : Exception do
  812. begin
  813. DoLog(etError,Format('Exception %s (%s) : Invalid capture content: not valid JSON: %s',[E.ClassName,E.Message,Copy(Cont,1,255)]));
  814. aResponse.Code:=400;
  815. aResponse.CodeText:='INVALID PARAM';
  816. aResponse.SendResponse;
  817. end;
  818. end;
  819. end;
  820. procedure THTTPCompilerApplication.ShowCaptureOutput(aJSON : TJSONData);
  821. var
  822. S : TJSONStringType;
  823. begin
  824. if aJSON.JSONType in StructuredJSONTypes then
  825. S:=aJSON.AsJSON
  826. else
  827. S:=aJSON.AsString;
  828. if Assigned(FCaptureStream) then
  829. begin
  830. S:=S+sLineBreak;
  831. FCaptureStream.WriteBuffer(S[1],Length(S)*SizeOf(TJSONCharType));
  832. end
  833. else
  834. DoLog(etInfo,'Capture : '+S);
  835. end;
  836. procedure THTTPCompilerApplication.DoCapture(ARequest: TRequest; AResponse: TResponse);
  837. Var
  838. aJSON : TJSONData;
  839. aArray : TJSONArray;
  840. I : Integer;
  841. begin
  842. aJSON:=Nil;
  843. aArray:=Nil;
  844. try
  845. aArray:=GetCaptureJSON(aRequest,aResponse,aJSON);
  846. if aArray<>Nil then
  847. begin
  848. For I:=0 to aArray.Count-1 do
  849. ShowCaptureOutput(aArray[i]);
  850. aResponse.Code:=200;
  851. aResponse.CodeText:='OK';
  852. aResponse.SendResponse;
  853. end;
  854. finally
  855. aJSON.Free;
  856. end;
  857. end;
  858. procedure THTTPCompilerApplication.SetupCapture(Const aFileName : string);
  859. Var
  860. Dest : String;
  861. begin
  862. if (aFileName<>'') and (aFileName<>'-') then
  863. begin
  864. FCaptureStream:=TFileStream.Create(aFileName,fmCreate);
  865. Dest:='file: '+aFileName
  866. end
  867. else
  868. Dest:='console';
  869. DoLog(etInfo,Format('Setting up capture on route "%s", writing to %s',[SCaptureRoute,Dest]));
  870. HTTPRouter.RegisterRoute(SCaptureRoute,rmPost,@DoCapture,False);
  871. end;
  872. end.