httpcompiler.pp 27 KB

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