/// shared DDD Infrastructure: Application/Daemon implementation classes // - this unit is a part of the freeware Synopse mORMot framework, // licensed under a MPL/GPL/LGPL tri-license; version 1.18 unit dddInfraApps; { This file is part of Synopse mORMot framework. Synopse mORMot framework. Copyright (C) 2022 Arnaud Bouchez Synopse Informatique - https://synopse.info *** BEGIN LICENSE BLOCK ***** Version: MPL 1.1/GPL 2.0/LGPL 2.1 The contents of this file are subject to the Mozilla Public License Version 1.1 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.mozilla.org/MPL Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the specific language governing rights and limitations under the License. The Original Code is Synopse mORMot framework. The Initial Developer of the Original Code is Arnaud Bouchez. Portions created by the Initial Developer are Copyright (C) 2022 the Initial Developer. All Rights Reserved. Contributor(s): Alternatively, the contents of this file may be used under the terms of either the GNU General Public License Version 2 or later (the "GPL"), or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), in which case the provisions of the GPL or the LGPL are applicable instead of those above. If you wish to allow use of your version of this file only under the terms of either the GPL or the LGPL, and not to allow others to use your version of this file under the terms of the MPL, indicate your decision by deleting the provisions above and replace them with the notice and other provisions required by the GPL or the LGPL. If you do not delete the provisions above, a recipient may use your version of this file under the terms of any one of the MPL, the GPL or the LGPL. ***** END LICENSE BLOCK ***** TODO: - store settings in database, or a centralized service? - allow to handle authentication via a centralized service or REST server } {$I Synopse.inc} // define HASINLINE CPU32 CPU64 OWNNORMTOUPPER interface uses {$ifdef MSWINDOWS} Windows, {$else} {$ifdef FPC} SynFPCLinux, {$endif} {$endif} mORMotService, // for running the daemon as a regular Windows Service SysUtils, Classes, Variants, SynCommons, SynTable, SynLog, SynCrypto, SynEcc, mORMot, mORMotDDD, dddInfraSettings, SynCrtSock, SynBidirSock, mORMotHttpServer, // for publishing a TSQLRestServer over HTTP mORMotHttpClient; // for publishing a TSQLRestClientURI over HTTP { ----- Implements Service/Daemon Applications } type /// abstract class to handle any kind of service/daemon executable // - would implement either a Windows Service, a stand-alone remotely // administrated daemon, or a console application, according to the command line // - you should inherit from this class, then override the abstract NewDaemon // protected method to launch and return a IAdministratedDaemon instance {$M+} { TDDDDaemon } TDDDDaemon = class protected fSettings: TDDDAdministratedDaemonSettings; fSettingsRef: IUnknown; /// the service/daemon will be stopped when this interface is set to nil fDaemon: IAdministratedDaemon; /// this abstract method should be overriden to return a new service/daemon // instance, using the (inherited) fSettings as parameters function NewDaemon: TDDDAdministratedDaemon; virtual; /// returns some text to be supplied to the console for /help - '' by default function CustomHelp: string; virtual; procedure DoStart{$ifdef MSWINDOWS}(Sender: TService=nil){$endif}; procedure DoStop{$ifdef MSWINDOWS}(Sender: TService=nil){$endif}; public /// initialize the service/daemon application thanks to some information // - actual settings would inherit from TDDDAdministratedDaemonSettingsFile, // to define much more parameters, according to the service/daemon process // - the supplied settings will be owned by this TDDDDaemon instance constructor Create(aSettings: TDDDAdministratedDaemonSettings); virtual; /// finalize the service/daemon application, and release its resources destructor Destroy; override; /// interpret the command line to run the application as expected // - under Windows, /install /uninstall /start /stop would control the // daemon as a Windows Service - you should run the program with // administrator rights to be able to change the Services settings // - /console or /c would run the program as a console application // - /verbose would run the program as a console application, in verbose mode // - /daemon or /d would run the program as a remotely administrated // daemon, using a published IAdministratedDaemon service // - /version would show the current revision information of the application // - no command line argument will run the program as a Service dispatcher // under Windows (as a regular service), or display the syntax - or would // run the service in /verbose mode if ForceRun is TRUE (may be useful e.g. // to debug an associated .dll using this daemon as host application) // - any invalid switch, or no switch under Linux, will display the syntax // - this method will output any error or information to the console // - as a result, a project .dpr could look like the following: //!begin //! with TMyApplicationDaemon.Create(TMyApplicationDaemonSettings.Create) do //! try //! ExecuteCommandLine; //! finally //! Free; //! end; //!end. procedure ExecuteCommandLine(ForceRun: boolean=false); /// start the daemon, until the instance is released procedure Execute; /// wait for Daemon property to be set // - Execute method should have been previously caleld // - returns true if the daemon has been started in the specified time function WaitStarted(TimeoutMS: integer=10000): boolean; /// start the daemon and wait for Daemon property to be set // - returns true if the daemon has been started in the specified time function ExecuteAndWaitStarted(TimeoutMS: integer=10000): boolean; /// read-only access to the underlying daemon instance // - equals nil if the daemon is not started property Daemon: IAdministratedDaemon read fDaemon; /// returns the class instance implementing the underlying Daemon // - you should transtype the returned instance using e.g. // ! myDaemon := mainDaemon.DaemonInstance as TMyDaemonClass; function DaemonInstance: TObject; published end; {$M-} /// abstract class to implement a IAdministratedDaemon service via a TThread // - as hosted by TDDDDaemon service/daemon application TDDDThreadDaemon = class(TDDDAdministratedThreadDaemon) protected // returns the system memory info as current state function InternalRetrieveState(var Status: variant): boolean; override; function GetAdministrationHTTPServer: TSQLHttpServer; {$ifdef HASINLINE}inline;{$endif} public /// IAdministratedDaemon command to subscribe to a set of events for // real-time remote monitoring of the specified log events // - this overriden method would disallow remote logs if low-level frames // logging is set (i.e. HttpClientFullWebSocketsLog / HttpServerFullWebSocketsLog) // to avoid an unexpected race condition procedure SubscribeLog(const Levels: TSynLogInfos; const Callback: ISynLogCallback; ReceiveExistingKB: cardinal); override; /// reference to the HTTP server publishing IAdministratedDaemon service // - may equal nil if TDDDAdministratedDaemonSettingsFile.AuthHttp.BindPort='' property AdministrationHTTPServer: TSQLHttpServer read GetAdministrationHTTPServer; end; /// abstract class to implement a IAdministratedDaemon service via a TSQLRestServer // - as hosted by TDDDDaemon service/daemon application TDDDRestDaemon = class(TDDDAdministratedRestDaemon, IAdministratedDaemon) protected fPreviousMonitorTix: Int64; function GetAdministrationHTTPServer: TSQLHttpServer; {$ifdef HASINLINE}inline;{$endif} // returns the current state from fRest.Stat() + system memory function InternalRetrieveState(var Status: variant): boolean; override; procedure InternalLogMonitoring; virtual; public /// IAdministratedDaemon command to subscribe to a set of events for // real-time remote monitoring of the specified log events // - this overriden method would disallow remote logs if low-level frames // logging is set (i.e. HttpClientFullWebSocketsLog / HttpServerFullWebSocketsLog) // to avoid an unexpected race condition procedure SubscribeLog(const Levels: TSynLogInfos; const Callback: ISynLogCallback; ReceiveExistingKB: cardinal); override; /// reference to the HTTP server publishing IAdministratedDaemon service // - may equal nil if TDDDAdministratedDaemonSettingsFile.AuthHttp.BindPort='' property AdministrationHTTPServer: TSQLHttpServer read GetAdministrationHTTPServer; end; /// abstract class to implement a IAdministratedDaemon service via a // TSQLRestServer, publishing its services as HTTP // - as hosted by TDDDDaemon service/daemon application TDDDRestHttpDaemon = class(TDDDRestDaemon) protected fHttpServer: TSQLHttpServer; fServicesLogRest: TSQLRest; function Settings: TDDDAdministratedDaemonHttpSettings; {$ifdef HASINLINE}inline;{$endif} // initialize HTTP Server into fHttpServer // (fRest should have been set by the overriden method) procedure InternalStart; override; // finalize HTTP Server and SOA log database procedure InternalStop; override; public /// generate API documentation corresponding to REST SOA interfaces procedure WrapperGenerate(const DestFile: TFileName; const Template: TFileName = 'API.adoc.mustache'); /// reference to the main HTTP server publishing this daemon Services // - may be nil outside a Start..Stop range property HttpServer: TSQLHttpServer read fHttpServer; /// reference to the associated REST server storing the SOA log database // - may be nil if the daemon did not implement this feature property ServicesLogRest: TSQLRest read fServicesLogRest; end; { ----- Implements ORM/SOA REST Client access } type /// exception raised by TDDDRestClientSettings classes EDDDRestClient = class(EDDDException); /// advanced parameters for TDDDRestClientSettings definition TDDDRestClientDefinition = class(TSynPersistentWithPassword) protected fRoot: RawUTF8; fConnectRetrySeconds: integer; published /// the URI Root to be used for the REST Model property Root: RawUTF8 read fRoot write fRoot; /// the encrypted password to be used to connect with WebSockets property WebSocketsPassword: RawUTF8 read fPassWord write fPassWord; /// how many seconds the client may try to connect after open socket failure property ConnectRetrySeconds: integer read fConnectRetrySeconds write fConnectRetrySeconds; end; /// storage class for initializing an ORM/SOA REST Client class // - this class will contain some generic properties to initialize a // TSQLRestClientURI pointing to a remote server, using WebSockets by default // - WebSockets support is the reason why this class is defined in // dddInfraApps, and not dddInfraSettings TDDDRestClientSettings = class(TSynAutoCreateFields) protected fORM: TSynConnectionDefinition; fClient: TDDDRestClientDefinition; fTimeout, fWebSocketsLoopDelay: integer; public /// set the default values for Client.Root, ORM.ServerName, // Client.WebSocketsPassword and ORM.Password procedure SetDefaults(const Root, Port, WebSocketPassword, UserPassword: RawUTF8; const User: RawUTF8 = 'User'; const Server: RawUTF8 = 'localhost'; ForceSetCredentials: boolean = false; ConnectRetrySeconds: integer = 0; WebSocketsLoopDelayMS: integer = 0); /// is able to instantiate a Client REST instance for the stored definition // - Definition.Kind is expected to specify a TSQLRestClient class to be // instantiated, not a TSQLRestServer instance // - will return nil if the supplied Definition is not correct // - note that the supplied Model.Root is expected to be the default root // URI, which will be overriden with this TDDDRestSettings.Root property // - will also set the TSQLRest.LogFamily.Level from LogLevels value, function NewRestClientInstance(aRootSettings: TDDDAppSettingsAbstract; aModel: TSQLModel = nil; aOptions: TDDDNewRestInstanceOptions = [riOwnModel, riCreateVoidModelIfNone, riHandleAuthentication, riRaiseExceptionIfNoRest]): TSQLRestClientURI; virtual; /// you may assign this method to a TSQLRestClientURI.OnAuthentificationFailed // property, so that the client would automatically try to re-connect function OnAuthentificationFailed(Retry: integer; var aUserName, aPassword: string; out aPasswordHashed: boolean): boolean; /// you can overload here the TCP timeout delay, in seconds property Timeout: integer read fTimeout write fTimeout; /// you can overload here the WebSockets internal loop delay, in milliseconds property WebSocketsLoopDelay: integer read fWebSocketsLoopDelay write fWebSocketsLoopDelay; published /// defines a mean of access to a TSQLRest instance // - using Kind/ServerName/DatabaseName/User/Password properties: Kind // would define the TSQLRest class to be instantiated by NewRestClientInstance() property ORM: TSynConnectionDefinition read fORM; /// advanced connection options // - ORM.Password defines the authentication main password, and // Client.WebSocketsPassword is used for WebSockets binary encryption property Client: TDDDRestClientDefinition read fClient; end; /// abstract client to connect to any daemon service via WebSockets // - will monitor the connection, to allow automatic reconnection, with proper // services resubscription TDDDRestClientWebSockets = class(TSQLHttpClientWebsockets) protected fApplicationName: RawUTF8; fOwnedSettings: TDDDRestClientSettings; fConnected: boolean; fServicesRegistered: boolean; fOnConnect, fOnDisconnect: TOnRestClientNotify; // called after connection, before fOnConnect event - to register callbacks procedure AfterConnection; virtual; // called after disconnection, before fOnDisconnect event - unregister callbacks procedure AfterDisconnection; virtual; // call ClientDisconnect if HTTP_NOTIMPLEMENTED (i.e. websocket link broken) procedure ClientFailed(Sender: TSQLRestClientURI; E: Exception; Call: PSQLRestURIParams); virtual; // notify AfterDisconnection + fOnDisconnect if needed procedure ClientDisconnect; procedure WebSocketsClosed(Sender: TObject); virtual; // notify AfterDisconnection+fOnDisconnect if needed, then AfterConnection+fOnConnect procedure ClientSetUser(Sender: TSQLRestClientURI); virtual; // inherited classes should override those abstract methods procedure DefineApplication; virtual; abstract; procedure RegisterServices; virtual; abstract; function CreateModel(aSettings: TDDDRestClientSettings): TSQLModel; virtual; public /// initialize the client instance with the supplied settings constructor Create(aSettings: TDDDRestClientSettings; aOnConnect: TOnRestClientNotify = nil; aOnDisconnect: TOnRestClientNotify = nil); reintroduce; overload; virtual; /// finalize the client instance destructor Destroy; override; /// returns the server version, using timestamp/info method-based service property ApplicationVersion: RawUTF8 read GetSessionVersion; /// reflects the current WebSockets connection state property Connected: boolean read fConnected; /// human-friendly application name, as set by overriden DefineApplication property ApplicationName: RawUTF8 read fApplicationName; end; /// abstract client to connect to any daemon service via HTTP or HTTPS // - defines a simple REST client, without connection tracking TDDDRestClientHttp = class(TSQLHttpsClient) protected fApplicationName: RawUTF8; // inherited classes should override those abstract methods procedure DefineApplication; virtual; abstract; procedure RegisterServices; virtual; abstract; public /// returns the server version, using timestamp/info method-based service property ApplicationVersion: RawUTF8 read GetSessionVersion; /// human-friendly application name, as set by overriden DefineApplication property ApplicationName: RawUTF8 read fApplicationName; end; /// create a client safe asynchronous connection to a IAdministratedDaemon service function AdministratedDaemonClient(Definition: TDDDRestClientSettings; Model: TSQLModel = nil): TSQLHttpClientWebsockets; { /// create a client safe asynchronous connection to a IAdministratedDaemon service // via a IAdministratedDaemonProxy service function AdministratedDaemonClientFromProxy(Definition: TDDDRestClientSettings; const ProxyIPPort: RawUTF8; Model: TSQLModel = nil): IAdministratedDaemon; } /// create a WebSockets server instance, publishing a IAdministratedDaemon service function AdministratedDaemonServer(Settings: TDDDAdministratedDaemonSettings; DaemonClass: TDDDAdministratedDaemonClass): TDDDAdministratedDaemon; { ----- Implements Thread Processing to access a TCP server } type TDDDSocketThread = class; /// the current connection state of the TCP client associated to a // TDDDSocketThread thread TDDDSocketThreadState = (tpsDisconnected, tpsConnecting, tpsConnected); /// the monitoring information of a TDDDSocketThread thread TDDDSocketThreadMonitoring = class(TDDDAdministratedDaemonMonitor) protected fState: TDDDSocketThreadState; fOwner: TObject; function GetSocket: variant; public /// may be a TDDDSocketThread instance, or not (to maintain a global state // over several threads) property Owner: TObject read fOwner write fOwner; published /// how this thread is currently connected to its associated TCP server property State: TDDDSocketThreadState read fState write fState; /// information about the associated socket property Socket: variant read GetSocket; end; /// interface allowing to customize/mock a socket connection IDDDSocket = interface /// connect to the host via the (mocked) socket // - should raise an exception on error procedure Connect; /// returns instance identifier // - e.g. the TCrtSocket.Sock number as text function Identifier: RawUTF8; /// get some low-level information about the last occurred error // - e.g. TCrtSocket.LastLowSocketError value function LastError: RawUTF8; /// returns the number of bytes pending in the (mocked) socket // - call e.g. TCrtSocket.SockInPending() with aSocketForceCheck=true, // to return bytes both in the instance memory buffer and the socket API function DataInPending(aTimeOut: integer): integer; /// get Length bytes from the (mocked) socket // - returns the number of bytes read into the Content buffer // - call e.g. TCrtSocket.SockInRead() method function DataIn(Content: PAnsiChar; ContentLength: integer): integer; /// send Length bytes to the (mocked) socket // - returns false on any error, true on success // - call e.g. TCrtSocket.TrySndLow() method function DataOut(Content: PAnsiChar; ContentLength: integer): boolean; /// returns the low-level handle of this connection // - is e.g. the socket file description function Handle: integer; end; /// implements IDDDSocket using a SynCrtSock.TCrtSocket instance // - used e.g. by TDDDSocketThread for its default network communication // - this class will also create two mutexes, one for DataIn/DataInPending, // another for DataOut thread-safe process TDDDSynCrtSocket = class(TInterfacedObjectLocked, IDDDSocket) protected fSocket: TCrtSocket; fOwner: TThread; fHost, fPort: SockString; fInternalBufferSize: integer; fOutput: TSynLocker; // input lock is TInterfacedObjectLocked.Safe public /// initialize the internal TCrtSocket instance constructor Create(aOwner: TThread; const aHost, aPort: SockString; aSocketTimeout, aInternalBufferSize: integer); reintroduce; virtual; /// finalize the internal TCrtSocket instance destructor Destroy; override; /// call TCrtSocket.OpenBind procedure Connect; /// returns TCrtSocket.Sock number as text function Identifier: RawUTF8; /// get information from TCrtSocket.LastLowSocketError function LastError: RawUTF8; /// call TCrtSocket.SockInPending() method // - with aSocketForceCheck=true, to return bytes both in the instance // memory buffer and the socket API function DataInPending(aTimeOut: integer): integer; /// call TCrtSocket.SockInRead() method function DataIn(Content: PAnsiChar; ContentLength: integer): integer; /// call TCrtSocket.TrySndLow() method function DataOut(Content: PAnsiChar; ContentLength: integer): boolean; /// returns TCrtsocket.Sock function Handle: integer; /// read-only access to the associated processing thread // - not published, to avoid stack overflow since TDDDSocketThreadMonitoring // would point to this instance property Owner: TThread read fOwner; published /// read-only access to the associated processing socket property Socket: TCrtSocket read fSocket; end; /// defines the potential mocked actions for TDDDMockedSocket.MockException() TDDDMockedSocketException = (msaConnectRaiseException, msaDataInPendingTimeout, msaDataInPendingFails, msaDataInRaiseException, msaDataOutRaiseException, msaDataOutReturnsFalse); /// defines a set of mocked actions for TDDDMockedSocket.MockException() TDDDMockedSocketExceptions = set of TDDDMockedSocketException; /// defines the potential mocked actions for TDDDMockedSocket.MockLatency() TDDDMockedSocketLatency = (mslConnect, mslDataIn, mslDataOut); /// defines a set of mocked actions for TDDDMockedSocket.MockLatency() TDDDMockedSocketLatencies = set of TDDDMockedSocketLatency; /// the default exception class raised by TDDDMockedSocket EDDDMockedSocket = class(EDDDInfraException); /// implements IDDDSocket using a fake/mocked in-memory input/ouput storage // - may be supplied to TDDDSocketThread to bypass its default network communication // - you could fake input/output of TCP/IP packets by calling MockDataIn() and // MockDataOut() methods - incoming and outgoing packets would be merged in // the internal in-memory buffers, as with a regular Socket // - you could fake exception, for any upcoming method call, via MockException() // - you could emulate network latency, for any upcoming method call, via // MockLatency() - to emulate remote/wireless access, or thread pool contention // - this implementation is thread-safe, so multiple threads could access // the same IDDDSocket instance, and settings be changed in real time TDDDMockedSocket = class(TInterfacedObjectLocked, IDDDSocket) protected fInput, fOutput: RawByteString; fExceptionActions: TDDDMockedSocketExceptions; fExceptionMessage: string; fExceptionClass: ExceptClass; fLatencyActions: TDDDMockedSocketLatencies; fLatencyMS: integer; fOwner: TThread; function GetPendingInBytes: integer; function GetPendingOutBytes: integer; procedure CheckLatency(Action: TDDDMockedSocketLatency); procedure CheckRaiseException(Action: TDDDMockedSocketException); public /// initialize the mocked socket instance constructor Create(aOwner: TThread); reintroduce; virtual; /// add some bytes to the internal fake input storage // - would be made accessible to the DataInPending/DataIn methods // - the supplied buffer would be gathered to any previous MockDataIn() // call, which has not been read yet by the DataIn() method procedure MockDataIn(const Content: RawByteString); /// return the bytes from the internal fake output storage // - as has be previously set by the DataOut() method // - will gather all data from several DataOut() calls in a single buffer function MockDataOut: RawByteString; /// the specified methods would raise an exception // - only a single registration is memorized: once raised, any further // method execution would continue as usual // - optional Exception.Message which should be raised with the exception // - also optional exception class instead of default EDDDMockedSocket // - msaDataOutReturnsFalse won't raise any exception, but let DataOut // method return false (which is the normal way of indicating a socket // error) - in this case, ExceptionMessage would be available from LastError // - msaDataInPendingTimeout won't raise any exception, but let DataInPending // sleep for the timeout period, and return 0 // - msaDataInPendingFails won't raise any exception, but let DataInPending // fails immediately, and return -1 (emulating a broken socket) // - you may use ALL_DDDMOCKED_EXCEPTIONS to set all possible actions // - you could reset any previous registered exception by calling // ! MockException([]); procedure MockException(NextActions: TDDDMockedSocketExceptions; const ExceptionMessage: string = ''; ExceptionClass: ExceptClass = nil); /// will let the specified methods to wait for a given number of milliseconds // - allow to emulate network latency, on purpose // - you may use ALL_DDDMOCKED_LATENCIES to slow down all possible actions procedure MockLatency(NextActions: TDDDMockedSocketLatencies; MilliSeconds: integer); public /// IDDDSocket method to connect to the host via the mocked socket // - won't raise any exception unless ConnectShouldCheckRaiseException is set procedure Connect; /// IDDDSocket method to return a fake instance identifier // - in fact, the hexa pointer of the TDDDMockedSocket instance function Identifier: RawUTF8; /// IDDDSocket method to get some low-level information about the last error // - i.e. the latest ExceptionMessage value as set by MockException() function LastError: RawUTF8; /// IDDDSocket method to return the number of bytes pending // - note that the total length of all pending data is returned as once, // i.e. all previous calls to MockDataIn() would be sum as a single count // - this method will emulate blocking process, just like a regular socket: // if there is no pending data, it will wait up to aTimeOut milliseconds function DataInPending(aTimeOut: integer): integer; /// IDDDSocket method to get Length bytes from the mocked socket // - returns the number of bytes read into the Content buffer // - note that all pending data is returned as once, i.e. all previous // calls to MockDataIn() would be gathered in a single buffer function DataIn(Content: PAnsiChar; ContentLength: integer): integer; /// IDDDSocket method to send Length bytes to the mocked socket // - returns false on any error, true on success // - then MockDataOut could be used to retrieve the sent data function DataOut(Content: PAnsiChar; ContentLength: integer): boolean; /// returns 0 (no associated file descriptor) function Handle: integer; /// read-only access to the associated processing thread // - not published, to avoid stack overflow since TDDDSocketThreadMonitoring // would point to this instance property Owner: TThread read fOwner; published /// how many bytes are actually in the internal input buffer property PendingInBytes: integer read GetPendingInBytes; /// how many bytes are actually in the internal output buffer property PendingOutBytes: integer read GetPendingOutBytes; end; /// a generic TThread able to connect and reconnect to a TCP server // - initialize and own a TCrtSocket instance for TCP transmission // - allow automatic reconnection // - inherit from TSQLRestThread, so should be associated with a REST instance TDDDSocketThread = class(TSQLRestThread) protected fSettings: TDDDSocketThreadSettings; fMonitoring: TDDDSocketThreadMonitoring; fPreviousMonitorTix: Int64; fSocket: IDDDSocket; fPerformConnection: boolean; fHost, fPort: SockString; fSocketInputBuffer: RawByteString; fShouldDisconnect: boolean; fSocketDisable: boolean; procedure InternalExecute; override; procedure ExecuteConnect; procedure ExecuteDisconnect; procedure ExecuteDisconnectAfterError; procedure ExecuteSocket; function TrySend(const aFrame: RawByteString; ImmediateDisconnectAfterError: boolean = true): Boolean; virtual; // inherited classes could override those methods for process customization procedure InternalExecuteConnected; virtual; procedure InternalExecuteDisconnect; virtual; procedure InternalExecuteIdle; virtual; procedure InternalExecuteSocket; virtual; abstract; // process FSocketInputBuffer procedure InternalLogMonitoring; virtual; public /// initialize the thread for a given REST instance constructor Create(aSettings: TDDDSocketThreadSettings; aRest: TSQLRest; aMonitoring: TDDDSocketThreadMonitoring); /// finalize the thread process, and its associted REST instance destructor Destroy; override; /// returns the Monitoring and Rest statistics as a JSON object // - resulting format is // $ {...MonitoringProperties...,"Rest":{...RestStats...}} function StatsAsJson: RawUTF8; /// will pause any communication with the associated socket // - could be used before stopping the service for cleaner shutdown procedure Shutdown(andTerminate: boolean); virtual; /// the parameters used to setup this thread process property Settings: TDDDSocketThreadSettings read fSettings; published /// the IP Host name used to connect with TCP property Host: SockString read fHost; /// the IP Port value used to connect with TCP property Port: SockString read fPort; end; const /// map realistic exceptions steps for a mocked socket // - could be used to simulate a global socket connection drop ALL_DDDMOCKED_EXCEPTIONS = [msaConnectRaiseException, msaDataInPendingFails, msaDataInRaiseException, msaDataOutReturnsFalse]; /// map realistic latencies steps for a mocked socket // - could be used to simulate a slow network ALL_DDDMOCKED_LATENCIES = [Low(TDDDMockedSocketLatency)..high(TDDDMockedSocketLatency)]; /// a Mustache template of a .rc version information // - could be used to compile a custom .res version file in an automated way // - if you use this generated .res, ensure your "Version Info" is disabled // (unchecked) in the Delphi IDE project options EXEVERSION_RCTEMPLATE: RawUTF8 = '1 VERSIONINFO'#13#10 + 'FILEVERSION {{maj}},{{min}},{{rel}},{{build}}'#13#10 + 'PRODUCTVERSION {{maj}},{{min}},{{rel}},{{build}}'#13#10 + 'FILEOS 0x4'#13#10 + 'FILETYPE 0x1'#13#10 + '{'#13#10 + 'BLOCK "StringFileInfo"'#13#10 + '{'#13#10 + ' BLOCK "040904E4"'#13#10 + ' {'#13#10 + ' VALUE "CompanyName", "{{compname}}\0"'#13#10 + ' VALUE "FileDescription", "{{compname}}, {{product}} {{name}}{{#isDaemon}} Daemon{{/isDaemon}}\0"'#13#10 + ' VALUE "FileVersion", "{{maj}}.{{min}}.{{rel}}.{{build}}\0"'#13#10 + ' VALUE "InternalName", "{{name}}\0"'#13#10 + ' VALUE "LegalCopyright", "(c){{year}} {{compname}}\0"'#13#10 + ' VALUE "LegalTrademarks", "All rights reserved to {{compname}}\0"'#13#10 + ' VALUE "OriginalFilename", "{{name}}\0"'#13#10 + ' VALUE "ProductName", "{{product}} {{name}}\0"'#13#10 + ' VALUE "ProductVersion", "{{maj}}.{{min}}.{{rel}}.{{build}}\0"'#13#10 + ' }'#13#10 + '}'#13#10 + #13#10 + 'BLOCK "VarFileInfo"'#13#10 + '{'#13#10 + ' VALUE "Translation", 0x0409 0x04E4'#13#10 + '}'#13#10 + '}'; var /// you could set a text to this global variable at runtime, so that // it would be displayed as copyright older name for the console GlobalCopyright: string = ''; function ToText(st: TDDDSocketThreadState): PShortString; overload; function ToText(exc: TDDDMockedSocketException): PShortString; overload; { ----- Applications Securization } type /// result codes of the ECCAuthorize() function TECCAuthorize = (eaSuccess, eaInvalidSecret, eaMissingUnlockFile, eaInvalidUnlockFile, eaInvalidJson); /// any sensitive, or licensed program, could call this method to check for // authorized execution for a given user on a given computer, using very secure // asymmetric ECC cryptography // - applock.public/.private keys pair should have been generated, applock.public // stored as aAppLockPublic64 in the executables, and applock.private kept secret // - will search for encrypted authorization in a local user@host.unlock file // - if no user@host.unlock file is found, will create local user@host.public // and user@host.secret files and return eaMissingUnlockFile: user should then send // user@host.public to the product support to receive its user@host.unlock file // (a dedicated UI may be developped, or an uncrypted email can be used for // transfer with the support team, thanks to asymmetric cryptography) // - local user@host.secret file is encrypted via DPAPI/CryptDataForCurrentUser // for the specific computer and user (to avoid .unlock reuse on another PC) // - support team should create a user@host.json file matching aContent: TObject // published properties, containing all application-specific settings and // authorization scope; then it could create the unlock file using e.g. an // unlock.bat file running the ECC tool over secret applock.private keys: // $ @echo off // $ echo Usage: unlock user@host // $ echo. // $ ecc sign -file %1.json -auth applock -pass applockprivatepassword -rounds 60000 // $ ecc crypt -file %1.json -out %1.unlock -auth %1 -saltpass decryptsalt -saltrounds 10000 // $ del %1.json.sign // - returns eaInvalidUnlockFile if the local user@host.unlock file is not // correctly signed and encrypted for this user (e.g. corrupted or deprecated) // - eaInvalidJson will indicate some error in the .json created by support team, // i.e. if it does not match aContent: TObject published properties // - eaSuccess should let the application execute, on the returned scope // - returns eaSuccess if a local user@host.unlock file has been successfully // decrypted and validated (using ECDSA over aAppLockPublic64) and successfully // unserialized from JSON into aContent object instance // - user@host.* files are searched in the executable folder if aSearchFolder='', // but you may specify a custom location, e.g. use ECCKeyFileFolder // - aSecretPass could be entered by the end-user, to authenticate its identity; // you may specify a string constant if local applock.public/.private key files // is enough secure for your application // - will use the supplied aDPAPI/aDecryptSalt parameters to restrict // this authorization to a specific product (i.e. isolate the execution context // to reduce forensic scope), for dedicated applock.public/.private keys pair - // just pass some application-specific string constant to those parameters // - aSecretInfo^ could be set to retrieve the user@host.secret information // (e.g. validity dates), and aLocalFile^ the'user@host' file prefix function ECCAuthorize(aContent: TObject; aSecretDays: integer; const aSecretPass, aDPAPI, aDecryptSalt, aAppLockPublic64: RawUTF8; const aSearchFolder: TFileName = ''; aSecretInfo: PECCCertificateSigned = nil; aLocalFile: PFileName = nil): TECCAuthorize; function ToText(auth: TECCAuthorize): PShortString; overload; implementation { ----- Implements Service/Daemon Applications } function ToText(st: TDDDSocketThreadState): PShortString; begin result := GetEnumName(TypeInfo(TDDDSocketThreadState),ord(st)); end; function ToText(exc: TDDDMockedSocketException): PShortString; begin result := GetEnumName(TypeInfo(TDDDMockedSocketException),ord(exc)); end; { TDDDDaemon } constructor TDDDDaemon.Create(aSettings: TDDDAdministratedDaemonSettings); begin inherited Create; if aSettings = nil then raise EDDDInfraException.CreateUTF8('%.Create(settings=nil)', [self]); fSettings := aSettings; fSettingsRef := aSettings; end; function TDDDDaemon.DaemonInstance: TObject; begin result := ObjectFromInterface(fDaemon); end; destructor TDDDDaemon.Destroy; begin fDaemon := nil; inherited; end; procedure TDDDDaemon.DoStart{$ifdef MSWINDOWS}(Sender: TService){$endif}; var log: ISynLog; res: TCQRSResult; begin {$ifdef WITHLOG} {$ifdef MSWINDOWS} SQLite3Log.Add.LogThreadName('Service Start Handler', true); {$endif} log := SQLite3Log.Enter(self, 'DoStart'); if log<>nil then with ExeVersion do log.Log(sllNewRun, 'Daemon Start svc=% ver=% usr=%', [fSettings.ServiceName, Version.Detailed, LowerCase(User)], self); {$endif} fDaemon := NewDaemon; res := fDaemon.Start; if log <> nil then log.Log(sllTrace, 'fDaemon.Start=%', [ToText(res)^], self); end; procedure TDDDDaemon.DoStop{$ifdef MSWINDOWS}(Sender: TService){$endif}; {$ifdef WITHLOG} var log: ISynLog; begin {$ifdef MSWINDOWS} SQLite3Log.Add.LogThreadName('Service Stop Handler', true); {$endif} log := SQLite3Log.Enter(self, 'DoStop'); if log<>nil then log.Log(sllNewRun, 'Daemon Stop svc=% ver=% usr=%', [fSettings.ServiceName, ExeVersion.Version.Detailed, LowerCase(ExeVersion.User)], self); {$else} begin {$endif} fDaemon := nil; // will stop the daemon end; function TDDDDaemon.NewDaemon: TDDDAdministratedDaemon; begin if Assigned(fSettings) then begin if fSettings.Log.LowLevelWebSocketsFrames then begin {$ifdef WITHLOG} WebSocketLog := SQLite3Log; {$endif} HttpServerFullWebSocketsLog := true; HttpClientFullWebSocketsLog := true; end; SQLite3Log.Family.AutoFlushTimeOut := fSettings.Log.AutoFlushTimeOut; // after /fork {$ifdef MSWINDOWS} // Windows 7+ SetAppUserModelID(fSettings.AppUserModelID); {$endif} end; result := nil; end; procedure TDDDDaemon.Execute; {$ifdef WITHLOG} var log: ISynLog; begin log := SQLite3Log.Enter(self, 'Execute'); {$else} begin {$endif} fDaemon := NewDaemon; fDaemon.Start; end; function TDDDDaemon.WaitStarted(TimeoutMS: integer): boolean; var tix: Int64; begin tix := GetTickCount64 + TimeoutMS; repeat SleepHiRes(50); result := Daemon <> nil; until result or (GetTickCount64 > tix); end; function TDDDDaemon.ExecuteAndWaitStarted(TimeoutMS: integer): boolean; begin try Execute; result := WaitStarted(TimeoutMS); except result := false; end; end; type TExecuteCommandLineCmd = ( cNone, cInstall, cUninstall, cStart, cStop, cState, cVersion, cVerbose, cHelp, cHardenPasswords, cPlainPasswords, cConsole, cDaemon, cRun, cFork, cKill); procedure TDDDDaemon.ExecuteCommandLine(ForceRun: boolean); var name, param: RawUTF8; p: PUTF8Char; passwords, exe: RawByteString; cmd: TExecuteCommandLineCmd; daemon: TDDDAdministratedDaemon; {$ifdef MSWINDOWS} service: TServiceSingle; ctrl: TServiceController; depend: string; i: integer; {$endif} {$I-} // no IO error for writeln() below function cmdText: RawUTF8; begin result := GetEnumNameTrimed(TypeInfo(TExecuteCommandLineCmd), cmd); end; procedure HardenSettings(const folder: TFileName; P: PUTF8Char); var B: PUTF8Char; fn, bak: TFileName; pass, appsec, plain, new: RawUTF8; doc: TDocVariantData; modified, any: boolean; v: PVariant; begin TextColor(ccLightCyan); writeln('Executing /', cmdText, ' with User "', ExeVersion.User, '"'#13#10); any := false; repeat B := pointer(GetNextLine(P,P)); if B=nil then exit; fn := FormatString('%%.settings', [folder, GetNextItem(B, '=')]); doc.InitJSONFromFile(fn, JSON_OPTIONS_FAST, true); modified := false; if doc.Count > 0 then while B <> nil do begin GetNextItem(B, '@', appsec); v := doc.GetPVariantByPath(GetNextItem(B)); if v = nil then continue; if VariantToUTF8(v^, pass) and (pass <> '') then begin plain := TSynPersistentWithPassword.ComputePlainPassword(pass, 0, appsec); if plain = '' then continue; // may occur with unexpected user case cmd of cHardenPasswords: new := FormatUTF8('%:%', [ExeVersion.User, BinToBase64(CryptDataForCurrentUser(plain, appsec, true))]); cPlainPasswords: new := TSynPersistentWithPassword.ComputePassword(plain); else exit; end; FillZero(plain); if new <> pass then begin RawUTF8ToVariant(new, v^); // replace modified := true; end; end; end; if modified then begin bak := ChangeFileExt(fn, '.bak'); DeleteFile(bak); RenameFile(fn, bak); FileFromString(doc.ToJSON('', '', jsonHumanReadable), fn); any := true; TextColor(ccLightCyan); end else TextColor(ccDarkGray); writeln(' ', fn); doc.Clear; until P=nil; if any and (cmd = cHardenPasswords) then begin TextColor(ccYellow); writeln(#13#10' Warning:'#13#10' /', cmdText, ' new .settings will work only with user "', ExeVersion.User, '"'); end; end; procedure Show(Success: Boolean); var msg: RawUTF8; error: integer; begin if Success then begin msg := 'Successfully executed'; TextColor(ccWhite); end else begin error := GetLastError; msg := FormatUTF8('Error % [%] occured with', [error, StringToUTF8(SysErrorMessage(error))]); TextColor(ccLightRed); ExitCode := 1; // notify error to caller batch end; msg := FormatUTF8('% [%] (%) on Service [%]', [msg, param, cmdText, fSettings.ServiceName]); writeln(msg); AppendToTextFile(ExeVersion.User + ' ' +msg, ChangeFileExt(ExeVersion.ProgramFileName, '.txt')); end; procedure Syntax; var spaces: string; begin writeln('Try with one of the switches:'); writeln({$ifdef MSWINDOWS}' '{$else}' ./'{$endif}, ExeVersion.ProgramName, ' /console -c /verbose /daemon -d /help -h /version'); spaces := StringOfChar(' ', length(ExeVersion.ProgramName) + 4); {$ifdef MSWINDOWS} writeln(spaces, '/install /uninstall /start /stop /state'); {$else} writeln(spaces, '/run -r /fork -f /kill -k'); {$endif} if passwords <> '' then writeln(spaces, '/hardenpasswords /plainpasswords'); writeln(spaces, CustomHelp); end; begin try if fSettings.ServiceDisplayName = '' then begin fDaemon := NewDaemon; // should initialize the default .settings fDaemon := nil; end; ResourceSynLZToRawByteString('passwords', passwords); TextColor(ccLightGreen); name := StringToUTF8(fSettings.ServiceDisplayName); if name = '' then // perhaps the settings file is still void name := ExeVersion.ProgramName; if ExeVersion.Version.Version32 <> 0 then name := FormatUTF8('% %', [name, ExeVersion.Version.Detailed]); writeln(#10' ', name); writeln(StringOfChar('-', length(name) + 2)); TextColor(ccGreen); if fSettings.Description <> '' then writeln(fSettings.Description); if GlobalCopyright <> '' then writeln('(c)', CurrentYear, ' ', GlobalCopyright); writeln; TextColor(ccLightCyan); if ForceRun then param := '/verbose' else param := trim(StringToUTF8(paramstr(1))); if (param = '') or not (param[1] in ['/', '-']) then cmd := cNone else begin p := @param[2]; if p^ = '-' then // allow e.g. --fork switch (idem to /f -f /fork -fork) inc(p); case NormToUpper[p^] of 'C': cmd := cConsole; 'D': cmd := cDaemon; 'R': cmd := cRun; 'F': cmd := cFork; 'K': cmd := cKill; else byte(cmd) := ord(cInstall) + IdemPCharArray(p, ['INST', 'UNINST', 'START', 'STOP', 'STAT', 'VERS', 'VERB', 'HELP', 'HARDEN', 'PLAIN']); end; end; case cmd of cHelp: Syntax; cVersion: begin exe := StringFromFile(ExeVersion.ProgramFileName); writeln(' ', ExeVersion.ProgramFileName, #13#10' Size: ', length(exe), ' bytes (', KB(exe), ')' + #13#10' Build date: ', ExeVersion.Version.BuildDateTimeString, #13#10' MD5: ', MD5(exe), #13#10' SHA256: ', SHA256(exe)); if ExeVersion.Version.Version32 <> 0 then writeln(' Version: ', ExeVersion.Version.Detailed); TextColor(ccCyan); writeln(#10'Powered by Synopse mORMot ' + SYNOPSE_FRAMEWORK_VERSION); end; cConsole, cDaemon, cVerbose: begin writeln('Launched in ', cmdText, ' mode'#10); TextColor(ccLightGray); {$ifdef WITHLOG} case cmd of cVerbose: // leave as in settings for -c (cConsole) SQLite3Log.Family.EchoToConsole := LOG_VERBOSE; end; SQLite3Log.Add.Log(sllNewRun, 'Start % /% %', [fSettings.ServiceName,cmdText, ExeVersion.Version.DetailedOrVoid], self); {$endif} daemon := NewDaemon; try fDaemon := daemon; {$ifdef WITHLOG} if cmd = cDaemon then if (daemon.AdministrationServer = nil) or not ({$ifdef MSWINDOWS} daemon.AdministrationServer.ExportedAsMessageOrNamedPipe or {$endif} (daemon.InheritsFrom(TDDDThreadDaemon) and (TDDDThreadDaemon(daemon).fAdministrationHTTPServer <> nil))) then daemon.Log.Synlog.Log(sllWarning, 'ExecuteCommandLine as Daemon ' + 'without external admnistrator acccess', self); {$endif} daemon.Execute(cmd = cDaemon); finally SQLite3Log.Add.Log(sllNewRun, 'Stop /%', [cmdText], self); fDaemon := nil; // will stop the daemon end; end; cHardenPasswords, cPlainPasswords: if passwords <> '' then HardenSettings(IncludeTrailingPathDelimiter(fSettings.SettingsFolder), pointer(passwords)) else begin TextColor(ccLightRed); writeln('No "passwords" resource bound to ', ExeVersion.ProgramFullSpec); end; {$ifdef MSWINDOWS} // implement the daemon as a Windows Service else with fSettings do if ServiceName = '' then if cmd = cNone then Syntax else begin TextColor(ccLightRed); writeln('No ServiceName specified - please fix the settings'); end else case cmd of cNone: if param = '' then begin // executed as a background service service := TServiceSingle.Create(ServiceName, ServiceDisplayName); try service.OnStart := DoStart; service.OnStop := DoStop; service.OnShutdown := DoStop; // sometimes, is called without Stop if ServicesRun then // blocking until service shutdown Show(true) else if GetLastError = 1063 then Syntax else Show(false); finally service.Free; end; end else Syntax; cInstall: begin if ServiceDependencies <> nil then begin depend := ServiceDependencies[0]; for i := 1 to high(ServiceDependencies) do depend := depend + #0 + ServiceDependencies[i]; end else if (ParamCount >= 3) and SameText(ParamStr(2), '/depend') then begin depend := ParamStr(3); for i := 4 to ParamCount do depend := depend + #0 + ParamStr(i); end; if depend <> '' then depend := depend + #0; // ensure ends with dual #0 Show(TServiceController.Install(ServiceName, ServiceDisplayName, Description, ServiceAutoStart, '', depend) <> ssNotInstalled); end; else begin ctrl := TServiceController.CreateOpenService('', '', ServiceName); try case cmd of cStart: Show(ctrl.Start([])); cStop: Show(ctrl.Stop); cUninstall: begin ctrl.Stop; Show(ctrl.Delete); end; cState: writeln(ServiceName, ' State=', ServiceStateText(ctrl.State)); else Show(false); end; finally ctrl.Free; end; end; end; {$else} cRun, cFork: RunUntilSigTerminated(self, (cmd=cFork), DoStart, DoStop {$ifdef WITHLOG},SQLite3Log.Add, fSettings.ServiceName{$endif}); cKill: if not RunUntilSigTerminatedForKill then raise EServiceException.Create('No forked process found to be killed'); else Syntax; {$endif MSWINDOWS} end; except on E: Exception do begin ConsoleShowFatalException(E); ExitCode := 1; // indicates error end; end; TextColor(ccLightGray); ioresult; end; {$I+} function TDDDDaemon.CustomHelp: string; begin result := ''; end; { TDDDThreadDaemon } function TDDDThreadDaemon.GetAdministrationHTTPServer: TSQLHttpServer; begin result := fAdministrationHTTPServer as TSQLHttpServer; end; function TDDDThreadDaemon.InternalRetrieveState(var Status: variant): boolean; begin Status := _ObjFast(['SystemMemory', TSynMonitorMemory.ToVariant]); result := true; end; function CanSubscribeLog(const Callback: ISynLogCallback): Boolean; begin result := false; if Assigned(Callback) then if {$ifdef WITHLOG}(WebSocketLog<>nil) or{$endif} HttpClientFullWebSocketsLog or HttpServerFullWebSocketsLog then begin Callback.Log(sllError, FormatUTF8( '%00% SubscribeLog is not allowed when low-level WebSockets ' + 'frame logging is enabled (otherwise a race condition happens)', [NowToString(False), LOG_LEVEL_TEXT[sllError]])); exit; end else result := true; end; procedure TDDDThreadDaemon.SubscribeLog(const Levels: TSynLogInfos; const Callback: ISynLogCallback; ReceiveExistingKB: cardinal); begin if CanSubscribeLog(Callback) then inherited SubscribeLog(Levels, Callback, ReceiveExistingKB); end; { TDDDRestDaemon } function TDDDRestDaemon.GetAdministrationHTTPServer: TSQLHttpServer; begin result := TSQLHttpServer(fAdministrationHTTPServer); end; procedure TDDDRestDaemon.InternalLogMonitoring; var status: variant; begin {$ifdef WITHLOG} if fLog <> nil then if (sllMonitoring in fLog.Level) and InternalRetrieveState(status) then fLog.SynLog.Log(sllMonitoring, '%', [status], Self); {$endif} end; function TDDDRestDaemon.InternalRetrieveState(var Status: variant): boolean; begin if fRest <> nil then begin Status := _ObjFast(['Rest', fRest.StatsAsDocVariant, 'SystemMemory', TSynMonitorMemory.ToVariant]); result := true; end else result := false; end; procedure TDDDRestDaemon.SubscribeLog(const Levels: TSynLogInfos; const Callback: ISynLogCallback; ReceiveExistingKB: cardinal); begin if CanSubscribeLog(Callback) then inherited SubscribeLog(Levels, Callback, ReceiveExistingKB); end; { TDDDRestHttpDaemon } function TDDDRestHttpDaemon.Settings: TDDDAdministratedDaemonHttpSettings; begin result := TDDDAdministratedDaemonHttpSettings(fInternalSettings); end; procedure TDDDRestHttpDaemon.InternalStart; begin if Settings.Http.BindPort <> '' then fHttpServer := TSQLHttpServer.Create(fRest, Settings.Http); end; procedure TDDDRestHttpDaemon.InternalStop; begin try if Assigned(fRest) then fRest.Shutdown; // no incoming TSQLRestServer.URI allowed from now on FreeAndNil(fHttpServer); FreeAndNil(fServicesLogRest); finally inherited InternalStop; // FreeAndNil(fRest) end; end; { ----- Implements Thread Processing to access a TCP server } procedure TDDDRestHttpDaemon.WrapperGenerate(const DestFile, Template: TFileName); begin Settings.Rest.WrapperGenerate(Rest, GetInteger(pointer(HttpServer.Port)), DestFile, Template); end; { TDDDSocketThreadMonitoring } function TDDDSocketThreadMonitoring.GetSocket: variant; begin if (fOwner = nil) or not fOwner.InheritsFrom(TDDDSocketThread) or (TDDDSocketThread(fOwner).fSocket = nil) then SetVariantNull(result) else ObjectToVariant(ObjectFromInterface(TDDDSocketThread(fOwner).fSocket), result); end; { TDDDSocketThread } constructor TDDDSocketThread.Create(aSettings: TDDDSocketThreadSettings; aRest: TSQLRest; aMonitoring: TDDDSocketThreadMonitoring); begin if aSettings = nil then raise EDDDInfraException.CreateUTF8('%.Create(aSettings=nil)', [self]); fSettings := aSettings; if aMonitoring = nil then raise EDDDInfraException.CreateUTF8('%.Create(aMonitoring=nil)', [self]); fMonitoring := aMonitoring; if fMonitoring.fOwner=nil then fMonitoring.fOwner := self; if fSettings.Host = '' then fSettings.Host := '127.0.0.1'; fHost := fSettings.Host; if fSettings.Port = 0 then raise EDDDInfraException.CreateUTF8('%.Create(Port=0)', [self]); fPort := UInt32ToUtf8(fSettings.Port); if fSettings.SocketLoopPeriod < 10 then fSettings.SocketLoopPeriod := 10; if fSettings.SocketTimeout < fSettings.SocketLoopPeriod then fSettings.SocketTimeout := 2000; fPerformConnection := true; inherited Create(aRest, true, true); // aOwnRest=true, aCreateSuspended=true end; destructor TDDDSocketThread.Destroy; var timeOut: Int64; begin {$ifdef WITHLOG} if fLog<>nil then // no log after NotifyThreadEnded call fLog.Family.SynLog.Log(sllTrace,'Destroy %:%',[fHost,fPort],self); {$endif} Terminate; timeOut := GetTickCount64 + 10000; repeat // wait until properly disconnected from remote TCP server SleepHiRes(10); until (fMonitoring.State = tpsDisconnected) or (GetTickCount64 > timeOut); inherited Destroy; if fMonitoring.fOwner=self then FreeAndNil(fMonitoring); end; procedure TDDDSocketThread.ExecuteConnect; {$ifdef WITHLOG} var log: ISynLog; begin log := FLog.Enter('ExecuteConnect %:%',[fHost,fPort],self); {$else} begin {$endif} if fSocket <> nil then raise EDDDInfraException.CreateUTF8('%.ExecuteConnect: fSocket<>nil', [self]); if fMonitoring.State <> tpsDisconnected then raise EDDDInfraException.CreateUTF8('%.ExecuteConnect: State=%', [self, ToText(fMonitoring.State)^]); fMonitoring.State := tpsConnecting; try if fSettings.SocketBufferBytes <= 0 then fSettings.SocketBufferBytes := 32768; if Assigned(fSettings.OnIDDDSocketThreadCreate) then fSettings.OnIDDDSocketThreadCreate(self, fSocket) else fSocket := TDDDSynCrtSocket.Create(self, fHost, fPort, fSettings.SocketTimeout, fSettings.SocketBufferBytes); fSocket.Connect; fMonitoring.State := tpsConnected; // to be done ASAP to allow sending InternalExecuteConnected; {$ifdef WITHLOG} if log<>nil then log.Log(sllTrace, 'ExecuteConnect: Connected via Socket % - %', [fSocket.Identifier, fMonitoring], self); {$endif} except on E: Exception do begin fMonitoring.ProcessException(E); {$ifdef WITHLOG} if log<>nil then log.Log(sllTrace, 'ExecuteConnect: Impossible to Connect to %:% (%) %', [Host, Port, E.ClassType, fMonitoring], self); {$endif} fSocket := nil; fMonitoring.State := tpsDisconnected; end; end; if (fMonitoring.State <> tpsConnected) and not Terminated then if fSettings.ConnectionAttemptsInterval < 0 then Terminate else if fSettings.ConnectionAttemptsInterval > 0 then // on error, retry if SleepOrTerminated(fSettings.ConnectionAttemptsInterval * 1000) then {$ifdef WITHLOG} begin if log<>nil then log.Log(sllTrace, 'ExecuteConnect: thread terminated', self) end else if log<>nil then log.Log(sllTrace, 'ExecuteConnect: wait finished -> retry connect', self) {$endif}; end; procedure TDDDSocketThread.ExecuteDisconnect; var info: RawUTF8; {$ifdef WITHLOG} log: ISynLog; begin log := FLog.Enter('ExecuteDisconnect %:%',[fHost,fPort],self); {$else} begin {$endif} try fSafe.Lock; try fShouldDisconnect := false; fMonitoring.State := tpsDisconnected; try if fSocket = nil then info := '[Unknown]' else info := fSocket.Identifier; InternalExecuteDisconnect; finally fSocket := nil; end; {$ifdef WITHLOG} if log<>nil then log.Log(sllTrace, 'Socket % disconnected', [info], self); {$endif} InternalLogMonitoring; finally fSafe.UnLock; end; except on E: Exception do begin fMonitoring.ProcessException(E); {$ifdef WITHLOG} if log<>nil then log.Log(sllTrace, 'Socket disconnection error (%)', [E.ClassType], self); {$endif} end; end; end; procedure TDDDSocketThread.ExecuteDisconnectAfterError; begin {$ifdef WITHLOG} if fSocket <> nil then FLog.Log(sllError, '%.ExecuteDisconnectAfterError: Sock=% LastError=%', [ClassType, fSocket.Identifier, fSocket.LastError], self); {$endif} ExecuteDisconnect; FSocketInputBuffer := ''; if fSettings.AutoReconnectAfterSocketError then FPerformConnection := true; end; procedure TDDDSocketThread.ExecuteSocket; var pending, len: integer; begin if Terminated or fSocketDisable then exit; if (fSettings.SocketMaxBufferBytes = 0) or (length(FSocketInputBuffer) < fSettings.SocketMaxBufferBytes) then begin pending := fSocket.DataInPending(fSettings.SocketLoopPeriod); if Terminated or (pending = 0) then exit; if pending < 0 then begin ExecuteDisconnectAfterError; exit; end; len := length(FSocketInputBuffer); SetLength(FSocketInputBuffer, len + pending); if fSocket.DataIn(@PByteArray(FSocketInputBuffer)[len], pending) <> pending then begin ExecuteDisconnectAfterError; exit; end; FMonitoring.Server.AddSize(pending, 0); end; InternalExecuteSocket; end; procedure TDDDSocketThread.InternalExecute; begin fPreviousMonitorTix := GetTickCount64; try repeat if fMonitoring.State = tpsConnected then ExecuteSocket else if fPerformConnection then ExecuteConnect else SleepHiRes(100); if Terminated then break; try if Elapsed(fPreviousMonitorTix, fSettings.MonitoringLogInterval) then InternalLogMonitoring; InternalExecuteIdle; except on E: Exception do begin fMonitoring.ProcessException(E); {$ifdef WITHLOG} FLog.Log(sllWarning, 'Skipped % exception in %.InternalExecuteIdle', [E, ClassType], self); {$endif} end; end; until Terminated; finally ExecuteDisconnect; end; end; procedure TDDDSocketThread.InternalExecuteConnected; begin end; procedure TDDDSocketThread.InternalExecuteDisconnect; begin end; procedure TDDDSocketThread.InternalExecuteIdle; begin fSafe.Lock; try if fShouldDisconnect then ExecuteDisconnectAfterError; finally fSafe.UnLock; end; end; procedure TDDDSocketThread.InternalLogMonitoring; var cached, flushed: integer; begin // CachedMemory method will also purge any outdated cached entries cached := fRest.CacheOrNil.CachedMemory(@flushed); {$ifdef WITHLOG} FLog.Log(sllMonitoring, '% CachedMemory=% Flushed=%', [FMonitoring, cached, flushed], Self); {$endif} fPreviousMonitorTix := GetTickCount64; end; procedure TDDDSocketThread.Shutdown(andTerminate: boolean); begin if (self <> nil) and not fSocketDisable then begin fSocketDisable := true; fRest.LogClass.Add.Log(sllDebug, 'Shutdown: % will stop any communication ' + 'with %:%', [ClassType, fHost, fPort], self); if andTerminate and not Terminated then begin SleepHiRes(100); Terminate; WaitFor; end; end; end; function TDDDSocketThread.StatsAsJson: RawUTF8; begin with TJSONSerializer.CreateOwnedStream do try WriteObject(FMonitoring); CancelLastChar('}'); if fRest.InheritsFrom(TSQLRestServer) then begin AddShort(',"Rest":'); AddNoJSONEscapeUTF8(TSQLRestServer(fRest).StatsAsJson); end; Add(',"Version":"%","DateTime":"%"}', [ExeVersion.Version.Detailed, NowUTCToString(True, 'T')]); SetText(result); finally Free; end; end; function TDDDSocketThread.TrySend(const aFrame: RawByteString; ImmediateDisconnectAfterError: boolean): Boolean; var tmpSock: IDDDSocket; // avoid GPF if fSocket=nil after fSafe.UnLock (unlikely) begin if fSocketDisable then begin result := false; exit; end; fSafe.Lock; try result := (aFrame <> '') and (fSocket <> nil) and (fMonitoring.State = tpsConnected) and not fShouldDisconnect; if result then tmpSock := fSocket; finally fSafe.UnLock; end; if not result then exit; result := tmpSock.DataOut(pointer(aFrame), length(aFrame)); if result then fMonitoring.Server.AddSize(0, length(aFrame)) else if ImmediateDisconnectAfterError then ExecuteDisconnectAfterError else begin fSafe.Lock; try fShouldDisconnect := true; // notify for InternalExecuteIdle finally fSafe.UnLock; end; end; end; { TDDDSynCrtSocket } constructor TDDDSynCrtSocket.Create(aOwner: TThread; const aHost, aPort: SockString; aSocketTimeout, aInternalBufferSize: integer); begin inherited Create; fOwner := aOwner; fHost := aHost; fPort := aPort; fSocket := TCrtSocket.Create(aSocketTimeout); if aInternalBufferSize < 512 then aInternalBufferSize := 512; fInternalBufferSize := aInternalBufferSize; fOutput.Init; // uses two locks to avoid race condition on multi-thread end; destructor TDDDSynCrtSocket.Destroy; begin FreeAndNil(fSocket); fOutput.Done; inherited; end; procedure TDDDSynCrtSocket.Connect; begin fSocket.OpenBind(fHost, fPort, False); fSocket.CreateSockIn(tlbsCRLF, fInternalBufferSize); // use SockIn safe buffer end; function TDDDSynCrtSocket.DataIn(Content: PAnsiChar; ContentLength: integer): integer; begin fSafe.Lock; try result := fSocket.SockInRead(Content, ContentLength, false); finally fSafe.UnLock; end; end; function TDDDSynCrtSocket.DataInPending(aTimeOut: integer): integer; begin fSafe.Lock; try result := fSocket.SockInPending(aTimeOut,{aPendingAlsoInSocket=}true); finally fSafe.UnLock; end; end; function TDDDSynCrtSocket.DataOut(Content: PAnsiChar; ContentLength: integer): boolean; begin fOutput.Lock; try result := fSocket.TrySndLow(Content, ContentLength); finally fOutput.UnLock; end; end; function TDDDSynCrtSocket.Identifier: RawUTF8; begin result := Int32ToUtf8(fSocket.Sock); end; function TDDDSynCrtSocket.LastError: RawUTF8; begin if fSocket.LastLowSocketError=0 then result := '' else StringToUTF8(SocketErrorMessage(fSocket.LastLowSocketError),result); end; function TDDDSynCrtSocket.Handle: integer; begin result := fSocket.Sock; end; { TDDDMockedSocket } constructor TDDDMockedSocket.Create(aOwner: TThread); begin inherited Create; fOwner := aOwner; end; procedure TDDDMockedSocket.Connect; begin CheckLatency(mslConnect); fSafe.Lock; try CheckRaiseException(msaConnectRaiseException); finally fSafe.UnLock; end; end; function TDDDMockedSocket.DataIn(Content: PAnsiChar; ContentLength: integer): integer; begin CheckLatency(mslDataIn); fSafe.Lock; try CheckRaiseException(msaDataInRaiseException); result := length(fInput); if ContentLength < result then result := ContentLength; if result <= 0 then exit; MoveFast(pointer(fInput)^, Content^, result); Delete(fInput, 1, result); finally fSafe.UnLock; end; end; function TDDDMockedSocket.DataInPending(aTimeOut: integer): integer; var forcedTimeout: boolean; endTix: Int64; begin forcedTimeout := false; endTix := GetTickCount64 + aTimeOut; repeat fSafe.Lock; try CheckRaiseException(msaDataInRaiseException); if msaDataInPendingFails in fExceptionActions then begin fExceptionActions := []; result := -1; // cspSocketError exit; end; if not forcedTimeout then if msaDataInPendingTimeout in fExceptionActions then begin fExceptionActions := []; forcedTimeout := true; end; if forcedTimeout then result := 0 else result := length(fInput); finally fSafe.UnLock; // wait outside the instance lock end; if (result <> 0) or ((fOwner <> nil) and TSQLRestThread(fOwner).Terminated) or (aTimeOut = 0) then break; SleepHiRes(1); // emulate blocking process, just like a regular socket if (fOwner <> nil) and TSQLRestThread(fOwner).Terminated then break; until GetTickCount64 > endTix; // warning: 10-16 ms resolution under Windows end; function TDDDMockedSocket.DataOut(Content: PAnsiChar; ContentLength: integer): boolean; var previous: integer; begin CheckLatency(mslDataOut); fSafe.Lock; try CheckRaiseException(msaDataOutRaiseException); if msaDataOutReturnsFalse in fExceptionActions then begin fExceptionActions := []; result := false; exit; end; result := true; if ContentLength <= 0 then exit; previous := length(fOutput); SetLength(fOutput, previous + ContentLength); MoveFast(Content^, PByteArray(fOutput)^[previous], ContentLength); finally fSafe.UnLock; end; end; function TDDDMockedSocket.Identifier: RawUTF8; begin result := PointerToHex(self); end; function TDDDMockedSocket.LastError: RawUTF8; begin fSafe.Lock; try StringToUTF8(fExceptionMessage, result); fExceptionMessage := ''; finally fSafe.UnLock; end; end; procedure TDDDMockedSocket.MockDataIn(const Content: RawByteString); begin fSafe.Lock; try fInput := fInput + Content; finally fSafe.UnLock; end; end; function TDDDMockedSocket.MockDataOut: RawByteString; begin fSafe.Lock; try result := fOutput; fOutput := ''; finally fSafe.UnLock; end; end; procedure TDDDMockedSocket.MockException(NextActions: TDDDMockedSocketExceptions; const ExceptionMessage: string; ExceptionClass: ExceptClass); begin fSafe.Lock; try fExceptionActions := NextActions; fExceptionMessage := ExceptionMessage; fExceptionClass := ExceptionClass; finally fSafe.UnLock; end; end; procedure TDDDMockedSocket.CheckRaiseException(Action: TDDDMockedSocketException); begin if not (Action in fExceptionActions) then exit; fExceptionActions := []; if fExceptionMessage = '' then FormatString('Mocked Exception for %', [ToText(Action)^], fExceptionMessage); if fExceptionClass = nil then fExceptionClass := EDDDMockedSocket; raise fExceptionClass.Create(fExceptionMessage); end; procedure TDDDMockedSocket.MockLatency(NextActions: TDDDMockedSocketLatencies; MilliSeconds: integer); begin fSafe.Lock; try fLatencyActions := NextActions; fLatencyMS := MilliSeconds; finally fSafe.UnLock; end; end; procedure TDDDMockedSocket.CheckLatency(Action: TDDDMockedSocketLatency); var waitMS: integer; begin fSafe.Lock; try if Action in fLatencyActions then waitMS := fLatencyMS else waitMS := 0; finally fSafe.UnLock; // wait outside the instance lock end; while (waitMS > 0) and not ((fOwner <> nil) and TSQLRestThread(fOwner).Terminated) do begin SleepHiRes(1); // do not use GetTickCount64 (poor resolution under Windows) dec(waitMS); end; end; function TDDDMockedSocket.GetPendingInBytes: integer; begin fSafe.Lock; try result := Length(fInput); finally fSafe.UnLock; end; end; function TDDDMockedSocket.GetPendingOutBytes: integer; begin fSafe.Lock; try result := Length(fOutput); finally fSafe.UnLock; end; end; function TDDDMockedSocket.Handle: integer; begin result := 0; end; { ----- Implements ORM/SOA REST Client access } { TDDDRestClientSettings } function TDDDRestClientSettings.NewRestClientInstance(aRootSettings: TDDDAppSettingsAbstract; aModel: TSQLModel; aOptions: TDDDNewRestInstanceOptions): TSQLRestClientURI; var pass: RawUTF8; begin if aModel = nil then if riCreateVoidModelIfNone in aOptions then begin aModel := TSQLModel.Create([], ''); include(aOptions, riOwnModel); end else raise EDDDRestClient.CreateUTF8('%.NewRestClientInstance(aModel=nil)', [self]); if fClient.Root = '' then // supplied TSQLModel.Root is the default root URI fClient.Root := aModel.Root else aModel.Root := fClient.Root; if fORM.Kind = '' then fORM.Kind := 'TSQLHttpClientWebsockets'; // assume we need HTTP + callbacks if fORM.ServerName = '' then fORM.ServerName := 'localhost'; result := nil; try try result := TSQLRest.CreateTryFrom(aModel, ORM, // will call SetUser() riHandleAuthentication in aOptions) as TSQLRestClientURI; if result = nil then exit; // no match or wrong parameters pass := fClient.PasswordPlain; if pass <> '' then (result as TSQLHttpClientWebsockets).WebSocketsConnect(pass) else if not result.ServerTimestampSynchronize then raise EDDDRestClient.CreateUTF8('%.Create: HTTP access failure on %/%', [self, ORM.ServerName, aModel.Root]); result.OnAuthentificationFailed := OnAuthentificationFailed; except FreeAndNil(result); end; finally if riOwnModel in aOptions then if result = nil then // avoid memory leak aModel.Free else aModel.Owner := result; if (result = nil) and (riRaiseExceptionIfNoRest in aOptions) then raise EDDDRestClient.CreateUTF8('Impossible to initialize % on %/%', [fORM.Kind, fORM.ServerName, fClient.Root]); end; end; function TDDDRestClientSettings.OnAuthentificationFailed(Retry: integer; var aUserName, aPassword: string; out aPasswordHashed: boolean): boolean; begin if (Retry = 1) and (fORM.User <> '') then begin aUserName := UTF8ToString(fORM.User); aPassword := UTF8ToString(fORM.PasswordPlain); aPasswordHashed := true; result := true; end else result := false; end; procedure TDDDRestClientSettings.SetDefaults(const Root, Port, WebSocketPassword, UserPassword, User, Server: RawUTF8; ForceSetCredentials: boolean; ConnectRetrySeconds, WebSocketsLoopDelayMS: integer); begin if fClient.Root = '' then fClient.Root := Root; if fORM.Kind = '' then if WebSocketPassword <> '' then fORM.Kind := 'TSQLHttpClientWebsockets' else fORM.Kind := TSQLHttpClient.ClassName; if ForceSetCredentials or (fORM.ServerName = '') then begin if fORM.ServerName = '' then if Port = '' then fORM.ServerName := Server else if Server = '' then fORM.ServerName := 'http://localhost:' + Port else if PosExChar(':', Server) = 0 then fORM.ServerName := 'http://' + Server + ':' + Port else fORM.ServerName := Server; if fClient.WebSocketsPassword = '' then fClient.WebSocketsPassword := WebSocketPassword; fWebSocketsLoopDelay := WebSocketsLoopDelayMS; fClient.ConnectRetrySeconds := ConnectRetrySeconds; if UserPassword <> '' then begin fORM.User := User; fORM.PasswordPlain := UserPassword; end; end; end; function AdministratedDaemonClient(Definition: TDDDRestClientSettings; Model: TSQLModel): TSQLHttpClientWebsockets; begin result := Definition.NewRestClientInstance(nil, Model) as TSQLHttpClientWebsockets; try result.ServiceDefine(IAdministratedDaemon, sicShared); except result.Free; raise; end; end; { TDDDAdministratedDaemonAsProxy } { FIXME: Proxy feature not finished yet (but not needed any more) } type TDDDAdministratedDaemonAsProxy = class(TCQRSService, IAdministratedDaemonAsProxy) protected fProxy: IAdministratedDaemon; fProxyClient: TSQLHttpClientWebsockets; /// IAdministratedDaemon methods function RetrieveState(out Status: variant): TCQRSResult; function Start: TCQRSResult; function Stop(out Information: variant): TCQRSResult; function Halt(out Information: variant): TCQRSResult; function DatabaseList: TRawUTF8DynArray; function DatabaseTables(const DatabaseName: RawUTF8): TRawUTF8DynArray; function DatabaseExecute(const DatabaseName, SQL: RawUTF8): TServiceCustomAnswer; procedure SubscribeLog(const Levels: TSynLogInfos; const Callback: ISynLogCallback; ReceiveExistingKB: cardinal); procedure CallbackReleased(const callback: IInvokable; const interfaceName: RawUTF8); function StartProxy(const aDDDRestClientSettings: variant): TCQRSResult; public destructor Destroy; override; end; procedure TDDDAdministratedDaemonAsProxy.CallbackReleased(const callback: IInvokable; const interfaceName: RawUTF8); begin fProxy.CallbackReleased(callback, interfaceName); end; function TDDDAdministratedDaemonAsProxy.DatabaseExecute(const DatabaseName, SQL: RawUTF8): TServiceCustomAnswer; begin result := fProxy.DatabaseExecute(DatabaseName, SQL); end; function TDDDAdministratedDaemonAsProxy.DatabaseList: TRawUTF8DynArray; begin result := fProxy.DatabaseList; end; function TDDDAdministratedDaemonAsProxy.DatabaseTables(const DatabaseName: RawUTF8): TRawUTF8DynArray; begin result := fProxy.DatabaseTables(DatabaseName); end; destructor TDDDAdministratedDaemonAsProxy.Destroy; begin fProxy := nil; FreeAndNil(fProxyClient); inherited Destroy; end; function TDDDAdministratedDaemonAsProxy.Halt(out Information: variant): TCQRSResult; begin result := fProxy.Halt(Information); end; function TDDDAdministratedDaemonAsProxy.RetrieveState(out Status: variant): TCQRSResult; begin result := fProxy.RetrieveState(Status); end; function TDDDAdministratedDaemonAsProxy.Start: TCQRSResult; begin result := fProxy.Start; end; function TDDDAdministratedDaemonAsProxy.StartProxy(const aDDDRestClientSettings: variant): TCQRSResult; var def: TDDDRestClientSettings; valid: boolean; begin CqrsBeginMethod(qaNone, result); if (fProxy <> nil) or (fProxyClient <> nil) then begin CqrsSetResult(cqrsAlreadyExists,result); exit; end; def := TDDDRestClientSettings.Create; try JSONToObject(def, pointer(VariantSaveJSON(aDDDRestClientSettings)), valid); if not valid then begin CqrsSetResultMsg(cqrsBadRequest, '%.StartProxy(%): invalid def', [self, aDDDRestClientSettings],result); exit; end; try fProxyClient := AdministratedDaemonClient(def); if not fProxyClient.Services.Resolve(IAdministratedDaemon, fProxy) then raise EDDDRestClient.CreateUTF8('%.StartProxy(%): IAdministratedDaemon not supported', [self, aDDDRestClientSettings]); CqrsSetResult(cqrsSuccess,result); except on E: Exception do begin fProxy := nil; FreeAndNil(fProxyClient); CqrsSetResult(E,result); end; end; finally def.Free; end; end; function TDDDAdministratedDaemonAsProxy.Stop(out Information: variant): TCQRSResult; begin result := fProxy.Stop(Information); end; procedure TDDDAdministratedDaemonAsProxy.SubscribeLog(const Levels: TSynLogInfos; const Callback: ISynLogCallback; ReceiveExistingKB: cardinal); begin fProxy.SubscribeLog(Levels, Callback, ReceiveExistingKB); end; function AdministratedDaemonClientFromProxy(Definition: TDDDRestClientSettings; const ProxyIPPort: RawUTF8; Model: TSQLModel = nil): IAdministratedDaemon; var proxy: TDDDRestClientSettings; client: TSQLHttpClientWebsockets; temp: IAdministratedDaemonAsProxy; res: TCQRSResult; begin proxy := TDDDRestClientSettings.Create; try CopyObject(Definition, proxy); // share same connection credentials proxy.ORM.ServerName := ProxyIPPort; client := proxy.NewRestClientInstance(nil, Model) as TSQLHttpClientWebsockets; finally proxy.Free; end; try client.ServiceDefine(IAdministratedDaemonAsProxy, sicClientDriven); client.Services.Resolve(IAdministratedDaemonAsProxy, temp); res := temp.StartProxy(_ObjFast(Definition)); if res <> cqrsSuccess then raise EDDDRestClient.CreateUTF8( 'AdministratedDaemonClientFromProxy(%) was not able to proxy via %: %', [Definition.ORM.ServerName, ProxyIPPort, ToText(res)^]); result := temp; except client.Free; raise; end; end; function AdministratedDaemonServer(Settings: TDDDAdministratedDaemonSettings; DaemonClass: TDDDAdministratedDaemonClass): TDDDAdministratedDaemon; begin if DaemonClass = nil then raise EDDDInfraException.Create('AdministratedDaemonServer(DaemonClass=nil)'); if Settings = nil then raise EDDDInfraException.Create('AdministratedDaemonServer(Settings=nil)'); with Settings.RemoteAdmin do result := DaemonClass.Create(AuthUserName, AuthHashedPassword, AuthRootURI, AuthNamedPipeName); result.InternalSettings := Settings; if Settings.Storage is TDDDAppSettingsStorageFile then result.InternalSettingsFolder := ExtractFilePath( TDDDAppSettingsStorageFile(Settings.Storage).SettingsJsonFileName); {$ifdef WITHLOG} result.Log.SynLog.Log(sllTrace, '%.Create(%)', [DaemonClass, Settings], result); {$endif} with Settings.RemoteAdmin do if AuthHttp.BindPort <> '' then result.AdministrationHTTPServer := TSQLHttpServer.Create( result.AdministrationServer, AuthHttp); end; { ----- Applications Securization } function ToText(auth: TECCAuthorize): PShortString; begin result := GetEnumName(TypeInfo(TECCAuthorize), ord(auth)); end; function ECCAuthorize(aContent: TObject; aSecretDays: integer; const aSecretPass, aDPAPI, aDecryptSalt, aAppLockPublic64: RawUTF8; const aSearchFolder: TFileName; aSecretInfo: PECCCertificateSigned; aLocalFile: PFileName): TECCAuthorize; var fileroot, fileunlock, filesecret, filepublic: TFileName; priv, new: TECCCertificateSecret; auth: TECCCertificate; signature: TECCSignatureCertifiedContent; unlock, secret, temp, json: RawByteString; decrypt: TECCDecrypt; hash: THash256; valid: TECCValidity; issuer: TECCCertificateIssuer; privok, jsonok: boolean; begin with ExeVersion do fileroot := SysUtils.LowerCase(FormatString('%@%', [User, Host])); if aSearchFolder = '' then fileroot := ExeVersion.ProgramFilePath + fileroot else fileroot := IncludeTrailingPathDelimiter(aSearchFolder) + fileroot; if aLocalFile <> nil then aLocalFile^ := fileroot; fileunlock := fileroot + '.unlock'; filepublic := fileroot + ECCCERTIFICATEPUBLIC_FILEEXT; filesecret := fileroot + '.secret'; // DPAPI-encrypted .private file try unlock := StringFromFile(fileunlock); secret := StringFromFile(filesecret); temp := CryptDataForCurrentUser(secret, aDPAPI, false); priv := TECCCertificateSecret.Create; try result := eaInvalidSecret; {$ifdef ENHANCEDRTL} {$ifdef VER150} if crc32cinlined($D26BE33F,@ECCAuthorize,14)<>$29743A4B then exit; // avoid stubbing (Delphi 7 ERTL only - just to show how it works) {$endif} {$endif} ECCIssuer(ExeVersion.User,Issuer); privok := priv.LoadFromSecureBinary(temp, aSecretPass, 100) and IsEqual(priv.Content.Signed.Issuer, Issuer); if aSecretInfo <> nil then aSecretInfo^ := priv.Content.Signed; if not privok or not ECCCheckDate(priv.Content) then begin new := TECCCertificateSecret.CreateNew(nil, ExeVersion.User, aSecretDays); try new.ToFile(filepublic); temp := new.SaveToSecureBinary(aSecretPass, 7, 100); secret := CryptDataForCurrentUser(temp, aDPAPI, true); FileFromString(secret, filesecret); finally new.Free; end; exit; end; result := eaMissingUnlockFile; if unlock = '' then exit; result := eaInvalidUnlockFile; decrypt := priv.Decrypt(unlock, json, @signature, nil, nil, aDecryptSalt, 10000); if decrypt <> ecdDecryptedWithSignature then exit; auth := TECCCertificate.CreateFromBase64(aAppLockPublic64); try hash := SHA256Digest(pointer(json), length(json)); valid := ECCVerify(signature, hash, auth.Content); if not (valid in ECC_VALIDSIGN) then exit; finally auth.Free; end; finally priv.Free; end; RemoveCommentsFromJSON(pointer(json)); JSONToObject(aContent, pointer(json), jsonok, nil, JSONTOOBJECT_TOLERANTOPTIONS); if jsonok then result := eaSuccess else result := eaInvalidJson; finally FillZero(hash); FillZero(json); FillZero(unlock); FillZero(temp); FillZero(secret); end; end; { TDDDRestClientWebSockets } constructor TDDDRestClientWebSockets.Create(aSettings: TDDDRestClientSettings; aOnConnect, aOnDisconnect: TOnRestClientNotify); var u: TURI; t: integer; log: ISynLog; begin DefineApplication; // should fill fApplicationName fOnFailed := ClientFailed; fOnSetUser := ClientSetUser; fOnConnect := aOnConnect; fOnDisconnect := aOnDisconnect; fOnWebSocketsClosed := WebSocketsClosed; if aSettings = nil then raise EDDDRestClient.CreateUTF8('%.Create(%) aSettings=nil', [fApplicationName, self]); if not u.From(aSettings.ORM.ServerName) then raise EDDDRestClient.CreateUTF8('%.Create(%): invalid ORM.ServerName=%', [self, fApplicationName, aSettings.ORM.ServerName]); log := SQLite3Log.Enter('Create(%): connect to %', [fApplicationName, aSettings.ORM.ServerName], self); t := aSettings.Timeout; fConnectRetrySeconds := aSettings.Client.ConnectRetrySeconds; inherited Create(u.Server, u.Port, CreateModel(aSettings), u.Https, '', '', t, t, t); Model.Owner := self; // just allocated by CreateModel() if aSettings.Client.WebSocketsPassword <> '' then begin fWebSocketLoopDelay := aSettings.WebSocketsLoopDelay; WebSocketsConnect(aSettings.Client.PasswordPlain); end; OnAuthentificationFailed := aSettings.OnAuthentificationFailed; if aSettings.ORM.Password = '' then RegisterServices // plain REST connection without authentication else if not SetUser(aSettings.ORM.User, aSettings.ORM.PasswordPlain, true) then raise EDDDRestClient.CreateUTF8('%.Create(%): invalid User=%', [self, fApplicationName, aSettings.ORM.User]); end; destructor TDDDRestClientWebSockets.Destroy; begin with fLogClass.Enter(self, 'Destroy') do try ClientDisconnect; finally inherited Destroy; fOwnedSettings.Free; end; end; procedure TDDDRestClientWebSockets.AfterConnection; begin // do nothing by default end; procedure TDDDRestClientWebSockets.AfterDisconnection; begin // do nothing by default end; procedure TDDDRestClientWebSockets.ClientDisconnect; var log: ISynLog; begin if not fConnected then exit; // notify once log := fLogClass.Enter('ClientDisconnect(%)', [fApplicationName], self); fConnected := false; if log<>nil then log.Log(sllTrace, 'ClientDisconnect -> AfterDisconnection', self); try AfterDisconnection; except if log<>nil then log.Log(sllWarning, 'Ignored AfterDisconnection exception', self); end; if Assigned(fOnDisconnect) then try if log<>nil then log.Log(sllTrace, 'ClientDisconnect -> OnDisconnect = %', [ToText(TMethod(fOnDisconnect))], self); fOnDisconnect(self); except if log<>nil then log.Log(sllWarning, 'Ignored OnDisconnect exception', self); end; end; procedure TDDDRestClientWebSockets.ClientFailed(Sender: TSQLRestClientURI; E: Exception; Call: PSQLRestURIParams); begin if Assigned(Call) and (Call^.OutStatus = HTTP_NOTIMPLEMENTED) then ClientDisconnect; end; procedure TDDDRestClientWebSockets.ClientSetUser(Sender: TSQLRestClientURI); var log, log2: ISynLog; begin if Assigned(Sender) and Assigned(Sender.SessionUser) then begin log := fLogClass.Enter('ClientSetUser(%) %', [fApplicationName, Sender.SessionUser], self); ClientDisconnect; fConnected := true; fSessionVersion := ''; if not fServicesRegistered then begin log2 := fLogClass.Enter('RegisterServices', [], self); RegisterServices; log2 := nil; fServicesRegistered := true; end; if log<>nil then log.Log(sllTrace, 'ClientSetUser -> AfterConnection', self); try AfterConnection; except if log<>nil then log.Log(sllWarning, 'Ignored AfterConnection exception', self); end; if Assigned(fOnConnect) then try if log<>nil then log.Log(sllTrace, 'ClientSetUser -> OnConnect = %', [ToText(TMethod(fOnConnect))], self); fOnConnect(self); except if log<>nil then log.Log(sllWarning, 'Ignored OnConnect exception', self); end; end; end; function TDDDRestClientWebSockets.CreateModel(aSettings: TDDDRestClientSettings): TSQLModel; begin // create a void data model by default result := TSQLModel.Create([], aSettings.Client.Root); end; procedure TDDDRestClientWebSockets.WebSocketsClosed(Sender: TObject); begin ClientDisconnect; end; initialization TJSONSerializer.RegisterObjArrayForJSON( [TypeInfo(TECCCertificateObjArray),TECCCertificate]); {$ifdef DEBUG} {$ifdef EnableMemoryLeakReporting} {$ifdef HASFASTMM4} // FastMM4 integrated in Delphi 2006 (and up) ReportMemoryLeaksOnShutdown := True; {$endif} {$endif} {$endif} end.