Browse Source

* Move simple server to unit so it can be reused (pas2js webcompiler)

Michaël Van Canneyt 9 months ago
parent
commit
9352e09849

+ 12 - 3
packages/fcl-web/examples/simpleserver/simpleserver.lpi

@@ -26,22 +26,26 @@
     </PublishOptions>
     <RunParams>
       <local>
-        <CommandLineParams Value="-p 3003 -s -H nickname.freepascal.org"/>
+        <CommandLineParams Value="-c /home/tixeo/.lazarus/simplewebservergui.ini --coi"/>
       </local>
       <FormatVersion Value="2"/>
       <Modes Count="1">
         <Mode0 Name="default">
           <local>
-            <CommandLineParams Value="-p 3003 -s -H nickname.freepascal.org"/>
+            <CommandLineParams Value="-c /home/tixeo/.lazarus/simplewebservergui.ini --coi"/>
           </local>
         </Mode0>
       </Modes>
     </RunParams>
-    <Units Count="1">
+    <Units Count="2">
       <Unit0>
         <Filename Value="simpleserver.pas"/>
         <IsPartOfProject Value="True"/>
       </Unit0>
+      <Unit1>
+        <Filename Value="../../src/base/fpsimpleserver.pp"/>
+        <IsPartOfProject Value="True"/>
+      </Unit1>
     </Units>
   </ProjectOptions>
   <CompilerOptions>
@@ -54,6 +58,11 @@
       <OtherUnitFiles Value="../../src/base"/>
       <UnitOutputDirectory Value="lib/$(TargetCPU)-$(TargetOS)"/>
     </SearchPaths>
+    <Linking>
+      <Debugging>
+        <DebugInfoType Value="dsDwarf3"/>
+      </Debugging>
+    </Linking>
   </CompilerOptions>
   <Debugging>
     <Exceptions Count="3">

+ 4 - 563
packages/fcl-web/examples/simpleserver/simpleserver.pas

@@ -16,7 +16,7 @@
 {$h+}
 
 { $DEFINE USEGNUTLS}
-{ $DEFINE USEMICROHTTP}
+{ $DEFINE USEMICROHTTP} // Note, this must match what is defined in fpsimpleserver
 
 program simpleserver;
 
@@ -35,575 +35,16 @@ uses
 {$else}
   opensslsockets,
 {$endif}
-  custhttpapp,
-{$ELSE}
-  custmicrohttpapp,
 {$ENDIF}
-  {$ifdef unix}
-  baseunix,
-  {$endif}
-  {$IFNDEF VER3_2}
-  fpdebugcapturesvc,
-  {$ENDIF}
-  sysutils, Classes, jsonparser, fpjson, strutils, inifiles, sslbase, httproute, httpdefs, fpmimetypes, fpwebfile, fpwebproxy,
-  webutil;
-
-Const
-  ServerVersion = '1.0';
+fpmkunit,
+  fpsimpleserver;
 
 Type
-
-  { THTTPApplication }
-{$IFDEF USEMICROHTTP}
-  TParentApp = TCustomMicroHTTPApplication;
-{$ELSE}
-  TParentApp = TCustomHTTPApplication;
-{$ENDIF}
-
-{$IFDEF VER3_2}
-  { TMySimpleFileModule }
-  TMySimpleFileModule = class(TFPCustomFileModule)
-  Public
-    Constructor CreateNew(AOwner: TComponent; CreateMode: Integer); override;
-    Procedure SendFile(const AFileName: String; AResponse: TResponse); override;
-  end;
-{$ENDIF}
-
-  THTTPApplication = Class(TParentApp)
-  private
-    FProxyDefs : TStrings;
-    FLocations : TStrings;
-    FHeaders: TStrings;
-    FAPISecret : String;
-    FBaseDir: string;
-    FIndexPageName: String;
-    FInterfaceAddress: String;
-    FMimeFile: String;
-    FNoIndexPage: Boolean;
-    FQuiet: Boolean;
-    FBackground : Boolean;
-    FPassword : string;
-    FEcho : Boolean;
-    FMaxAge : Integer;
-    FCrossOriginIsolation : Boolean;
-    procedure AddProxy(const aProxyDef: String);
-    procedure ApplyCoi(Sender: TObject; aResponse: TResponse);
-    procedure DoEcho(ARequest: TRequest; AResponse: TResponse);
-    procedure DoProxyLog(Sender: TObject; const aMethod, aLocation, aFromURL, aToURL: String);
-    procedure DoQuit(ARequest: TRequest; AResponse: TResponse);
-    procedure LoadMimeTypes;
-    procedure ProcessOptions;
-    procedure ReadConfigFile(const ConfigFile: string);
-    {$IFNDEF VER3_2}
-    procedure SetupCapture;
-    Procedure RegisterCustomHeaders;
-    {$ENDIF}
-    procedure Usage(Msg: String);
-    procedure Writeinfo;
-    procedure RegisterFileLocations;
-    Procedure RegisterProxies;
-  Public
-    constructor create(aOwner : TComponent); override;
-    Destructor Destroy; override;
-  published
-    procedure DoLog(EventType: TEventType; const Msg: String); override;
-    Procedure DoRun; override;
-    property Quiet : Boolean read FQuiet Write FQuiet;
-    Property MimeFile : String Read FMimeFile Write FMimeFile;
-    Property BaseDir : string Read FBaseDir Write FBaseDir;
-    Property NoIndexPage : Boolean Read FNoIndexPage Write FNoIndexPage;
-    Property IndexPageName : String Read FIndexPageName Write FIndexPageName;
-    Property InterfaceAddress : String Read FInterfaceAddress Write FInterfaceAddress;
-  end;
+  THTTPApplication = Class(TFPSimpleServerApplication);
 
 Var
   Application : THTTPApplication;
 
-{$IFDEF VER3_2}
-{ TMySimpleFileModule }
-
-constructor TMySimpleFileModule.CreateNew(AOwner: TComponent; CreateMode: Integer);
-begin
-  inherited CreateNew(AOwner, CreateMode);
-end;
-
-procedure TMySimpleFileModule.SendFile(const AFileName: String; AResponse: TResponse);
-begin
-  AResponse.SetCustomHeader('Cross-Origin-Embedder-Policy','require-corp');
-  AResponse.SetCustomHeader('Cross-Origin-Opener-Policy','same-origin');
-  inherited SendFile(AFileName, AResponse);
-end;
-{$ENDIF}
-
-{ THTTPApplication }
-constructor THTTPApplication.create(aOwner : TComponent);
-
-begin
-  Inherited;
-  FProxyDefs:=TStringList.Create;
-  FLocations:=TStringList.Create;
-  FHeaders:=TStringList.Create;
-end;
-
-procedure THTTPApplication.ApplyCoi(Sender : TObject; aResponse : TResponse);
-
-begin
-  AResponse.SetCustomHeader('Cross-Origin-Embedder-Policy','require-corp');
-  AResponse.SetCustomHeader('Cross-Origin-Opener-Policy','same-origin');
-end;
-
-procedure THTTPApplication.DoEcho(ARequest: TRequest; AResponse: TResponse);
-
-Var
-  L : TStrings;
-
-begin
-  L:=TStringList.Create;
-  try
-    L.AddStrings(['<!doctype html>',
-      '<html>',
-      '<head>',
-      '<title>Echo request</title>',
-      '</head>',
-      '<body>'
-    ]);
-    DumpRequest(aRequest,L);
-    L.AddStrings(['</body>','</html>']);
-    AResponse.Content:=L.Text;
-    AResponse.ContentLength:=Length(AResponse.Content);
-    AResponse.SendResponse;
-  finally
-    L.Free;
-  end;
-end;
-
-
-procedure THTTPApplication.DoQuit(ARequest: TRequest; AResponse: TResponse);
-
-Var
-  PWD : String;
-
-begin
-  PWD:=ARequest.QueryFields.Values['password'];
-  if PWD='' then
-    ARequest.ContentFields.Values['password'];
-  if PWD=FPassword then
-    begin
-    AResponse.Content:='OK';
-    AResponse.SendContent;
-    Terminate;
-    end
-  else
-    begin
-    AResponse.Code:=403;
-    AResponse.CodeText:='Forbidden';
-    AResponse.SendContent;
-    end;
-end;
-
-procedure THTTPApplication.DoLog(EventType: TEventType; const Msg: String);
-begin
-  if Quiet then
-    exit;
-  if IsConsole then
-    Writeln(FormatDateTime('yyyy-mm-dd hh:nn:ss.zzz',Now),' [',EventType,'] ',Msg)
-  else
-    inherited DoLog(EventType, Msg);
-end;
-
-procedure THTTPApplication.DoProxyLog(Sender: TObject; const aMethod, aLocation, aFromURL, aToURL: String);
-
-Var
-  Msg : String;
-
-begin
-  if Quiet then
-    exit;
-  Msg:=Format('(Proxy redirect) location: %s, Method: %s, From: %s, to: %s.',[aLocation,aMethod,aFromURl,atoURL]);
-  if IsConsole then
-    Writeln(FormatDateTime('yyyy-mm-dd hh:nn:ss.zzz',Now),' [',etInfo,'] ',Msg)
-  else
-    inherited DoLog(etInfo, Msg);
-end;
-
-procedure THTTPApplication.Usage(Msg : String);
-
-begin
-  if (Msg<>'') then
-    Writeln('Error: ',Msg);
-  Writeln('Usage ',ExtractFileName(ParamStr(0)),' [options] ');
-  Writeln('Version : ',ServerVersion);
-  Writeln('Where options is one or more of : ');
-  Writeln('-A --api=path,secret  Activate location API on path, using secret as accepted bearer token.');
-  Writeln('-a --max-age=age      Set max-age expiry header on returned file requests.');
-  Writeln('-c --config=file      Ini configuration file (default: simpleserver.ini).');
-{$ifdef unix}
-  Writeln('-b --background       fork to background.');
-{$endif}
-  Writeln('-d --directory=dir    Base directory from which to serve files.');
-  Writeln('                      Default is current working directory: ',GetCurrentDir);
-  Writeln('-e --echo             Activate /echo URL.');
-  Writeln('-h --help             This help text.');
-  Writeln('-H --hostname=NAME    Set hostname for self-signed SSL certificate.');
-  Writeln('-i --indexpage=name   Directory index page to use (default: index.html)/');
-  Writeln('-I --interface=IP     Listen on this interface address only.');
-  Writeln('-m --mimetypes=file   Path of mime.types. Loaded in addition to OS known types.');
-  Writeln('-n --noindexpage      Do not allow index page.');
-  Writeln('-o --coi              Enable Cross-Origin Isolation headers.');
-  Writeln('-p --port=NNNN        TCP/IP port to listen on (default is 3000).');
-  Writeln('-q --quiet            Do not write diagnostic messages.');
-  Writeln('-Q --quit=PWD         Register /quit URL. Send request with password variable equal to PWD to stop.');
-  Writeln('-s --ssl              Use SSL.');
-  {$IFNDEF VER3_2}
-  Writeln('-u --capture[=FILE]   Set up /debugcapture route to capture output sent by browser.');
-  Writeln('                      If FILE is specified, write to file. If not specified, writes to STDOUT.');
-  {$ENDIF}
-  Writeln('-V --version          Display server version and exit.');         
-  Writeln('-x --proxy=proxydef   Add proxy definition. Definition is of form:');
-  Writeln('                      name:BaseURL');
-  Writeln('');
-  Writeln('Config file is ini file, section [Server]. Key names are long option names.');
-  Writeln('Proxies are defined in section [Proxy], Key is name, value is URL.');
-  Writeln('Locations are defined in section [Locations], Key is location name, value is path.');
-  Halt(Ord(Msg<>''));
-end;
-
-procedure THTTPApplication.LoadMimeTypes;
-
-begin
-  MimeTypes.LoadKnownTypes;
-  if MimeFile<>'' then
-    begin
-    MimeTypesFile:=MimeFile;
-    if (MimeTypesFile<>'') and not FileExists(MimeTypesFile) then
-      begin
-      Log(etWarning,'mimetypes file not found: '+MimeTypesFile);
-      MimeTypesFile:='';
-      end;
-    end;
-  If MimeTypesFile<>'' then
-    MimeTypes.LoadFromFile(MimeTypesFile);  
-end;
-
-procedure THTTPApplication.AddProxy(const aProxyDef: String);
-
-Var
-  P : Integer;
-  N,URL : String;
-
-begin
-  P:=Pos(':',aProxyDef);
-  If P=0 then Raise
-    EHTTP.CreateFmt('Invalid proxy definition: %s',[aProxyDef]);
-  N:=Copy(aProxyDef,1,P-1);
-  URL:=Copy(aProxyDef,P+1,Length(aProxyDef));
-  ProxyManager.RegisterLocation(N,URL).AppendPathInfo:=True;
-end;
-
-Const
-  SCaptureRoute = '/debugcapture';
-
-Const
-  SConfig  = 'Server';
-  SProxy = 'Proxy';
-  SLocations = 'Locations';
-  SHeaders = 'Headers';
-
-  KeyPort  = 'Port';
-  KeyInterface = 'Interface';
-  KeyDir   = 'Directory';
-  KeyIndexPage = 'IndexPage';
-  KeyHostName = 'hostname';
-  keyMimetypes = 'mimetypes';
-  KeySSL = 'SSL';
-  KeyQuiet = 'quiet';
-  KeyQuit = 'quit';
-  KeyEcho = 'echo';
-  KeyNoIndexPage = 'noindexpage';
-  KeyBackground = 'background';
-  KeyMaxAge = 'MaxAge';
-  KeyAPI = 'API';
-  KeyCOI = 'CrossOriginIsolation';
-  KeyCapture = 'DebugCapture';
-
-procedure THTTPApplication.ReadConfigFile(const ConfigFile: string);
-
-begin
-  if (ConfigFile='') or Not FileExists(ConfigFile) then exit;
-  With TMemIniFile.Create(ConfigFile) do
-    try
-      BaseDir:=ReadString(SConfig,KeyDir,BaseDir);
-      Port:=ReadInteger(SConfig,KeyPort,Port);
-      InterfaceAddress:=ReadString(SConfig,KeyInterface,InterfaceAddress);
-      Quiet:=ReadBool(SConfig,KeyQuiet,Quiet);
-      MimeFile:=ReadString(SConfig,keyMimetypes,MimeFile);
-      NoIndexPage:=ReadBool(SConfig,KeyNoIndexPage,NoIndexPage);
-      IndexPageName:=ReadString(SConfig,KeyIndexPage,IndexPageName);
-      HostName:=ReadString(SConfig,KeyHostName,HostName);
-      UseSSL:=ReadBool(SConfig,KeySSL,UseSSL);
-      FBackground:=ReadBool(SConfig,Keybackground,FBackGround);
-      FPassword:=ReadString(SConfig,KeyQuit,FPassword);
-      FEcho:=ReadBool(SConfig,KeyEcho,FEcho);
-      FMaxAge:=ReadInteger(SConfig,KeyMaxAge,FMaxAge);
-      FAPISecret:=ReadString(SConfig,KeyAPI,'');
-      FCrossOriginIsolation:=ReadBool(SConfig,KeyCOI,FCrossOriginIsolation);
-      {$IFNDEF VER3_2}
-      if ValueExists(SConfig,KeyCapture) then
-        begin
-        TDebugCaptureService.Instance.LogFileName:=ReadString(SConfig,keyCapture,'');
-        end;
-      {$ENDIF}
-      ReadSectionValues(SProxy,FProxyDefs,[]);
-      ReadSectionValues(SLocations,FLocations,[]);
-      ReadSectionValues(SHeaders,FHeaders,[]);
-    finally
-      Free;
-    end;
-end;
-
-procedure THTTPApplication.RegisterProxies;
-
-var
-  I : integer;
-  Proxy,URL : String;
-  
-begin
-  For I:=0 to FProxyDefs.Count-1 do
-    begin
-    FProxyDefs.GetNameValue(I,Proxy,Url);
-    if (Proxy<>'') and (Url<>'') then
-      ProxyManager.RegisterLocation(Proxy,Url).AppendPathInfo:=true;
-    end;
-end;
-
-{$IFNDEF VER3_2}
-procedure THTTPApplication.RegisterCustomHeaders;
-var
-  I : integer;
-  lName,lValue : String;
-
-begin
-  For I:=0 to FLocations.Count-1 do
-    begin
-    FLocations.GetNameValue(I,lName,lValue);
-    if (lName<>'') and (lValue<>'') then
-      TFPCustomFileModule.RegisterGlobalResponseHeader(lName,lValue);
-    end;
-end;
-
-procedure THTTPApplication.SetupCapture;
-
-Var
-  Dest : String;
-  Svc : TDebugCaptureService;
-
-begin
-  Svc:=TDebugCaptureService.Instance;
-  Dest:=Svc.LogFileName;
-  if (Dest='') and Svc.LogToConsole then
-    Dest:='Console';
-  if Dest<>'' then
-    begin
-    DoLog(etInfo,Format('Setting up capture on route "%s", writing to: %s',[SCaptureRoute,Dest]));
-    HTTPRouter.RegisterRoute(SCaptureRoute,rmPost,@Svc.HandleRequest,False);
-    end;
-end;
-{$ENDIF}
-
-procedure THTTPApplication.RegisterFileLocations;
-
-var
-  I : integer;
-  loc,Dir : String;
-  
-begin
-  For I:=0 to FLocations.Count-1 do
-    begin
-    FLocations.GetNameValue(I,Loc,Dir);
-    if (Loc<>'') and (Dir<>'') then
-      RegisterFileLocation(Loc,Dir);
-    end;
-end;        
-
-procedure THTTPApplication.ProcessOptions;
-
-  procedure HasGetOptionValue(var aValue: string; Const C: Char; Const S : String);
-  begin
-    if HasOption(C,S) then
-      aValue:=GetOptionValue(C,S);
-  end;
-
-Var
-  S : String;
-
-begin
-  for S in GetOptionValues('x','proxy') do
-    AddProxy(S);
-  HasGetOptionValue(FAPISecret,'A','api');
-  if HasOption('e','echo') then
-    FEcho:=true;
-  if HasOption('q','quiet') then
-    Quiet:=true;
-  HasGetOptionValue(FPassword,'Q','quit');
-  if HasOption('p','port') then
-    Port:=StrToIntDef(GetOptionValue('p','port'),Port);
-  LoadMimeTypes;
-  if HasOption('d','directory') then
-    BaseDir:=GetOptionValue('d','directory');
-  UseSSL:=HasOption('s','ssl');
-  if HasOption('H','hostname') then
-    HostName:=GetOptionValue('H','hostname');
-  if HasOption('n','noindexpage') then
-    NoIndexPage:=True
-  else
-    IndexPageName:=GetOptionValue('i','indexpage');
-  if HasOption('I','interface') then
-    InterfaceAddress:=GetOptionValue('I','interface');
-  if HasOption('a','max-age') then
-    FMaxAge:=StrToIntDef(GetOptionValue('a','max-age'),FMaxAge);
-  if HasOption('b','background') then
-    FBackground:=true;
-  if hasOption('o','coi') then
-    FCrossOriginIsolation:=true;
-  {$IFNDEF VER3_2}
-  if HasOption('u','capture') then
-    begin
-    S:=GetOptionValue('u','capture');
-    if S='' then
-      TDebugCaptureService.Instance.LogToConsole:=True
-    else
-      TDebugCaptureService.Instance.LogFileName:=S;
-    end;
-  {$ENDIF}
-end;
-
-procedure THTTPApplication.Writeinfo;
-
-  function BtoS(B : Boolean) : string;
-
-  begin
-    Result:=BoolToStr(B,'True','False');
-  end;
-
-Var
-  I : Integer;
-  Base,N,V : String;
-
-begin
-  Log(etInfo,'Listening on port %d',[Port]);
-  Log(etInfo,'Serving files from directory: %s',[BaseDir]);
-  For I:=0 to ProxyManager.LocationCount-1 do
-    with ProxyManager.Locations[i] do
-      Log(etInfo,'Proxy location /proxy/%s redirects to: %s',[Path,URL]);
-  For I:=0 to FLocations.Count-1 do
-    begin
-    FLocations.GetNameValue(I,N,V);
-    Log(etInfo,'Enabled file location "%s", serving from: %s',[N,V]);
-    end;
-  Log(etInfo,'Enabled index page: %s',[BToS(NoIndexPage)]);
-  if not NoIndexPage then
-    Log(etInfo,'Index page name: %s',[IndexPageName]);
-  Log(etInfo,'Enabled SSL: %s',[BtoS(UseSSL)]);
-  Log(etInfo,'Enabled COI/CORP: %s',[BToS(FCrossOriginIsolation)]);
-  Log(etInfo,'Enabled /quit route: %s',[BtoS(Self.FPassword<>'')]);
-  Log(etInfo,'Enabled /echo route: %s',[BtoS(FEcho)]);
-  Log(etInfo,'Enabled location REST API: %s',[BtoS(FAPISecret<>'')]);
-  Base:='http'+IfThen(UseSSL,'s','')+'://localhost:'+IntToStr(Port)+'/';
-  Log(etInfo,'Navigate to: %s',[Base]);
-  For I:=0 to FLocations.Count-1 do
-    begin
-    FLocations.GetNameValue(I,N,V);
-    Log(etInfo,'Navigate to location "%s" at: %s/',[N,Base+N]);
-    end;
-end;
-
-destructor THTTPApplication.Destroy;
-begin
-  inherited Destroy;
-end;
-
-procedure THTTPApplication.DoRun;
-
-Var
-  S,ConfigFile : String;
-
-begin
-  FMaxAge:=31557600;
-  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']);
-  if (S<>'') or HasOption('h','help') then
-    usage(S);
-  if HasOption('V','version') then
-    begin
-    Terminate;
-    Writeln(ServerVersion);
-    Exit;
-    end;
-  if HasOption('c','config') then
-    ConfigFile:=GetOptionValue('c','config')
-  else
-    ConfigFile:='simpleserver.ini';
-  ReadConfigFile(ConfigFile);
-  ProcessOptions;
-  if FBackground then
-    begin
-{$ifdef unix}
-    if FPFork>0 then Halt(0);
-{$else}
-    Log(etError,'Background option not supported.');
-{$endif}
-    end;
-  {$IFNDEF VER3_2}
-  SetupCapture;
-  {$ENDIF}
-  if FPassword<>'' then
-    begin
-    HTTPRouter.RegisterRoute('/quit',rmAll,@Doquit,False);
-    end;
-  if FEcho  then
-    HTTPRouter.RegisterRoute('/echo',rmAll,@DoEcho,False);
-  if ProxyManager.LocationCount>0 then
-    begin
-    TProxyWebModule.RegisterModule('Proxy',True);
-    ProxyManager.OnLog:=@DoProxyLog;
-    end;
-  DefaultCacheControlMaxAge:=FMaxAge; // one year by default
-  if BaseDir='' then
-    BaseDir:=GetCurrentDir;
-  if (BaseDir<>'') then
-    BaseDir:=IncludeTrailingPathDelimiter(BaseDir);
-  {$IFNDEF VER3_2_2}
-  if FAPISecret<>'' then
-    TFPWebFileLocationAPIModule.RegisterFileLocationAPI(ExtractWord(1,FAPISecret,[',']),ExtractWord(2,FAPISecret,[',']));
-  {$ENDIF}
-  if FCrossOriginIsolation then  
-    begin
-    {$IFDEF VER3_2_2}
-    DefaultFileModuleClass:=TMySimpleFileModule;
-    {$ELSE}
-    TFPCustomFileModule.OnPrepareResponse:=@ApplyCoi;
-    {$ENDIF}
-    end;
-  RegisterProxies;
-  RegisterFileLocations;
-  {$IFNDEF VER_3_2}
-  RegisterCustomHeaders;
-  {$ENDIF}
-  TSimpleFileModule.RegisterDefaultRoute;
-  TSimpleFileModule.BaseDir:=BaseDir;
-  TSimpleFileModule.OnLog:=@Log;
-  If not NoIndexPage then
-    begin
-    if (IndexPageName='') then
-      IndexPageName:='index.html';
-    TSimpleFileModule.IndexPageName:=IndexPageName;
-    end;
-  if not Quiet then
-    WriteInfo;
-  if InterfaceAddress<>'' then
-    HTTPHandler.Address:=InterfaceAddress;
-  inherited;
-end;
-
 begin
   Application:=THTTPApplication.Create(Nil);
   Application.Initialize;

+ 18 - 1
packages/fcl-web/fpmake.pp

@@ -22,6 +22,8 @@ Const
 Var
   T : TTarget;
   P : TPackage;
+  lOSes : TOSes;
+  
 begin
   With Installer do
     begin
@@ -561,7 +563,22 @@ begin
       AddUnit('fpfcmstrings');
       AddUnit('fpfcmtypes');
       end;
-      
+    T:=P.Targets.AddUnit('fpsimpleserver.pp');
+    With T.Dependencies do
+      begin
+      AddUnit('httpdefs');
+      AddUnit('httproute');
+      AddUnit('fpwebfile');
+      AddUnit('fpwebproxy');
+      AddUnit('webutil');
+      AddUnit('fpdebugcapturesvc');
+      AddUnit('custhttpapp');
+      lOSes := LibMicroHttpdOSes;
+      if Defaults.CPU=jvm then
+        lOSes := lOSes - [java,android];
+      AddUnit('custmicrohttpapp',lOSes);
+      end;
+        
 end;
     
 {$ifndef ALLPACKAGES}

+ 1 - 0
packages/fcl-web/namespaces.lst

@@ -20,6 +20,7 @@ src/base/iniwebsession.pp=namespaced/FpWeb.Session.Ini.pp
 src/base/httpprotocol.pp=namespaced/FpWeb.Http.Protocol.pp
 src/base/custapache24.pp=namespaced/FpWeb.HostApp.Custom.Apache24.pp
 src/base/custmicrohttpapp.pp=namespaced/FpWeb.HostApp.Custom.MicroHttpApp.pp
+src/base/fpsimpleserver.pp=namespaced/FpWeb.HostApp.Simple.pp
 src/base/fpcgi.pp=namespaced/FpWeb.HostApp.Cgi.pp
 src/base/fpdatasetform.pp=namespaced/FpWeb.DatasetForm.pp
 src/base/fpwebproxy.pp=namespaced/FpWeb.Modules.Proxy.pp

+ 716 - 0
packages/fcl-web/src/base/fpsimpleserver.pp

@@ -0,0 +1,716 @@
+{$IFNDEF FPC_DOTTEDUNITS}
+unit fpsimpleserver;
+{$ENDIF}
+{
+    This file is part of the Free Pascal run time library.
+    Copyright (c) 2019 by the Free Pascal development team
+
+    Sample HTTP server application
+
+    See the file COPYING.FPC, included in this distribution,
+    for details about the copyright.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ **********************************************************************}
+
+{$mode ObjFPC}{$H+}
+{ $DEFINE USEMICROHTTP }
+
+interface
+
+uses
+{$IFDEF FPC_DOTTEDUNITS}
+  System.SysUtils, System.Classes, System.Types, System.StrUtils, System.IniFiles, FpJson.Parser,
+  {$ifdef unix}
+  UnixApi.Base,
+  {$endif}
+  {$IFNDEF VER3_2}
+  FpWeb.Http.DebugCapture,
+  {$ENDIF}
+  {$IFNDEF USEMICROHTTP}
+    FpWeb.HostApp.Custom.HttpApp,
+  {$ELSE}
+    FpWeb.HostApp.Custom.MicroHttpApp,
+  {$ENDIF}
+  FpWeb.Route, FpWeb.Http.Defs, FpWeb.MimeTypes, FpWeb.Modules.Files, FpWeb.Modules.Proxy,
+  FpWeb.Utils;
+{$ELSE}
+  SysUtils, Classes,
+  {$ifdef unix}
+  baseunix,
+  {$endif}
+  {$IFNDEF VER3_2}
+  fpdebugcapturesvc,
+  {$ENDIF}
+  {$IFNDEF USEMICROHTTP}
+    custhttpapp,
+  {$ELSE}
+    custmicrohttpapp,
+  {$ENDIF}
+  types, jsonparser, strutils, inifiles, httproute, httpdefs, fpmimetypes, fpwebfile, fpwebproxy,
+  webutil;
+{$ENDIF}
+Const
+  ServerVersion = '1.1';
+
+Type
+
+  { TFPSimpleServerApplication }
+{$IFDEF USEMICROHTTP}
+  TParentApp = TCustomMicroHTTPApplication;
+{$ELSE}
+  TParentApp = TCustomHTTPApplication;
+{$ENDIF}
+
+{$IFDEF VER3_2}
+  { TMySimpleFileModule }
+  TMySimpleFileModule = class(TFPCustomFileModule)
+  Public
+    Constructor CreateNew(AOwner: TComponent; CreateMode: Integer); override;
+    Procedure SendFile(const AFileName: String; AResponse: TResponse); override;
+  end;
+{$ENDIF}
+
+  TFPSimpleServerApplication = Class(TParentApp)
+  private
+    FProxyDefs : TStrings;
+    FLocations : TStrings;
+    FHeaders: TStrings;
+    FAPISecret : String;
+    FBaseDir: string;
+    FIndexPageName: String;
+    FInterfaceAddress: String;
+    FMimeFile: String;
+    FNoIndexPage: Boolean;
+    FQuiet: Boolean;
+    FBackground : Boolean;
+    FPassword : string;
+    FEcho : Boolean;
+    FMaxAge : Integer;
+    FCrossOriginIsolation : Boolean;
+    procedure DoProxyLog(Sender: TObject; const aMethod, aLocation, aFromURL, aToURL: String);
+  Protected
+    // Log
+    procedure DoLog(EventType: TEventType; const Msg: String); override;
+    // Override doRun to run server
+    Procedure DoRun; override;
+    // Configure server. Called before registering routes.
+    Procedure ConfigureServer; virtual;
+    // Register all HTTP routes
+    Procedure RegisterRoutes; virtual;
+    {$IFNDEF VER3_2}
+    // Set up debug capture
+    procedure SetupCapture; virtual;
+    // Register custom headers
+    Procedure RegisterCustomHeaders; virtual;
+    {$ENDIF}
+    // Apply COI/CORP headers to file request
+    procedure ApplyCoi(Sender: TObject; aResponse: TResponse); virtual;
+    // Handle echo URL
+    procedure DoEcho(ARequest: TRequest; AResponse: TResponse); virtual;
+    // Handle Quit URL
+    procedure DoQuit(ARequest: TRequest; AResponse: TResponse); virtual;
+    // Get valid command-line options
+    procedure GetValidOptions(out aShort: String; out aLong: TStringDynArray); virtual;
+    // Load mime types
+    procedure LoadMimeTypes; virtual;
+    // Process command-line options
+    procedure ProcessOptions; virtual;
+    // Write usage to console;
+    procedure WriteOptions; virtual;
+    // Read config file.
+    procedure DoReadConfigFile(const aIni : TCustomIniFile); virtual;
+    // Write run info to console
+    procedure Writeinfo; virtual;
+    // Register file locatons
+    procedure RegisterFileLocations; virtual;
+    // Register proxy
+    Procedure RegisterProxies; virtual;
+    // Get name of default config file
+    Function GetDefaultConfigFile : string; virtual;
+  Public
+    constructor create(aOwner : TComponent); override;
+    Destructor Destroy; override;
+    // Write usage and halt application. If msg is non-empty halt application with exit code 1
+    procedure Usage(Msg: String);
+    // Add a proxy definition
+    procedure AddProxy(const aProxyDef: String);
+    // Read config ini file
+    procedure ReadConfigFile(const ConfigFile: string); virtual;
+    {$IFDEF UNIX}
+    Property Background : Boolean Read FBackGround Write FBackGround;
+    {$ENDIF}
+    // Emit CORP headers
+    Property CrossOriginIsolation : Boolean Read FCrossOriginIsolation Write FCrossOriginIsolation;
+    // List of proxy definitions: /proxy/:Location=URL
+    Property ProxyDefs : TStrings Read FProxyDefs;
+    // List of file location definitions :Location=URL
+    Property Locations : TStrings Read FLocations;
+    // Extra headers to add during requests
+    Property Headers : Tstrings Read FHeaders;
+    // Enable echo response
+    Property Echo : Boolean Read FEcho Write FEcho;
+    // Do not log
+    property Quiet : Boolean read FQuiet Write FQuiet;
+    // MaxAge on files
+    Property MaxAge : Integer Read FMaxAge Write FMaxAge;
+    // Mime file to use for determining MIME types (content-type)
+    Property MimeFile : String Read FMimeFile Write FMimeFile;
+    // Base directory to serve files from
+    Property BaseDir : string Read FBaseDir Write FBaseDir;
+    // Password to use for locations API
+    Property APISecret : String Read FAPISecret Write FAPISecret;
+    // Password to use for /quit URL
+    Property Password : String Read FPassword Write FPassword;
+    // Do not use index page
+    Property NoIndexPage : Boolean Read FNoIndexPage Write FNoIndexPage;
+    // Name of index page
+    Property IndexPageName : String Read FIndexPageName Write FIndexPageName;
+    // Listen only on this interface address.
+    Property InterfaceAddress : String Read FInterfaceAddress Write FInterfaceAddress;
+  end;
+
+implementation
+
+Const
+  SCaptureRoute = '/debugcapture';
+
+Const
+  SConfig  = 'Server';
+  SProxy = 'Proxy';
+  SLocations = 'Locations';
+  SHeaders = 'Headers';
+
+  KeyPort  = 'Port';
+  KeyInterface = 'Interface';
+  KeyDir   = 'Directory';
+  KeyIndexPage = 'IndexPage';
+  KeyHostName = 'hostname';
+  keyMimetypes = 'mimetypes';
+  KeySSL = 'SSL';
+  KeyQuiet = 'quiet';
+  KeyQuit = 'quit';
+  KeyEcho = 'echo';
+  KeyNoIndexPage = 'noindexpage';
+  KeyBackground = 'background';
+  KeyMaxAge = 'MaxAge';
+  KeyAPI = 'API';
+  KeyCOI = 'CrossOriginIsolation';
+  KeyCapture = 'DebugCapture';
+
+
+{$IFDEF VER3_2}
+{ TMySimpleFileModule }
+
+constructor TMySimpleFileModule.CreateNew(AOwner: TComponent; CreateMode: Integer);
+begin
+  inherited CreateNew(AOwner, CreateMode);
+end;
+
+procedure TMySimpleFileModule.SendFile(const AFileName: String; AResponse: TResponse);
+begin
+  AResponse.SetCustomHeader('Cross-Origin-Embedder-Policy','require-corp');
+  AResponse.SetCustomHeader('Cross-Origin-Opener-Policy','same-origin');
+  inherited SendFile(AFileName, AResponse);
+end;
+{$ENDIF}
+
+
+{ THTTPApplication }
+constructor TFPSimpleServerApplication.create(aOwner : TComponent);
+
+begin
+  Inherited;
+  FProxyDefs:=TStringList.Create;
+  FLocations:=TStringList.Create;
+  FHeaders:=TStringList.Create;
+end;
+
+destructor TFPSimpleServerApplication.Destroy;
+begin
+  FreeAndNil(FProxyDefs);
+  FreeAndNil(FLocations);
+  FreeAndNil(FHeaders);
+  inherited Destroy;
+end;
+
+
+procedure TFPSimpleServerApplication.ApplyCoi(Sender : TObject; aResponse : TResponse);
+
+begin
+  AResponse.SetCustomHeader('Cross-Origin-Embedder-Policy','require-corp');
+  AResponse.SetCustomHeader('Cross-Origin-Opener-Policy','same-origin');
+end;
+
+procedure TFPSimpleServerApplication.DoEcho(ARequest: TRequest; AResponse: TResponse);
+
+Var
+  L : TStrings;
+
+begin
+  L:=TStringList.Create;
+  try
+    L.AddStrings(['<!doctype html>',
+      '<html>',
+      '<head>',
+      '<title>Echo request</title>',
+      '</head>',
+      '<body>'
+    ]);
+    DumpRequest(aRequest,L);
+    L.AddStrings(['</body>','</html>']);
+    AResponse.Content:=L.Text;
+    AResponse.ContentLength:=Length(AResponse.Content);
+    AResponse.SendResponse;
+  finally
+    L.Free;
+  end;
+end;
+
+
+procedure TFPSimpleServerApplication.DoQuit(ARequest: TRequest; AResponse: TResponse);
+
+Var
+  PWD : String;
+
+begin
+  PWD:=ARequest.QueryFields.Values['password'];
+  if PWD='' then
+    ARequest.ContentFields.Values['password'];
+  if PWD=FPassword then
+    begin
+    AResponse.Content:='OK';
+    AResponse.SendContent;
+    Terminate;
+    end
+  else
+    begin
+    AResponse.Code:=403;
+    AResponse.CodeText:='Forbidden';
+    AResponse.SendContent;
+    end;
+end;
+
+procedure TFPSimpleServerApplication.DoLog(EventType: TEventType; const Msg: String);
+begin
+  if Quiet then
+    exit;
+  if IsConsole then
+    Writeln(FormatDateTime('yyyy-mm-dd hh:nn:ss.zzz',Now),' [',EventType,'] ',Msg)
+  else
+    inherited DoLog(EventType, Msg);
+end;
+
+procedure TFPSimpleServerApplication.DoProxyLog(Sender: TObject; const aMethod, aLocation, aFromURL, aToURL: String);
+
+Var
+  Msg : String;
+
+begin
+  if Quiet then
+    exit;
+  Msg:=Format('(Proxy redirect) location: %s, Method: %s, From: %s, to: %s.',[aLocation,aMethod,aFromURl,atoURL]);
+  if IsConsole then
+    Writeln(FormatDateTime('yyyy-mm-dd hh:nn:ss.zzz',Now),' [',etInfo,'] ',Msg)
+  else
+    inherited DoLog(etInfo, Msg);
+end;
+
+procedure TFPSimpleServerApplication.Usage(Msg : String);
+
+begin
+  if (Msg<>'') then
+    Writeln('Error: ',Msg);
+  Writeln('Usage ',ExtractFileName(ParamStr(0)),' [options] ');
+  WriteOptions;
+  Writeln('');
+  Writeln('Config file is ini file, section [Server]. Key names are long option names.');
+  Writeln('Proxies are defined in section [Proxy], Key is name, value is URL.');
+  Writeln('Locations are defined in section [Locations], Key is location name, value is path.');
+  Halt(Ord(Msg<>''));
+end;
+
+procedure TFPSimpleServerApplication.LoadMimeTypes;
+
+begin
+  MimeTypes.LoadKnownTypes;
+  if MimeFile<>'' then
+    begin
+    MimeTypesFile:=MimeFile;
+    if (MimeTypesFile<>'') and not FileExists(MimeTypesFile) then
+      begin
+      Log(etWarning,'mimetypes file not found: '+MimeTypesFile);
+      MimeTypesFile:='';
+      end;
+    end;
+  If MimeTypesFile<>'' then
+    MimeTypes.LoadFromFile(MimeTypesFile);
+end;
+
+procedure TFPSimpleServerApplication.AddProxy(const aProxyDef: String);
+
+Var
+  P : Integer;
+  N,URL : String;
+
+begin
+  P:=Pos(':',aProxyDef);
+  If P=0 then Raise
+    EHTTP.CreateFmt('Invalid proxy definition: %s',[aProxyDef]);
+  N:=Copy(aProxyDef,1,P-1);
+  URL:=Copy(aProxyDef,P+1,Length(aProxyDef));
+  ProxyManager.RegisterLocation(N,URL).AppendPathInfo:=True;
+end;
+
+procedure TFPSimpleServerApplication.ReadConfigFile(const ConfigFile: string);
+
+var
+   Ini : TCustomIniFile;
+begin
+  if (ConfigFile='') or Not FileExists(ConfigFile) then exit;
+  Ini:=TMemIniFile.Create(ConfigFile);
+  try
+    DoReadConfigFile(Ini);
+  finally
+    Ini.free;
+  end;
+end;
+
+procedure TFPSimpleServerApplication.RegisterProxies;
+
+var
+  I : integer;
+  Proxy,URL : String;
+
+begin
+  For I:=0 to FProxyDefs.Count-1 do
+    begin
+    FProxyDefs.GetNameValue(I,Proxy,Url);
+    if (Proxy<>'') and (Url<>'') then
+      ProxyManager.RegisterLocation(Proxy,Url).AppendPathInfo:=true;
+    end;
+end;
+
+function TFPSimpleServerApplication.GetDefaultConfigFile: string;
+begin
+  Result:='simpleserver.ini';
+end;
+
+{$IFNDEF VER3_2}
+procedure TFPSimpleServerApplication.RegisterCustomHeaders;
+var
+  I : integer;
+  lName,lValue : String;
+
+begin
+  For I:=0 to FHeaders.Count-1 do
+    begin
+    FHeaders.GetNameValue(I,lName,lValue);
+    if (lName<>'') and (lValue<>'') then
+      TFPCustomFileModule.RegisterGlobalResponseHeader(lName,lValue);
+    end;
+end;
+
+procedure TFPSimpleServerApplication.SetupCapture;
+
+Var
+  Dest : String;
+  Svc : TDebugCaptureService;
+
+begin
+  Svc:=TDebugCaptureService.Instance;
+  Dest:=Svc.LogFileName;
+  if (Dest='') and Svc.LogToConsole then
+    Dest:='Console';
+  if Dest<>'' then
+    begin
+    DoLog(etInfo,Format('Setting up capture on route "%s", writing to: %s',[SCaptureRoute,Dest]));
+    HTTPRouter.RegisterRoute(SCaptureRoute,rmPost,@Svc.HandleRequest,False);
+    end;
+end;
+{$ENDIF}
+
+procedure TFPSimpleServerApplication.RegisterFileLocations;
+
+var
+  I : integer;
+  loc,Dir : String;
+
+begin
+  For I:=0 to FLocations.Count-1 do
+    begin
+    FLocations.GetNameValue(I,Loc,Dir);
+    if (Loc<>'') and (Dir<>'') then
+      RegisterFileLocation(Loc,Dir);
+    end;
+end;
+
+procedure TFPSimpleServerApplication.ProcessOptions;
+
+  procedure HasGetOptionValue(var aValue: string; Const C: Char; Const S : String);
+  begin
+    if HasOption(C,S) then
+      aValue:=GetOptionValue(C,S);
+  end;
+
+Var
+  S : String;
+
+begin
+  for S in GetOptionValues('x','proxy') do
+    AddProxy(S);
+  HasGetOptionValue(FAPISecret,'A','api');
+  if HasOption('e','echo') then
+    FEcho:=true;
+  if HasOption('q','quiet') then
+    Quiet:=true;
+  HasGetOptionValue(FPassword,'Q','quit');
+  if HasOption('p','port') then
+    Port:=StrToIntDef(GetOptionValue('p','port'),Port);
+  LoadMimeTypes;
+  if HasOption('d','directory') then
+    BaseDir:=GetOptionValue('d','directory');
+  UseSSL:=HasOption('s','ssl');
+  if HasOption('H','hostname') then
+    HostName:=GetOptionValue('H','hostname');
+  if HasOption('n','noindexpage') then
+    NoIndexPage:=True
+  else
+    IndexPageName:=GetOptionValue('i','indexpage');
+  if HasOption('I','interface') then
+    InterfaceAddress:=GetOptionValue('I','interface');
+  if HasOption('a','max-age') then
+    FMaxAge:=StrToIntDef(GetOptionValue('a','max-age'),FMaxAge);
+  if HasOption('b','background') then
+    FBackground:=true;
+  if hasOption('o','coi') then
+    FCrossOriginIsolation:=true;
+  {$IFNDEF VER3_2}
+  if HasOption('u','capture') then
+    begin
+    S:=GetOptionValue('u','capture');
+    if S='' then
+      TDebugCaptureService.Instance.LogToConsole:=True
+    else
+      TDebugCaptureService.Instance.LogFileName:=S;
+    end;
+  {$ENDIF}
+end;
+
+procedure TFPSimpleServerApplication.WriteOptions;
+begin
+  Writeln('Version : ',ServerVersion);
+  Writeln('Where options is one or more of : ');
+  Writeln('-A --api=path,secret  Activate location API on path, using secret as accepted bearer token.');
+  Writeln('-a --max-age=age      Set max-age expiry header on returned file requests.');
+  Writeln('-c --config=file      Ini configuration file (default: simpleserver.ini).');
+{$ifdef unix}
+  Writeln('-b --background       fork to background.');
+{$endif}
+  Writeln('-d --directory=dir    Base directory from which to serve files.');
+  Writeln('                      Default is current working directory: ',GetCurrentDir);
+  Writeln('-e --echo             Activate /echo URL.');
+  Writeln('-h --help             This help text.');
+  Writeln('-H --hostname=NAME    Set hostname for self-signed SSL certificate.');
+  Writeln('-i --indexpage=name   Directory index page to use (default: index.html)/');
+  Writeln('-I --interface=IP     Listen on this interface address only.');
+  Writeln('-m --mimetypes=file   Path of mime.types. Loaded in addition to OS known types.');
+  Writeln('-n --noindexpage      Do not allow index page.');
+  Writeln('-o --coi              Enable Cross-Origin Isolation headers.');
+  Writeln('-p --port=NNNN        TCP/IP port to listen on (default is 3000).');
+  Writeln('-q --quiet            Do not write diagnostic messages.');
+  Writeln('-Q --quit=PWD         Register /quit URL. Send request with password variable equal to PWD to stop.');
+  Writeln('-s --ssl              Use SSL.');
+  {$IFNDEF VER3_2}
+  Writeln('-u --capture[=FILE]   Set up /debugcapture route to capture output sent by browser.');
+  Writeln('                      If FILE is specified, write to file. If not specified, writes to STDOUT.');
+  {$ENDIF}
+  Writeln('-V --version          Display server version and exit.');
+  Writeln('-x --proxy=proxydef   Add proxy definition. Definition is of form:');
+  Writeln('                      name:BaseURL');
+end;
+
+procedure TFPSimpleServerApplication.DoReadConfigFile(const aIni: TCustomIniFile);
+
+begin
+  With aIni do
+    begin
+    BaseDir:=ReadString(SConfig,KeyDir,BaseDir);
+    Port:=ReadInteger(SConfig,KeyPort,Port);
+    InterfaceAddress:=ReadString(SConfig,KeyInterface,InterfaceAddress);
+    Quiet:=ReadBool(SConfig,KeyQuiet,Quiet);
+    MimeFile:=ReadString(SConfig,keyMimetypes,MimeFile);
+    NoIndexPage:=ReadBool(SConfig,KeyNoIndexPage,NoIndexPage);
+    IndexPageName:=ReadString(SConfig,KeyIndexPage,IndexPageName);
+    HostName:=ReadString(SConfig,KeyHostName,HostName);
+    UseSSL:=ReadBool(SConfig,KeySSL,UseSSL);
+    FBackground:=ReadBool(SConfig,Keybackground,FBackGround);
+    FPassword:=ReadString(SConfig,KeyQuit,FPassword);
+    FEcho:=ReadBool(SConfig,KeyEcho,FEcho);
+    FMaxAge:=ReadInteger(SConfig,KeyMaxAge,FMaxAge);
+    FAPISecret:=ReadString(SConfig,KeyAPI,'');
+    FCrossOriginIsolation:=ReadBool(SConfig,KeyCOI,FCrossOriginIsolation);
+    {$IFNDEF VER3_2}
+    if ValueExists(SConfig,KeyCapture) then
+      begin
+      TDebugCaptureService.Instance.LogFileName:=ReadString(SConfig,keyCapture,'');
+      end;
+    {$ENDIF}
+    ReadSectionValues(SProxy,FProxyDefs,[]);
+    ReadSectionValues(SLocations,FLocations,[]);
+    ReadSectionValues(SHeaders,FHeaders,[]);
+    end;
+end;
+
+procedure TFPSimpleServerApplication.Writeinfo;
+
+  function BtoS(B : Boolean) : string;
+
+  begin
+    Result:=BoolToStr(B,'True','False');
+  end;
+
+Var
+  I : Integer;
+  Base,N,V : String;
+
+begin
+  Log(etInfo,'Listening on port %d',[Port]);
+  Log(etInfo,'Serving files from directory: %s',[BaseDir]);
+  For I:=0 to ProxyManager.LocationCount-1 do
+    with ProxyManager.Locations[i] do
+      Log(etInfo,'Proxy location /proxy/%s redirects to: %s',[Path,URL]);
+  For I:=0 to FLocations.Count-1 do
+    begin
+    FLocations.GetNameValue(I,N,V);
+    Log(etInfo,'Enabled file location "%s", serving from: %s',[N,V]);
+    end;
+  Log(etInfo,'Enabled index page: %s',[BToS(NoIndexPage)]);
+  if not NoIndexPage then
+    Log(etInfo,'Index page name: %s',[IndexPageName]);
+  Log(etInfo,'Enabled SSL: %s',[BtoS(UseSSL)]);
+  Log(etInfo,'Enabled COI/CORP: %s',[BToS(FCrossOriginIsolation)]);
+  Log(etInfo,'Enabled /quit route: %s',[BtoS(Self.FPassword<>'')]);
+  Log(etInfo,'Enabled /echo route: %s',[BtoS(FEcho)]);
+  Log(etInfo,'Enabled location REST API: %s',[BtoS(FAPISecret<>'')]);
+  Base:='http'+IfThen(UseSSL,'s','')+'://localhost:'+IntToStr(Port)+'/';
+  Log(etInfo,'Navigate to: %s',[Base]);
+  For I:=0 to FLocations.Count-1 do
+    begin
+    FLocations.GetNameValue(I,N,V);
+    Log(etInfo,'Navigate to location "%s" at: %s/',[N,Base+N]);
+    end;
+end;
+
+procedure TFPSimpleServerApplication.GetValidOptions(out aShort: String; out aLong: TStringDynArray);
+
+Const
+  LongOpts : TStringDynArray =
+     ('help','quiet','noindexpage','directory:','port:','indexpage:','ssl','hostname:','mimetypes:','proxy:','config:','background','echo','quit:','max-age:','api:','coi','capture','version','interface');
+
+begin
+  aShort:='hqd:ni:p:sH:m:x:c:beQ:a:A:ou::VI';
+  aLong:=LongOpts;
+end;
+
+procedure TFPSimpleServerApplication.DoRun;
+
+Var
+  lShort : String;
+  lLong : TStringDynArray;
+  S,ConfigFile : String;
+
+begin
+  FMaxAge:=31557600;
+  GetValidOptions(lShort,lLong);
+  S:=CheckOptions(lShort,lLong);
+  if (S<>'') or HasOption('h','help') then
+    usage(S);
+  if HasOption('V','version') then
+    begin
+    Terminate;
+    Writeln(ServerVersion);
+    Exit;
+    end;
+  if HasOption('c','config') then
+    ConfigFile:=GetOptionValue('c','config')
+  else
+    ConfigFile:=GetDefaultConfigFile;
+  ReadConfigFile(ConfigFile);
+  ProcessOptions;
+  if FBackground then
+    begin
+{$ifdef unix}
+    if FPFork>0 then Halt(0);
+{$else}
+    Log(etError,'Background option not supported.');
+{$endif}
+    end;
+  // Set up
+  ConfigureServer;
+  RegisterRoutes;
+  if not Quiet then
+    WriteInfo;
+  if InterfaceAddress<>'' then
+    HTTPHandler.Address:=InterfaceAddress;
+  inherited;
+end;
+
+procedure TFPSimpleServerApplication.ConfigureServer;
+begin
+  DefaultCacheControlMaxAge:=FMaxAge; // one year by default
+  if BaseDir='' then
+    BaseDir:=GetCurrentDir;
+  if (BaseDir<>'') then
+    BaseDir:=IncludeTrailingPathDelimiter(BaseDir);
+  TSimpleFileModule.BaseDir:=BaseDir;
+  TSimpleFileModule.OnLog:=@Log;
+  If not NoIndexPage then
+    begin
+    if (IndexPageName='') then
+      IndexPageName:='index.html';
+    TSimpleFileModule.IndexPageName:=IndexPageName;
+    end;
+  {$IFNDEF VER_3_2}
+  RegisterCustomHeaders;
+  {$ENDIF}
+  RegisterProxies;
+  RegisterFileLocations;
+end;
+
+procedure TFPSimpleServerApplication.RegisterRoutes;
+begin
+  {$IFNDEF VER3_2}
+  SetupCapture;
+  {$ENDIF}
+  if FPassword<>'' then
+    begin
+    HTTPRouter.RegisterRoute('/quit',rmAll,@Doquit,False);
+    end;
+  if FEcho  then
+    HTTPRouter.RegisterRoute('/echo',rmAll,@DoEcho,False);
+  if ProxyManager.LocationCount>0 then
+    begin
+    TProxyWebModule.RegisterModule('Proxy',True);
+    ProxyManager.OnLog:=@DoProxyLog;
+    end;
+  {$IFNDEF VER3_2_2}
+  if FAPISecret<>'' then
+    TFPWebFileLocationAPIModule.RegisterFileLocationAPI(ExtractWord(1,FAPISecret,[',']),ExtractWord(2,FAPISecret,[',']));
+  {$ENDIF}
+  if FCrossOriginIsolation then
+    begin
+    {$IFDEF VER3_2_2}
+    DefaultFileModuleClass:=TMySimpleFileModule;
+    {$ELSE}
+    TFPCustomFileModule.OnPrepareResponse:=@ApplyCoi;
+    {$ENDIF}
+    end;
+  TSimpleFileModule.RegisterDefaultRoute;
+end;
+
+end.
+