xtool/contrib/mORMot/SynCrtSock.pas

13145 lines
483 KiB
ObjectPascal

/// classes implementing TCP/UDP/HTTP client and server protocol
// - this unit is a part of the freeware Synopse mORMot framework,
// licensed under a MPL/GPL/LGPL tri-license; version 1.18
unit SynCrtSock;
{
This file is part of Synopse framework.
Synopse 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):
- Alfred Glaenzer (alf)
- Cybexr
- Darian Miller
- EMartin
- Eric Grange
- Eugene Ilyin
- EvaF
- f-vicente
- macc2010
- Maciej Izak (hnb)
- Marius Maximus
- Mr Yang (ysair)
- Pavel Mashlyakovskii (mpv)
- Willo vd Merwe
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 *****
}
{$I Synopse.inc} // define HASINLINE ONLYUSEHTTPSOCKET USELIBCURL SYNCRTDEBUGLOW
{.$define SYNCRTDEBUGLOW}
// internal use: enable some low-level log messages for HTTP socket debugging
interface
uses
SysUtils, // put first to use SynFPCLinux/SynKylix GetTickCount64
{$ifndef LVCL}
Contnrs,
SyncObjs, // for TEvent (in Classes.pas for LVCL)
{$endif LVCL}
{$ifdef SYNCRTDEBUGLOW}
SynCommons,
SynLog,
{$endif SYNCRTDEBUGLOW}
{$ifdef USELIBCURL}
SynCurl,
{$endif USELIBCURL}
{$ifdef FPC}
dynlibs,
{$endif FPC}
{$ifdef MSWINDOWS}
Windows,
SynWinSock,
{$ifdef USEWININET}
WinInet,
{$endif USEWININET}
{$ifndef DELPHI5OROLDER}
Types,
{$endif DELPHI5OROLDER}
{$else MSWINDOWS}
{$undef USEWININET}
{$ifdef FPC}
SynFPCSock,
SynFPCLinux,
BaseUnix, // for fpgetrlimit/fpsetrlimit
{$ifdef LINUXNOTBSD}
Linux,
{$endif LINUXNOTBSD}
{$else}
{$ifndef DELPHI5OROLDER}
Types,
{$endif DELPHI5OROLDER}
{$endif FPC}
{$ifdef KYLIX3}
KernelIoctl, // for IoctlSocket/ioctl FION* constants
LibC,
SynFPCSock, // shared with Kylix
SynKylix,
{$endif KYLIX3}
{$endif MSWINDOWS}
Classes;
const
/// the full text of the current Synopse mORMot framework version
// - match the value defined in SynCommons.pas and SynopseCommit.inc
// - we don't supply full version number with build revision, to reduce
// potential attack surface
XPOWEREDPROGRAM = 'mORMot 1.18';
/// the running Operating System
XPOWEREDOS = {$ifdef MSWINDOWS} 'Windows' {$else}
{$ifdef LINUXNOTBSD} 'Linux' {$else} 'Posix' {$endif LINUXNOTBSD}
{$endif MSWINDOWS};
/// internal HTTP content-type for efficient static file sending
// - detected e.g. by http.sys' THttpApiServer.Request or via the NGINX
// X-Accel-Redirect header's THttpServer.Process (see
// THttpServer.NginxSendFileFrom) for direct sending with no local bufferring
// - the OutCustomHeader should contain the proper 'Content-type: ....'
// corresponding to the file (e.g. by calling GetMimeContentType() function
// from SynCommons supplyings the file name)
// - should match HTML_CONTENT_STATICFILE constant defined in mORMot.pas unit
HTTP_RESP_STATICFILE = '!STATICFILE';
/// used to notify e.g. the THttpServerRequest not to wait for any response
// from the client
// - is not to be used in normal HTTP process, but may be used e.g. by
// TWebSocketProtocolRest.ProcessFrame() to avoid to wait for an incoming
// response from the other endpoint
// - should match NORESPONSE_CONTENT_TYPE constant defined in mORMot.pas unit
HTTP_RESP_NORESPONSE = '!NORESPONSE';
var
/// THttpRequest timeout default value for DNS resolution
// - leaving to 0 will let system default value be used
HTTP_DEFAULT_RESOLVETIMEOUT: integer = 0;
/// THttpRequest timeout default value for remote connection
// - default is 60 seconds
// - used e.g. by THttpRequest, TSQLHttpClientRequest and TSQLHttpClientGeneric
HTTP_DEFAULT_CONNECTTIMEOUT: integer = 60000;
/// THttpRequest timeout default value for data sending
// - default is 30 seconds
// - used e.g. by THttpRequest, TSQLHttpClientRequest and TSQLHttpClientGeneric
// - you can override this value by setting the corresponding parameter in
// THttpRequest.Create() constructor
HTTP_DEFAULT_SENDTIMEOUT: integer = 30000;
/// THttpRequest timeout default value for data receiving
// - default is 30 seconds
// - used e.g. by THttpRequest, TSQLHttpClientRequest and TSQLHttpClientGeneric
// - you can override this value by setting the corresponding parameter in
// THttpRequest.Create() constructor
HTTP_DEFAULT_RECEIVETIMEOUT: integer = 30000;
type
{$ifdef HASCODEPAGE} // FPC may expect a CP, e.g. to compare two string constants
SockString = type RawByteString;
{$else}
/// define a 8-bit raw storage string type, used for data buffer management
SockString = type AnsiString;
{$endif}
/// points to a 8-bit raw storage variable, used for data buffer management
PSockString = ^SockString;
/// defines a dynamic array of SockString
TSockStringDynArray = array of SockString;
{$ifdef HASVARUSTRING}
SockUnicode = UnicodeString;
{$else}
/// define the fastest 16-bit Unicode string type of the compiler
SockUnicode = WideString;
{$endif}
{$ifdef DELPHI5OROLDER}
// not defined in Delphi 5 or older
PPointer = ^Pointer;
TTextLineBreakStyle = (tlbsLF, tlbsCRLF);
UTF8String = AnsiString;
UTF8Encode = AnsiString;
{$endif}
{$ifndef FPC}
/// FPC 64-bit compatibility integer type
{$ifdef CPU64}
PtrInt = NativeInt;
PtrUInt = NativeUInt;
{$else}
PtrInt = integer;
PtrUInt = cardinal;
{$endif}
PPtrInt = ^PtrInt;
PPtrUInt = ^PtrUInt;
{$endif FPC}
{$M+}
/// exception thrown by the classes of this unit
ECrtSocket = class(Exception)
protected
fLastError: integer;
public
/// will concat the message with the WSAGetLastError information
constructor Create(const Msg: string); overload;
/// will concat the message with the supplied WSAGetLastError information
constructor Create(const Msg: string; Error: integer); overload;
/// will concat the message with the supplied WSAGetLastError information
constructor CreateFmt(const Msg: string; const Args: array of const; Error: integer); overload;
published
/// the associated WSAGetLastError value
property LastError: integer read fLastError;
end;
{$M-}
TCrtSocketClass = class of TCrtSocket;
/// the available available network transport layer
// - either TCP/IP, UDP/IP or Unix sockets
TCrtSocketLayer = (cslTCP, cslUDP, cslUNIX);
/// identify the incoming data availability in TCrtSocket.SockReceivePending
TCrtSocketPending = (cspSocketError, cspNoData, cspDataAvailable);
PTextFile = ^TextFile;
{$M+}
/// Fast low-level Socket implementation
// - direct access to the OS (Windows, Linux) network layer API
// - use Open constructor to create a client to be connected to a server
// - use Bind constructor to initialize a server
// - use SockIn and SockOut (after CreateSock*) to read/readln or write/writeln
// as with standard Delphi text files (see SendEmail implementation)
// - even if you do not use read(SockIn^), you may call CreateSockIn then
// read the (binary) content via SockInRead/SockInPending methods, which would
// benefit of the SockIn^ input buffer to maximize reading speed
// - to write data, CreateSockOut and write(SockOut^) is not mandatory: you
// rather may use SockSend() overloaded methods, followed by a SockFlush call
// - in fact, you can decide whatever to use none, one or both SockIn/SockOut
// - since this class rely on its internal optimized buffering system,
// TCP_NODELAY is set to disable the Nagle algorithm
// - our classes are (much) faster than the Indy or Synapse implementation
TCrtSocket = class
protected
fSock: TSocket;
fServer: SockString;
fPort: SockString;
fSockIn: PTextFile;
fSockOut: PTextFile;
fTimeOut: PtrInt;
fBytesIn: Int64;
fBytesOut: Int64;
fSocketLayer: TCrtSocketLayer;
fSockInEofError: integer;
fTLS, fWasBind: boolean;
// updated by every SockSend() call
fSndBuf: SockString;
fSndBufLen: integer;
// set by AcceptRequest() from TVarSin
fRemoteIP: SockString;
// updated during UDP connection, accessed via PeerAddress/PeerPort
fPeerAddr: TSockAddr;
{$ifdef MSWINDOWS}
fSecure: TSChannelClient;
{$endif MSWINDOWS}
procedure SetInt32OptionByIndex(OptName, OptVal: integer); virtual;
public
/// common initialization of all constructors
// - do not call directly, but use Open / Bind constructors instead
constructor Create(aTimeOut: PtrInt=10000); reintroduce; virtual;
/// connect to aServer:aPort
// - you may ask for a TLS secured client connection (only available under
// Windows by now, using the SChannel API)
constructor Open(const aServer, aPort: SockString; aLayer: TCrtSocketLayer=cslTCP;
aTimeOut: cardinal=10000; aTLS: boolean=false);
/// bind to an address
// - aAddr='1234' - bind to a port on all interfaces, the same as '0.0.0.0:1234'
// - aAddr='IP:port' - bind to specified interface only, e.g. '1.2.3.4:1234'
// - aAddr='unix:/path/to/file' - bind to unix domain socket, e.g. 'unix:/run/mormot.sock'
// - aAddr='' - bind to systemd descriptor on linux. See
// http://0pointer.de/blog/projects/socket-activation.html
constructor Bind(const aAddr: SockString; aLayer: TCrtSocketLayer=cslTCP;
aTimeOut: integer=10000);
/// low-level internal method called by Open() and Bind() constructors
// - raise an ECrtSocket exception on error
// - you may ask for a TLS secured client connection (only available under
// Windows by now, using the SChannel API)
procedure OpenBind(const aServer, aPort: SockString; doBind: boolean;
aSock: integer=-1; aLayer: TCrtSocketLayer=cslTCP; aTLS: boolean=false);
/// initialize the instance with the supplied accepted socket
// - is called from a bound TCP Server, just after Accept()
procedure AcceptRequest(aClientSock: TSocket; aClientSin: PVarSin);
/// initialize SockIn for receiving with read[ln](SockIn^,...)
// - data is buffered, filled as the data is available
// - read(char) or readln() is indeed very fast
// - multithread applications would also use this SockIn pseudo-text file
// - by default, expect CR+LF as line feed (i.e. the HTTP way)
procedure CreateSockIn(LineBreak: TTextLineBreakStyle=tlbsCRLF;
InputBufferSize: Integer=1024);
/// initialize SockOut for sending with write[ln](SockOut^,....)
// - data is sent (flushed) after each writeln() - it's a compiler feature
// - use rather SockSend() + SockSendFlush to send headers at once e.g.
// since writeln(SockOut^,..) flush buffer each time
procedure CreateSockOut(OutputBufferSize: Integer=1024);
/// finalize SockIn receiving buffer
// - you may call this method when you are sure that you don't need the
// input buffering feature on this connection any more (e.g. after having
// parsed the HTTP header, then rely on direct socket comunication)
procedure CloseSockIn;
/// finalize SockOut receiving buffer
// - you may call this method when you are sure that you don't need the
// output buffering feature on this connection any more (e.g. after having
// parsed the HTTP header, then rely on direct socket comunication)
procedure CloseSockOut;
/// close and shutdown the connection (called from Destroy)
procedure Close;
/// close the opened socket, and corresponding SockIn/SockOut
destructor Destroy; override;
/// read Length bytes from SockIn buffer + Sock if necessary
// - if SockIn is available, it first gets data from SockIn^.Buffer,
// then directly receive data from socket if UseOnlySockIn=false
// - if UseOnlySockIn=true, it will return the data available in SockIn^,
// and returns the number of bytes
// - can be used also without SockIn: it will call directly SockRecv()
// in such case (assuming UseOnlySockin=false)
function SockInRead(Content: PAnsiChar; Length: integer;
UseOnlySockIn: boolean=false): integer;
/// returns the number of bytes in SockIn buffer or pending in Sock
// - if SockIn is available, it first check from any data in SockIn^.Buffer,
// then call InputSock to try to receive any pending data if the buffer is void
// - if aPendingAlsoInSocket is TRUE, returns the bytes available in both the buffer
// and the socket (sometimes needed, e.g. to process a whole block at once)
// - will wait up to the specified aTimeOutMS value (in milliseconds) for
// incoming data - may wait a little less time on Windows due to a select bug
// - returns -1 in case of a socket error (e.g. broken/closed connection);
// you can raise a ECrtSocket exception to propagate the error
function SockInPending(aTimeOutMS: integer; aPendingAlsoInSocket: boolean=false): integer;
/// check the connection status of the socket
function SockConnected: boolean;
/// simulate writeln() with direct use of Send(Sock, ..) - includes trailing #13#10
// - useful on multi-treaded environnement (as in THttpServer.Process)
// - no temp buffer is used
// - handle SockString, ShortString, Char, Integer parameters
// - raise ECrtSocket exception on socket error
procedure SockSend(const Values: array of const); overload;
/// simulate writeln() with a single line - includes trailing #13#10
procedure SockSend(const Line: SockString=''); overload;
/// append P^ data into SndBuf (used by SockSend(), e.g.) - no trailing #13#10
// - call SockSendFlush to send it through the network via SndLow()
procedure SockSend(P: pointer; Len: integer); overload;
/// flush all pending data to be sent, optionally with some body content
// - raise ECrtSocket on error
procedure SockSendFlush(const aBody: SockString=''); virtual;
/// flush all pending data to be sent
// - returning true on success
function TrySockSendFlush: boolean;
/// how many bytes could be added by SockSend() in the internal buffer
function SockSendRemainingSize: integer;
/// fill the Buffer with Length bytes
// - use TimeOut milliseconds wait for incoming data
// - bypass the SockIn^ buffers
// - raise ECrtSocket exception on socket error
procedure SockRecv(Buffer: pointer; Length: integer);
/// check if there are some pending bytes in the input sockets API buffer
// - returns cspSocketError if the connection is broken or closed
// - warning: on Windows, may wait a little less than TimeOutMS (select bug)
function SockReceivePending(TimeOutMS: integer): TCrtSocketPending;
/// returns the socket input stream as a string
function SockReceiveString: SockString;
/// fill the Buffer with Length bytes
// - use TimeOut milliseconds wait for incoming data
// - bypass the SockIn^ buffers
// - return false on any fatal socket error, true on success
// - call Close if the socket is identified as shutdown from the other side
// - you may optionally set StopBeforeLength=true, then the read bytes count
// are set in Length, even if not all expected data has been received - in
// this case, Close method won't be called
function TrySockRecv(Buffer: pointer; var Length: integer; StopBeforeLength: boolean=false): boolean;
/// call readln(SockIn^,Line) or simulate it with direct use of Recv(Sock, ..)
// - char are read one by one if needed
// - use TimeOut milliseconds wait for incoming data
// - raise ECrtSocket exception on socket error
// - by default, will handle #10 or #13#10 as line delimiter (as normal text
// files), but you can delimit lines using #13 if CROnly is TRUE
procedure SockRecvLn(out Line: SockString; CROnly: boolean=false); overload;
/// call readln(SockIn^) or simulate it with direct use of Recv(Sock, ..)
// - char are read one by one
// - use TimeOut milliseconds wait for incoming data
// - raise ECrtSocket exception on socket error
// - line content is ignored
procedure SockRecvLn; overload;
/// direct send data through network
// - raise a ECrtSocket exception on any error
// - bypass the SockSend() or SockOut^ buffers
procedure SndLow(P: pointer; Len: integer);
/// direct send data through network
// - return false on any error, true on success
// - bypass the SndBuf or SockOut^ buffers
function TrySndLow(P: pointer; Len: integer): boolean;
/// returns the low-level error number
// - i.e. returns WSAGetLastError
function LastLowSocketError: Integer;
/// direct send data through network
// - raise a ECrtSocket exception on any error
// - bypass the SndBuf or SockOut^ buffers
// - raw Data is sent directly to OS: no LF/CRLF is appened to the block
procedure Write(const Data: SockString);
/// direct accept an new incoming connection on a bound socket
// - instance should have been setup as a server via a previous Bind() call
// - returns nil on error or a ResultClass instance on success
// - if ResultClass is nil, will return a plain TCrtSocket, but you may
// specify e.g. THttpServerSocket if you expect incoming HTTP requests
function AcceptIncoming(ResultClass: TCrtSocketClass=nil): TCrtSocket;
/// remote IP address after AcceptRequest() call over TCP
// - is either the raw connection IP to the current server socket, or
// a custom header value set by a local proxy as retrieved by inherited
// THttpServerSocket.GetRequest, searching the header named in
// THttpServerGeneric.RemoteIPHeader (e.g. 'X-Real-IP' for nginx)
property RemoteIP: SockString read fRemoteIP write fRemoteIP;
/// remote IP address of the last packet received (SocketLayer=slUDP only)
function PeerAddress: SockString;
/// remote IP port of the last packet received (SocketLayer=slUDP only)
function PeerPort: integer;
/// set the TCP_NODELAY option for the connection
// - default 1 (true) will disable the Nagle buffering algorithm; it should
// only be set for applications that send frequent small bursts of information
// without getting an immediate response, where timely delivery of data
// is required - so it expects buffering before calling Write() or SndLow()
// - you can set 0 (false) here to enable the Nagle algorithm, if needed
// - see http://www.unixguide.net/network/socketfaq/2.16.shtml
property TCPNoDelay: Integer index TCP_NODELAY write SetInt32OptionByIndex;
/// set the SO_SNDTIMEO option for the connection
// - i.e. the timeout, in milliseconds, for blocking send calls
// - see http://msdn.microsoft.com/en-us/library/windows/desktop/ms740476
property SendTimeout: Integer index SO_SNDTIMEO write SetInt32OptionByIndex;
/// set the SO_RCVTIMEO option for the connection
// - i.e. the timeout, in milliseconds, for blocking receive calls
// - see http://msdn.microsoft.com/en-us/library/windows/desktop/ms740476
property ReceiveTimeout: Integer index SO_RCVTIMEO write SetInt32OptionByIndex;
/// set the SO_KEEPALIVE option for the connection
// - 1 (true) will enable keep-alive packets for the connection
// - see http://msdn.microsoft.com/en-us/library/windows/desktop/ee470551
property KeepAlive: Integer index SO_KEEPALIVE write SetInt32OptionByIndex;
/// set the SO_LINGER option for the connection, to control its shutdown
// - by default (or Linger<0), Close will return immediately to the caller,
// and any pending data will be delivered if possible
// - Linger > 0 represents the time in seconds for the timeout period
// to be applied at Close; under Linux, will also set SO_REUSEADDR; under
// Darwin, set SO_NOSIGPIPE
// - Linger = 0 causes the connection to be aborted and any pending data
// is immediately discarded at Close
property Linger: Integer index SO_LINGER write SetInt32OptionByIndex;
/// after CreateSockIn, use Readln(SockIn^,s) to read a line from the opened socket
property SockIn: PTextFile read fSockIn;
/// after CreateSockOut, use Writeln(SockOut^,s) to send a line to the opened socket
property SockOut: PTextFile read fSockOut;
published
/// low-level socket handle, initialized after Open() with socket
property Sock: TSocket read fSock write fSock;
/// low-level socket type, initialized after Open() with socket
property SocketLayer: TCrtSocketLayer read fSocketLayer;
/// IP address, initialized after Open() with Server name
property Server: SockString read fServer;
/// IP port, initialized after Open() with port number
property Port: SockString read fPort;
/// if higher than 0, read loop will wait for incoming data till
// TimeOut milliseconds (default value is 10000) - used also in SockSend()
property TimeOut: PtrInt read fTimeOut;
/// total bytes received
property BytesIn: Int64 read fBytesIn;
/// total bytes sent
property BytesOut: Int64 read fBytesOut;
end;
{$M-}
/// event used to compress or uncompress some data during HTTP protocol
// - should always return the protocol name for ACCEPT-ENCODING: header
// e.g. 'gzip' or 'deflate' for standard HTTP format, but you can add
// your own (like 'synlzo' or 'synlz')
// - the data is compressed (if Compress=TRUE) or uncompressed (if
// Compress=FALSE) in the Data variable (i.e. it is modified in-place)
// - to be used with THttpSocket.RegisterCompress method
// - DataRawByteStringtype should be a generic AnsiString/RawByteString, which
// should be in practice a SockString or a RawByteString
THttpSocketCompress = function(var DataRawByteString; Compress: boolean): AnsiString;
/// used to maintain a list of known compression algorithms
THttpSocketCompressRec = record
/// the compression name, as in ACCEPT-ENCODING: header (gzip,deflate,synlz)
Name: SockString;
/// the function handling compression and decompression
Func: THttpSocketCompress;
/// the size in bytes after which compress will take place
// - will be 1024 e.g. for 'zip' or 'deflate'
// - could be 0 e.g. when encrypting the content, meaning "always compress"
CompressMinSize: integer;
end;
/// list of known compression algorithms
THttpSocketCompressRecDynArray = array of THttpSocketCompressRec;
/// identify some items in a list of known compression algorithms
// - filled from ACCEPT-ENCODING: header value
THttpSocketCompressSet = set of 0..31;
/// parent of THttpClientSocket and THttpServerSocket classes
// - contain properties for implementing HTTP/1.1 using the Socket API
// - handle chunking of body content
// - can optionaly compress and uncompress on the fly the data, with
// standard gzip/deflate or custom (synlzo/synlz) protocols
THttpSocket = class(TCrtSocket)
protected
/// used by RegisterCompress method
fCompress: THttpSocketCompressRecDynArray;
/// set by RegisterCompress method
fCompressAcceptEncoding: SockString;
/// GetHeader set index of protocol in fCompress[], from ACCEPT-ENCODING:
fCompressAcceptHeader: THttpSocketCompressSet;
/// same as HeaderGetValue('CONTENT-ENCODING'), but retrieved during Request
// and mapped into the fCompress[] array
fContentCompress: integer;
/// to call GetBody only once
fBodyRetrieved: boolean;
/// compress the data, adding corresponding headers via SockSend()
// - always add a 'Content-Length: ' header entry (even if length=0)
// - e.g. 'Content-Encoding: synlz' header if compressed using synlz
// - and if Data is not '', will add 'Content-Type: ' header
procedure CompressDataAndWriteHeaders(const OutContentType: SockString;
var OutContent: SockString);
public
/// TCP/IP prefix to mask HTTP protocol
// - if not set, will create full HTTP/1.0 or HTTP/1.1 compliant content
// - in order to make the TCP/IP stream not HTTP compliant, you can specify
// a prefix which will be put before the first header line: in this case,
// the TCP/IP stream won't be recognized as HTTP, and will be ignored by
// most AntiVirus programs, and increase security - but you won't be able
// to use an Internet Browser nor AJAX application for remote access any more
TCPPrefix: SockString;
/// will contain the first header line:
// - 'GET /path HTTP/1.1' for a GET request with THttpServer, e.g.
// - 'HTTP/1.0 200 OK' for a GET response after Get() e.g.
Command: SockString;
/// will contain all header lines after a Request
// - use HeaderGetValue() to get one HTTP header item value by name
Headers: SockString;
/// will contain the data retrieved from the server, after the Request
Content: SockString;
/// same as HeaderGetValue('CONTENT-LENGTH'), but retrieved during Request
// - is overridden with real Content length during HTTP body retrieval
ContentLength: integer;
/// same as HeaderGetValue('SERVER-INTERNALSTATE'), but retrieved during Request
// - proprietary header, used with our RESTful ORM access
ServerInternalState: integer;
/// same as HeaderGetValue('CONTENT-TYPE'), but retrieved during Request
ContentType: SockString;
/// same as HeaderGetValue('UPGRADE'), but retrieved during Request
Upgrade: SockString;
/// same as HeaderGetValue('X-POWERED-BY'), but retrieved during Request
XPoweredBy: SockString;
/// map the presence of some HTTP headers, but retrieved during Request
HeaderFlags: set of(transferChuked,
connectionClose, connectionUpgrade, connectionKeepAlive, hasRemoteIP);
/// retrieve the HTTP headers into Headers[] and fill most properties below
// - only relevant headers are retrieved, unless HeadersUnFiltered is set
procedure GetHeader(HeadersUnFiltered: boolean=false);
/// retrieve the HTTP body (after uncompression if necessary) into Content
procedure GetBody;
/// add an header 'name: value' entry
procedure HeaderAdd(const aValue: SockString);
/// set all Header values at once, from CRLF delimited text
procedure HeaderSetText(const aText: SockString;
const aForcedContentType: SockString='');
/// get all Header values at once, as CRLF delimited text
// - you can optionally specify a value to be added as 'RemoteIP: ' header
function HeaderGetText(const aRemoteIP: SockString=''): SockString;
/// HeaderGetValue('CONTENT-TYPE')='text/html', e.g.
// - supplied aUpperName should be already uppercased
function HeaderGetValue(const aUpperName: SockString): SockString;
/// will register a compression algorithm
// - used e.g. to compress on the fly the data, with standard gzip/deflate
// or custom (synlzo/synlz) protocols
// - returns true on success, false if this function or this
// ACCEPT-ENCODING: header was already registered
// - you can specify a minimal size (in bytes) before which the content won't
// be compressed (1024 by default, corresponding to a MTU of 1500 bytes)
// - the first registered algorithm will be the prefered one for compression
function RegisterCompress(aFunction: THttpSocketCompress;
aCompressMinSize: integer=1024): boolean;
end;
THttpServer = class;
/// results of THttpServerSocket.GetRequest virtual method
// - return grError if the socket was not connected any more, or grException
// if any exception occured during the process
// - grOversizedPayload is returned when MaximumAllowedContentLength is reached
// - grRejected is returned when OnBeforeBody returned not 200
// - grTimeout is returned when HeaderRetrieveAbortDelay is reached
// - grHeaderReceived is returned for GetRequest({withbody=}false)
// - grBodyReceived is returned for GetRequest({withbody=}true)
// - grOwned indicates that this connection is now handled by another thread,
// e.g. asynchronous WebSockets
THttpServerSocketGetRequestResult = (
grError, grException, grOversizedPayload, grRejected, grTimeout,
grHeaderReceived, grBodyReceived, grOwned);
/// a genuine identifier for a given client connection on server side
// - maps http.sys ID, or is a genuine 31-bit value from increasing sequence
THttpServerConnectionID = Int64;
/// a dynamic array of client connection identifiers, e.g. for broadcasting
THttpServerConnectionIDDynArray = array of THttpServerConnectionID;
/// Socket API based HTTP/1.1 server class used by THttpServer Threads
THttpServerSocket = class(THttpSocket)
protected
fMethod: SockString;
fURL: SockString;
fKeepAliveClient: boolean;
fRemoteConnectionID: THttpServerConnectionID;
fServer: THttpServer;
public
/// create the socket according to a server
// - will register the THttpSocketCompress functions from the server
// - once created, caller should call AcceptRequest() to accept the socket
constructor Create(aServer: THttpServer); reintroduce;
/// main object function called after aClientSock := Accept + Create:
// - get Command, Method, URL, Headers and Body (if withBody is TRUE)
// - get sent data in Content (if withBody=true and ContentLength<>0)
// - returned enumeration will indicates the processing state
function GetRequest(withBody: boolean; headerMaxTix: Int64): THttpServerSocketGetRequestResult; virtual;
/// contains the method ('GET','POST'.. e.g.) after GetRequest()
property Method: SockString read fMethod;
/// contains the URL ('/' e.g.) after GetRequest()
property URL: SockString read fURL;
/// true if the client is HTTP/1.1 and 'Connection: Close' is not set
// - default HTTP/1.1 behavior is "keep alive", unless 'Connection: Close'
// is specified, cf. RFC 2068 page 108: "HTTP/1.1 applications that do not
// support persistent connections MUST include the "close" connection option
// in every message"
property KeepAliveClient: boolean read fKeepAliveClient write fKeepAliveClient;
/// the recognized connection ID, after a call to GetRequest()
// - identifies either the raw connection on the current server, or is
// a custom header value set by a local proxy, e.g.
// THttpServerGeneric.RemoteConnIDHeader='X-Conn-ID' for nginx
property RemoteConnectionID: THttpServerConnectionID read fRemoteConnectionID;
end;
/// Socket API based REST and HTTP/1.1 compatible client class
// - this component is HTTP/1.1 compatible, according to RFC 2068 document
// - the REST commands (GET/POST/PUT/DELETE) are directly available
// - open connection with the server with inherited Open(server,port) function
// - if KeepAlive>0, the connection is not broken: a further request (within
// KeepAlive milliseconds) will use the existing connection if available,
// or recreate a new one if the former is outdated or reset by server
// (will retry only once); this is faster, uses less resources (especialy
// under Windows), and is the recommended way to implement a HTTP/1.1 server
// - on any error (timeout, connection closed) will retry once to get the value
// - don't forget to use Free procedure when you are finished
THttpClientSocket = class(THttpSocket)
protected
fUserAgent: SockString;
fProcessName: SockString;
procedure RequestSendHeader(const url, method: SockString); virtual;
public
/// common initialization of all constructors
// - this overridden method will set the UserAgent with some default value
// - you can customize the default client timeouts by setting appropriate
// aTimeout parameters (in ms) if you left the 0 default parameters,
// it would use global HTTP_DEFAULT_RECEIVETIMEOUT variable values
constructor Create(aTimeOut: PtrInt=0); override;
/// low-level HTTP/1.1 request
// - called by all Get/Head/Post/Put/Delete REST methods
// - after an Open(server,port), return 200,202,204 if OK, http status error otherwise
// - retry is false by caller, and will be recursively called with true to retry once
function Request(const url, method: SockString; KeepAlive: cardinal;
const header, Data, DataType: SockString; retry: boolean): integer; virtual;
/// after an Open(server,port), return 200 if OK, http status error otherwise
// - get the page data in Content
function Get(const url: SockString; KeepAlive: cardinal=0; const header: SockString=''): integer;
/// after an Open(server,port), return 200 if OK, http status error otherwise
// - get the page data in Content
// - if AuthToken<>'', will add an header with 'Authorization: Bearer '+AuthToken
function GetAuth(const url, AuthToken: SockString; KeepAlive: cardinal=0): integer;
/// after an Open(server,port), return 200 if OK, http status error otherwise - only
// header is read from server: Content is always '', but Headers are set
function Head(const url: SockString; KeepAlive: cardinal=0; const header: SockString=''): integer;
/// after an Open(server,port), return 200,201,204 if OK, http status error otherwise
function Post(const url, Data, DataType: SockString; KeepAlive: cardinal=0;
const header: SockString=''): integer;
/// after an Open(server,port), return 200,201,204 if OK, http status error otherwise
function Put(const url, Data, DataType: SockString; KeepAlive: cardinal=0;
const header: SockString=''): integer;
/// after an Open(server,port), return 200,202,204 if OK, http status error otherwise
function Delete(const url: SockString; KeepAlive: cardinal=0; const header: SockString=''): integer;
/// by default, the client is identified as IE 5.5, which is very
// friendly welcome by most servers :(
// - you can specify a custom value here
property UserAgent: SockString read fUserAgent write fUserAgent;
/// the associated process name
property ProcessName: SockString read fProcessName write fProcessName;
end;
/// class-reference type (metaclass) of a HTTP client socket access
// - may be either THttpClientSocket or THttpClientWebSockets (from
// SynBidirSock unit)
THttpClientSocketClass = class of THttpClientSocket;
{$ifndef LVCL}
/// event prototype used e.g. by THttpServerGeneric.OnHttpThreadStart
TNotifyThreadEvent = procedure(Sender: TThread) of object;
{$endif}
{$M+}
TSynThreadPool = class;
/// a simple TThread with a "Terminate" event run in the thread context
// - the TThread.OnTerminate event is run within Synchronize() so did not
// match our expectations to be able to release the resources in the thread
// context which created them (e.g. for COM objects, or some DB drivers)
// - used internally by THttpServerGeneric.NotifyThreadStart() - you should
// not have to use the protected fOnThreadTerminate event handler
// - also define a Start method for compatibility with older versions of Delphi
TSynThread = class(TThread)
protected
// ensure fOnThreadTerminate is called only if NotifyThreadStart has been done
fStartNotified: TObject;
{$ifndef LVCL} // already available in LVCL
// we defined an fOnThreadTerminate event which would be run in the terminated
// thread context (whereas TThread.OnTerminate is called in the main thread)
// -> see THttpServerGeneric.OnHttpThreadTerminate event property
fOnThreadTerminate: TNotifyThreadEvent;
procedure DoTerminate; override;
{$endif}
public
/// initialize the server instance, in non suspended state
constructor Create(CreateSuspended: boolean); reintroduce; virtual;
{$ifndef HASTTHREADSTART}
/// method to be called when the thread was created as suspended
// - Resume is deprecated in the newest RTL, since some OS - e.g. Linux -
// do not implement this pause/resume feature
// - we define here this method for older versions of Delphi
procedure Start;
{$endif}
/// safe version of Sleep() which won't break the thread process
// - returns TRUE if the thread was Terminated
// - returns FALSE if successfully waited up to MS milliseconds
function SleepOrTerminated(MS: cardinal): boolean;
/// defined as public since may be used to terminate the processing methods
property Terminated;
end;
{$M-}
/// HTTP response Thread as used by THttpServer Socket API based class
// - Execute procedure get the request and calculate the answer, using
// the thread for a single client connection, until it is closed
// - you don't have to overload the protected THttpServerResp Execute method:
// override THttpServer.Request() function or, if you need a lower-level access
// (change the protocol, e.g.) THttpServer.Process() method itself
THttpServerResp = class(TSynThread)
protected
fServer: THttpServer;
fServerSock: THttpServerSocket;
fClientSock: TSocket;
fClientSin: TVarSin;
fConnectionID: THttpServerConnectionID;
/// main thread loop: read request from socket, send back answer
procedure Execute; override;
public
/// initialize the response thread for the corresponding incoming socket
// - this version will get the request directly from an incoming socket
constructor Create(aSock: TSocket; const aSin: TVarSin; aServer: THttpServer); reintroduce; overload;
/// initialize the response thread for the corresponding incoming socket
// - this version will handle KeepAlive, for such an incoming request
constructor Create(aServerSock: THttpServerSocket; aServer: THttpServer);
reintroduce; overload; virtual;
/// the associated socket to communicate with the client
property ServerSock: THttpServerSocket read fServerSock;
/// the associated main HTTP server instance
property Server: THttpServer read fServer;
/// the unique identifier of this connection
property ConnectionID: THttpServerConnectionID read fConnectionID;
end;
/// metaclass of HTTP response Thread
THttpServerRespClass = class of THttpServerResp;
{$ifdef MSWINDOWS}
// I/O completion ports API is the best option under Windows
// under Linux/POSIX, we fallback to a classical event-driven pool
{$define USE_WINIOCP}
{$endif MSWINDOWS}
/// defines the sub-threads used by TSynThreadPool
TSynThreadPoolWorkThread = class(TSynThread)
protected
fOwner: TSynThreadPool;
fNotifyThreadStartName: AnsiString;
fThreadNumber: integer;
{$ifndef USE_WINIOCP}
fProcessingContext: pointer;
fEvent: TEvent;
{$endif USE_WINIOCP}
procedure NotifyThreadStart(Sender: TSynThread);
procedure DoTask(Context: pointer); // exception-safe call of fOwner.Task()
public
/// initialize the thread
constructor Create(Owner: TSynThreadPool); reintroduce;
/// finalize the thread
destructor Destroy; override;
/// will loop for any pending task, and execute fOwner.Task()
procedure Execute; override;
end;
TSynThreadPoolWorkThreads = array of TSynThreadPoolWorkThread;
{$M+}
/// a simple Thread Pool, used e.g. for fast handling HTTP requests
// - implemented over I/O Completion Ports under Windows, or a classical
// Event-driven approach under Linux/POSIX
TSynThreadPool = class
protected
fWorkThread: TSynThreadPoolWorkThreads;
fWorkThreadCount: integer;
fRunningThreads: integer;
fExceptionsCount: integer;
fOnThreadTerminate: TNotifyThreadEvent;
fOnThreadStart: TNotifyThreadEvent;
fTerminated: boolean;
fContentionAbortCount: cardinal;
fContentionTime: Int64;
fContentionCount: cardinal;
fContentionAbortDelay: integer;
{$ifdef USE_WINIOCP}
fRequestQueue: THandle; // IOCSP has its own internal queue
{$else}
fQueuePendingContext: boolean;
fPendingContext: array of pointer;
fPendingContextCount: integer;
fSafe: TRTLCriticalSection;
function GetPendingContextCount: integer;
function PopPendingContext: pointer;
function QueueLength: integer; virtual;
{$endif USE_WINIOCP}
/// end thread on IO error
function NeedStopOnIOError: boolean; virtual;
/// process to be executed after notification
procedure Task(aCaller: TSynThread; aContext: Pointer); virtual; abstract;
procedure TaskAbort(aContext: Pointer); virtual;
public
/// initialize a thread pool with the supplied number of threads
// - abstract Task() virtual method will be called by one of the threads
// - up to 256 threads can be associated to a Thread Pool
// - can optionaly accept aOverlapHandle - a handle previously
// opened for overlapped I/O (IOCP) under Windows
// - aQueuePendingContext=true will store the pending context into
// an internal queue, so that Push() always returns true
constructor Create(NumberOfThreads: Integer=32;
{$ifdef USE_WINIOCP}aOverlapHandle: THandle=INVALID_HANDLE_VALUE
{$else}aQueuePendingContext: boolean=false{$endif});
/// shut down the Thread pool, releasing all associated threads
destructor Destroy; override;
/// let a task (specified as a pointer) be processed by the Thread Pool
// - returns false if there is no idle thread available in the pool and
// Create(aQueuePendingContext=false) was used (caller should retry later);
// if aQueuePendingContext was true in Create, or IOCP is used, the supplied
// context will be added to an internal list and handled when possible
// - if aWaitOnContention is default false, returns immediately when the
// queue is full; set aWaitOnContention=true to wait up to
// ContentionAbortDelay ms and retry to queue the task
function Push(aContext: pointer; aWaitOnContention: boolean=false): boolean;
{$ifndef USE_WINIOCP}
/// may be called after Push() returned false to see if queue was actually full
// - returns false if QueuePendingContext is false
function QueueIsFull: boolean;
/// parameter as supplied to Create constructor
property QueuePendingContext: boolean read fQueuePendingContext;
{$endif USE_WINIOCP}
/// low-level access to the threads defined in this thread pool
property WorkThread: TSynThreadPoolWorkThreads read fWorkThread;
published
/// how many threads have been defined in this thread pool
property WorkThreadCount: integer read fWorkThreadCount;
/// how many threads are currently running in this thread pool
property RunningThreads: integer read fRunningThreads;
/// how many tasks were rejected due to thread pool contention
// - if this number is high, consider setting a higher number of threads,
// or profile and tune the Task method
property ContentionAbortCount: cardinal read fContentionAbortCount;
/// milliseconds delay to reject a connection due to contention
// - default is 5000, i.e. 5 seconds wait for some room to be available
// in the IOCP or aQueuePendingContext internal list
// - during this delay, no new connection is available (i.e. Accept is not
// called), so that a load balancer could detect the contention and switch
// to another instance in the pool, or a direct client may eventually have
// its connection rejected, so won't start sending data
property ContentionAbortDelay: integer read fContentionAbortDelay
write fContentionAbortDelay;
/// total milliseconds spent waiting for an available slot in the queue
// - contention won't fail immediately, but will retry until ContentionAbortDelay
// - any high number here requires code refactoring of the Task method
property ContentionTime: Int64 read fContentionTime;
/// how many times the pool waited for an available slot in the queue
// - contention won't fail immediately, but will retry until ContentionAbortDelay
// - any high number here may better increase the threads count
// - use this property and ContentionTime to compute the average contention time
property ContentionCount: cardinal read fContentionCount;
{$ifndef USE_WINIOCP}
/// how many input tasks are currently waiting to be affected to threads
property PendingContextCount: integer read GetPendingContextCount;
{$endif}
end;
{$M-}
/// a simple Thread Pool, used for fast handling HTTP requests of a THttpServer
// - will handle multi-connection with less overhead than creating a thread
// for each incoming request
// - will create a THttpServerResp response thread, if the incoming request is
// identified as HTTP/1.1 keep alive, or HTTP body length is bigger than 1 MB
TSynThreadPoolTHttpServer = class(TSynThreadPool)
protected
fServer: THttpServer;
{$ifndef USE_WINIOCP}
function QueueLength: integer; override;
{$endif}
// here aContext is a THttpServerSocket instance
procedure Task(aCaller: TSynThread; aContext: Pointer); override;
procedure TaskAbort(aContext: Pointer); override;
public
/// initialize a thread pool with the supplied number of threads
// - Task() overridden method processs the HTTP request set by Push()
// - up to 256 threads can be associated to a Thread Pool
constructor Create(Server: THttpServer; NumberOfThreads: Integer=32); reintroduce;
end;
{$M+} // to have existing RTTI for published properties
THttpServerGeneric = class;
{$M-}
/// the server-side available authentication schemes
// - as used by THttpServerRequest.AuthenticationStatus
// - hraNone..hraKerberos will match low-level HTTP_REQUEST_AUTH_TYPE enum as
// defined in HTTP 2.0 API and
THttpServerRequestAuthentication = (
hraNone, hraFailed, hraBasic, hraDigest, hraNtlm, hraNegotiate, hraKerberos);
/// a generic input/output structure used for HTTP server requests
// - URL/Method/InHeaders/InContent properties are input parameters
// - OutContent/OutContentType/OutCustomHeader are output parameters
THttpServerRequest = class
protected
fRemoteIP, fURL, fMethod, fInHeaders, fInContent, fInContentType,
fAuthenticatedUser, fOutContent, fOutContentType, fOutCustomHeaders: SockString;
fServer: THttpServerGeneric;
fRequestID: integer;
fConnectionID: THttpServerConnectionID;
fConnectionThread: TSynThread;
fUseSSL: boolean;
fAuthenticationStatus: THttpServerRequestAuthentication;
{$ifdef MSWINDOWS}
fHttpApiRequest: Pointer;
fFullURL: SockUnicode;
{$endif}
public
/// low-level property which may be used during requests processing
Status: integer;
/// initialize the context, associated to a HTTP server instance
constructor Create(aServer: THttpServerGeneric;
aConnectionID: THttpServerConnectionID; aConnectionThread: TSynThread); virtual;
/// prepare an incoming request
// - will set input parameters URL/Method/InHeaders/InContent/InContentType
// - will reset output parameters
procedure Prepare(const aURL,aMethod,aInHeaders,aInContent,aInContentType,
aRemoteIP: SockString; aUseSSL: boolean=false);
/// append some lines to the InHeaders input parameter
procedure AddInHeader(additionalHeader: SockString);
{$ifdef MSWINDOWS}
/// input parameter containing the caller Full URL
property FullURL: SockUnicode read fFullURL;
{$endif}
/// input parameter containing the caller URI
property URL: SockString read fURL;
/// input parameter containing the caller method (GET/POST...)
property Method: SockString read fMethod;
/// input parameter containing the caller message headers
property InHeaders: SockString read fInHeaders;
/// input parameter containing the caller message body
// - e.g. some GET/POST/PUT JSON data can be specified here
property InContent: SockString read fInContent;
// input parameter defining the caller message body content type
property InContentType: SockString read fInContentType;
/// output parameter to be set to the response message body
property OutContent: SockString read fOutContent write fOutContent;
/// output parameter to define the reponse message body content type
// - if OutContentType is HTTP_RESP_STATICFILE (i.e. '!STATICFILE', defined
// as STATICFILE_CONTENT_TYPE in mORMot.pas), then OutContent is the UTF-8
// file name of a file which must be sent to the client via http.sys or
// NGINX's X-Accel-Redirect header (faster than local buffering/sending)
// - if OutContentType is HTTP_RESP_NORESPONSE (i.e. '!NORESPONSE', defined
// as NORESPONSE_CONTENT_TYPE in mORMot.pas), then the actual transmission
// protocol may not wait for any answer - used e.g. for WebSockets
property OutContentType: SockString read fOutContentType write fOutContentType;
/// output parameter to be sent back as the response message header
// - e.g. to set Content-Type/Location
property OutCustomHeaders: SockString read fOutCustomHeaders write fOutCustomHeaders;
/// the associated server instance
// - may be a THttpServer or a THttpApiServer class
property Server: THttpServerGeneric read fServer;
/// the client remote IP, as specified to Prepare()
property RemoteIP: SockString read fRemoteIP write fRemoteIP;
/// a 31-bit sequential number identifying this instance on the server
property RequestID: integer read fRequestID;
/// the ID of the connection which called this execution context
// - e.g. SynBidirSock's TWebSocketProcess.NotifyCallback method would use
// this property to specify the client connection to be notified
// - is set as an Int64 to match http.sys ID type, but will be an
// increasing 31-bit integer sequence for (web)socket-based servers
property ConnectionID: THttpServerConnectionID read fConnectionID;
/// the thread which owns the connection of this execution context
// - depending on the HTTP server used, may not follow ConnectionID
property ConnectionThread: TSynThread read fConnectionThread;
/// is TRUE if the caller is connected via HTTPS
// - only set for THttpApiServer class yet
property UseSSL: boolean read fUseSSL;
/// contains the THttpServer-side authentication status
// - e.g. when using http.sys authentication with HTTP API 2.0
property AuthenticationStatus: THttpServerRequestAuthentication
read fAuthenticationStatus;
/// contains the THttpServer-side authenticated user name, UTF-8 encoded
// - e.g. when using http.sys authentication with HTTP API 2.0, the
// domain user name is retrieved from the supplied AccessToken
// - could also be set by the THttpServerGeneric.Request() method, after
// proper authentication, so that it would be logged as expected
property AuthenticatedUser: SockString read fAuthenticatedUser;
{$ifdef MSWINDOWS}
/// for THttpApiServer, points to a PHTTP_REQUEST structure
// - not used by now for other servers
property HttpApiRequest: Pointer read fHttpApiRequest;
{$endif}
end;
/// event handler used by THttpServerGeneric.OnRequest property
// - Ctxt defines both input and output parameters
// - result of the function is the HTTP error code (200 if OK, e.g.)
// - OutCustomHeader will handle Content-Type/Location
// - if OutContentType is HTTP_RESP_STATICFILE (i.e. '!STATICFILE' aka
// STATICFILE_CONTENT_TYPE in mORMot.pas), then OutContent is the UTF-8 file
// name of a file which must be sent directly to the client via http.sys or
// NGINX's X-Accel-Redirect; the OutCustomHeader should contain the
// proper 'Content-type: ....' value
TOnHttpServerRequest = function(Ctxt: THttpServerRequest): cardinal of object;
/// event handler used by THttpServerGeneric.OnAfterResponse property
// - Ctxt defines both input and output parameters
// - Code defines the HTTP response code the (200 if OK, e.g.)
TOnHttpServerAfterResponse = procedure(Ctxt: THttpServerRequest;
const Code: cardinal) of object;
/// event handler used by THttpServerGeneric.OnBeforeBody property
// - if defined, is called just before the body is retrieved from the client
// - supplied parameters reflect the current input state
// - should return STATUS_SUCCESS=200 to continue the process, or an HTTP
// error code (e.g. STATUS_FORBIDDEN or STATUS_PAYLOADTOOLARGE) to reject
// the request
TOnHttpServerBeforeBody = function(const aURL,aMethod,aInHeaders,
aInContentType,aRemoteIP: SockString; aContentLength: integer;
aUseSSL: boolean): cardinal of object;
{$M+}
/// abstract class to implement a server thread
// - do not use this class, but rather the THttpServer, THttpApiServer
// or TAsynchFrameServer (as defined in SynBidirSock)
TServerGeneric = class(TSynThread)
protected
fProcessName: SockString;
fOnHttpThreadStart: TNotifyThreadEvent;
procedure SetOnTerminate(const Event: TNotifyThreadEvent); virtual;
procedure NotifyThreadStart(Sender: TSynThread);
public
/// initialize the server instance, in non suspended state
constructor Create(CreateSuspended: boolean; OnStart,OnStop: TNotifyThreadEvent;
const ProcessName: SockString); reintroduce; virtual;
end;
/// abstract class to implement a HTTP server
// - do not use this class, but rather the THttpServer or THttpApiServer
THttpServerGeneric = class(TServerGeneric)
protected
fShutdownInProgress: boolean;
/// optional event handlers for process interception
fOnRequest: TOnHttpServerRequest;
fOnBeforeBody: TOnHttpServerBeforeBody;
fOnBeforeRequest: TOnHttpServerRequest;
fOnAfterRequest: TOnHttpServerRequest;
fOnAfterResponse: TOnHttpServerAfterResponse;
fMaximumAllowedContentLength: cardinal;
/// list of all registered compression algorithms
fCompress: THttpSocketCompressRecDynArray;
/// set by RegisterCompress method
fCompressAcceptEncoding: SockString;
fServerName: SockString;
fCurrentConnectionID: integer; // 31-bit NextConnectionID sequence
fCurrentRequestID: integer;
fCanNotifyCallback: boolean;
fRemoteIPHeader, fRemoteIPHeaderUpper: SockString;
fRemoteConnIDHeader, fRemoteConnIDHeaderUpper: SockString;
function GetAPIVersion: string; virtual; abstract;
procedure SetServerName(const aName: SockString); virtual;
procedure SetOnRequest(const aRequest: TOnHttpServerRequest); virtual;
procedure SetOnBeforeBody(const aEvent: TOnHttpServerBeforeBody); virtual;
procedure SetOnBeforeRequest(const aEvent: TOnHttpServerRequest); virtual;
procedure SetOnAfterRequest(const aEvent: TOnHttpServerRequest); virtual;
procedure SetOnAfterResponse(const aEvent: TOnHttpServerAfterResponse); virtual;
procedure SetMaximumAllowedContentLength(aMax: cardinal); virtual;
procedure SetRemoteIPHeader(const aHeader: SockString); virtual;
procedure SetRemoteConnIDHeader(const aHeader: SockString); virtual;
function GetHTTPQueueLength: Cardinal; virtual; abstract;
procedure SetHTTPQueueLength(aValue: Cardinal); virtual; abstract;
function DoBeforeRequest(Ctxt: THttpServerRequest): cardinal;
function DoAfterRequest(Ctxt: THttpServerRequest): cardinal;
procedure DoAfterResponse(Ctxt: THttpServerRequest;
const Code: cardinal); virtual;
function NextConnectionID: integer; // 31-bit internal sequence
public
/// initialize the server instance, in non suspended state
constructor Create(CreateSuspended: boolean; OnStart,OnStop: TNotifyThreadEvent;
const ProcessName: SockString); reintroduce; virtual;
/// override this function to customize your http server
// - InURL/InMethod/InContent properties are input parameters
// - OutContent/OutContentType/OutCustomHeader are output parameters
// - result of the function is the HTTP error code (200 if OK, e.g.),
// - OutCustomHeader is available to handle Content-Type/Location
// - if OutContentType is HTTP_RESP_STATICFILE (i.e. '!STATICFILE' or
// STATICFILE_CONTENT_TYPE defined in mORMot.pas), then OutContent is the
// UTF-8 file name of a file which must be sent to the client via http.sys or
// NGINX's X-Accel-Redirect (much faster than manual buffering/sending);
// the OutCustomHeader should contain the proper 'Content-type: ....'
// - default implementation is to call the OnRequest event (if existing),
// and will return STATUS_NOTFOUND if OnRequest was not set
// - warning: this process must be thread-safe (can be called by several
// threads simultaneously, but with a given Ctxt instance for each)
function Request(Ctxt: THttpServerRequest): cardinal; virtual;
/// server can send a request back to the client, when the connection has
// been upgraded e.g. to WebSockets
// - InURL/InMethod/InContent properties are input parameters (InContentType
// is ignored)
// - OutContent/OutContentType/OutCustomHeader are output parameters
// - CallingThread should be set to the client's Ctxt.CallingThread
// value, so that the method could know which connnection is to be used -
// it will return STATUS_NOTFOUND (404) if the connection is unknown
// - result of the function is the HTTP error code (200 if OK, e.g.)
// - warning: this void implementation will raise an ECrtSocket exception -
// inherited classes should override it, e.g. as in TWebSocketServerRest
function Callback(Ctxt: THttpServerRequest; aNonBlocking: boolean): cardinal; virtual;
/// will register a compression algorithm
// - used e.g. to compress on the fly the data, with standard gzip/deflate
// or custom (synlzo/synlz) protocols
// - you can specify a minimal size (in bytes) before which the content won't
// be compressed (1024 by default, corresponding to a MTU of 1500 bytes)
// - the first registered algorithm will be the prefered one for compression
procedure RegisterCompress(aFunction: THttpSocketCompress;
aCompressMinSize: integer=1024); virtual;
/// you can call this method to prepare the HTTP server for shutting down
procedure Shutdown;
/// event handler called by the default implementation of the
// virtual Request method
// - warning: this process must be thread-safe (can be called by several
// threads simultaneously)
property OnRequest: TOnHttpServerRequest read fOnRequest write SetOnRequest;
/// event handler called just before the body is retrieved from the client
// - should return STATUS_SUCCESS=200 to continue the process, or an HTTP
// error code to reject the request immediatly, and close the connection
property OnBeforeBody: TOnHttpServerBeforeBody read fOnBeforeBody write SetOnBeforeBody;
/// event handler called after HTTP body has been retrieved, before OnProcess
// - may be used e.g. to return a STATUS_ACCEPTED (202) status to client and
// continue a long-term job inside the OnProcess handler in the same thread;
// or to modify incoming information before passing it to main businnes logic,
// (header preprocessor, body encoding etc...)
// - if the handler returns > 0 server will send a response immediately,
// unless return code is STATUS_ACCEPTED (202), then OnRequest will be called
// - warning: this handler must be thread-safe (can be called by several
// threads simultaneously)
property OnBeforeRequest: TOnHttpServerRequest read fOnBeforeRequest write SetOnBeforeRequest;
/// event handler called after request is processed but before response
// is sent back to client
// - main purpose is to apply post-processor, not part of request logic
// - if handler returns value > 0 it will override the OnProcess response code
// - warning: this handler must be thread-safe (can be called by several
// threads simultaneously)
property OnAfterRequest: TOnHttpServerRequest read fOnAfterRequest write SetOnAfterRequest;
/// event handler called after response is sent back to client
// - main purpose is to apply post-response analysis, logging, etc.
// - warning: this handler must be thread-safe (can be called by several
// threads simultaneously)
property OnAfterResponse: TOnHttpServerAfterResponse read fOnAfterResponse write SetOnAfterResponse;
/// event handler called after each working Thread is just initiated
// - called in the thread context at first place in THttpServerGeneric.Execute
property OnHttpThreadStart: TNotifyThreadEvent
read fOnHttpThreadStart write fOnHttpThreadStart;
/// event handler called when a working Thread is terminating
// - called in the corresponding thread context
// - the TThread.OnTerminate event will be called within a Synchronize()
// wrapper, so it won't fit our purpose
// - to be used e.g. to call CoUnInitialize from thread in which CoInitialize
// was made, for instance via a method defined as such:
// ! procedure TMyServer.OnHttpThreadTerminate(Sender: TObject);
// ! begin // TSQLDBConnectionPropertiesThreadSafe
// ! fMyConnectionProps.EndCurrentThread;
// ! end;
// - is used e.g. by TSQLRest.EndCurrentThread for proper multi-threading
property OnHttpThreadTerminate: TNotifyThreadEvent read fOnThreadTerminate write SetOnTerminate;
/// reject any incoming request with a body size bigger than this value
// - default to 0, meaning any input size is allowed
// - returns STATUS_PAYLOADTOOLARGE = 413 error if "Content-Length" incoming
// header overflow the supplied number of bytes
property MaximumAllowedContentLength: cardinal read fMaximumAllowedContentLength
write SetMaximumAllowedContentLength;
/// defines request/response internal queue length
// - default value if 1000, which sounds fine for most use cases
// - for THttpApiServer, will return 0 if the system does not support HTTP
// API 2.0 (i.e. under Windows XP or Server 2003)
// - for THttpServer, will shutdown any incoming accepted socket if the
// internal TSynThreadPool.PendingContextCount+ThreadCount exceeds this limit;
// each pending connection is a THttpServerSocket instance in the queue
// - increase this value if you don't have any load-balancing in place, and
// in case of e.g. many 503 HTTP answers or if many "QueueFull" messages
// appear in HTTP.sys log files (normally in
// C:\Windows\System32\LogFiles\HTTPERR\httperr*.log) - may appear with
// thousands of concurrent clients accessing at once the same server -
// see @http://msdn.microsoft.com/en-us/library/windows/desktop/aa364501
// - you can use this property with a reverse-proxy as load balancer, e.g.
// with nginx configured as such:
// $ location / {
// $ proxy_pass http://balancing_upstream;
// $ proxy_next_upstream error timeout invalid_header http_500 http_503;
// $ proxy_connect_timeout 2;
// $ proxy_set_header Host $host;
// $ proxy_set_header X-Real-IP $remote_addr;
// $ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
// $ proxy_set_header X-Conn-ID $connection
// $ }
// see https://synopse.info/forum/viewtopic.php?pid=28174#p28174
property HTTPQueueLength: cardinal read GetHTTPQueueLength write SetHTTPQueueLength;
/// TRUE if the inherited class is able to handle callbacks
// - only TWebSocketServer has this ability by now
property CanNotifyCallback: boolean read fCanNotifyCallback;
/// the value of a custom HTTP header containing the real client IP
// - by default, the RemoteIP information will be retrieved from the socket
// layer - but if the server runs behind some proxy service, you should
// define here the HTTP header name which indicates the true remote client
// IP value, mostly as 'X-Real-IP' or 'X-Forwarded-For'
property RemoteIPHeader: SockString read fRemoteIPHeader write SetRemoteIPHeader;
/// the value of a custom HTTP header containing the real client connection ID
// - by default, Ctxt.ConnectionID information will be retrieved from our
// socket layer - but if the server runs behind some proxy service, you should
// define here the HTTP header name which indicates the real remote connection,
// for example as 'X-Conn-ID', setting in nginx config:
// $ proxy_set_header X-Conn-ID $connection
property RemoteConnIDHeader: SockString read fRemoteConnIDHeader write SetRemoteConnIDHeader;
published
/// returns the API version used by the inherited implementation
property APIVersion: string read GetAPIVersion;
/// the Server name, UTF-8 encoded, e.g. 'mORMot/1.18 (Linux)'
// - will be served as "Server: ..." HTTP header
// - for THttpApiServer, when called from the main instance, will propagate
// the change to all cloned instances, and included in any HTTP API 2.0 log
property ServerName: SockString read fServerName write SetServerName;
/// the associated process name
property ProcessName: SockString read fProcessName write fProcessName;
end;
{$ifndef UNICODE}
ULONGLONG = Int64;
{$endif}
{$ifdef MSWINDOWS}
HTTP_OPAQUE_ID = ULONGLONG;
HTTP_REQUEST_ID = HTTP_OPAQUE_ID;
HTTP_URL_GROUP_ID = HTTP_OPAQUE_ID;
HTTP_SERVER_SESSION_ID = HTTP_OPAQUE_ID;
/// http.sys API 2.0 logging file supported layouts
// - match low-level HTTP_LOGGING_TYPE as defined in HTTP 2.0 API
THttpApiLoggingType = (
hltW3C, hltIIS, hltNCSA, hltRaw);
/// http.sys API 2.0 logging file rollover types
// - match low-level HTTP_LOGGING_ROLLOVER_TYPE as defined in HTTP 2.0 API
THttpApiLoggingRollOver = (
hlrSize, hlrDaily, hlrWeekly, hlrMonthly, hlrHourly);
/// http.sys API 2.0 logging option flags
// - used to alter the default logging behavior
// - hlfLocalTimeRollover would force the log file rollovers by local time,
// instead of the default GMT time
// - hlfUseUTF8Conversion will use UTF-8 instead of default local code page
// - only one of hlfLogErrorsOnly and hlfLogSuccessOnly flag could be set
// at a time: if neither of them are present, both errors and success will
// be logged, otherwise mutually exclusive flags could be set to force only
// errors or success logging
// - match low-level HTTP_LOGGING_FLAG_* constants as defined in HTTP 2.0 API
THttpApiLoggingFlags = set of (
hlfLocalTimeRollover, hlfUseUTF8Conversion,
hlfLogErrorsOnly, hlfLogSuccessOnly);
/// http.sys API 2.0 fields used for W3C logging
// - match low-level HTTP_LOG_FIELD_* constants as defined in HTTP 2.0 API
THttpApiLogFields = set of (
hlfDate, hlfTime, hlfClientIP, hlfUserName, hlfSiteName, hlfComputerName,
hlfServerIP, hlfMethod, hlfURIStem, hlfURIQuery, hlfStatus, hlfWIN32Status,
hlfBytesSent, hlfBytesRecv, hlfTimeTaken, hlfServerPort, hlfUserAgent,
hlfCookie, hlfReferer, hlfVersion, hlfHost, hlfSubStatus);
/// http.sys API 2.0 fields used for server-side authentication
// - as used by THttpApiServer.SetAuthenticationSchemes/AuthenticationSchemes
// - match low-level HTTP_AUTH_ENABLE_* constants as defined in HTTP 2.0 API
THttpApiRequestAuthentications = set of (
haBasic, haDigest, haNtlm, haNegotiate, haKerberos);
THttpApiServer = class;
THttpApiServers = array of THttpApiServer;
/// HTTP server using fast http.sys kernel-mode server
// - The HTTP Server API enables applications to communicate over HTTP without
// using Microsoft Internet Information Server (IIS). Applications can register
// to receive HTTP requests for particular URLs, receive HTTP requests, and send
// HTTP responses. The HTTP Server API includes SSL support so that applications
// can exchange data over secure HTTP connections without IIS. It is also
// designed to work with I/O completion ports.
// - The HTTP Server API is supported on Windows Server 2003 operating systems
// and on Windows XP with Service Pack 2 (SP2). Be aware that Microsoft IIS 5
// running on Windows XP with SP2 is not able to share port 80 with other HTTP
// applications running simultaneously.
THttpApiServer = class(THttpServerGeneric)
protected
/// the internal request queue
fReqQueue: THandle;
/// contain list of THttpApiServer cloned instances
fClones: THttpApiServers;
// if cloned, fOwner contains the main THttpApiServer instance
fOwner: THttpApiServer;
/// list of all registered URL
fRegisteredUnicodeUrl: array of SockUnicode;
fServerSessionID: HTTP_SERVER_SESSION_ID;
fUrlGroupID: HTTP_URL_GROUP_ID;
fLogData: pointer;
fLogDataStorage: array of byte;
fLoggingServiceName: SockString;
fAuthenticationSchemes: THttpApiRequestAuthentications;
fReceiveBufferSize: cardinal;
procedure SetReceiveBufferSize(Value: cardinal);
function GetRegisteredUrl: SockUnicode;
function GetCloned: boolean;
function GetHTTPQueueLength: Cardinal; override;
procedure SetHTTPQueueLength(aValue: Cardinal); override;
function GetMaxBandwidth: Cardinal;
procedure SetMaxBandwidth(aValue: Cardinal);
function GetMaxConnections: Cardinal;
procedure SetMaxConnections(aValue: Cardinal);
procedure SetOnTerminate(const Event: TNotifyThreadEvent); override;
function GetAPIVersion: string; override;
function GetLogging: boolean;
procedure SetServerName(const aName: SockString); override;
procedure SetOnRequest(const aRequest: TOnHttpServerRequest); override;
procedure SetOnBeforeBody(const aEvent: TOnHttpServerBeforeBody); override;
procedure SetOnBeforeRequest(const aEvent: TOnHttpServerRequest); override;
procedure SetOnAfterRequest(const aEvent: TOnHttpServerRequest); override;
procedure SetOnAfterResponse(const aEvent: TOnHttpServerAfterResponse); override;
procedure SetMaximumAllowedContentLength(aMax: cardinal); override;
procedure SetRemoteIPHeader(const aHeader: SockString); override;
procedure SetRemoteConnIDHeader(const aHeader: SockString); override;
procedure SetLoggingServiceName(const aName: SockString);
/// server main loop - don't change directly
// - will call the Request public virtual method with the appropriate
// parameters to retrive the content
procedure Execute; override;
/// retrieve flags for SendHttpResponse
// - if response content type is not HTTP_RESP_STATICFILE
function GetSendResponseFlags(Ctxt: THttpServerRequest): integer; virtual;
/// create a clone
constructor CreateClone(From: THttpApiServer); virtual;
/// free resources (for not cloned server)
procedure DestroyMainThread; virtual;
public
/// initialize the HTTP Service
// - will raise an exception if http.sys is not available e.g. before
// Windows XP SP2) or if the request queue creation failed
// - if you override this contructor, put the AddUrl() methods within,
// and you can set CreateSuspended to FALSE
// - if you will call AddUrl() methods later, set CreateSuspended to TRUE,
// then call explicitely the Resume method, after all AddUrl() calls, in
// order to start the server
constructor Create(CreateSuspended: boolean; QueueName: SockUnicode='';
OnStart: TNotifyThreadEvent=nil; OnStop: TNotifyThreadEvent=nil;
const ProcessName: SockString=''); reintroduce;
/// release all associated memory and handles
destructor Destroy; override;
/// will clone this thread into multiple other threads
// - could speed up the process on multi-core CPU
// - will work only if the OnProcess property was set (this is the case
// e.g. in TSQLHttpServer.Create() constructor)
// - maximum value is 256 - higher should not be worth it
procedure Clone(ChildThreadCount: integer);
/// register the URLs to Listen On
// - e.g. AddUrl('root','888')
// - aDomainName could be either a fully qualified case-insensitive domain
// name, an IPv4 or IPv6 literal string, or a wildcard ('+' will bound
// to all domain names for the specified port, '*' will accept the request
// when no other listening hostnames match the request for that port)
// - return 0 (NO_ERROR) on success, an error code if failed: under Vista
// and Seven, you could have ERROR_ACCESS_DENIED if the process is not
// running with enough rights (by default, UAC requires administrator rights
// for adding an URL to http.sys registration list) - solution is to call
// the THttpApiServer.AddUrlAuthorize class method during program setup
// - if this method is not used within an overridden constructor, default
// Create must have be called with CreateSuspended = TRUE and then call the
// Resume method after all Url have been added
// - if aRegisterURI is TRUE, the URI will be registered (need adminitrator
// rights) - default is FALSE, as defined by Windows security policy
function AddUrl(const aRoot, aPort: SockString; Https: boolean=false;
const aDomainName: SockString='*'; aRegisterURI: boolean=false;
aContext: Int64=0): integer;
/// un-register the URLs to Listen On
// - this method expect the same parameters as specified to AddUrl()
// - return 0 (NO_ERROR) on success, an error code if failed (e.g.
// -1 if the corresponding parameters do not match any previous AddUrl)
function RemoveUrl(const aRoot, aPort: SockString; Https: boolean=false;
const aDomainName: SockString='*'): integer;
/// will authorize a specified URL prefix
// - will allow to call AddUrl() later for any user on the computer
// - if aRoot is left '', it will authorize any root for this port
// - must be called with Administrator rights: this class function is to be
// used in a Setup program for instance, especially under Vista or Seven,
// to reserve the Url for the server
// - add a new record to the http.sys URL reservation store
// - return '' on success, an error message otherwise
// - will first delete any matching rule for this URL prefix
// - if OnlyDelete is true, will delete but won't add the new authorization;
// in this case, any error message at deletion will be returned
class function AddUrlAuthorize(const aRoot, aPort: SockString; Https: boolean=false;
const aDomainName: SockString='*'; OnlyDelete: boolean=false): string;
/// will register a compression algorithm
// - overridden method which will handle any cloned instances
procedure RegisterCompress(aFunction: THttpSocketCompress;
aCompressMinSize: integer=1024); override;
/// access to the internal THttpApiServer list cloned by this main instance
// - as created by Clone() method
property Clones: THttpApiServers read fClones;
public { HTTP API 2.0 methods and properties }
/// can be used to check if the HTTP API 2.0 is available
function HasAPI2: boolean;
/// enable HTTP API 2.0 advanced timeout settings
// - all those settings are set for the current URL group
// - will raise an EHttpApiServer exception if the old HTTP API 1.x is used
// so you should better test the availability of the method first:
// ! if aServer.HasAPI2 then
// ! SetTimeOutLimits(....);
// - aEntityBody is the time, in seconds, allowed for the request entity
// body to arrive - default value is 2 minutes
// - aDrainEntityBody is the time, in seconds, allowed for the HTTP Server
// API to drain the entity body on a Keep-Alive connection - default value
// is 2 minutes
// - aRequestQueue is the time, in seconds, allowed for the request to
// remain in the request queue before the application picks it up - default
// value is 2 minutes
// - aIdleConnection is the time, in seconds, allowed for an idle connection;
// is similar to THttpServer.ServerKeepAliveTimeOut - default value is
// 2 minutes
// - aHeaderWait is the time, in seconds, allowed for the HTTP Server API
// to parse the request header - default value is 2 minutes
// - aMinSendRate is the minimum send rate, in bytes-per-second, for the
// response - default value is 150 bytes-per-second
// - any value set to 0 will set the HTTP Server API default value
procedure SetTimeOutLimits(aEntityBody, aDrainEntityBody,
aRequestQueue, aIdleConnection, aHeaderWait, aMinSendRate: cardinal);
/// enable HTTP API 2.0 logging
// - will raise an EHttpApiServer exception if the old HTTP API 1.x is used
// so you should better test the availability of the method first:
// ! if aServer.HasAPI2 then
// ! LogStart(....);
// - this method won't do anything on the cloned instances, but the main
// instance logging state will be replicated to all cloned instances
// - you can select the output folder and the expected logging layout
// - aSoftwareName will set the optional W3C-only software name string
// - aRolloverSize will be used only when aRolloverType is hlrSize
procedure LogStart(const aLogFolder: TFileName;
aType: THttpApiLoggingType=hltW3C;
const aSoftwareName: TFileName='';
aRolloverType: THttpApiLoggingRollOver=hlrDaily;
aRolloverSize: cardinal=0;
aLogFields: THttpApiLogFields=[hlfDate..hlfSubStatus];
aFlags: THttpApiLoggingFlags=[hlfUseUTF8Conversion]);
/// disable HTTP API 2.0 logging
// - this method won't do anything on the cloned instances, but the main
// instance logging state will be replicated to all cloned instances
procedure LogStop;
/// enable HTTP API 2.0 server-side authentication
// - once enabled, the client sends an unauthenticated request: it is up to
// the server application to generate the initial 401 challenge with proper
// WWW-Authenticate headers; any further authentication steps will be
// perform in kernel mode, until the authentication handshake is finalized;
// later on, the application can check the AuthenticationStatus property
// of THttpServerRequest and its associated AuthenticatedUser value
// see https://msdn.microsoft.com/en-us/library/windows/desktop/aa364452
// - will raise an EHttpApiServer exception if the old HTTP API 1.x is used
// so you should better test the availability of the method first:
// ! if aServer.HasAPI2 then
// ! SetAuthenticationSchemes(....);
// - this method will work on the current group, for all instances
// - see HTTPAPI_AUTH_ENABLE_ALL constant to set all available schemes
// - optional Realm parameters can be used when haBasic scheme is defined
// - optional DomainName and Realm parameters can be used for haDigest
procedure SetAuthenticationSchemes(schemes: THttpApiRequestAuthentications;
const DomainName: SockUnicode=''; const Realm: SockUnicode='');
/// read-only access to HTTP API 2.0 server-side enabled authentication schemes
property AuthenticationSchemes: THttpApiRequestAuthentications
read fAuthenticationSchemes;
/// read-only access to check if the HTTP API 2.0 logging is enabled
// - use LogStart/LogStop methods to change this property value
property Logging: boolean read GetLogging;
/// the current HTTP API 2.0 logging Service name
// - should be UTF-8 encoded, if LogStart(aFlags=[hlfUseUTF8Conversion])
// - this value is dedicated to one instance, so the main instance won't
// propagate the change to all cloned instances
property LoggingServiceName: SockString
read fLoggingServiceName write SetLoggingServiceName;
/// read-only access to the low-level HTTP API 2.0 Session ID
property ServerSessionID: HTTP_SERVER_SESSION_ID read fServerSessionID;
/// read-only access to the low-level HTTP API 2.0 URI Group ID
property UrlGroupID: HTTP_URL_GROUP_ID read fUrlGroupID;
/// how many bytes are retrieved in a single call to ReceiveRequestEntityBody
// - set by default to 1048576, i.e. 1 MB - practical limit is around 20 MB
// - you may customize this value if you encounter HTTP error STATUS_NOTACCEPTABLE
// (406) from client, corresponding to an ERROR_NO_SYSTEM_RESOURCES (1450)
// exception on server side, when uploading huge data content
property ReceiveBufferSize: cardinal read fReceiveBufferSize write SetReceiveBufferSize;
published
/// TRUE if this instance is in fact a cloned instance for the thread pool
property Cloned: boolean read GetCloned;
/// return the list of registered URL on this server instance
property RegisteredUrl: SockUnicode read GetRegisteredUrl;
/// the maximum allowed bandwidth rate in bytes per second (via HTTP API 2.0)
// - Setting this value to 0 allows an unlimited bandwidth
// - by default Windows not limit bandwidth (actually limited to 4 Gbit/sec).
// - will return 0 if the system does not support HTTP API 2.0 (i.e.
// under Windows XP or Server 2003)
property MaxBandwidth: Cardinal read GetMaxBandwidth write SetMaxBandwidth;
/// the maximum number of HTTP connections allowed (via HTTP API 2.0)
// - Setting this value to 0 allows an unlimited number of connections
// - by default Windows does not limit number of allowed connections
// - will return 0 if the system does not support HTTP API 2.0 (i.e.
// under Windows XP or Server 2003)
property MaxConnections: Cardinal read GetMaxConnections write SetMaxConnections;
end;
/// low-level API reference to a WebSocket session
WEB_SOCKET_HANDLE = Pointer;
/// WebSocket close status as defined by http://tools.ietf.org/html/rfc6455#section-7.4
WEB_SOCKET_CLOSE_STATUS = Word;
/// the bit values used to construct the WebSocket frame header for httpapi.dll
// - not equals to WINHTTP_WEB_SOCKET_BUFFER_TYPE from winhttp.dll
WEB_SOCKET_BUFFER_TYPE = ULONG;
TSynThreadPoolHttpApiWebSocketServer = class;
TSynWebSocketGuard = class;
THttpApiWebSocketServer = class;
THttpApiWebSocketServerProtocol = class;
/// current state of a THttpApiWebSocketConnection
TWebSocketState = (wsConnecting, wsOpen,
wsClosing, wsClosedByClient, wsClosedByServer, wsClosedByGuard, wsClosedByShutdown);
/// structure representing a single WebSocket connection
{$ifdef UNICODE}
THttpApiWebSocketConnection = record
{$else}
THttpApiWebSocketConnection = object
{$endif}
private
fOverlapped: TOverlapped;
fState: TWebSocketState;
fProtocol: THttpApiWebSocketServerProtocol;
fOpaqueHTTPRequestId: HTTP_REQUEST_ID;
fWSHandle: WEB_SOCKET_HANDLE;
fLastActionContext: Pointer;
fLastReceiveTickCount: Int64;
fPrivateData: pointer;
fBuffer: SockString;
fCloseStatus: WEB_SOCKET_CLOSE_STATUS;
fIndex: integer;
function ProcessActions(ActionQueue: Cardinal): boolean;
function ReadData(const WebsocketBufferData): integer;
procedure WriteData(const WebsocketBufferData);
procedure BeforeRead;
procedure DoOnMessage(aBufferType: WEB_SOCKET_BUFFER_TYPE;
aBuffer: Pointer; aBufferSize: ULONG);
procedure DoOnConnect;
procedure DoOnDisconnect();
procedure InternalSend(aBufferType: WEB_SOCKET_BUFFER_TYPE; WebsocketBufferData: pointer);
procedure Ping;
procedure Disconnect;
procedure CheckIsActive;
// call onAccept Method of protocol, and if protocol not accept connection or
// can not be accepted from other reasons return false else return true
function TryAcceptConnection(aProtocol: THttpApiWebSocketServerProtocol; Ctxt: THttpServerRequest; aNeedHeader: boolean): boolean;
public
/// Index of connection in protocol's connection list
property Index: integer read fIndex;
/// Protocol of connection
property Protocol: THttpApiWebSocketServerProtocol read fProtocol;
/// Custom user data
property PrivateData: pointer read fPrivateData write fPrivateData;
/// Access to the current state of this connection
property State: TWebSocketState read fState;
/// Send data to client
procedure Send(aBufferType: WEB_SOCKET_BUFFER_TYPE; aBuffer: Pointer; aBufferSize: ULONG);
/// Close connection
procedure Close(aStatus: WEB_SOCKET_CLOSE_STATUS; aBuffer: Pointer; aBufferSize: ULONG);
end;
PHttpApiWebSocketConnection = ^THttpApiWebSocketConnection;
THttpApiWebSocketConnectionVector = array[0..MaxInt div SizeOf(PHttpApiWebSocketConnection) - 1] of PHttpApiWebSocketConnection;
PHttpApiWebSocketConnectionVector = ^THttpApiWebSocketConnectionVector;
/// Event handlers for WebSocket
THttpApiWebSocketServerOnAcceptEvent = function(Ctxt: THttpServerRequest;
var Conn: THttpApiWebSocketConnection): Boolean of object;
THttpApiWebSocketServerOnMessageEvent = procedure(const Conn: THttpApiWebSocketConnection;
aBufferType: WEB_SOCKET_BUFFER_TYPE; aBuffer: Pointer; aBufferSize: ULONG) of object;
THttpApiWebSocketServerOnConnectEvent = procedure(const Conn: THttpApiWebSocketConnection) of object;
THttpApiWebSocketServerOnDisconnectEvent = procedure(const Conn: THttpApiWebSocketConnection;
aStatus: WEB_SOCKET_CLOSE_STATUS; aBuffer: Pointer; aBufferSize: ULONG) of object;
/// Protocol Handler of websocket endpoints events
// - maintains a list of all WebSockets clients for a given protocol
THttpApiWebSocketServerProtocol = class
private
fName: SockString;
fManualFragmentManagement: Boolean;
fOnAccept: THttpApiWebSocketServerOnAcceptEvent;
fOnMessage: THttpApiWebSocketServerOnMessageEvent;
fOnFragment: THttpApiWebSocketServerOnMessageEvent;
fOnConnect: THttpApiWebSocketServerOnConnectEvent;
fOnDisconnect: THttpApiWebSocketServerOnDisconnectEvent;
fConnections: PHttpApiWebSocketConnectionVector;
fConnectionsCapacity: Integer;
//Count of used connections. Some of them can be nil(if not used more)
fConnectionsCount: Integer;
fFirstEmptyConnectionIndex: Integer;
fServer: THttpApiWebSocketServer;
fSafe: TRTLCriticalSection;
fPendingForClose: {$ifdef FPC}TFPList{$else}TList{$endif};
fIndex: integer;
function AddConnection(aConn: PHttpApiWebSocketConnection): Integer;
procedure RemoveConnection(index: integer);
procedure doShutdown;
public
/// initialize the WebSockets process
// - if aManualFragmentManagement is true, onMessage will appear only for whole
// received messages, otherwise OnFragment handler must be passed (for video
// broadcast, for example)
constructor Create(const aName: SockString; aManualFragmentManagement: Boolean;
aServer: THttpApiWebSocketServer;
aOnAccept: THttpApiWebSocketServerOnAcceptEvent;
aOnMessage: THttpApiWebSocketServerOnMessageEvent;
aOnConnect: THttpApiWebSocketServerOnConnectEvent;
aOnDisconnect: THttpApiWebSocketServerOnDisconnectEvent;
aOnFragment: THttpApiWebSocketServerOnMessageEvent=nil);
/// finalize the process
destructor Destroy; override;
/// text identifier
property Name: SockString read fName;
/// identify the endpoint instance
property Index: integer read fIndex;
/// OnFragment event will be called for each fragment
property ManualFragmentManagement: Boolean read fManualFragmentManagement;
/// event triggerred when a WebSockets client is initiated
property OnAccept: THttpApiWebSocketServerOnAcceptEvent read fOnAccept;
/// event triggerred when a WebSockets message is received
property OnMessage: THttpApiWebSocketServerOnMessageEvent read fOnMessage;
/// event triggerred when a WebSockets client is connected
property OnConnect: THttpApiWebSocketServerOnConnectEvent read fOnConnect;
/// event triggerred when a WebSockets client is gracefully disconnected
property OnDisconnect: THttpApiWebSocketServerOnDisconnectEvent read fOnDisconnect;
/// event triggerred when a non complete frame is received
// - required if ManualFragmentManagement is true
property OnFragment: THttpApiWebSocketServerOnMessageEvent read fOnFragment;
/// Send message to the WebSocket connection identified by its index
function Send(index: Integer; aBufferType: ULONG; aBuffer: Pointer; aBufferSize: ULONG): boolean;
/// Send message to all connections of this protocol
function Broadcast(aBufferType: ULONG; aBuffer: Pointer; aBufferSize: ULONG): boolean;
/// Close WebSocket connection identified by its index
function Close(index: Integer; aStatus: WEB_SOCKET_CLOSE_STATUS; aBuffer: Pointer; aBufferSize: ULONG): boolean;
end;
THttpApiWebSocketServerProtocolDynArray = array of THttpApiWebSocketServerProtocol;
PHttpApiWebSocketServerProtocolDynArray = ^THttpApiWebSocketServerProtocolDynArray;
/// HTTP & WebSocket server using fast http.sys kernel-mode server
// - can be used like simple THttpApiServer
// - when AddUrlWebSocket is called WebSocket support are added
// in this case WebSocket will receiving the frames in asynchronous
THttpApiWebSocketServer = class(THttpApiServer)
private
fThreadPoolServer: TSynThreadPoolHttpApiWebSocketServer;
fGuard: TSynWebSocketGuard;
fLastConnection: PHttpApiWebSocketConnection;
fPingTimeout: integer;
fRegisteredProtocols: PHttpApiWebSocketServerProtocolDynArray;
fOnWSThreadStart: TNotifyThreadEvent;
fOnWSThreadTerminate: TNotifyThreadEvent;
fSendOverlaped: TOverlapped;
fServiceOverlaped: TOverlapped;
fOnServiceMessage: TThreadMethod;
procedure SetOnWSThreadTerminate(const Value: TNotifyThreadEvent);
function GetProtocol(index: integer): THttpApiWebSocketServerProtocol;
function getProtocolsCount: Integer;
procedure SetOnWSThreadStart(const Value: TNotifyThreadEvent);
protected
function UpgradeToWebSocket(Ctxt: THttpServerRequest): cardinal;
procedure DoAfterResponse(Ctxt: THttpServerRequest;
const Code: cardinal); override;
function GetSendResponseFlags(Ctxt: THttpServerRequest): Integer; override;
constructor CreateClone(From: THttpApiServer); override;
procedure DestroyMainThread; override;
public
/// initialize the HTTPAPI based Server with WebSocket support
// - will raise an exception if http.sys or websocket.dll is not available
// (e.g. before Windows 8) or if the request queue creation failed
// - for aPingTimeout explanation see PingTimeout property documentation
constructor Create(CreateSuspended: Boolean; aSocketThreadsCount: integer=1;
aPingTimeout: integer=0; QueueName: SockUnicode='';
aOnWSThreadStart: TNotifyThreadEvent=nil;
aOnWSThreadTerminate: TNotifyThreadEvent=nil); reintroduce;
/// prepare the process for a given THttpApiWebSocketServerProtocol
procedure RegisterProtocol(const aName: SockString; aManualFragmentManagement: Boolean;
aOnAccept: THttpApiWebSocketServerOnAcceptEvent;
aOnMessage: THttpApiWebSocketServerOnMessageEvent;
aOnConnect: THttpApiWebSocketServerOnConnectEvent;
aOnDisconnect: THttpApiWebSocketServerOnDisconnectEvent;
aOnFragment: THttpApiWebSocketServerOnMessageEvent=nil);
/// register the URLs to Listen on using WebSocket
// - aProtocols is an array of a recond with callbacks, server call during
// WebSocket activity
function AddUrlWebSocket(const aRoot, aPort: SockString; Https: boolean=false;
const aDomainName: SockString='*'; aRegisterURI: boolean=false): integer;
function Request(Ctxt: THttpServerRequest): cardinal; override;
/// Ping timeout in seconds. 0 mean no ping.
// - if connection not receive messages longer than this timeout
// TSynWebSocketGuard will send ping frame
// - if connection not receive any messages longer than double of
// this timeout it will be closed
property PingTimeout: integer read fPingTimeout;
/// access to the associated endpoints
property Protocols[index: integer]: THttpApiWebSocketServerProtocol read GetProtocol;
/// access to the associated endpoints count
property ProtocolsCount: Integer read getProtocolsCount;
/// event called when the processing thread starts
property OnWSThreadStart: TNotifyThreadEvent read FOnWSThreadStart
write SetOnWSThreadStart;
/// event called when the processing thread termintes
property OnWSThreadTerminate: TNotifyThreadEvent read FOnWSThreadTerminate
write SetOnWSThreadTerminate;
/// can be called from any thread
// - will send a "service" message to a WebSocketServer to wake up a WebSocket thread
// - When a webSocket thread receives such a message it will call onServiceMessage in the thread context
procedure SendServiceMessage;
/// event called when a service message is raised
property OnServiceMessage: TThreadMethod read fOnServiceMessage write fOnServiceMessage;
end;
/// a Thread Pool, used for fast handling WebSocket requests
TSynThreadPoolHttpApiWebSocketServer = class(TSynThreadPool)
protected
fServer: THttpApiWebSocketServer;
procedure OnThreadStart(Sender: TThread);
procedure OnThreadTerminate(Sender: TThread);
function NeedStopOnIOError: Boolean; override;
// aContext is a PHttpApiWebSocketConnection, or fServer.fServiceOverlaped
// (SendServiceMessage) or fServer.fSendOverlaped (WriteData)
procedure Task(aCaller: TSynThread; aContext: Pointer); override;
public
/// initialize the thread pool
constructor Create(Server: THttpApiWebSocketServer; NumberOfThreads: Integer=1); reintroduce;
end;
/// Thread for closing WebSocket connections which not response more than PingTimeout interval
TSynWebSocketGuard = class(TThread)
protected
fServer: THttpApiWebSocketServer;
fSmallWait, fWaitCount: integer;
procedure Execute; override;
public
/// initialize the thread
constructor Create(Server: THttpApiWebSocketServer); reintroduce;
end;
{$endif MSWINDOWS}
/// meta-class of the THttpServerSocket process
// - used to override THttpServerSocket.GetRequest for instance
THttpServerSocketClass = class of THttpServerSocket;
/// event handler used by THttpServer.Process to send a local file
// when HTTP_RESP_STATICFILE content-type is returned by the service
// - can be defined e.g. to use NGINX X-Accel-Redirect header
// - should return true if the Context has been modified to serve the file, or
// false so that the file will be manually read and sent from memory
// - any exception during process will be returned as a STATUS_NOTFOUND page
TOnHttpServerSendFile = function(Context: THttpServerRequest;
const LocalFileName: TFileName): boolean of object;
/// main HTTP server Thread using the standard Sockets API (e.g. WinSock)
// - bind to a port and listen to incoming requests
// - assign this requests to THttpServerResp threads from a ThreadPool
// - it implements a HTTP/1.1 compatible server, according to RFC 2068 specifications
// - if the client is also HTTP/1.1 compatible, KeepAlive connection is handled:
// multiple requests will use the existing connection and thread;
// this is faster and uses less resources, especialy under Windows
// - a Thread Pool is used internaly to speed up HTTP/1.0 connections - a
// typical use, under Linux, is to run this class behind a NGINX frontend,
// configured as https reverse proxy, leaving default "proxy_http_version 1.0"
// and "proxy_request_buffering on" options for best performance, and
// setting KeepAliveTimeOut=0 in the THttpServer.Create constructor
// - under windows, will trigger the firewall UAC popup at first run
// - don't forget to use Free method when you are finished
THttpServer = class(THttpServerGeneric)
protected
/// used to protect Process() call
fProcessCS: TRTLCriticalSection;
fHeaderRetrieveAbortDelay: integer;
fThreadPool: TSynThreadPoolTHttpServer;
fInternalHttpServerRespList: {$ifdef FPC}TFPList{$else}TList{$endif};
fServerConnectionCount: integer;
fServerConnectionActive: integer;
fServerKeepAliveTimeOut: cardinal;
fSockPort, fTCPPrefix: SockString;
fSock: TCrtSocket;
fThreadRespClass: THttpServerRespClass;
fOnSendFile: TOnHttpServerSendFile;
fNginxSendFileFrom: array of TFileName;
fHTTPQueueLength: cardinal;
fExecuteState: (esNotStarted, esBinding, esRunning, esFinished);
fStats: array[THttpServerSocketGetRequestResult] of integer;
fSocketClass: THttpServerSocketClass;
fHeadersNotFiltered: boolean;
fExecuteMessage: string;
function GetStat(one: THttpServerSocketGetRequestResult): integer;
function GetHTTPQueueLength: Cardinal; override;
procedure SetHTTPQueueLength(aValue: Cardinal); override;
procedure InternalHttpServerRespListAdd(resp: THttpServerResp);
procedure InternalHttpServerRespListRemove(resp: THttpServerResp);
function OnNginxAllowSend(Context: THttpServerRequest; const LocalFileName: TFileName): boolean;
// this overridden version will return e.g. 'Winsock 2.514'
function GetAPIVersion: string; override;
/// server main loop - don't change directly
procedure Execute; override;
/// this method is called on every new client connection, i.e. every time
// a THttpServerResp thread is created with a new incoming socket
procedure OnConnect; virtual;
/// this method is called on every client disconnection to update stats
procedure OnDisconnect; virtual;
/// override this function in order to low-level process the request;
// default process is to get headers, and call public function Request
procedure Process(ClientSock: THttpServerSocket;
ConnectionID: THttpServerConnectionID; ConnectionThread: TSynThread); virtual;
public
/// create a Server Thread, ready to be bound and listening on a port
// - this constructor will raise a EHttpServer exception if binding failed
// - expects the port to be specified as string, e.g. '1234'; you can
// optionally specify a server address to bind to, e.g. '1.2.3.4:1234'
// - can listed on UDS in case port is specified with 'unix:' prefix, e.g.
// 'unix:/run/myapp.sock'
// - on Linux in case aPort is empty string will check if external fd
// is passed by systemd and use it (so called systemd socked activation)
// - you can specify a number of threads to be initialized to handle
// incoming connections. Default is 32, which may be sufficient for most
// cases, maximum is 256. If you set 0, the thread pool will be disabled
// and one thread will be created for any incoming connection
// - you can also tune (or disable with 0) HTTP/1.1 keep alive delay and
// how incoming request Headers[] are pushed to the processing method
// - this constructor won't actually do the port binding, which occurs in
// the background thread: caller should therefore call WaitStarted after
// THttpServer.Create()
constructor Create(const aPort: SockString; OnStart,OnStop: TNotifyThreadEvent;
const ProcessName: SockString; ServerThreadPoolCount: integer=32;
KeepAliveTimeOut: integer=30000; HeadersUnFiltered: boolean=false;
CreateSuspended: boolean = false); reintroduce; virtual;
/// ensure the HTTP server thread is actually bound to the specified port
// - TCrtSocket.Bind() occurs in the background in the Execute method: you
// should call and check this method result just after THttpServer.Create
// - initial THttpServer design was to call Bind() within Create, which
// works fine on Delphi + Windows, but fails with a EThreadError on FPC/Linux
// - raise a ECrtSocket if binding failed within the specified period (if
// port is free, it would be almost immediate)
// - calling this method is optional, but if the background thread didn't
// actually bind the port, the server will be stopped and unresponsive with
// no explicit error message, until it is terminated
procedure WaitStarted(Seconds: integer = 30); virtual;
/// enable NGINX X-Accel internal redirection for HTTP_RESP_STATICFILE
// - will define internally a matching OnSendFile event handler
// - generating "X-Accel-Redirect: " header, trimming any supplied left
// case-sensitive file name prefix, e.g. with NginxSendFileFrom('/var/www'):
// $ # Will serve /var/www/protected_files/myfile.tar.gz
// $ # When passed URI /protected_files/myfile.tar.gz
// $ location /protected_files {
// $ internal;
// $ root /var/www;
// $ }
// - call this method several times to register several folders
procedure NginxSendFileFrom(const FileNameLeftTrim: TFileName);
/// release all memory and handlers
destructor Destroy; override;
/// by default, only relevant headers are added to internal headers list
// - for instance, Content-Length, Content-Type and Content-Encoding are
// stored as fields in this THttpSocket, but not included in its Headers[]
// - set this property to true to include all incoming headers
property HeadersNotFiltered: boolean read fHeadersNotFiltered;
/// access to the main server low-level Socket
// - it's a raw TCrtSocket, which only need a socket to be bound, listening
// and accept incoming request
// - THttpServerSocket are created on the fly for every request, then
// a THttpServerResp thread is created for handling this THttpServerSocket
property Sock: TCrtSocket read fSock;
/// custom event handler used to send a local file for HTTP_RESP_STATICFILE
// - see also NginxSendFileFrom() method
property OnSendFile: TOnHttpServerSendFile read fOnSendFile write fOnSendFile;
published
/// will contain the current number of connections to the server
property ServerConnectionActive: integer
read fServerConnectionActive write fServerConnectionActive;
/// will contain the total number of connections to the server
// - it's the global count since the server started
property ServerConnectionCount: integer
read fServerConnectionCount write fServerConnectionCount;
/// time, in milliseconds, for the HTTP/1.1 connections to be kept alive
// - default is 30000 ms, i.e. 30 seconds
// - setting 0 here (or in KeepAliveTimeOut constructor parameter) will
// disable keep-alive, and fallback to HTTP.1/0 for all incoming requests
// (may be a good idea e.g. behind a NGINX reverse proxy)
// - see THttpApiServer.SetTimeOutLimits(aIdleConnection) parameter
property ServerKeepAliveTimeOut: cardinal
read fServerKeepAliveTimeOut write fServerKeepAliveTimeOut;
/// the bound TCP port, as specified to Create() constructor
// - TCrtSocket.Bind() occurs in the Execute method
property SockPort: SockString read fSockPort;
/// TCP/IP prefix to mask HTTP protocol
// - if not set, will create full HTTP/1.0 or HTTP/1.1 compliant content
// - in order to make the TCP/IP stream not HTTP compliant, you can specify
// a prefix which will be put before the first header line: in this case,
// the TCP/IP stream won't be recognized as HTTP, and will be ignored by
// most AntiVirus programs, and increase security - but you won't be able
// to use an Internet Browser nor AJAX application for remote access any more
property TCPPrefix: SockString read fTCPPrefix write fTCPPrefix;
/// the associated thread pool
// - may be nil if ServerThreadPoolCount was 0 on constructor
property ThreadPool: TSynThreadPoolTHttpServer read fThreadPool;
/// milliseconds delay to reject a connection due to too long header retrieval
// - default is 0, i.e. not checked (typically not needed behind a reverse proxy)
property HeaderRetrieveAbortDelay: integer read fHeaderRetrieveAbortDelay write fHeaderRetrieveAbortDelay;
/// how many invalid HTTP headers have been rejected
property StatHeaderErrors: integer index grError read GetStat;
/// how many invalid HTTP headers raised an exception
property StatHeaderException: integer index grException read GetStat;
/// how many HTTP requests pushed more than MaximumAllowedContentLength bytes
property StatOversizedPayloads: integer index grOversizedPayload read GetStat;
/// how many HTTP requests were rejected by the OnBeforeBody event handler
property StatRejected: integer index grRejected read GetStat;
/// how many HTTP requests were rejected after HeaderRetrieveAbortDelay timeout
property StatHeaderTimeout: integer index grTimeout read GetStat;
/// how many HTTP headers have been processed
property StatHeaderProcessed: integer index grHeaderReceived read GetStat;
/// how many HTTP bodies have been processed
property StatBodyProcessed: integer index grBodyReceived read GetStat;
/// how many HTTP connections were passed to an asynchronous handler
// - e.g. for background WebSockets processing after proper upgrade
property StatOwnedConnections: integer index grOwned read GetStat;
end;
{$M-}
/// structure used to parse an URI into its components
// - ready to be supplied e.g. to a THttpRequest sub-class
// - used e.g. by class function THttpRequest.Get()
// - will decode standard HTTP/HTTPS urls or Unix sockets URI like
// 'http://unix:/path/to/socket.sock:/url/path'
{$ifdef USERECORDWITHMETHODS}TURI = record
{$else}TURI = object{$endif}
public
/// if the server is accessible via https:// and not plain http://
Https: boolean;
/// either cslTcp for HTTP/HTTPS or cslUnix for Unix socket URI
Layer: TCrtSocketLayer;
/// if the server is accessible via something else than http:// or https://
// - e.g. 'ws' or 'wss' for ws:// or wss://
Scheme: SockString;
/// the server name
// - e.g. 'www.somewebsite.com' or 'path/to/socket.sock' Unix socket URI
Server: SockString;
/// the server port
// - e.g. '80'
Port: SockString;
/// the resource address, including optional parameters
// - e.g. '/category/name/10?param=1'
Address: SockString;
/// fill the members from a supplied URI
// - recognize e.g. 'http://Server:Port/Address', 'https://Server/Address',
// 'Server/Address' (as http), or 'http://unix:/Server:/Address'
// - returns TRUE is at least the Server has been extracted, FALSE on error
function From(aURI: SockString; const DefaultPort: SockString=''): boolean;
/// compute the whole normalized URI
// - e.g. 'https://Server:Port/Address' or 'http://unix:/Server:/Address'
function URI: SockString;
/// the server port, as integer value
function PortInt: integer;
/// compute the root resource Address, without any URI-encoded parameter
// - e.g. '/category/name/10'
function Root: SockString;
/// reset all stored information
procedure Clear;
end;
/// the supported authentication schemes which may be used by HTTP clients
// - supported only by TWinHTTP class yet
THttpRequestAuthentication = (wraNone,wraBasic,wraDigest,wraNegotiate);
/// a record to set some extended options for HTTP clients
// - allow easy propagation e.g. from a TSQLHttpClient* wrapper class to
// the actual SynCrtSock's THttpRequest implementation class
THttpRequestExtendedOptions = record
/// let HTTPS be less paranoid about SSL certificates
// - IgnoreSSLCertificateErrors is handled by TWinHttp and TCurlHTTP
IgnoreSSLCertificateErrors: boolean;
/// allow HTTP authentication to take place at connection
// - Auth.Scheme and UserName/Password properties are handled
// by the TWinHttp class only by now
Auth: record
UserName: SockUnicode;
Password: SockUnicode;
Scheme: THttpRequestAuthentication;
end;
/// allow to customize the User-Agent header
UserAgent: SockString;
end;
{$M+} // to have existing RTTI for published properties
/// abstract class to handle HTTP/1.1 request
// - never instantiate this class, but inherited TWinHTTP, TWinINet or TCurlHTTP
THttpRequest = class
protected
fServer: SockString;
fProxyName: SockString;
fProxyByPass: SockString;
fPort: cardinal;
fHttps: boolean;
fLayer: TCrtSocketLayer;
fKeepAlive: cardinal;
fExtendedOptions: THttpRequestExtendedOptions;
/// used by RegisterCompress method
fCompress: THttpSocketCompressRecDynArray;
/// set by RegisterCompress method
fCompressAcceptEncoding: SockString;
/// set index of protocol in fCompress[], from ACCEPT-ENCODING: header
fCompressAcceptHeader: THttpSocketCompressSet;
fTag: PtrInt;
class function InternalREST(const url,method,data,header: SockString;
aIgnoreSSLCertificateErrors: boolean; outHeaders: PSockString=nil;
outStatus: PInteger=nil): SockString;
// inherited class should override those abstract methods
procedure InternalConnect(ConnectionTimeOut,SendTimeout,ReceiveTimeout: DWORD); virtual; abstract;
procedure InternalCreateRequest(const aMethod,aURL: SockString); virtual; abstract;
procedure InternalSendRequest(const aMethod,aData: SockString); virtual; abstract;
function InternalRetrieveAnswer(var Header,Encoding,AcceptEncoding,
Data: SockString): integer; virtual; abstract;
procedure InternalCloseRequest; virtual; abstract;
procedure InternalAddHeader(const hdr: SockString); virtual; abstract;
public
/// returns TRUE if the class is actually supported on this system
class function IsAvailable: boolean; virtual; abstract;
/// connect to http://aServer:aPort or https://aServer:aPort
// - optional aProxyName may contain the name of the proxy server to use,
// and aProxyByPass an optional semicolon delimited list of host names or
// IP addresses, or both, that should not be routed through the proxy:
// aProxyName/aProxyByPass will be recognized by TWinHTTP and TWinINet,
// and aProxyName will set the CURLOPT_PROXY option to TCurlHttp
// (see https://curl.haxx.se/libcurl/c/CURLOPT_PROXY.html as reference)
// - you can customize the default client timeouts by setting appropriate
// SendTimeout and ReceiveTimeout parameters (in ms) - note that after
// creation of this instance, the connection is tied to the initial
// parameters, so we won't publish any properties to change those
// initial values once created - if you left the 0 default parameters, it
// would use global HTTP_DEFAULT_CONNECTTIMEOUT, HTTP_DEFAULT_SENDTIMEOUT
// and HTTP_DEFAULT_RECEIVETIMEOUT variable values
// - *TimeOut parameters are currently ignored by TCurlHttp
constructor Create(const aServer, aPort: SockString; aHttps: boolean;
const aProxyName: SockString=''; const aProxyByPass: SockString='';
ConnectionTimeOut: DWORD=0; SendTimeout: DWORD=0; ReceiveTimeout: DWORD=0;
aLayer: TCrtSocketLayer=cslTCP); overload; virtual;
/// connect to the supplied URI
// - is just a wrapper around TURI and the overloaded Create() constructor
constructor Create(const aURI: SockString;
const aProxyName: SockString=''; const aProxyByPass: SockString='';
ConnectionTimeOut: DWORD=0; SendTimeout: DWORD=0; ReceiveTimeout: DWORD=0;
aIgnoreSSLCertificateErrors: boolean=false); overload;
/// low-level HTTP/1.1 request
// - after an Create(server,port), return 200,202,204 if OK,
// http status error otherwise
// - KeepAlive is in milliseconds, 0 for "Connection: Close" HTTP/1.0 requests
function Request(const url, method: SockString; KeepAlive: cardinal;
const InHeader, InData, InDataType: SockString;
out OutHeader, OutData: SockString): integer; virtual;
/// wrapper method to retrieve a resource via an HTTP GET
// - will parse the supplied URI to check for the http protocol (HTTP/HTTPS),
// server name and port, and resource name
// - aIgnoreSSLCerticateErrors will ignore the error when using untrusted certificates
// - it will internally create a THttpRequest inherited instance: do not use
// THttpRequest.Get() but either TWinHTTP.Get(), TWinINet.Get() or
// TCurlHTTP.Get() methods
class function Get(const aURI: SockString; const aHeader: SockString='';
aIgnoreSSLCertificateErrors: boolean=true; outHeaders: PSockString=nil;
outStatus: PInteger=nil): SockString;
/// wrapper method to create a resource via an HTTP POST
// - will parse the supplied URI to check for the http protocol (HTTP/HTTPS),
// server name and port, and resource name
// - aIgnoreSSLCerticateErrors will ignore the error when using untrusted certificates
// - the supplied aData content is POSTed to the server, with an optional
// aHeader content
// - it will internally create a THttpRequest inherited instance: do not use
// THttpRequest.Post() but either TWinHTTP.Post(), TWinINet.Post() or
// TCurlHTTP.Post() methods
class function Post(const aURI, aData: SockString; const aHeader: SockString='';
aIgnoreSSLCertificateErrors: boolean=true; outHeaders: PSockString=nil;
outStatus: PInteger=nil): SockString;
/// wrapper method to update a resource via an HTTP PUT
// - will parse the supplied URI to check for the http protocol (HTTP/HTTPS),
// server name and port, and resource name
// - aIgnoreSSLCerticateErrors will ignore the error when using untrusted certificates
// - the supplied aData content is PUT to the server, with an optional
// aHeader content
// - it will internally create a THttpRequest inherited instance: do not use
// THttpRequest.Put() but either TWinHTTP.Put(), TWinINet.Put() or
// TCurlHTTP.Put() methods
class function Put(const aURI, aData: SockString; const aHeader: SockString='';
aIgnoreSSLCertificateErrors: boolean=true; outHeaders: PSockString=nil;
outStatus: PInteger=nil): SockString;
/// wrapper method to delete a resource via an HTTP DELETE
// - will parse the supplied URI to check for the http protocol (HTTP/HTTPS),
// server name and port, and resource name
// - aIgnoreSSLCerticateErrors will ignore the error when using untrusted certificates
// - it will internally create a THttpRequest inherited instance: do not use
// THttpRequest.Delete() but either TWinHTTP.Delete(), TWinINet.Delete() or
// TCurlHTTP.Delete() methods
class function Delete(const aURI: SockString; const aHeader: SockString='';
aIgnoreSSLCertificateErrors: boolean=true; outHeaders: PSockString=nil;
outStatus: PInteger=nil): SockString;
/// will register a compression algorithm
// - used e.g. to compress on the fly the data, with standard gzip/deflate
// or custom (synlzo/synlz) protocols
// - returns true on success, false if this function or this
// ACCEPT-ENCODING: header was already registered
// - you can specify a minimal size (in bytes) before which the content won't
// be compressed (1024 by default, corresponding to a MTU of 1500 bytes)
// - the first registered algorithm will be the prefered one for compression
function RegisterCompress(aFunction: THttpSocketCompress;
aCompressMinSize: integer=1024): boolean;
/// allows to ignore untrusted SSL certificates
// - similar to adding a security exception for a domain in the browser
property IgnoreSSLCertificateErrors: boolean
read fExtendedOptions.IgnoreSSLCertificateErrors
write fExtendedOptions.IgnoreSSLCertificateErrors;
/// optional Authentication Scheme
property AuthScheme: THttpRequestAuthentication
read fExtendedOptions.Auth.Scheme write fExtendedOptions.Auth.Scheme;
/// optional User Name for Authentication
property AuthUserName: SockUnicode
read fExtendedOptions.Auth.UserName write fExtendedOptions.Auth.UserName;
/// optional Password for Authentication
property AuthPassword: SockUnicode
read fExtendedOptions.Auth.Password write fExtendedOptions.Auth.Password;
/// custom HTTP "User Agent:" header value
property UserAgent: SockString
read fExtendedOptions.UserAgent write fExtendedOptions.UserAgent;
/// internal structure used to store extended options
// - will be replicated by IgnoreSSLCertificateErrors and Auth* properties
property ExtendedOptions: THttpRequestExtendedOptions
read fExtendedOptions write fExtendedOptions;
/// some internal field, which may be used by end-user code
property Tag: PtrInt read fTag write fTag;
published
/// the remote server host name, as stated specified to the class constructor
property Server: SockString read fServer;
/// the remote server port number, as specified to the class constructor
property Port: cardinal read fPort;
/// if the remote server uses HTTPS, as specified to the class constructor
property Https: boolean read fHttps;
/// the remote server optional proxy, as specified to the class constructor
property ProxyName: SockString read fProxyName;
/// the remote server optional proxy by-pass list, as specified to the class
// constructor
property ProxyByPass: SockString read fProxyByPass;
end;
{$M-}
/// store the actual class of a HTTP/1.1 client instance
// - may be used to define at runtime which API to be used (e.g. WinHTTP,
// WinINet or LibCurl), following the Liskov substitution principle
THttpRequestClass = class of THttpRequest;
{$ifdef USEWININET}
TWinHttpAPI = class;
/// event callback to track download progress, e.g. in the UI
// - used in TWinHttpAPI.OnProgress property
// - CurrentSize is the current total number of downloaded bytes
// - ContentLength is retrieved from HTTP headers, but may be 0 if not set
TWinHttpProgress = procedure(Sender: TWinHttpAPI;
CurrentSize, ContentLength: DWORD) of object;
/// event callback to process the download by chunks, not in memory
// - used in TWinHttpAPI.OnDownload property
// - CurrentSize is the current total number of downloaded bytes
// - ContentLength is retrieved from HTTP headers, but may be 0 if not set
// - ChunkSize is the size of the latest downloaded chunk, available in
// the untyped ChunkData memory buffer
// - implementation should return TRUE to continue the download, or FALSE
// to abort the download process
TWinHttpDownload = function(Sender: TWinHttpAPI;
CurrentSize, ContentLength, ChunkSize: DWORD; const ChunkData): boolean of object;
/// event callback to track upload progress, e.g. in the UI
// - used in TWinHttpAPI.OnUpload property
// - CurrentSize is the current total number of uploaded bytes
// - ContentLength is the size of content
// - implementation should return TRUE to continue the upload, or FALSE
// to abort the upload process
TWinHttpUpload = function(Sender: TWinHttpAPI;
CurrentSize, ContentLength: DWORD): boolean of object;
/// a class to handle HTTP/1.1 request using either WinINet or WinHTTP API
// - both APIs have a common logic, which is encapsulated by this parent class
// - this abstract class defined some abstract methods which will be
// implemented by TWinINet or TWinHttp with the proper API calls
TWinHttpAPI = class(THttpRequest)
protected
fOnProgress: TWinHttpProgress;
fOnDownload: TWinHttpDownload;
fOnUpload : TWinHttpUpload;
fOnDownloadChunkSize: cardinal;
/// used for internal connection
fSession, fConnection, fRequest: HINTERNET;
/// do not add "Accept: */*" HTTP header by default
fNoAllAccept: boolean;
function InternalGetInfo(Info: DWORD): SockString; virtual; abstract;
function InternalGetInfo32(Info: DWORD): DWORD; virtual; abstract;
function InternalQueryDataAvailable: DWORD; virtual; abstract;
function InternalReadData(var Data: SockString; Read: integer;
Size: cardinal): cardinal; virtual; abstract;
function InternalRetrieveAnswer(
var Header, Encoding, AcceptEncoding, Data: SockString): integer; override;
public
/// returns TRUE if the class is actually supported on this system
class function IsAvailable: boolean; override;
/// do not add "Accept: */*" HTTP header by default
property NoAllAccept: boolean read fNoAllAccept write fNoAllAccept;
/// download would call this method to notify progress of incoming data
property OnProgress: TWinHttpProgress read fOnProgress write fOnProgress;
/// download would call this method instead of filling Data: SockString value
// - may be used e.g. when downloading huge content, and saving directly
// the incoming data on disk or database
// - if this property is set, raw TCP/IP incoming data would be supplied:
// compression and encoding won't be handled by the class
property OnDownload: TWinHttpDownload read fOnDownload write fOnDownload;
/// upload would call this method to notify progress of outgoing data
// - and optionally abort sending the data by returning FALSE
property OnUpload : TWinHttpUpload read fOnUpload write fOnUpload;
/// how many bytes should be retrieved for each OnDownload event chunk
// - if default 0 value is left, would use 65536, i.e. 64KB
property OnDownloadChunkSize: cardinal
read fOnDownloadChunkSize write fOnDownloadChunkSize;
end;
/// a class to handle HTTP/1.1 request using the WinINet API
// - The Microsoft Windows Internet (WinINet) application programming interface
// (API) enables applications to access standard Internet protocols, such as
// FTP and HTTP/HTTPS, similar to what IE offers
// - by design, the WinINet API should not be used from a service, since this
// API may require end-user GUI interaction
// - note: WinINet is MUCH slower than THttpClientSocket or TWinHttp: do not
// use this, only if you find some configuration benefit on some old networks
// (e.g. to diaplay the dialup popup window for a GUI client application)
TWinINet = class(TWinHttpAPI)
protected
// those internal methods will raise an EWinINet exception on error
procedure InternalConnect(ConnectionTimeOut,SendTimeout,ReceiveTimeout: DWORD); override;
procedure InternalCreateRequest(const aMethod,aURL: SockString); override;
procedure InternalCloseRequest; override;
procedure InternalAddHeader(const hdr: SockString); override;
procedure InternalSendRequest(const aMethod,aData: SockString); override;
function InternalGetInfo(Info: DWORD): SockString; override;
function InternalGetInfo32(Info: DWORD): DWORD; override;
function InternalQueryDataAvailable: DWORD; override;
function InternalReadData(var Data: SockString; Read: integer;
Size: cardinal): cardinal; override;
public
/// relase the connection
destructor Destroy; override;
end;
/// WinINet exception type
EWinINet = class(ECrtSocket)
public
/// create a WinINet exception, with the error message as text
constructor Create;
end;
/// a class to handle HTTP/1.1 request using the WinHTTP API
// - has a common behavior as THttpClientSocket() but seems to be faster
// over a network and is able to retrieve the current proxy settings
// (if available) and handle secure https connection - so it seems to be the
// class to use in your client programs
// - WinHTTP does not share any proxy settings with Internet Explorer.
// The WinHTTP proxy configuration is set by either
// $ proxycfg.exe
// on Windows XP and Windows Server 2003 or earlier, either
// $ netsh.exe
// on Windows Vista and Windows Server 2008 or later; for instance,
// you can run either:
// $ proxycfg -u
// $ netsh winhttp import proxy source=ie
// to use the current user's proxy settings for Internet Explorer (under 64-bit
// Vista/Seven, to configure applications using the 32 bit WinHttp settings,
// call netsh or proxycfg bits from %SystemRoot%\SysWOW64 folder explicitely)
// - Microsoft Windows HTTP Services (WinHTTP) is targeted at middle-tier and
// back-end server applications that require access to an HTTP client stack
TWinHTTP = class(TWinHttpAPI)
protected
// you can override this method e.g. to disable/enable some protocols
function InternalGetProtocols: cardinal; virtual;
// those internal methods will raise an EOSError exception on error
procedure InternalConnect(ConnectionTimeOut,SendTimeout,ReceiveTimeout: DWORD); override;
procedure InternalCreateRequest(const aMethod,aURL: SockString); override;
procedure InternalCloseRequest; override;
procedure InternalAddHeader(const hdr: SockString); override;
procedure InternalSendRequest(const aMethod,aData: SockString); override;
function InternalGetInfo(Info: DWORD): SockString; override;
function InternalGetInfo32(Info: DWORD): DWORD; override;
function InternalQueryDataAvailable: DWORD; override;
function InternalReadData(var Data: SockString; Read: integer;
Size: cardinal): cardinal; override;
public
/// relase the connection
destructor Destroy; override;
end;
/// WinHTTP exception type
EWinHTTP = class(Exception);
/// types of WebSocket buffers for winhttp.dll
// it is the different thing than WEB_SOCKET_BUFFER_TYPE for httpapi.dll
WINHTTP_WEB_SOCKET_BUFFER_TYPE = ULONG;
/// A class to establish a client connection to a WebSocket server using Windows API
// - used by TWinWebSocketClient class
TWinHTTPUpgradeable = class(TWinHTTP)
private
fSocket: HINTERNET;
protected
function InternalRetrieveAnswer(var Header, Encoding, AcceptEncoding,
Data: SockString): integer; override;
procedure InternalSendRequest(const aMethod,aData: SockString); override;
public
/// initialize the instance
constructor Create(const aServer, aPort: SockString; aHttps: boolean;
const aProxyName: SockString=''; const aProxyByPass: SockString='';
ConnectionTimeOut: DWORD=0; SendTimeout: DWORD=0;
ReceiveTimeout: DWORD=0; aLayer: TCrtSocketLayer=cslTCP); override;
end;
/// WebSocket client implementation
TWinHTTPWebSocketClient = class
protected
fSocket: HINTERNET;
function CheckSocket: Boolean;
public
/// initialize the instance
// - all parameters do match TWinHTTP.Create except url: address of WebSocketServer
// for sending upgrade request
constructor Create(const aServer, aPort: SockString; aHttps: boolean;
const url: SockString; const aSubProtocol: SockString = '';
const aProxyName: SockString=''; const aProxyByPass: SockString='';
ConnectionTimeOut: DWORD=0; SendTimeout: DWORD=0; ReceiveTimeout: DWORD=0);
/// Send buffer
function Send(aBufferType: WINHTTP_WEB_SOCKET_BUFFER_TYPE; aBuffer: pointer;
aBufferLength: DWORD): DWORD;
/// Receive buffer
function Receive(aBuffer: pointer; aBufferLength: DWORD;
out aBytesRead: DWORD; out aBufferType: WINHTTP_WEB_SOCKET_BUFFER_TYPE): DWORD;
/// Close current connection
function CloseConnection(const aCloseReason: SockString): DWORD;
destructor Destroy; override;
end;
{$endif USEWININET}
{$ifdef USELIBCURL}
type
/// libcurl exception type
ECurlHTTP = class(Exception);
/// a class to handle HTTP/1.1 request using the libcurl library
// - libcurl is a free and easy-to-use cross-platform URL transfer library,
// able to directly connect via HTTP or HTTPS on most Linux systems
// - under a 32 bit Linux system, the libcurl library (and its dependencies,
// like OpenSSL) may not be installed - you can add it via your package
// manager, e.g. on Ubuntu:
// $ sudo apt-get install libcurl3
// - under a 64-bit Linux system, if compiled with Kylix, you should install
// the 32-bit flavor of libcurl, e.g. on Ubuntu:
// $ sudo apt-get install libcurl3:i386
// - will use in fact libcurl.so, so either libcurl.so.3 or libcurl.so.4,
// depending on the default version available on the system
TCurlHTTP = class(THttpRequest)
protected
fHandle: pointer;
fRootURL: SockString;
fIn: record
Headers: pointer;
DataOffset: integer;
URL, Method, Data: SockString;
end;
fOut: record
Header, Encoding, AcceptEncoding, Data: SockString;
end;
fSSL: record
CertFile, CACertFile, KeyName, PassPhrase: SockString;
end;
procedure InternalConnect(ConnectionTimeOut,SendTimeout,ReceiveTimeout: DWORD); override;
procedure InternalCreateRequest(const aMethod,aURL: SockString); override;
procedure InternalSendRequest(const aMethod,aData: SockString); override;
function InternalRetrieveAnswer(var Header,Encoding,AcceptEncoding, Data: SockString): integer; override;
procedure InternalCloseRequest; override;
procedure InternalAddHeader(const hdr: SockString); override;
function GetCACertFile: SockString;
procedure SetCACertFile(const aCertFile: SockString);
public
/// returns TRUE if the class is actually supported on this system
class function IsAvailable: boolean; override;
/// release the connection
destructor Destroy; override;
/// allow to set a CA certification file without touching the client certification
property CACertFile: SockString read GetCACertFile write SetCACertFile;
/// set the client SSL certification details
// - see CACertFile if you don't want to change the whole client cert info
// - used e.g. as
// ! UseClientCertificate('testcert.pem','cacert.pem','testkey.pem','pass');
procedure UseClientCertificate(
const aCertFile, aCACertFile, aKeyName, aPassPhrase: SockString);
end;
{$endif USELIBCURL}
/// simple wrapper around THttpClientSocket/THttpRequest instances
// - this class will reuse the previous connection if possible, and select the
// best connection class available on this platform for a given URI
TSimpleHttpClient = class
protected
fHttp: THttpClientSocket;
fHttps: THttpRequest;
fProxy, fBody, fHeaders, fUserAgent: SockString;
fOnlyUseClientSocket, fIgnoreSSLCertificateErrors: boolean;
public
/// initialize the instance
constructor Create(aOnlyUseClientSocket: boolean=false); reintroduce;
/// finalize the connection
destructor Destroy; override;
/// low-level entry point of this instance
function RawRequest(const Uri: TURI; const Method, Header, Data, DataType: SockString;
KeepAlive: cardinal): integer; overload;
/// simple-to-use entry point of this instance
// - use Body and Headers properties to retrieve the HTTP body and headers
function Request(const uri: SockString; const method: SockString='GET';
const header: SockString = ''; const data: SockString = '';
const datatype: SockString = ''; keepalive: cardinal=10000): integer; overload;
/// returns the HTTP body as returned by a previous call to Request()
property Body: SockString read fBody;
/// returns the HTTP headers as returned by a previous call to Request()
property Headers: SockString read fHeaders;
/// allows to customize the user-agent header
property UserAgent: SockString read fUserAgent write fUserAgent;
/// allows to customize HTTPS connection and allow weak certificates
property IgnoreSSLCertificateErrors: boolean read fIgnoreSSLCertificateErrors
write fIgnoreSSLCertificateErrors;
/// alows to customize the connection using a proxy
property Proxy: SockString read fProxy write fProxy;
end;
/// returns the best THttpRequest class, depending on the system it runs on
// - e.g. TWinHTTP or TCurlHTTP
// - consider using TSimpleHttpClient if you just need a simple connection
function MainHttpClass: THttpRequestClass;
/// low-level forcing of another THttpRequest class
// - could be used if we found out that the current MainHttpClass failed (which
// could easily happen with TCurlHTTP if the library is missing or deprecated)
procedure ReplaceMainHttpClass(aClass: THttpRequestClass);
/// create a TCrtSocket, returning nil on error
// (useful to easily catch socket error exception ECrtSocket)
function Open(const aServer, aPort: SockString; aTLS: boolean=false): TCrtSocket;
/// create a THttpClientSocket, returning nil on error
// - useful to easily catch socket error exception ECrtSocket
function OpenHttp(const aServer, aPort: SockString; aTLS: boolean=false;
aLayer: TCrtSocketLayer = cslTCP): THttpClientSocket; overload;
/// create a THttpClientSocket, returning nil on error
// - useful to easily catch socket error exception ECrtSocket
function OpenHttp(const aURI: SockString; aAddress: PSockString=nil): THttpClientSocket; overload;
/// retrieve the content of a web page, using the HTTP/1.1 protocol and GET method
// - this method will use a low-level THttpClientSock socket: if you want
// something able to use your computer proxy, take a look at TWinINet.Get() or
// the overloaded HttpGet() methods
function HttpGet(const server, port: SockString; const url: SockString;
const inHeaders: SockString; outHeaders: PSockString=nil;
aLayer: TCrtSocketLayer = cslTCP): SockString; overload;
/// retrieve the content of a web page, using the HTTP/1.1 protocol and GET method
// - this method will use a low-level THttpClientSock socket for plain http URI,
// or TWinHTTP/TCurlHTTP for any https URI, or if forceNotSocket is set to true
function HttpGet(const aURI: SockString; outHeaders: PSockString=nil;
forceNotSocket: boolean=false; outStatus: PInteger=nil): SockString; overload;
/// retrieve the content of a web page, using the HTTP/1.1 protocol and GET method
// - this method will use a low-level THttpClientSock socket for plain http URI,
// or TWinHTTP/TCurlHTTP for any https URI
function HttpGet(const aURI: SockString; const inHeaders: SockString;
outHeaders: PSockString=nil; forceNotSocket: boolean=false;
outStatus: PInteger=nil): SockString; overload;
/// retrieve the content of a web page, using HTTP/1.1 GET method and a token
// - this method will use a low-level THttpClientSock socket and its GetAuth method
// - if AuthToken<>'', will add an header with 'Authorization: Bearer '+AuthToken
function HttpGetAuth(const aURI, aAuthToken: SockString;
outHeaders: PSockString=nil; forceNotSocket: boolean=false;
outStatus: PInteger=nil): SockString;
/// send some data to a remote web server, using the HTTP/1.1 protocol and POST method
function HttpPost(const server, port: SockString; const url, Data, DataType: SockString;
outData: PSockString=nil; const auth: SockString=''): boolean;
/// send some data to a remote web server, using the HTTP/1.1 protocol and PUT method
function HttpPut(const server, port: SockString; const url, Data, DataType: SockString;
outData: PSockString=nil; const auth: SockString=''): boolean;
/// compute the 'Authorization: Bearer ####' HTTP header of a given token value
function AuthorizationBearer(const AuthToken: SockString): SockString;
/// compute the '1.2.3.4' text representation of a raw IP4 binary
procedure IP4Text(const ip4addr; var result: SockString); overload;
/// compute the text representation of a IP4/IP6 low-level connection
procedure IPText(const sin: TVarSin; var result: SockString;
localasvoid: boolean=false);
var
/// defines if a connection from the loopback should be reported as ''
// (no Remote-IP - which is the default) or as '127.0.0.1' (force to false)
// - used by both TCrtSock.AcceptRequest and THttpApiServer.Execute servers
RemoteIPLocalHostAsVoidInServers: boolean = true;
const
/// the layout of TSMTPConnection.FromText method
SMTP_DEFAULT = 'user:password@smtpserver:port';
type
/// may be used to store a connection to a SMTP server
// - see SendEmail() overloaded function
{$ifdef USERECORDWITHMETHODS}TSMTPConnection = record
{$else}TSMTPConnection = object{$endif}
public
/// the SMTP server IP or host name
Host: SockString;
/// the SMTP server port (25 by default)
Port: SockString;
/// the SMTP user login (if any)
User: SockString;
/// the SMTP user password (if any)
Pass: SockString;
/// fill the STMP server information from a single text field
// - expects 'user:password@smtpserver:port' format
// - if aText equals SMTP_DEFAULT ('user:password@smtpserver:port'),
// does nothing
function FromText(const aText: SockString): boolean;
end;
/// send an email using the SMTP protocol
// - retry true on success
// - the Subject is expected to be in plain 7 bit ASCII, so you could use
// SendEmailSubject() to encode it as Unicode, if needed
// - you can optionally set the encoding charset to be used for the Text body
function SendEmail(const Server, From, CSVDest, Subject, Text: SockString;
const Headers: SockString=''; const User: SockString=''; const Pass: SockString='';
const Port: SockString='25'; const TextCharSet: SockString = 'ISO-8859-1';
aTLS: boolean=false): boolean; overload;
/// send an email using the SMTP protocol
// - retry true on success
// - the Subject is expected to be in plain 7 bit ASCII, so you could use
// SendEmailSubject() to encode it as Unicode, if needed
// - you can optionally set the encoding charset to be used for the Text body,
// or even TextCharSet='JSON' to force application/json
function SendEmail(const Server: TSMTPConnection;
const From, CSVDest, Subject, Text: SockString; const Headers: SockString='';
const TextCharSet: SockString = 'ISO-8859-1'; aTLS: boolean=false): boolean; overload;
/// convert a supplied subject text into an Unicode encoding
// - will convert the text into UTF-8 and append '=?UTF-8?B?'
// - for pre-Unicode versions of Delphi, Text is expected to be already UTF-8
// encoded - since Delphi 2010, it will be converted from UnicodeString
function SendEmailSubject(const Text: string): SockString;
const
/// HTTP Status Code for "Success"
STATUS_SUCCESS = 200;
/// HTTP Status Code for "Created"
STATUS_CREATED = 201;
/// HTTP Status Code for "Accepted"
STATUS_ACCEPTED = 202;
/// HTTP Status Code for "No Content"
STATUS_NOCONTENT = 204;
/// HTTP Status Code for "Partial Content"
STATUS_PARTIALCONTENT = 206;
/// HTTP Status Code for "Not Modified"
STATUS_NOTMODIFIED = 304;
/// HTTP Status Code for "Bad Request"
STATUS_BADREQUEST = 400;
/// HTTP Status Code for "Unauthorized"
STATUS_UNAUTHORIZED = 401;
/// HTTP Status Code for "Forbidden"
STATUS_FORBIDDEN = 403;
/// HTTP Status Code for "Not Found"
STATUS_NOTFOUND = 404;
/// HTTP Status Code for "Not Acceptable"
STATUS_NOTACCEPTABLE = 406;
/// HTTP Status Code for "Payload Too Large"
STATUS_PAYLOADTOOLARGE = 413;
/// HTTP Status Code for "Internal Server Error"
STATUS_SERVERERROR = 500;
/// HTTP Status Code for "Not Implemented"
STATUS_NOTIMPLEMENTED = 501;
/// HTTP Status Code for "HTTP Version Not Supported"
STATUS_HTTPVERSIONNONSUPPORTED = 505;
{$ifdef MSWINDOWS}
/// can be used with THttpApiServer.AuthenticationSchemes to enable all schemes
HTTPAPI_AUTH_ENABLE_ALL = [hraBasic..hraKerberos];
/// the buffer contains the last, and possibly only, part of a UTF8 message
WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE: WEB_SOCKET_BUFFER_TYPE = $80000000;
/// the buffer contains part of a UTF8 message
WEB_SOCKET_UTF8_FRAGMENT_BUFFER_TYPE: WEB_SOCKET_BUFFER_TYPE = $80000001;
/// the buffer contains the last, and possibly only, part of a binary message
WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE: WEB_SOCKET_BUFFER_TYPE = $80000002;
/// the buffer contains part of a binary message
WEB_SOCKET_BINARY_FRAGMENT_BUFFER_TYPE: WEB_SOCKET_BUFFER_TYPE = $80000003;
/// the buffer contains a close message
WEB_SOCKET_CLOSE_BUFFER_TYPE: WEB_SOCKET_BUFFER_TYPE = $80000004;
/// the buffer contains a ping or pong message
// - when sending, this value means 'ping'
// - when processing received data, this value means 'pong'
WEB_SOCKET_PING_PONG_BUFFER_TYPE: WEB_SOCKET_BUFFER_TYPE = $80000005;
/// the buffer contains an unsolicited pong message
WEB_SOCKET_UNSOLICITED_PONG_BUFFER_TYPE: WEB_SOCKET_BUFFER_TYPE = $80000006;
// https://msdn.microsoft.com/en-us/library/windows/desktop/hh449347
WEB_SOCKET_MAX_CLOSE_REASON_LENGTH = 123;
/// Close completed successfully
WEB_SOCKET_SUCCESS_CLOSE_STATUS : WEB_SOCKET_CLOSE_STATUS = 1000;
/// The endpoint is going away and thus closing the connection
WEB_SOCKET_ENDPOINT_UNAVAILABLE_CLOSE_STATUS : WEB_SOCKET_CLOSE_STATUS = 1001;
/// Peer detected protocol error and it is closing the connection
WEB_SOCKET_PROTOCOL_ERROR_CLOSE_STATUS : WEB_SOCKET_CLOSE_STATUS = 1002;
/// The endpoint cannot receive this type of data
WEB_SOCKET_INVALID_DATA_TYPE_CLOSE_STATUS : WEB_SOCKET_CLOSE_STATUS = 1003;
/// No close status code was provided
WEB_SOCKET_EMPTY_CLOSE_STATUS : WEB_SOCKET_CLOSE_STATUS = 1005;
/// The connection was closed without sending or receiving a close frame
WEB_SOCKET_ABORTED_CLOSE_STATUS : WEB_SOCKET_CLOSE_STATUS = 1006;
/// Data within a message is not consistent with the type of the message
WEB_SOCKET_INVALID_PAYLOAD_CLOSE_STATUS : WEB_SOCKET_CLOSE_STATUS = 1007;
/// The message violates an endpoint's policy
WEB_SOCKET_POLICY_VIOLATION_CLOSE_STATUS : WEB_SOCKET_CLOSE_STATUS = 1008;
/// The message sent was too large to process
WEB_SOCKET_MESSAGE_TOO_BIG_CLOSE_STATUS : WEB_SOCKET_CLOSE_STATUS = 1009;
/// A client endpoint expected the server to negotiate one or more extensions,
// but the server didn't return them in the response message of the WebSocket handshake
WEB_SOCKET_UNSUPPORTED_EXTENSIONS_CLOSE_STATUS : WEB_SOCKET_CLOSE_STATUS = 1010;
/// An unexpected condition prevented the server from fulfilling the request
WEB_SOCKET_SERVER_ERROR_CLOSE_STATUS : WEB_SOCKET_CLOSE_STATUS = 1011;
/// The TLS handshake could not be completed
WEB_SOCKET_SECURE_HANDSHAKE_ERROR_CLOSE_STATUS : WEB_SOCKET_CLOSE_STATUS = 1015;
{$endif MSWINDOWS}
/// retrieve the HTTP reason text from a code
// - e.g. StatusCodeToReason(200)='OK'
// - see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
// - mORMot.StatusCodeToErrorMsg() will call this function
function StatusCodeToReason(Code: cardinal): SockString;
/// retrieve the IP address from a computer name
function ResolveName(const Name: SockString;
Family: Integer=AF_INET; SockProtocol: Integer=IPPROTO_TCP;
SockType: integer=SOCK_STREAM): SockString;
/// Base64 encoding of a string
// - used internally for STMP email sending
// - consider using more efficient BinToBase64() from SynCommons.pas instead
function SockBase64Encode(const s: SockString): SockString;
/// Base64 decoding of a string
// - consider using more efficient Base64ToBin() from SynCommons.pas instead
function SockBase64Decode(const s: SockString): SockString;
/// escaping of HTML codes like < > & "
function HtmlEncode(const s: SockString): SockString;
/// decode a HTTP chunk length
function HttpChunkToHex32(p: PAnsiChar): integer;
{$ifdef MSWINDOWS}
/// remotly get the MAC address of a computer, from its IP Address
// - only works under Win2K and later
// - return the MAC address as a 12 hexa chars ('0050C204C80A' e.g.)
function GetRemoteMacAddress(const IP: SockString): SockString;
{$else}
/// returns how many files could be opened at once on this POSIX system
// - hard=true is for the maximum allowed limit, false for the current process
// - returns -1 if the getrlimit() API call failed
function GetFileOpenLimit(hard: boolean=false): integer;
/// changes how many files could be opened at once on this POSIX system
// - hard=true is for the maximum allowed limit (requires root priviledges),
// false for the current process
// - returns the new value set (may not match the expected max value on error)
// - returns -1 if the getrlimit().setrlimit() API calls failed
// - for instance, to set the limit of the current process to its highest value:
// ! SetFileOpenLimit(GetFileOpenLimit(true));
function SetFileOpenLimit(max: integer; hard: boolean=false): integer;
{$endif MSWINDOWS}
type
TIPAddress = (tiaAny, tiaPublic, tiaPrivate);
/// enumerate all IP addresses of the current computer
// - may be used to enumerate all adapters
function GetIPAddresses(Kind: TIPAddress = tiaAny): TSockStringDynArray;
/// returns all IP addresses of the current computer as a single CSV text
// - may be used to enumerate all adapters
function GetIPAddressesText(const Sep: SockString = ' ';
PublicOnly: boolean = false): SockString;
type
/// interface name/address pairs as returned by GetMacAddresses
TMacAddress = record
/// contains e.g. 'eth0' on Linux
name: SockString;
/// contains e.g. '12:50:b6:1e:c6:aa' from /sys/class/net/eth0/adddress
address: SockString;
end;
TMacAddressDynArray = array of TMacAddress;
/// enumerate all Mac addresses of the current computer
function GetMacAddresses: TMacAddressDynArray;
/// enumerate all Mac addresses of the current computer as 'name1=addr1 name2=addr2'
function GetMacAddressesText: SockString;
/// low-level text description of Socket error code
// - if Error is -1, will call WSAGetLastError to retrieve the last error code
function SocketErrorMessage(Error: integer=-1): string;
/// low-level direct creation of a TSocket handle for TCP, UDP or UNIX layers
// - doBind=true will call Bind() to create a server socket instance
// - doBind=false will call Connect() to create a client socket instance
function CallServer(const Server, Port: SockString; doBind: boolean;
aLayer: TCrtSocketLayer; ConnectTimeout: DWORD): TSocket;
/// retrieve the text-converted remote IP address of a client socket
function GetRemoteIP(aClientSock: TSocket): SockString;
/// low-level direct shutdown of a given socket
procedure DirectShutdown(sock: TSocket; rdwr: boolean=false);
/// low-level change of a socket to be in non-blocking mode
// - used e.g. by TPollAsynchSockets.Start
function AsynchSocket(sock: TSocket): boolean;
/// low-level direct call of the socket recv() function
// - by-pass overriden blocking recv() e.g. in SynFPCSock, so will work if
// the socket is in non-blocking mode, as with AsynchSocket/TPollAsynchSockets
function AsynchRecv(sock: TSocket; buf: pointer; buflen: integer): integer;
/// low-level direct call of the socket send() function
// - by-pass overriden blocking send() e.g. in SynFPCSock, so will work if
// the socket is in non-blocking mode, as with AsynchSocket/TPollAsynchSockets
function AsynchSend(sock: TSocket; buf: pointer; buflen: integer): integer;
{ ************ socket polling optimized for multiple connections }
type
/// the events monitored by TPollSocketAbstract classes
// - we don't make any difference between urgent or normal read/write events
TPollSocketEvent = (pseRead, pseWrite, pseError, pseClosed);
/// set of events monitored by TPollSocketAbstract classes
TPollSocketEvents = set of TPollSocketEvent;
/// some opaque value (which may be a pointer) associated with a polling event
TPollSocketTag = type PtrInt;
/// modifications notified by TPollSocketAbstract.WaitForModified
TPollSocketResult = record
/// the events which are notified
events: TPollSocketEvents;
/// opaque value as defined by TPollSocketAbstract.Subscribe
tag: TPollSocketTag;
end;
/// all modifications returned by TPollSocketAbstract.WaitForModified
TPollSocketResults = array of TPollSocketResult;
{$M+}
/// abstract parent class for efficient socket polling
// - works like Linux epoll API in level-triggered (LT) mode
// - implements libevent-like cross-platform features
// - use PollSockClass global function to retrieve the best class depending
// on the running Operating System
TPollSocketAbstract = class
protected
fCount: integer;
fMaxSockets: integer;
public
/// class function factory, returning a socket polling instance matching
// at best the current operating system
// - returns a TPollSocketSelect/TPollSocketPoll instance under Windows,
// a TPollSocketEpoll instance under Linux, or a TPollSocketPoll on BSD
// - just a wrapper around PollSockClass.Create
class function New: TPollSocketAbstract;
/// initialize the polling
constructor Create; virtual;
/// track status modifications on one specified TSocket
// - you can specify which events are monitored - pseError and pseClosed
// will always be notified
// - tag parameter will be returned as TPollSocketResult - you may set
// here the socket file descriptor value, or a transtyped class instance
// - similar to epoll's EPOLL_CTL_ADD control interface
function Subscribe(socket: TSocket; events: TPollSocketEvents;
tag: TPollSocketTag): boolean; virtual; abstract;
/// stop status modifications tracking on one specified TSocket
// - the socket should have been monitored by a previous call to Subscribe()
// - on success, returns true and fill tag with the associated opaque value
// - similar to epoll's EPOLL_CTL_DEL control interface
function Unsubscribe(socket: TSocket): boolean; virtual; abstract;
/// waits for status modifications of all tracked TSocket
// - will wait up to timeoutMS milliseconds, 0 meaning immediate return
// and -1 for infinite blocking
// - returns -1 on error (e.g. no TSocket currently registered), or
// the number of modifications stored in results[] (may be 0 if none)
function WaitForModified(out results: TPollSocketResults;
timeoutMS: integer): integer; virtual; abstract;
published
/// how many TSocket instances could be tracked, at most
// - depends on the API used
property MaxSockets: integer read fMaxSockets;
/// how many TSocket instances are currently tracked
property Count: integer read fCount;
end;
{$M-}
/// meta-class of TPollSocketAbstract socket polling classes
// - since TPollSocketAbstract.Create is declared as virtual, could be used
// to specify the proper polling class to add
// - see PollSockClass function and TPollSocketAbstract.New method
TPollSocketClass = class of TPollSocketAbstract;
/// returns the TPollSocketAbstract class best fitting with the current
// Operating System
// - as used by TPollSocketAbstract.New method
function PollSocketClass: TPollSocketClass;
type
{$ifdef MSWINDOWS}
/// socket polling via Windows' Select() API
// - under Windows, Select() handles up to 64 TSocket, and is available
// in Windows XP, whereas WSAPoll() is available only since Vista
// - under Linux, select() is very limited, so poll/epoll APIs are to be used
// - in practice, TPollSocketSelect is slighlty FASTER than TPollSocketPoll
// when tracking a lot of connections (at least under Windows): WSAPoll()
// seems to be just an emulation API - very disapointing :(
TPollSocketSelect = class(TPollSocketAbstract)
protected
fHighestSocket: integer;
fRead: TFDSet;
fWrite: TFDSet;
fTag: array[0..FD_SETSIZE-1] of record
socket: TSocket;
tag: TPollSocketTag;
end;
public
/// initialize the polling via creating an epoll file descriptor
constructor Create; override;
/// track status modifications on one specified TSocket
// - you can specify which events are monitored - pseError and pseClosed
// will always be notified
function Subscribe(socket: TSocket; events: TPollSocketEvents;
tag: TPollSocketTag): boolean; override;
/// stop status modifications tracking on one specified TSocket
// - the socket should have been monitored by a previous call to Subscribe()
function Unsubscribe(socket: TSocket): boolean; override;
/// waits for status modifications of all tracked TSocket
// - will wait up to timeoutMS milliseconds, 0 meaning immediate return
// and -1 for infinite blocking
// - returns -1 on error (e.g. no TSocket currently registered), or
// the number of modifications stored in results[] (may be 0 if none)
function WaitForModified(out results: TPollSocketResults;
timeoutMS: integer): integer; override;
end;
{$endif MSWINDOWS}
/// socket polling via poll/WSAPoll API
// - direct call of the Linux/POSIX poll() API, or Windows WSAPoll() API
TPollSocketPoll = class(TPollSocketAbstract)
protected
fFD: TPollFDDynArray; // fd=-1 for ignored fields
fTags: array of TPollSocketTag;
fFDCount: integer;
procedure FDVacuum;
public
/// initialize the polling using poll/WSAPoll API
constructor Create; override;
/// track status modifications on one specified TSocket
// - you can specify which events are monitored - pseError and pseClosed
// will always be notified
function Subscribe(socket: TSocket; events: TPollSocketEvents;
tag: TPollSocketTag): boolean; override;
/// stop status modifications tracking on one specified TSocket
// - the socket should have been monitored by a previous call to Subscribe()
function Unsubscribe(socket: TSocket): boolean; override;
/// waits for status modifications of all tracked TSocket
// - will wait up to timeoutMS milliseconds, 0 meaning immediate return
// and -1 for infinite blocking
// - returns -1 on error (e.g. no TSocket currently registered), or
// the number of modifications stored in results[] (may be 0 if none)
function WaitForModified(out results: TPollSocketResults;
timeoutMS: integer): integer; override;
end;
{$ifdef LINUXNOTBSD}
/// socket polling via Linux epoll optimized API
// - not available under Windows or BSD/Darwin
// - direct call of the epoll API in level-triggered (LT) mode
// - only available on Linux - use TPollSocketPoll for using cross-plaform
// poll/WSAPoll API
TPollSocketEpoll = class(TPollSocketAbstract)
protected
fEPFD: integer;
fResults: TEPollEventDynArray;
public
/// initialize the polling via creating an epoll file descriptor
constructor Create; override;
/// finalize the polling by closing the epoll file descriptor
destructor Destroy; override;
/// track status modifications on one specified TSocket
// - you can specify which events are monitored - pseError and pseClosed
// will always be notified
// - directly calls epoll's EPOLL_CTL_ADD control interface
function Subscribe(socket: TSocket; events: TPollSocketEvents;
tag: TPollSocketTag): boolean; override;
/// stop status modifications tracking on one specified TSocket
// - the socket should have been monitored by a previous call to Subscribe()
// - directly calls epoll's EPOLL_CTL_DEL control interface
function Unsubscribe(socket: TSocket): boolean; override;
/// waits for status modifications of all tracked TSocket
// - will wait up to timeoutMS milliseconds, 0 meaning immediate return
// and -1 for infinite blocking
// - returns -1 on error (e.g. no TSocket currently registered), or
// the number of modifications stored in results[] (may be 0 if none)
// - directly calls epool_wait() function
function WaitForModified(out results: TPollSocketResults;
timeoutMS: integer): integer; override;
/// read-only access to the low-level epoll_create file descriptor
property EPFD: integer read fEPFD;
end;
{$endif LINUXNOTBSD}
type
{$M+}
/// implements efficient polling of multiple sockets
// - will maintain a pool of TPollSocketAbstract instances, to monitor
// incoming data or outgoing availability for a set of active connections
// - call Subscribe/Unsubscribe to setup the monitored sockets
// - call GetOne from any consumming threads to process new events
TPollSockets = class
protected
fPollClass: TPollSocketClass;
fPoll: array of TPollSocketAbstract;
fPollIndex: integer;
fPending: TPollSocketResults;
fPendingIndex: integer;
fTerminated: boolean;
fCount: integer;
fPollLock: TRTLCriticalSection;
fPendingLock: TRTLCriticalSection;
public
/// initialize the sockets polling
// - you can specify the TPollSocketAbsract class to be used, if the
// default is not the one expected
// - under Linux/POSIX, will set the open files maximum number for the
// current process to match the system hard limit: if your system has a
// low "ulimit -H -n" value, you may add the following line in your
// /etc/limits.conf or /etc/security/limits.conf file:
// $ * hard nofile 65535
constructor Create(aPollClass: TPollSocketClass=nil);
/// finalize the sockets polling, and release all used memory
destructor Destroy; override;
/// track modifications on one specified TSocket and tag
// - the supplied tag value - maybe a PtrInt(aObject) - will be part of
// GetOne method results
// - will create as many TPollSocketAbstract instances as needed, depending
// on the MaxSockets capability of the actual implementation class
// - this method is thread-safe
function Subscribe(socket: TSocket; tag: TPollSocketTag;
events: TPollSocketEvents): boolean; virtual;
/// stop status modifications tracking on one specified TSocket and tag
// - the socket should have been monitored by a previous call to Subscribe()
// - this method is thread-safe
function Unsubscribe(socket: TSocket; tag: TPollSocketTag): boolean; virtual;
/// retrieve the next pending notification, or let the poll wait for new
// - if there is no pending notification, will poll and wait up to
// timeoutMS milliseconds for pending data
// - returns true and set notif.events/tag with the corresponding notification
// - returns false if no pending event was handled within the timeoutMS period
// - this method is thread-safe, and could be called from several threads
function GetOne(timeoutMS: integer; out notif: TPollSocketResult): boolean; virtual;
/// retrieve the next pending notification
// - returns true and set notif.events/tag with the corresponding notification
// - returns false if no pending event is available
// - this method is thread-safe, and could be called from several threads
function GetOneWithinPending(out notif: TPollSocketResult): boolean;
/// notify any GetOne waiting method to stop its polling loop
procedure Terminate;
/// the actual polling class used to track socket state changes
property PollClass: TPollSocketClass read fPollClass;
/// set to true by the Terminate method
property Terminated: boolean read fTerminated;
published
/// how many sockets are currently tracked
property Count: integer read fCount;
end;
{$M-}
/// store information of one TPollAsynchSockets connection
{$ifdef USERECORDWITHMETHODS}TPollSocketsSlot = record
{$else}TPollSocketsSlot = object{$endif}
/// the associated TCP connection
// - equals 0 after TPollAsynchSockets.Stop
socket: TSocket;
/// Lock/Unlock R/W thread acquisition (lighter than a TRTLCriticalSection)
lockcounter: array[boolean] of integer;
/// the last error reported by WSAGetLastError before the connection ends
lastWSAError: integer;
/// the current read data buffer of this slot
readbuf: SockString;
/// the current write data buffer of this slot
writebuf: SockString;
/// acquire an exclusive R/W access to this connection
// - returns true if slot has been acquired
// - returns false if it is used by another thread
// - warning: this method is not re-entrant
function Lock(writer: boolean): boolean;
/// try to acquire an exclusive R/W access to this connection
// - returns true if slot has been acquired
// - returns false if it is used by another thread, after the timeoutMS period
// - warning: this method is not re-entrant
function TryLock(writer: boolean; timeoutMS: cardinal): boolean;
/// release exclusive R/W access to this connection
procedure UnLock(writer: boolean);
end;
/// points to thread-safe information of one TPollAsynchSockets connection
PPollSocketsSlot = ^TPollSocketsSlot;
/// possible options for TPollAsynchSockets process
// - by default, TPollAsynchSockets.Write will first try to send the data
// using Send() in non-blocking mode, unless paoWritePollOnly is defined,
// and fWrite will be used to poll output state and send it asynchronously
TPollAsynchSocketsOptions = set of (paoWritePollOnly);
/// let TPollAsynchSockets.OnRead shutdown the socket if needed
TPollAsynchSocketOnRead = (sorContinue, sorClose);
{$M+}
/// read/write buffer-oriented process of multiple non-blocking connections
// - to be used e.g. for stream protocols (e.g. WebSockets or IoT communication)
// - assigned sockets will be set in non-blocking mode, so that polling will
// work as expected: you should then never use direclty the socket (e.g. via
// blocking TCrtSocket), but rely on this class for asynchronous process:
// OnRead() overriden method will receive all incoming data from input buffer,
// and Write() should be called to add some data to asynchronous output buffer
// - connections are identified as TObject instances, which should hold a
// TPollSocketsSlot record as private values for the polling process
// - ProcessRead/ProcessWrite methods are to be run for actual communication:
// either you call those methods from multiple threads, or you run them in
// loop from a single thread, then define a TSynThreadPool for running any
// blocking process (e.g. computing requests answers) from OnRead callbacks
// - inherited classes should override abstract OnRead, OnClose, OnError and
// SlotFromConnection methods according to the actual connection class
TPollAsynchSockets = class
protected
fRead: TPollSockets;
fWrite: TPollSockets;
fReadCount: integer;
fWriteCount: integer;
fReadBytes: Int64;
fWriteBytes: Int64;
fProcessing: integer;
fOptions: TPollAsynchSocketsOptions;
function GetCount: integer;
// warning: abstract methods below should be properly overriden
// return low-level socket information from connection instance
function SlotFromConnection(connection: TObject): PPollSocketsSlot; virtual; abstract;
// extract frames from slot.readbuf, and handle them
function OnRead(connection: TObject): TPollAsynchSocketOnRead; virtual; abstract;
// called when slot.writebuf has been sent through the socket
procedure AfterWrite(connection: TObject); virtual; abstract;
// pseClosed: should do connection.free - Stop() has been called (socket=0)
procedure OnClose(connection: TObject); virtual; abstract;
// pseError: return false to close socket and connection (calling OnClose)
function OnError(connection: TObject; events: TPollSocketEvents): boolean; virtual; abstract;
public
/// initialize the read/write sockets polling
// - fRead and fWrite TPollSocketsBuffer instances will track pseRead or
// pseWrite events, and maintain input and output data buffers
constructor Create; virtual;
/// finalize buffer-oriented sockets polling, and release all used memory
destructor Destroy; override;
/// assign a new connection to the internal poll
// - the TSocket handle will be retrieved via SlotFromConnection, and
// set in non-blocking mode from now on - it is not recommended to access
// it directly any more, but use Write() and handle OnRead() callback
// - fRead will poll incoming packets, then call OnRead to handle them,
// or Unsubscribe and delete the socket when pseClosed is notified
// - fWrite will poll for outgoing packets as specified by Write(), then
// send any pending data once the socket is ready
function Start(connection: TObject): boolean; virtual;
/// remove a connection from the internal poll, and shutdown its socket
// - most of the time, the connection is released by OnClose when the other
// end shutdown the socket; but you can explicitely call this method when
// the connection (and its socket) is to be shutdown
// - this method won't call OnClose, since it is initiated by the class
function Stop(connection: TObject): boolean; virtual;
/// add some data to the asynchronous output buffer of a given connection
// - this method may block if the connection is currently writing from
// another thread (which is not possible from TPollAsynchSockets.Write),
// up to timeout milliseconds
function Write(connection: TObject; const data; datalen: integer;
timeout: integer=5000): boolean; virtual;
/// add some data to the asynchronous output buffer of a given connection
function WriteString(connection: TObject; const data: SockString): boolean;
/// one or several threads should execute this method
// - thread-safe handle of any incoming packets
// - if this method is called from a single thread, you should use
// a TSynThreadPool for any blocking process of OnRead events
// - otherwise, this method is thread-safe, and incoming packets may be
// consumed from a set of threads, and call OnRead with newly received data
procedure ProcessRead(timeoutMS: integer);
/// one or several threads should execute this method
// - thread-safe handle of any outgoing packets
procedure ProcessWrite(timeoutMS: integer);
/// notify internal socket polls to stop their polling loop ASAP
procedure Terminate(waitforMS: integer);
/// low-level access to the polling class used for incoming data
property PollRead: TPollSockets read fRead;
/// low-level access to the polling class used for outgoind data
property PollWrite: TPollSockets write fWrite;
/// some processing options
property Options: TPollAsynchSocketsOptions read fOptions write fOptions;
published
/// how many connections are currently managed by this instance
property Count: integer read GetCount;
/// how many times data has been received by this instance
property ReadCount: integer read fReadCount;
/// how many times data has been sent by this instance
property WriteCount: integer read fWriteCount;
/// how many data bytes have been received by this instance
property ReadBytes: Int64 read fReadBytes;
/// how many data bytes have been sent by this instance
property WriteBytes: Int64 read fWriteBytes;
end;
{$M-}
function SysErrorMessagePerModule(Code: DWORD; ModuleName: PChar): string;
{$ifdef MSWINDOWS}
/// is HTTP.SYS web socket API available on the target system Windows 8 and UP
function WinHTTP_WebSocketEnabled: boolean;
{$endif}
var
/// Queue length for completely established sockets waiting to be accepted,
// a backlog parameter for listen() function. If queue overflows client
// got ECONNREFUSED error for connect() call
// - for windows default is taken from SynWinSock ($7fffffff) and should
// not be modified. Actual limit is 200;
// - for Unix default is taken from SynFPCSock (128 as in linux kernel >2.2),
// but actual value is min(DefaultListenBacklog, /proc/sys/net/core/somaxconn)
DefaultListenBacklog: integer = SOMAXCONN;
implementation
{ ************ some shared helper functions and classes }
var
ReasonCache: array[1..5,0..13] of SockString; // avoid memory allocation
function StatusCodeToReasonInternal(Code: cardinal): SockString;
begin
case Code of
100: result := 'Continue';
101: result := 'Switching Protocols';
200: result := 'OK';
201: result := 'Created';
202: result := 'Accepted';
203: result := 'Non-Authoritative Information';
204: result := 'No Content';
205: result := 'Reset Content';
206: result := 'Partial Content';
207: result := 'Multi-Status';
300: result := 'Multiple Choices';
301: result := 'Moved Permanently';
302: result := 'Found';
303: result := 'See Other';
304: result := 'Not Modified';
305: result := 'Use Proxy';
307: result := 'Temporary Redirect';
308: result := 'Permanent Redirect';
400: result := 'Bad Request';
401: result := 'Unauthorized';
403: result := 'Forbidden';
404: result := 'Not Found';
405: result := 'Method Not Allowed';
406: result := 'Not Acceptable';
407: result := 'Proxy Authentication Required';
408: result := 'Request Timeout';
409: result := 'Conflict';
410: result := 'Gone';
411: result := 'Length Required';
412: result := 'Precondition Failed';
413: result := 'Payload Too Large';
414: result := 'URI Too Long';
415: result := 'Unsupported Media Type';
416: result := 'Requested Range Not Satisfiable';
426: result := 'Upgrade Required';
500: result := 'Internal Server Error';
501: result := 'Not Implemented';
502: result := 'Bad Gateway';
503: result := 'Service Unavailable';
504: result := 'Gateway Timeout';
505: result := 'HTTP Version Not Supported';
511: result := 'Network Authentication Required';
else result := 'Invalid Request';
end;
end;
function StatusCodeToReason(Code: cardinal): SockString;
var Hi,Lo: cardinal;
begin
if Code=200 then begin // optimistic approach :)
Hi := 2;
Lo := 0;
end else begin
Hi := Code div 100;
Lo := Code-Hi*100;
if not ((Hi in [1..5]) and (Lo in [0..13])) then begin
result := StatusCodeToReasonInternal(Code);
exit;
end;
end;
result := ReasonCache[Hi,Lo];
if result<>'' then
exit;
result := StatusCodeToReasonInternal(Code);
ReasonCache[Hi,Lo] := result;
end;
function Hex2Dec(c: integer): integer; {$ifdef HASINLINE}inline;{$endif}
begin
result := c;
case c of
ord('A')..ord('Z'): dec(result,(ord('A') - 10));
ord('a')..ord('z'): dec(result,(ord('a') - 10));
ord('0')..ord('9'): dec(result,ord('0'));
else result := -1;
end;
end;
function SockBase64Encode(const s: SockString): SockString;
procedure Encode(rp, sp: PAnsiChar; len: integer);
const
b64: array[0..63] of AnsiChar =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
var i: integer;
c: cardinal;
begin
for i := 1 to len div 3 do begin
c := ord(sp[0]) shl 16 + ord(sp[1]) shl 8 + ord(sp[2]);
rp[0] := b64[(c shr 18) and $3f];
rp[1] := b64[(c shr 12) and $3f];
rp[2] := b64[(c shr 6) and $3f];
rp[3] := b64[c and $3f];
inc(rp,4);
inc(sp,3);
end;
case len mod 3 of
1: begin
c := ord(sp[0]) shl 16;
rp[0] := b64[(c shr 18) and $3f];
rp[1] := b64[(c shr 12) and $3f];
rp[2] := '=';
rp[3] := '=';
end;
2: begin
c := ord(sp[0]) shl 16 + ord(sp[1]) shl 8;
rp[0] := b64[(c shr 18) and $3f];
rp[1] := b64[(c shr 12) and $3f];
rp[2] := b64[(c shr 6) and $3f];
rp[3] := '=';
end;
end;
end;
var len: integer;
begin
result:='';
len := length(s);
if len = 0 then exit;
SetLength(result, ((len + 2) div 3) * 4);
Encode(pointer(result),pointer(s),len);
end;
function SockBase64Decode(const s: SockString): SockString;
var i, j, len: integer;
sp, rp: PAnsiChar;
c, ch: integer;
begin
result:= '';
len := length(s);
if (len <= 0) or (len and 3 <> 0) then
exit;
len := len shr 2;
SetLength(result, len * 3);
sp := pointer(s);
rp := pointer(result);
for i := 1 to len do begin
c := 0;
j := 0;
while true do begin
ch := ord(sp[j]);
case chr(ch) of
'A'..'Z': c := c or (ch - ord('A'));
'a'..'z': c := c or (ch - (ord('a')-26));
'0'..'9': c := c or (ch - (ord('0')-52));
'+': c := c or 62;
'/': c := c or 63;
else
if j=3 then begin
rp[0] := AnsiChar(c shr 16);
rp[1] := AnsiChar(c shr 8);
SetLength(result, len*3-1);
exit;
end else begin
rp[0] := AnsiChar(c shr 10);
SetLength(result, len*3-2);
exit;
end;
end;
if j=3 then break;
inc(j);
c := c shl 6;
end;
rp[2] := AnsiChar(c);
c := c shr 8;
rp[1] := AnsiChar(c);
c := c shr 8;
rp[0] := AnsiChar(c);
inc(rp,3);
inc(sp,4);
end;
end;
function HtmlEncode(const s: SockString): SockString;
var i: integer;
begin // not very fast, but working
result := '';
for i := 1 to length(s) do
case s[i] of
'<': result := result+'&lt;';
'>': result := result+'&gt;';
'&': result := result+'&amp;';
'"': result := result+'&quot;';
else result := result+s[i];
end;
end;
function HtmlEncodeString(const s: string): string;
var i: integer;
begin // not very fast, but working
result := '';
for i := 1 to length(s) do
case s[i] of
'<': result := result+'&lt;';
'>': result := result+'&gt;';
'&': result := result+'&amp;';
'"': result := result+'&quot;';
else result := result+s[i];
end;
end;
const
CRLF: array[0..1] of AnsiChar = (#13,#10);
function StrLen(S: PAnsiChar): PtrInt;
{$ifdef CPUX64}
{$ifdef FPC}nostackframe; assembler; asm {$else}
asm .noframe // rcx=S (Linux: rdi)
{$endif FPC} // from GPL strlen64.asm by Agner Fog - www.agner.org/optimize
{$ifdef win64}
mov rax, rcx // get pointer to string from rcx
mov r8, rcx // copy pointer
test rcx, rcx
{$else}
mov rax, rdi
mov ecx, edi
test rdi, rdi
{$endif}
jz @null // returns 0 if S=nil
// rax=s,ecx=32-bit of s
pxor xmm0, xmm0 // set to zero
and ecx, 15 // lower 4 bits indicate misalignment
and rax, -16 // align pointer by 16
// will never read outside a memory page boundary, so won't trigger GPF
movaps xmm1, [rax] // read from nearest preceding boundary
pcmpeqb xmm1, xmm0 // compare 16 bytes with zero
pmovmskb edx, xmm1 // get one bit for each byte result
shr edx, cl // shift out false bits
shl edx, cl // shift back again
bsf edx, edx // find first 1-bit
jnz @L2 // found
// Main loop, search 16 bytes at a time
{$ifdef FPC} align 16 {$else} .align 16 {$endif}
@L1: add rax, 10H // increment pointer by 16
movaps xmm1, [rax] // read 16 bytes aligned
pcmpeqb xmm1, xmm0 // compare 16 bytes with zero
pmovmskb edx, xmm1 // get one bit for each byte result
bsf edx, edx // find first 1-bit
// (moving the bsf out of the loop and using test here would be faster
// for long strings on old processors, but we are assuming that most
// strings are short, and newer processors have higher priority)
jz @L1 // loop if not found
@L2: // Zero-byte found. Compute string length
{$ifdef win64}
sub rax, r8 // subtract start address
{$else}
sub rax, rdi
{$endif}
add rax, rdx // add byte index
@null:
end;
{$else}
begin
result := 0;
if S<>nil then
while true do
if S[0]<>#0 then
if S[1]<>#0 then
if S[2]<>#0 then
if S[3]<>#0 then begin
inc(S,4);
inc(result,4);
end else begin
inc(result,3);
exit;
end else begin
inc(result,2);
exit;
end else begin
inc(result);
exit;
end else
exit;
end;
{$endif CPUX64}
type
TNormToUpper = array[byte] of byte;
PPByteArray = ^PByteArray;
var
NormToUpper: TNormToUpper;
function IdemPCharUp(p: PByteArray; up: PByte; toup: PByteArray): boolean;
{$ifdef HASINLINE}inline;{$endif}
var u: byte;
begin
result := false;
dec(PtrUInt(p),PtrUInt(up));
repeat
u := up^;
if u=0 then
break;
if toup[p[PtrUInt(up)]]<>u then
exit;
inc(up);
until false;
result := true;
end;
function IdemPChar(p, up: pAnsiChar): boolean;
// if the beginning of p^ is same as up^ (ignore case - up^ must be already Upper)
begin
if p=nil then
result := false else
if up=nil then
result := true else
result := IdemPCharUp(pointer(p),pointer(up),@NormToUpper);
end;
function IdemPCharArray(p: PAnsiChar; const upArray: array of PAnsiChar): integer;
var w: word;
toup: PByteArray;
up: ^PAnsiChar;
begin
if p<>nil then begin
toup := @NormToUpper;
w := toup[ord(p[0])]+toup[ord(p[1])]shl 8;
up := @upArray[0];
for result := 0 to high(upArray) do
if (PWord(up^)^=w) and IdemPCharUp(pointer(p+2),pointer(up^+2),toup) then
exit else
inc(up);
end;
result := -1;
end;
procedure GetNextItem(var P: PAnsiChar; Sep: AnsiChar; var result: SockString);
// return next CSV string in P, nil if no more
var S: PAnsiChar;
begin
if P=nil then
result := '' else begin
S := P;
while (S^<>#0) and (S^<>Sep) do
inc(S);
SetString(result,P,S-P);
if S^<>#0 then
P := S+1 else
P := nil;
end;
end;
function SameText(const a,b: SockString): boolean;
var n,i: integer;
begin
result := false;
n := length(a);
if length(b)<>n then
exit;
for i := 1 to n do
if NormToUpper[ord(a[i])]<>NormToUpper[ord(b[i])] then
exit;
result := true;
end;
function GetNextItemUInt64(var P: PAnsiChar): ULONGLONG;
var c: PtrUInt;
begin
result := 0;
if P<>nil then
repeat
c := byte(P^)-48;
if c>9 then
break else
result := result*10+ULONGLONG(c);
inc(P);
until false;
end; // P^ will point to the first non digit char
procedure GetNextLine(var P: PAnsiChar; var result: SockString);
var S: PAnsiChar;
begin
if P=nil then
result := '' else begin
S := P;
while S^>=' ' do // break on any control char
inc(S);
SetString(result,P,S-P);
while (S^<>#0) and (S^<' ') do inc(S); // ignore e.g. #13 or #10
if S^<>#0 then
P := S else
P := nil;
end;
end;
// rewrite some functions to avoid unattempted ansi<->unicode conversion
function PosCh(ch: AnsiChar; const s: SockString): PtrInt;
{$ifdef HASINLINE}inline;{$endif}
begin // Pos() overloads are quite cumbersome on Delphi/FPC
for result := 1 to length(s) do
if s[result]=ch then
exit;
result := 0;
end;
procedure TrimCopy(const S: SockString; start,count: PtrInt;
out result: SockString); // faster alternative to Trim(copy())
var L: PtrInt;
begin
if count<=0 then
exit;
if start<=0 then
start := 1;
L := Length(S);
while (start<=L) and (S[start]<=' ') do begin
inc(start); dec(count); end;
dec(start);
dec(L,start);
if count<L then
L := count;
while L>0 do
if S[start+L]<=' ' then
dec(L) else
break;
if L>0 then
SetString(result,PAnsiChar(@PByteArray(S)[start]),L);
end;
{$ifdef FPC_OR_PUREPASCAL}
function Trim(const S: SockString): SockString;
var i, L: PtrInt;
begin
L := Length(S);
i := 1;
while (i<=L) and (S[i]<=' ') do
inc(i);
if i>L then
result := '' else
if (i=1) and (S[L]>' ') then
result := S else begin
while S[L]<=' ' do
dec(L);
result := copy(S,i,L-i+1);
end;
end;
{$else}
function Trim(const S: SockString): SockString;
asm // fast implementation by John O'Harrow
test eax,eax {S = nil?}
xchg eax,edx
jz System.@LStrClr {Yes, Return Empty String}
mov ecx,[edx-4] {Length(S)}
cmp byte ptr [edx],' ' {S[1] <= ' '?}
jbe @@TrimLeft {Yes, Trim Leading Spaces}
cmp byte ptr [edx+ecx-1],' ' {S[Length(S)] <= ' '?}
jbe @@TrimRight {Yes, Trim Trailing Spaces}
jmp System.@LStrLAsg {No, Result := S (which occurs most time)}
@@TrimLeft: {Strip Leading Whitespace}
dec ecx
jle System.@LStrClr {All Whitespace}
inc edx
cmp byte ptr [edx],' '
jbe @@TrimLeft
@@CheckDone:
cmp byte ptr [edx+ecx-1],' '
{$ifdef UNICODE}
jbe @@TrimRight
push 65535 // SockString code page for Delphi 2009 and up
call System.@LStrFromPCharLen // we need a call, not a direct jmp
ret
{$else}
ja System.@LStrFromPCharLen
{$endif}
@@TrimRight: {Strip Trailing Whitespace}
dec ecx
jmp @@CheckDone
end;
{$endif}
function ExistNameValue(p,up: PAnsiChar): PAnsiChar;
var tab: PByteArray;
begin
result := p;
if p=nil then
exit;
tab := @NormToUpper;
repeat
if IdemPCharUp(pointer(result),pointer(up),tab) then
exit;
while result^>#13 do
inc(result);
while result^<=#13 do
if result^=#0 then begin
result := nil;
exit;
end else
inc(result);
until false;
end;
function FindHeaderValue(p: PAnsiChar; const up: SockString): PAnsiChar;
begin
result := ExistNameValue(p,pointer(up));
if result=nil then
exit;
inc(result,length(up));
if result^<>':' then
result := nil else
repeat
inc(result);
until (result^>' ') or (result^=#0);
end;
procedure GetHeaderValue(const s, up: SockString; var res: SockString);
var p: PAnsiChar;
L: PtrInt;
begin
p := FindHeaderValue(pointer(s),up);
if (p=nil) or (p^=#0) then
exit;
L := 0;
while p[L]>#13 do
inc(L);
while p[L-1]=' ' do
dec(L);
SetString(res,p,L);
end;
procedure ExtractNameValue(var headers: SockString; const upname: SockString;
out res: SockString);
var i,j,k: PtrInt;
begin
if (headers='') or (upname='') then
exit;
i := 1;
repeat
k := length(headers)+1;
for j := i to k-1 do
if headers[j]<' ' then begin
k := j;
break;
end;
if IdemPCharUp(@PByteArray(headers)[i-1],pointer(upname),@NormToUpper) then begin
j := i;
inc(i,length(upname));
TrimCopy(headers,i,k-i,res);
while true do // delete also ending #13#10
if (headers[k]=#0) or (headers[k]>=' ') then
break else
inc(k);
delete(headers,j,k-j);
exit;
end;
i := k;
while headers[i]<' ' do
if headers[i]=#0 then
exit else
inc(i);
until false;
end;
procedure UpperMove(Source, Dest: PByte; ToUp: PByteArray; L: cardinal);
begin
repeat
Dest^ := ToUp[Source^];
dec(L);
inc(Source);
inc(Dest);
until L=0;
end;
function UpperCase(const S: SockString): SockString;
var L: cardinal;
begin
result := '';
L := Length(S);
if L=0 then
exit;
SetLength(result,L);
UpperMove(pointer(S),pointer(result),@NormToUpper,L);
end;
function GetCardinal(P: PAnsiChar): cardinal; overload;
var c: cardinal;
begin
if P=nil then begin
result := 0;
exit;
end;
if P^=' ' then repeat inc(P) until P^<>' ';
c := byte(P^)-48;
if c>9 then
result := 0 else begin
result := c;
inc(P);
repeat
c := byte(P^)-48;
if c>9 then
break else
result := result*10+c;
inc(P);
until false;
end;
end;
function GetCardinal(P,PEnd: PAnsiChar): cardinal; overload;
var c: cardinal;
begin
result := 0;
if (P=nil) or (P>=PEnd) then
exit;
if P^=' ' then repeat
inc(P);
if P=PEnd then exit;
until P^<>' ';
c := byte(P^)-48;
if c>9 then
exit;
result := c;
inc(P);
while P<PEnd do begin
c := byte(P^)-48;
if c>9 then
break else
result := result*10+c;
inc(P);
end;
end;
function HttpChunkToHex32(p: PAnsiChar): integer;
var v0,v1: integer;
begin
result := 0;
if p<>nil then begin
while p^=' ' do inc(p);
repeat
v0 := Hex2Dec(ord(p[0]));
if v0<0 then break; // not in '0'..'9','a'..'f'
v1 := Hex2Dec(ord(p[1]));
inc(p);
if v1<0 then begin
result := (result shl 4) or v0; // only one char left
break;
end;
result := (result shl 8) or (v0 shl 4) or v1;
inc(p);
until false;
end;
end;
{$ifdef DELPHI5OROLDER}
function Utf8ToAnsi(const UTF8: SockString): SockString;
begin
result := UTF8; // fallback to no conversion
end;
{$endif}
const
ENGLISH_LANGID = $0409;
// see http://msdn.microsoft.com/en-us/library/windows/desktop/aa383770
ERROR_WINHTTP_CANNOT_CONNECT = 12029;
ERROR_WINHTTP_TIMEOUT = 12002;
ERROR_WINHTTP_INVALID_SERVER_RESPONSE = 12152;
function SysErrorMessagePerModule(Code: DWORD; ModuleName: PChar): string;
{$ifdef MSWINDOWS}
var tmpLen: DWORD;
err: PChar;
{$endif}
begin
result := '';
if Code=NO_ERROR then
exit;
{$ifdef MSWINDOWS}
tmpLen := FormatMessage(
FORMAT_MESSAGE_FROM_HMODULE or FORMAT_MESSAGE_ALLOCATE_BUFFER,
pointer(GetModuleHandle(ModuleName)),Code,ENGLISH_LANGID,@err,0,nil);
// if string is empty, it may be because english is not found
if (tmpLen = 0) then
tmpLen := FormatMessage(
FORMAT_MESSAGE_FROM_HMODULE or FORMAT_MESSAGE_ALLOCATE_BUFFER or FORMAT_MESSAGE_IGNORE_INSERTS,
pointer(GetModuleHandle(ModuleName)),Code,0,@err,0,nil);
try
while (tmpLen>0) and (ord(err[tmpLen-1]) in [0..32,ord('.')]) do
dec(tmpLen);
SetString(result,err,tmpLen);
finally
LocalFree(HLOCAL(err));
end;
{$endif}
if result='' then begin
result := SysErrorMessage(Code);
if result='' then
if Code=ERROR_WINHTTP_CANNOT_CONNECT then
result := 'cannot connect' else
if Code=ERROR_WINHTTP_TIMEOUT then
result := 'timeout' else
if Code=ERROR_WINHTTP_INVALID_SERVER_RESPONSE then
result := 'invalid server response' else
result := IntToHex(Code,8);
end;
end;
procedure RaiseLastModuleError(ModuleName: PChar; ModuleException: ExceptClass);
var LastError: Integer;
Error: Exception;
begin
LastError := GetLastError;
if LastError<>NO_ERROR then
Error := ModuleException.CreateFmt('%s error %d (%s)',
[ModuleName,LastError,SysErrorMessagePerModule(LastError,ModuleName)]) else
Error := ModuleException.CreateFmt('Undefined %s error',[ModuleName]);
raise Error;
end;
function Ansi7ToUnicode(const Ansi: SockString): SockString;
var n, i: PtrInt;
begin // fast ANSI 7 bit conversion
result := '';
if Ansi='' then
exit;
n := length(Ansi);
SetLength(result,n*2+1);
for i := 0 to n do // to n = including last #0
PWordArray(pointer(result))^[i] := PByteArray(pointer(Ansi))^[i];
end;
function DefaultUserAgent(Instance: TObject): SockString;
begin
// note: some part of mORMot.pas would identify 'mORMot' pattern in the
// agent header to enable advanced behavior e.g. about JSON transmission
result := 'Mozilla/5.0 ('+XPOWEREDOS+'; '+XPOWEREDPROGRAM+' '+
SockString(Instance.ClassName)+')';
end;
/// decode 'CONTENT-ENCODING: ' parameter from registered compression list
function ComputeContentEncoding(const Compress: THttpSocketCompressRecDynArray;
P: PAnsiChar): THttpSocketCompressSet;
var i: PtrInt;
aName: SockString;
Beg: PAnsiChar;
begin
integer(result) := 0;
if P<>nil then
repeat
while P^ in [' ',','] do inc(P);
Beg := P; // 'gzip;q=1.0, deflate' -> aName='gzip' then 'deflate'
while not (P^ in [';',',',#0]) do inc(P);
SetString(aName,Beg,P-Beg);
for i := 0 to high(Compress) do
if aName=Compress[i].Name then
include(result,i);
while not (P^ in [',',#0]) do inc(P);
until P^=#0;
end;
function RegisterCompressFunc(var Compress: THttpSocketCompressRecDynArray;
aFunction: THttpSocketCompress; var aAcceptEncoding: SockString;
aCompressMinSize: integer): SockString;
var i, n: PtrInt;
dummy, aName: SockString;
begin
result := '';
if @aFunction=nil then
exit;
n := length(Compress);
aName := aFunction(dummy,true);
for i := 0 to n-1 do
with Compress[i] do
if Name=aName then begin // already set
if @Func=@aFunction then // update min. compress size value
CompressMinSize := aCompressMinSize;
exit;
end;
if n=sizeof(integer)*8 then
exit; // fCompressAcceptHeader is 0..31 (casted as integer)
SetLength(Compress,n+1);
with Compress[n] do begin
Name := aName;
@Func := @aFunction;
CompressMinSize := aCompressMinSize;
end;
if aAcceptEncoding='' then
aAcceptEncoding := 'Accept-Encoding: '+aName else
aAcceptEncoding := aAcceptEncoding+','+aName;
result := aName;
end;
function CompressDataAndGetHeaders(Accepted: THttpSocketCompressSet;
const Handled: THttpSocketCompressRecDynArray; const OutContentType: SockString;
var OutContent: SockString): SockString;
var i, OutContentLen: integer;
compressible: boolean;
OutContentTypeP: PAnsiChar absolute OutContentType;
begin
if (integer(Accepted)<>0) and (OutContentType<>'') and (Handled<>nil) then begin
OutContentLen := length(OutContent);
case IdemPCharArray(OutContentTypeP,['TEXT/','IMAGE/','APPLICATION/']) of
0: compressible := true;
1: compressible := IdemPCharArray(OutContentTypeP+6,['SVG','X-ICO'])>=0;
2: compressible := IdemPCharArray(OutContentTypeP+12,['JSON','XML','JAVASCRIPT'])>=0;
else compressible := false;
end;
for i := 0 to high(Handled) do
if i in Accepted then
with Handled[i] do
if (CompressMinSize=0) or // 0 here means "always" (e.g. for encryption)
(compressible and (OutContentLen>=CompressMinSize)) then begin
// compression of the OutContent + update header
result := Func(OutContent,true);
exit; // first in fCompress[] is prefered
end;
end;
result := '';
end;
procedure AppendI32(value: integer; var dest: shortstring); {$ifdef FPC}inline;{$endif}
var temp: shortstring;
begin
str(value,temp);
move(temp[1],dest[ord(dest[0])+1],ord(temp[0]));
inc(dest[0],ord(temp[0]));
end;
procedure AppendI64(value: Int64; var dest: shortstring);
var temp: shortstring;
begin
str(value,temp);
move(temp[1],dest[ord(dest[0])+1],ord(temp[0]));
inc(dest[0],ord(temp[0]));
end;
procedure AppendChar(chr: AnsiChar; var dest: shortstring); {$ifdef FPC}inline;{$endif}
begin
inc(dest[0]);
dest[ord(dest[0])] := chr;
end;
var
IP4local: SockString; // contains '127.0.0.1'
procedure IP4Text(const ip4addr; var result: SockString);
var b: array[0..3] of byte absolute ip4addr;
s: shortstring;
i: PtrInt;
begin
if cardinal(ip4addr)=0 then
result := '' else
if cardinal(ip4addr)=$0100007f then
result := IP4local else begin
s := '';
i := 0;
repeat
AppendI32(b[i],s);
if i=3 then
break;
AppendChar('.',s);
inc(i);
until false;
SetString(result,PAnsiChar(@s[1]),ord(s[0]));
end;
end;
procedure IPText(const sin: TVarSin; var result: SockString; localasvoid: boolean);
begin
if sin.sin_family=AF_INET then
if localasvoid and (cardinal(sin.sin_addr)=$0100007f) then
result := '' else
IP4Text(sin.sin_addr,result) else begin
result := GetSinIP(sin); // AF_INET6 may be optimized in a future revision
if result='::1' then
if localasvoid then
result := '' else
result := IP4local; // IP6 localhost loopback benefits of matching IP4
end;
end;
function IsPublicIP(ip4: cardinal): boolean;
begin
result := false;
case ip4 and 255 of // ignore IANA private IP4 address spaces
10: exit;
172: if ((ip4 shr 8) and 255) in [16..31] then exit;
192: if (ip4 shr 8) and 255=168 then exit;
end;
result := true;
end;
{$ifdef MSWINDOWS}
{$ifdef FPC} // oddly not defined in fpc\rtl\win
function SwitchToThread: BOOL; stdcall; external kernel32 name 'SwitchToThread';
{$endif}
procedure SleepHiRes(ms: cardinal); // see SynKylix/SynFPCLinux for POSIX
begin
if (ms<>0) or not SwitchToThread then
Windows.Sleep(ms);
end;
const
HexCharsLower: array[0..15] of AnsiChar = '0123456789abcdef';
procedure BinToHexDisplayW(Bin: PByte; BinBytes: integer; var result: SockUnicode);
var j: PtrInt;
P: PWideChar;
begin
SetString(Result,nil,BinBytes*2);
P := pointer(Result);
for j := BinBytes-1 downto 0 do begin
P[j*2] := WideChar(HexCharsLower[Bin^ shr 4]);
P[j*2+1] := WideChar(HexCharsLower[Bin^ and $F]);
inc(Bin);
end;
end;
function MacToText(pMacAddr: PByteArray): SockString;
var P: PAnsiChar;
i: PtrInt;
begin
SetLength(result,17);
P := pointer(result);
i := 0;
repeat
P[0] := HexCharsLower[pMacAddr[i] shr 4];
P[1] := HexCharsLower[pMacAddr[i] and $F];
if i = 5 then
break;
P[2] := ':'; // as in Linux
inc(P,3);
inc(i);
until false;
end;
function SendARP(DestIp: DWORD; srcIP: DWORD; pMacAddr: pointer;
PhyAddrLen: Pointer): DWORD; stdcall; external 'iphlpapi.dll';
function GetRemoteMacAddress(const IP: SockString): SockString;
// implements http://msdn.microsoft.com/en-us/library/aa366358
var dwRemoteIP: DWORD;
PhyAddrLen: Longword;
pMacAddr: array [0..7] of byte;
begin
result := '';
dwremoteIP := inet_addr(pointer(IP));
if dwremoteIP<>0 then begin
PhyAddrLen := 8;
if SendARP(dwremoteIP,0,@pMacAddr,@PhyAddrLen)=NO_ERROR then begin
if PhyAddrLen=6 then
result := MacToText(@pMacAddr);
end;
end;
end;
type
PMIB_IPADDRTABLE = ^MIB_IPADDRTABLE;
MIB_IPADDRTABLE = record
dwNumEntries: DWORD;
ip: array[0..200] of record
dwAddr: DWORD;
dwIndex: DWORD;
dwMask: DWORD;
dwBCastAddr: DWORD;
dwReasmSize: DWORD;
unused1: Word;
wType: Word;
end;
end;
function GetIpAddrTable(pIpAddrTable: PMIB_IPADDRTABLE;
var pdwSize: DWORD; bOrder: BOOL): DWORD; stdcall; external 'iphlpapi.dll';
const
MAX_ADAPTER_ADDRESS_LENGTH = 8;
GAA_FLAG_SKIP_UNICAST = $1;
GAA_FLAG_SKIP_ANYCAST = $2;
GAA_FLAG_SKIP_MULTICAST = $4;
GAA_FLAG_SKIP_DNS_SERVER = $8;
GAA_FLAG_SKIP_FRIENDLY_NAME = $20;
GAA_FLAG_INCLUDE_ALL_INTERFACES = $100; // Vista+
GAA_FLAGS = GAA_FLAG_SKIP_UNICAST or GAA_FLAG_SKIP_ANYCAST or
GAA_FLAG_SKIP_MULTICAST or GAA_FLAG_SKIP_DNS_SERVER or
GAA_FLAG_SKIP_FRIENDLY_NAME; // or GAA_FLAG_INCLUDE_ALL_INTERFACES;
IfOperStatusUp = 1;
type
SOCKET_ADDRESS = record
lpSockaddr: PSOCKADDR;
iSockaddrLength: Integer;
end;
PIP_ADAPTER_UNICAST_ADDRESS = pointer;
PIP_ADAPTER_ANYCAST_ADDRESS = pointer;
PIP_ADAPTER_DNS_SERVER_ADDRESS = pointer;
PIP_ADAPTER_MULTICAST_ADDRESS = pointer;
PIP_ADAPTER_ADDRESSES = ^_IP_ADAPTER_ADDRESSES;
_IP_ADAPTER_ADDRESSES = record
Union: record
case Integer of
0: (
Alignment: ULONGLONG);
1: (
Length: ULONG;
IfIndex: DWORD);
end;
Next: PIP_ADAPTER_ADDRESSES;
AdapterName: PAnsiChar;
FirstUnicastAddress: PIP_ADAPTER_UNICAST_ADDRESS;
FirstAnycastAddress: PIP_ADAPTER_ANYCAST_ADDRESS;
FirstMulticastAddress: PIP_ADAPTER_MULTICAST_ADDRESS;
FirstDnsServerAddress: PIP_ADAPTER_DNS_SERVER_ADDRESS;
DnsSuffix: PWCHAR;
Description: PWCHAR;
FriendlyName: PWCHAR;
PhysicalAddress: array [0..MAX_ADAPTER_ADDRESS_LENGTH - 1] of BYTE;
PhysicalAddressLength: DWORD;
Flags: DWORD;
Mtu: DWORD;
IfType: ULONG;
OperStatus: DWORD;
// below fields are only available on Windows XP with SP1 and later
Ipv6IfIndex: ULONG;
ZoneIndices: array [0..15] of DWORD;
FirstPrefix: pointer;
// below fields are only available on Windows Vista and later
TransmitLinkSpeed: Int64;
ReceiveLinkSpeed: Int64;
FirstWinsServerAddress: pointer;
FirstGatewayAddress: pointer;
Ipv4Metric: ULONG;
Ipv6Metric: ULONG;
Luid: Int64;
Dhcpv4Server: SOCKET_ADDRESS;
CompartmentId: DWORD;
NetworkGuid: TGUID;
ConnectionType: DWORD;
TunnelType: DWORD;
// DHCP v6 Info following
end;
function GetAdaptersAddresses(Family: ULONG; Flags: DWORD; Reserved: pointer;
pAdapterAddresses: PIP_ADAPTER_ADDRESSES; pOutBufLen: PULONG): DWORD; stdcall;
external 'iphlpapi.dll';
function GetIPAddresses(Kind: TIPAddress): TSockStringDynArray;
var Table: MIB_IPADDRTABLE;
Size: DWORD;
i: integer;
n: cardinal;
begin
result := nil;
Size := SizeOf(Table);
if GetIpAddrTable(@Table,Size,false)<>NO_ERROR then
exit;
SetLength(result,Table.dwNumEntries);
n := 0;
for i := 0 to Table.dwNumEntries-1 do
with Table.ip[i] do
if (dwAddr<>$0100007f) and (dwAddr<>0) then begin
case Kind of
tiaPublic: if not IsPublicIP(dwAddr) then continue;
tiaPrivate: if IsPublicIP(dwAddr) then continue;
end;
IP4Text(dwAddr,result[n]);
inc(n);
end;
if n<>Table.dwNumEntries then
SetLength(result,n);
end;
{$else MSWINDOWS}
function GetFileOpenLimit(hard: boolean=false): integer;
var limit: TRLIMIT;
begin
{$ifdef FPC}
if fpgetrlimit(RLIMIT_NOFILE,@limit)=0 then
{$else}
if getrlimit(RLIMIT_NOFILE,limit)=0 then
{$endif}
if hard then
result := limit.rlim_max else
result := limit.rlim_cur else
result := -1;
end;
function SetFileOpenLimit(max: integer; hard: boolean=false): integer;
var limit: TRLIMIT;
begin
result := -1;
{$ifdef FPC}
if fpgetrlimit(RLIMIT_NOFILE,@limit)<>0 then
{$else}
if getrlimit(RLIMIT_NOFILE,limit)<>0 then
{$endif}
exit;
if (hard and (integer(limit.rlim_max)=max)) or
(not hard and (integer(limit.rlim_cur)=max)) then begin
result := max; // already to the expected value
exit;
end;
if hard then
limit.rlim_max := max else
limit.rlim_cur := max;
{$ifdef FPC}
if fpsetrlimit(RLIMIT_NOFILE,@limit)=0 then
{$else}
if setrlimit(RLIMIT_NOFILE,limit)=0 then
{$endif}
result := GetFileOpenLimit(hard);
end;
{$define USE_IFADDRS}
{$ifdef USE_IFADDRS}
type
Pifaddrs = ^ifaddrs;
ifaddrs = record
ifa_next: Pifaddrs;
ifa_name: PAnsiChar;
ifa_flags: cardinal;
ifa_addr: Psockaddr;
ifa_netmask: Psockaddr;
ifa_dstaddr: Psockaddr;
ifa_data: Pointer;
end;
const
IFF_UP = $1;
IFF_LOOPBACK = $8;
{$ifndef KYLIX3}
libcmodulename = 'c';
{$endif}
function getifaddrs(var ifap: Pifaddrs): Integer; cdecl;
external libcmodulename name 'getifaddrs';
procedure freeifaddrs(ifap: Pifaddrs); cdecl;
external libcmodulename name 'freeifaddrs';
function GetIPAddresses(Kind: TIPAddress): TSockStringDynArray;
var list, info: Pifaddrs;
n, dwAddr: integer;
s: SockString;
begin
result := nil;
n := 0;
if getifaddrs(list)=0 then
try
info := list;
repeat
if (info^.ifa_addr<>nil) and (info^.ifa_flags and IFF_LOOPBACK=0) and
(info^.ifa_flags and IFF_UP<>0) then begin
s := '';
case info^.ifa_addr^.sa_family of
AF_INET: begin
dwAddr := integer(info^.ifa_addr^.sin_addr);
if (dwAddr<>$0100007f) and (dwAddr<>0) then
case Kind of
tiaPublic: if IsPublicIP(dwAddr) then IP4Text(dwAddr,s);
tiaPrivate: if not IsPublicIP(dwAddr) then IP4Text(dwAddr,s);
tiaAny: IP4Text(dwAddr,s);
end;
//s := s+'@'+info^.ifa_name;
end;
//AF_INET6: IPText(PVarSin(info^.ifa_addr)^,s);
end;
if s<>'' then begin
if n=length(result) then
SetLength(result,n+8);
result[n] := s;
inc(n);
end;
end;
info := info^.ifa_next;
until info=nil;
finally
freeifaddrs(list);
end;
if n<>length(result) then
SetLength(result,n);
end;
{$else}
function GetIPAddresses(Kind: TIPAddress): TSockStringDynArray;
begin
result := nil;
end;
{$endif USE_IFADDRS}
{$endif MSWINDOWS}
{$ifdef MSWINDOWS}
var // not available before Vista -> Lazy loading
GetTick64: function: Int64; stdcall;
GetTickXP: Int64Rec;
function GetTick64ForXP: Int64; stdcall;
var t32: cardinal;
t64: Int64Rec absolute result;
begin // warning: GetSystemTimeAsFileTime() is fast, but not monotonic!
t32 := Windows.GetTickCount;
t64 := GetTickXP; // (almost) atomic read
if t32<t64.Lo then
inc(t64.Hi); // wrap-up overflow after 49 days
t64.Lo := t32;
GetTickXP := t64; // (almost) atomic write
end; // warning: FPC's GetTickCount64 doesn't handle 49 days wrap :(
{$else}
function GetTick64: Int64;
begin
result := {$ifdef FPC}SynFPCLinux.{$endif}GetTickCount64;
end;
{$endif MSWINDOWS}
var // GetIPAddressesText(Sep=' ') cache
IPAddressesText: array[boolean] of SockString;
IPAddressesTix: array[boolean] of integer;
function GetIPAddressesText(const Sep: SockString; PublicOnly: boolean): SockString;
var ip: TSockStringDynArray;
tix, i: integer;
begin
result := '';
if Sep=' ' then begin
tix := GetTick64 shr 16; // refresh every minute
if tix<>IPAddressesTix[PublicOnly] then
IPAddressesTix[PublicOnly] := tix else begin
result := IPAddressesText[PublicOnly];
if result<>'' then
exit;
end;
end;
if PublicOnly then
ip := GetIPAddresses(tiaPublic) else
ip := GetIPAddresses(tiaAny);
if ip=nil then
exit;
result := ip[0];
for i := 1 to high(ip) do
result := result+Sep+ip[i];
if Sep=' ' then
IPAddressesText[PublicOnly] := result;
end;
var
MacAddressesSearched: boolean; // will not change during process lifetime
MacAddresses: TMacAddressDynArray;
MacAddressesText: SockString;
{$ifdef LINUX}
procedure GetSmallFile(const fn: TFileName; out result: SockString);
var tmp: array[byte] of AnsiChar;
F: THandle;
t: PtrInt;
begin
F := FileOpen(fn, fmOpenRead or fmShareDenyNone);
if PtrInt(F) < 0 then
exit;
t := FileRead(F, tmp, SizeOf(tmp));
FileClose(F);
while (t > 0) and (tmp[t - 1] <= ' ') do dec(t); // trim right
if t > 0 then
SetString(result, PAnsiChar(@tmp), t);
end;
{$endif LINUX}
procedure RetrieveMacAddresses;
var n: integer;
{$ifdef LINUX}
SR: TSearchRec;
fn: TFileName;
f: SockString;
{$endif LINUX}
{$ifdef MSWINDOWS}
tmp: array[word] of byte;
siz: ULONG;
p: PIP_ADAPTER_ADDRESSES;
{$endif MSWINDOWS}
begin
EnterCriticalSection(SynSockCS);
try
if MacAddressesSearched then
exit;
n := 0;
{$ifdef LINUX}
if FindFirst('/sys/class/net/*', faDirectory, SR) = 0 then begin
repeat
if (SR.Name <> 'lo') and (SR.Name[1] <> '.') then begin
fn := '/sys/class/net/' + SR.Name;
GetSmallFile(fn + '/flags', f);
if (length(f) > 2) and // e.g. '0x40' or '0x1043'
(HttpChunkToHex32(@f[3]) and (IFF_UP or IFF_LOOPBACK) = IFF_UP) then begin
GetSmallFile(fn + '/address', f);
if f <> '' then begin
SetLength(MacAddresses, n + 1);
MacAddresses[n].name := SR.Name;
MacAddresses[n].address := f;
inc(n);
end;
end;
end;
until FindNext(SR) <> 0;
FindClose(SR);
end;
{$endif LINUX}
{$ifdef MSWINDOWS}
siz := SizeOf(tmp);
p := @tmp;
if GetAdaptersAddresses(AF_UNSPEC, GAA_FLAGS, nil, p, @siz) = ERROR_SUCCESS then begin
repeat
if (p^.Flags <> 0) and (p^.OperStatus = IfOperStatusUp) and
(p^.PhysicalAddressLength = 6) then begin
SetLength(MacAddresses, n + 1);
MacAddresses[n].name := {$ifdef UNICODE}UTF8String{$else}UTF8Encode{$endif}(WideString(p^.Description));
MacAddresses[n].address := MacToText(@p^.PhysicalAddress);
inc(n);
end;
p := p^.Next;
until p = nil;
end;
{$endif MSWINDOWS}
{ TODO : RetrieveMacAddresses() for BSD
see e.g. https://gist.github.com/OrangeTide/909204 }
finally
LeaveCriticalSection(SynSockCS);
end;
MacAddressesSearched := true;
end;
function GetMacAddresses: TMacAddressDynArray;
begin
if not MacAddressesSearched then
RetrieveMacAddresses;
result := MacAddresses;
end;
function GetMacAddressesText: SockString;
var i: integer;
begin
result := MacAddressesText;
if (result <> '') or MacAddressesSearched then
exit;
RetrieveMacAddresses;
result := '';
if MacAddresses = nil then
exit;
for i := 0 to high(MacAddresses) do
with MacAddresses[i] do
result := result + name + '=' + address + ' ';
SetLength(result, length(result) - 1);
MacAddressesText := result;
end;
{$ifndef NOXPOWEREDNAME}
const
XPOWEREDNAME = 'X-Powered-By';
XPOWEREDVALUE = XPOWEREDPROGRAM + ' synopse.info';
{$endif}
{ TURI }
const
DEFAULT_PORT: array[boolean] of SockString = ('80','443');
UNIX_LOW = ord('u')+ord('n')shl 8+ord('i')shl 16+ord('x')shl 24;
procedure TURI.Clear;
begin
Https := false;
Layer := cslTCP;
Finalize(self);
end;
function TURI.From(aURI: SockString; const DefaultPort: SockString): boolean;
var P,S: PAnsiChar;
begin
Clear;
result := false;
aURI := Trim(aURI);
if aURI='' then
exit;
P := pointer(aURI);
S := P;
while S^ in ['a'..'z','A'..'Z','+','-','.','0'..'9'] do inc(S);
if PInteger(S)^ and $ffffff=ord(':')+ord('/')shl 8+ord('/')shl 16 then begin
SetString(Scheme,P,S-P);
if IdemPChar(P,'HTTPS') then
Https := true;
P := S+3;
end;
S := P;
if (PInteger(S)^=UNIX_LOW) and (S[4]=':') then begin
inc(S,5); // 'http://unix:/path/to/socket.sock:/url/path'
inc(P,5);
Layer := cslUNIX;
while not(S^ in [#0,':']) do inc(S); // Server='path/to/socket.sock'
end else
while not(S^ in [#0,':','/']) do inc(S);
SetString(Server,P,S-P);
if S^=':' then begin
inc(S);
P := S;
while not(S^ in [#0,'/']) do inc(S);
SetString(Port,P,S-P); // Port='' for cslUnix
end else
if DefaultPort<>'' then
Port := DefaultPort else
Port := DEFAULT_PORT[Https];
if S^<>#0 then // ':' or '/'
inc(S);
Address := S;
if Server<>'' then
result := true;
end;
function TURI.URI: SockString;
const Prefix: array[boolean] of SockString = ('http://','https://');
begin
if Layer=cslUNIX then
result := 'http://unix:'+Server+':/'+Address else
if (Port='') or (Port='0') or (Port=DEFAULT_PORT[Https]) then
result := Prefix[Https]+Server+'/'+Address else
result := Prefix[Https]+Server+':'+Port+'/'+Address;
end;
function TURI.PortInt: integer;
begin
result := GetCardinal(pointer(port));
end;
function TURI.Root: SockString;
var i: PtrInt;
begin
i := PosCh('?',Address);
if i=0 then
Root := Address else
Root := copy(Address,1,i-1);
end;
{ ************ Socket API access - TCrtSocket and THttp*Socket }
var
WsaDataOnce: TWSADATA;
SO_TRUE: integer = ord(true);
function ResolveName(const Name: SockString;
Family, SockProtocol, SockType: integer): SockString;
var l: TStringList;
begin
l := TStringList.Create;
try
ResolveNameToIP(Name, Family, SockProtocol, SockType, l);
if l.Count=0 then
result := Name else
result := SockString(l[0]);
finally
l.Free;
end;
end;
procedure SetInt32Option(Sock: TSocket; OptName, OptVal: integer);
var li: TLinger;
{$ifndef MSWINDOWS}
timeval: TTimeval;
{$endif}
begin
if Sock<=0 then
raise ECrtSocket.CreateFmt('Unexpected SetOption(%d,%d)',[OptName,OptVal]);
case OptName of
SO_SNDTIMEO, SO_RCVTIMEO: begin
{$ifndef MSWINDOWS} // POSIX expects a timeval parameter for time out values
timeval.tv_sec := OptVal div 1000;
timeval.tv_usec := (OptVal mod 1000)*1000;
if SetSockOpt(Sock,SOL_SOCKET,OptName,@timeval,sizeof(timeval))=0 then
{$else}
// WinAPI expects the time out directly as ms integer
if SetSockOpt(Sock,SOL_SOCKET,OptName,pointer(@OptVal),sizeof(OptVal))=0 then
{$endif}
exit;
end;
SO_KEEPALIVE: // boolean (0/1) value
if SetSockOpt(Sock,SOL_SOCKET,OptName,pointer(@OptVal),sizeof(OptVal))=0 then
exit;
SO_LINGER: begin // not available on UDP
if OptVal<0 then
li.l_onoff := Ord(false) else begin
li.l_onoff := Ord(true);
li.l_linger := OptVal;
end;
SetSockOpt(Sock,SOL_SOCKET, SO_LINGER, @li, SizeOf(li));
if OptVal>0 then begin
{$ifdef LINUX}
{$ifdef BSD}
SetSockOpt(Sock,SOL_SOCKET,SO_REUSEPORT,@SO_TRUE,SizeOf(SO_TRUE));
{$ifndef OpenBSD}
SetSockOpt(Sock,SOL_SOCKET,SO_NOSIGPIPE,@SO_TRUE,SizeOf(SO_TRUE));
{$endif OpenBSD}
{$else}
SetSockOpt(Sock,SOL_SOCKET, SO_REUSEADDR,@SO_TRUE,SizeOf(SO_TRUE));
{$endif BSD}
{$endif LINUX}
end;
exit;
end;
TCP_NODELAY: // boolean (0/1) value
if SetSockOpt(Sock,IPPROTO_TCP,OptName,@OptVal,sizeof(OptVal))=0 then
exit;
end;
raise ECrtSocket.CreateFmt('SetOption(%d,%d)',[OptName,OptVal],-1);
end;
function CallServer(const Server, Port: SockString; doBind: boolean;
aLayer: TCrtSocketLayer; ConnectTimeout: DWORD): TSocket;
var sin: TVarSin;
IP: SockString;
socktype, ipproto, family: integer;
{$ifndef MSWINDOWS}
//serveraddr: sockaddr_un;
{$endif}
begin
result := -1;
case aLayer of
cslTCP: begin
socktype := SOCK_STREAM;
ipproto := IPPROTO_TCP;
end;
cslUDP: begin
socktype := SOCK_DGRAM;
ipproto := IPPROTO_UDP;
end;
cslUNIX: begin
{$ifdef MSWINDOWS}
exit; // not handled under Win32
{$else}
socktype := SOCK_STREAM;
ipproto := 0;
{$endif}
end;
else exit;
end;
if SameText(Server,'localhost')
{$ifndef MSWINDOWS}or ((Server='') and not doBind){$endif} then
IP := cLocalHost else
if aLayer=cslUNIX then
IP := Server else
IP := ResolveName(Server,AF_INET,ipproto,socktype);
{$ifndef MSWINDOWS}
if aLayer=cslUNIX then
family := AF_UNIX else
{$endif}
// use AF_INET instead of AF_UNSPEC: IP6 is buggy!
family := AF_INET;
if SetVarSin(sin,IP,Port,family,ipproto,socktype,false)<>0 then
exit;
result := Socket(integer(sin.AddressFamily),socktype,ipproto);
if result=-1 then
exit;
if doBind then begin
// Socket should remain open for 5 seconds after a closesocket() call
SetInt32Option(result,SO_LINGER,5);
// bind and listen to this port as server
if (Bind(result,sin)<>0) or
((aLayer<>cslUDP) and (Listen(result,DefaultListenBacklog)<>0)) then begin
CloseSocket(result);
result := -1;
end;
end else begin
// open client connection
if ConnectTimeout>0 then begin
SetInt32Option(result,SO_RCVTIMEO,ConnectTimeout);
SetInt32Option(result,SO_SNDTIMEO,ConnectTimeout);
end;
if Connect(result,sin)<>0 then begin
CloseSocket(result);
result := -1;
end;
end;
end;
type
PCrtSocket = ^TCrtSocket;
function OutputSock(var F: TTextRec): integer;
begin
if F.BufPos=0 then
result := 0 else
if PCrtSocket(@F.UserData)^.TrySndLow(F.BufPtr,F.BufPos) then begin
F.BufPos := 0;
result := 0;
end else
result := -1; // on socket error -> raise ioresult error
end;
function WSAIsFatalError(anothernonfatal: integer=NO_ERROR): boolean;
var err: integer;
begin
err := WSAGetLastError;
result := (err<>NO_ERROR) and (err<>WSATRY_AGAIN) and
{$ifdef MSWINDOWS}(err<>WSAETIMEDOUT) and (err<>WSAEWOULDBLOCK) and{$endif}
(err<>anothernonfatal); // allow WSAEADDRNOTAVAIL from OpenBind()
end;
function WSAErrorAtShutdown(sock: TSocket): integer;
var dummy: byte;
begin
if AsynchRecv(sock,@dummy,SizeOf(dummy))<0 then
result := WSAGetLastError else
result := 0; // read access allowed = socket was closed gracefully
end;
function InputSock(var F: TTextRec): Integer;
// SockIn pseudo text file fill its internal buffer only with available data
// -> no unwanted wait time is added
// -> very optimized use for readln() in HTTP stream
var Size: integer;
Sock: TCRTSocket;
{$ifdef MSWINDOWS}
iSize: integer;
{$else}
sin: TVarSin;
{$endif}
begin
F.BufEnd := 0;
F.BufPos := 0;
Sock := PCrtSocket(@F.UserData)^;
if (Sock=nil) or (Sock.Sock<=0) then begin
result := WSAECONNABORTED; // on socket error -> raise ioresult error
exit; // file closed = no socket -> error
end;
result := Sock.fSockInEofError;
if result<>0 then
exit; // already reached error below
Size := F.BufSize;
if Sock.SocketLayer=cslUDP then begin
{$ifdef MSWINDOWS}
iSize := SizeOf(TSockAddr);
Size := RecvFrom(Sock.Sock, F.BufPtr, Size, 0, @Sock.fPeerAddr, @iSize);
{$else}
Size := RecvFrom(Sock.Sock, F.BufPtr, Size, 0, sin);
Sock.fPeerAddr.sin_port := sin.sin_port;
Sock.fPeerAddr.sin_addr := sin.sin_addr;
{$endif}
end else // cslTCP/cslUNIX
if not Sock.TrySockRecv(F.BufPtr,Size,{StopBeforeLength=}true) then
Size := -1; // fatal socket error
// TrySockRecv() may return Size=0 if no data is pending, but no TCP/IP error
if Size>=0 then begin
F.BufEnd := Size;
inc(Sock.fBytesIn,Size);
result := 0; // no error
end else begin
if Sock.Sock<=0 then // socket broken or closed
result := WSAECONNABORTED else begin
result := -integer(WSAGetLastError); // integer() for FPC+Win target
if result=0 then
result := WSAETIMEDOUT;
end;
Sock.fSockInEofError := result; // error -> mark end of SockIn
// result <0 will update ioresult and raise an exception if {$I+}
end;
end;
function CloseSock(var F: TTextRec): integer;
begin
if PCrtSocket(@F.UserData)^<>nil then
PCrtSocket(@F.UserData)^.Close;
PCrtSocket(@F.UserData)^ := nil;
Result := 0;
end;
function OpenSock(var F: TTextRec): integer;
begin
F.BufPos := 0;
F.BufEnd := 0;
if F.Mode=fmInput then begin // ReadLn
F.InOutFunc := @InputSock;
F.FlushFunc := nil;
end else begin // WriteLn
F.Mode := fmOutput;
F.InOutFunc := @OutputSock;
F.FlushFunc := @OutputSock;
end;
F.CloseFunc := @CloseSock;
Result := 0;
end;
{ TCrtSocket }
function Split(const Text: SockString; Sep: AnsiChar; var Before,After: SockString): boolean;
var i: integer;
begin
for i := length(Text)-1 downto 2 do
if Text[i]=Sep then begin
trimcopy(Text,1,i-1,Before);
trimcopy(Text,i+1,maxInt,After);
result := true;
exit;
end;
result := false;
end;
constructor TCrtSocket.Bind(const aAddr: SockString; aLayer: TCrtSocketLayer;
aTimeOut: integer);
var s,p: SockString;
aSock: integer;
{$ifdef LINUXNOTBSD}
n: integer;
{$endif}
begin
Create(aTimeOut);
if aAddr='' then begin
{$ifdef LINUXNOTBSD} // try systemd
if not SystemdIsAvailable then
raise ECrtSocket.Create('Bind('''') but Systemd is not available');
n := ExternalLibraries.sd_listen_fds(0);
if n > 1 then
raise ECrtSocket.Create('Bind(''''): Systemd activation failed - too ' +
'many file descriptors received');
aSock := SD_LISTEN_FDS_START + 0;
{$else}
raise ECrtSocket.Create('Bind('''') is not allowed on this platform');
{$endif}
end else begin
aSock := -1; // force OpenBind to create listening socket
if not Split(aAddr,':',s,p) then begin
s := '0.0.0.0';
p := aAddr;
end;
{$ifndef MSWINDOWS}
if s='unix' then begin
aLayer := cslUNIX;
s := p;
p := '';
end;
{$endif MSWINDOWS}
end;
OpenBind(s,p,{dobind=}true,aSock,aLayer); // raise a ECrtSocket on error
end;
constructor TCrtSocket.Open(const aServer, aPort: SockString; aLayer: TCrtSocketLayer;
aTimeOut: cardinal; aTLS: boolean);
begin
Create(aTimeOut); // default read timeout is 10 seconds
OpenBind(aServer,aPort,{dobind=}false,-1,aLayer,aTLS); // raise an ECrtSocket on error
end;
type
PTextRec = ^TTextRec;
procedure TCrtSocket.Close;
begin
if self=nil then
exit;
fSndBufLen := 0; // always reset (e.g. in case of further Open)
fSockInEofError := 0;
ioresult; // reset ioresult value if SockIn/SockOut were used
if SockIn<>nil then begin
PTextRec(SockIn)^.BufPos := 0; // reset input buffer
PTextRec(SockIn)^.BufEnd := 0;
end;
if SockOut<>nil then begin
PTextRec(SockOut)^.BufPos := 0; // reset output buffer
PTextRec(SockOut)^.BufEnd := 0;
end;
if fSock<=0 then
exit; // no opened connection, or Close already executed
{$ifdef LINUXNOTBSD}
if (fWasBind and (fPort='')) then begin // binded on external socket
fSock := -1;
exit;
end;
{$endif}
{$ifdef MSWINDOWS}
if fSecure.Initialized then
fSecure.BeforeDisconnection(fSock);
{$endif MSWINDOWS}
DirectShutdown(fSock,{rdwr=}fWasBind);
fSock := -1; // don't change Server or Port, since may try to reconnect
end;
constructor TCrtSocket.Create(aTimeOut: PtrInt);
begin
fTimeOut := aTimeOut;
end;
procedure TCrtSocket.SetInt32OptionByIndex(OptName, OptVal: integer);
begin
SetInt32Option(Sock,OptName,OptVal);
end;
procedure TCrtSocket.OpenBind(const aServer, aPort: SockString;
doBind: boolean; aSock: integer; aLayer: TCrtSocketLayer; aTLS: boolean);
const BINDTXT: array[boolean] of string[4] = ('open','bind');
BINDMSG: array[boolean] of string = ('Is a server running on this address:port?',
'Another process may be currently listening to this port!');
var retry: integer;
begin
fSocketLayer := aLayer;
fWasBind := doBind;
if aSock<=0 then begin
if (aPort='') and (aLayer<>cslUNIX) then
fPort := DEFAULT_PORT[aTLS] else // default port is 80/443 (HTTP/S)
fPort := aPort;
fServer := aServer;
if doBind then // allow small number of retries (e.g. XP or BSD during aggressive tests)
retry := 10 else
retry := {$ifdef BSD}10{$else}2{$endif};
repeat
fSock := CallServer(aServer,Port,doBind,aLayer,Timeout); // OPEN or BIND
if (fSock>0) then
break;
dec(retry);
if WSAIsFatalError(WSAEADDRNOTAVAIL) or (retry<=0) then
raise ECrtSocket.CreateFmt('OpenBind(%s:%s,%s) failed: %s',
[aServer,fPort,BINDTXT[doBind],BINDMSG[doBind]],-1);
sleep(10);
until false;
end else
fSock := aSock; // ACCEPT mode -> socket is already created by caller
if TimeOut>0 then begin // set timout values for both directions
ReceiveTimeout := TimeOut;
SendTimeout := TimeOut;
end;
if aLayer=cslTCP then begin
if (aSock<0) or ((aSock>0) and not doBind) then begin // do not touch externally created socket
TCPNoDelay := 1; // disable Nagle algorithm since we use our own buffers
KeepAlive := 1; // enable TCP keepalive (even if we rely on transport layer)
end;
if aTLS and (aSock<=0) and not doBind then
try
{$ifdef MSWINDOWS}
fSecure.AfterConnection(fSock,pointer(aServer));
{$else}
raise ECrtSocket.Create('TLS is unsupported on this system');
{$endif MSWINDOWS}
fTLS := true;
except
on E: Exception do
raise ECrtSocket.CreateFmt('OpenBind(%s:%s,%s): TLS failed [%s %s]',
[aServer,Port,BINDTXT[doBind],E.ClassName,E.Message],-1);
end;
end;
{$ifdef SYNCRTDEBUGLOW}
TSynLog.Add.Log(sllCustom2, 'OpenBind(%:%) % sock=% (accept=%) ',
[fServer,fPort,BINDTXT[doBind], fSock, aSock], self);
{$endif}
end;
procedure TCrtSocket.AcceptRequest(aClientSock: TSocket; aClientSin: PVarSin);
begin
{$ifdef LINUXNOTBSD}
// on Linux fd returned from accept() inherits all parent fd options
// except O_NONBLOCK and O_ASYNC
fSock := aClientSock;
{$else}
// on other OS inheritance is undefined, so call OpenBind to set all fd options
OpenBind('','',false,aClientSock,fSocketLayer); // set the ACCEPTed aClientSock
Linger := 5; // should remain open for 5 seconds after a closesocket() call
{$endif LINUXNOTBSD}
if aClientSin<>nil then
IPText(aClientSin^,fRemoteIP,RemoteIPLocalHostAsVoidInServers);
end;
procedure TCrtSocket.SockSend(const Values: array of const);
var i: integer;
tmp: shortstring;
begin
for i := 0 to high(Values) do
with Values[i] do
case VType of
vtString:
SockSend(@VString^[1],pByte(VString)^);
vtAnsiString:
SockSend(VAnsiString,length(SockString(VAnsiString)));
{$ifdef HASVARUSTRING}
vtUnicodeString: begin
tmp := ShortString(UnicodeString(VUnicodeString)); // convert into ansi
SockSend(@tmp[1],length(tmp));
end;
{$endif}
vtPChar:
SockSend(VPChar,StrLen(VPChar));
vtChar:
SockSend(@VChar,1);
vtWideChar:
SockSend(@VWideChar,1); // only ansi part of the character
vtInteger: begin
Str(VInteger,tmp);
SockSend(@tmp[1],length(tmp));
end;
vtInt64{$ifdef FPC},vtQWord{$endif}: begin
Str(VInt64^,tmp);
SockSend(@tmp[1],length(tmp));
end;
end;
SockSend(@CRLF,2);
end;
procedure TCrtSocket.SockSend(const Line: SockString);
begin
if Line<>'' then
SockSend(pointer(Line),length(Line));
SockSend(@CRLF,2);
end;
procedure TCrtSocket.SockSendFlush(const aBody: SockString);
var body,avail: integer;
begin
body := Length(aBody);
if body>0 then begin
avail := SockSendRemainingSize; // around 1800 bytes
if avail>=body then begin
SockSend(pointer(aBody),body); // append to buffer as single TCP packet
body := 0;
end;
end;
{$ifdef SYNCRTDEBUGLOW}
TSynLog.Add.Log(sllCustom2, 'SockSend sock=% flush len=% body=% %',
[fSock,fSndBufLen,length(aBody),LogEscapeFull(pointer(fSndBuf),fSndBufLen)],self);
if body>0 then
TSynLog.Add.Log(sllCustom2, 'SockSend sock=% body len=% %',
[fSock,body,LogEscapeFull(pointer(aBody),body)],self);
{$endif}
if not TrySockSendFlush then
raise ECrtSocket.CreateFmt('SockSendFlush(%s) len=%d',[fServer,fSndBufLen],-1);
if body>0 then
SndLow(pointer(aBody),body); // direct sending of biggest packets
end;
function TCrtSocket.TrySockSendFlush: boolean;
begin
if fSndBufLen=0 then
result := true else begin
result := TrySndLow(pointer(fSndBuf),fSndBufLen);
if result then
fSndBufLen := 0;
end;
end;
function TCrtSocket.SockSendRemainingSize: integer;
begin
result := length(fSndBuf)-fSndBufLen;
end;
procedure TCrtSocket.SndLow(P: pointer; Len: integer);
begin
if not TrySndLow(P,Len) then
raise ECrtSocket.CreateFmt('SndLow(%s) len=%d',[fServer,Len],-1);
end;
function TCrtSocket.TrySndLow(P: pointer; Len: integer): boolean;
var sent: integer;
now, start: Int64;
begin
result := Len=0;
if (self=nil) or (fSock<=0) or (Len<=0) or (P=nil) then
exit;
start := {$ifdef MSWINDOWS}GetTick64{$else}0{$endif};
repeat
{$ifdef MSWINDOWS}
if fSecure.Initialized then
sent := fSecure.Send(fSock, P, Len) else
{$endif MSWINDOWS}
sent := AsynchSend(fSock, P, Len);
if sent>0 then begin
inc(fBytesOut,sent);
dec(Len,sent);
if Len<=0 then
break;
inc(PByte(P),sent);
end else if WSAIsFatalError then
exit; // fatal socket error
now := GetTick64;
if (start=0) or (sent>0) then
start := now else // measure timeout since nothing written
if now-start>TimeOut then
exit; // identify timeout as error
SleepHiRes(1);
until false;
result := true;
end;
procedure TCrtSocket.Write(const Data: SockString);
begin
SndLow(pointer(Data),length(Data));
end;
function TCrtSocket.AcceptIncoming(ResultClass: TCrtSocketClass): TCrtSocket;
var client: TSocket;
sin: TVarSin;
begin
result := nil;
if (self=nil) or (fSock<=0) then
exit;
client := Accept(fSock,sin);
if client<=0 then
exit;
if ResultClass=nil then
ResultClass := TCrtSocket;
result := ResultClass.Create(Timeout);
result.AcceptRequest(client,@sin);
result.CreateSockIn; // use SockIn with 1KB input buffer: 2x faster
end;
function TCrtSocket.SockInRead(Content: PAnsiChar; Length: integer;
UseOnlySockIn: boolean): integer;
var len,res: integer;
// read Length bytes from SockIn^ buffer + Sock if necessary
begin
// get data from SockIn buffer, if any (faster than ReadChar)
result := 0;
if Length<=0 then
exit;
if SockIn<>nil then
with PTextRec(SockIn)^ do
repeat
len := BufEnd-BufPos;
if len>0 then begin
if len>Length then
len := Length;
move(BufPtr[BufPos],Content^,len);
inc(BufPos,len);
inc(Content,len);
dec(Length,len);
inc(result,len);
end;
if Length=0 then
exit; // we got everything we wanted
if not UseOnlySockIn then
break;
res := InputSock(PTextRec(SockIn)^);
if res<0 then
raise ECrtSocket.CreateFmt('SockInRead InputSock=%d',[res],-1);
until Timeout=0;
// direct receiving of the remaining bytes from socket
if Length>0 then begin
SockRecv(Content,Length); // raise ECrtSocket if failed to read Length
inc(result,Length);
end;
end;
function TCrtSocket.SockInPending(aTimeOutMS: integer; aPendingAlsoInSocket: boolean): integer;
var backup: PtrInt;
insocket: integer;
begin
if SockIn=nil then
raise ECrtSocket.Create('SockInPending without SockIn');
if aTimeOutMS<0 then
raise ECrtSocket.Create('SockInPending(aTimeOutMS<0)');
with PTextRec(SockIn)^ do
result := BufEnd-BufPos;
if result=0 then
// no data in SockIn^.Buffer, so try if some pending at socket level
case SockReceivePending(aTimeOutMS) of
cspDataAvailable: begin
backup := fTimeOut;
fTimeOut := 0; // not blocking call to fill SockIn buffer
try
// call InputSock() to actually retrieve any pending data
if InputSock(PTextRec(SockIn)^)=NO_ERROR then
with PTextRec(SockIn)^ do
result := BufEnd-BufPos else
result := -1; // indicates broken socket
finally
fTimeOut := backup;
end;
end;
cspSocketError:
result := -1; // indicates broken/closed socket
end; // cspNoData will leave result=0
{$ifdef MSWINDOWS}
// under Unix SockReceivePending use poll(fSocket) and if data available
// ioctl syscall is redundant
if aPendingAlsoInSocket then
// also includes data in socket bigger than TTextRec's buffer
if (IOCtlSocket(Sock,FIONREAD,insocket)=0) and (insocket>0) then
inc(result,insocket);
{$endif MSWINDOWS}
end;
destructor TCrtSocket.Destroy;
begin
Close;
CloseSockIn;
CloseSockOut;
inherited;
end;
procedure TCrtSocket.SockSend(P: pointer; Len: integer);
var cap: integer;
begin
if Len<=0 then
exit;
cap := Length(fSndBuf);
if Len+fSndBufLen>cap then
SetLength(fSndBuf,len+cap+cap shr 3+2048);
move(P^,PByteArray(fSndBuf)[fSndBufLen],Len);
inc(fSndBufLen,Len);
end;
const
SOCKMINBUFSIZE = 1024; // big enough for headers (content will be read directly)
{$ifdef FPC}
procedure SetLineBreakStyle(var T: Text; Style: TTextLineBreakStyle);
begin
case Style Of
tlbsCR: TextRec(T).LineEnd := #13;
tlbsLF: TextRec(T).LineEnd := #10;
tlbsCRLF: TextRec(T).LineEnd := #13#10;
end;
end;
{$endif FPC}
procedure TCrtSocket.CreateSockIn(LineBreak: TTextLineBreakStyle;
InputBufferSize: Integer);
begin
if (Self=nil) or (SockIn<>nil) then
exit; // initialization already occured
if InputBufferSize<SOCKMINBUFSIZE then
InputBufferSize := SOCKMINBUFSIZE;
GetMem(fSockIn,sizeof(TTextRec)+InputBufferSize);
fillchar(SockIn^,sizeof(TTextRec),0);
with TTextRec(SockIn^) do begin
PCrtSocket(@UserData)^ := self;
Mode := fmClosed;
BufSize := InputBufferSize;
BufPtr := pointer(PAnsiChar(SockIn)+sizeof(TTextRec)); // ignore Buffer[] (Delphi 2009+)
OpenFunc := @OpenSock;
Handle := -1;
end;
{$ifndef DELPHI5OROLDER}
SetLineBreakStyle(SockIn^,LineBreak); // http does break lines with #13#10
{$endif}
Reset(SockIn^);
end;
procedure TCrtSocket.CreateSockOut(OutputBufferSize: Integer);
begin
if SockOut<>nil then
exit; // initialization already occured
if OutputBufferSize<SOCKMINBUFSIZE then
OutputBufferSize := SOCKMINBUFSIZE;
GetMem(fSockOut,sizeof(TTextRec)+OutputBufferSize);
fillchar(SockOut^,sizeof(TTextRec),0);
with TTextRec(SockOut^) do begin
PCrtSocket(@UserData)^ := self;
Mode := fmClosed;
BufSize := OutputBufferSize;
BufPtr := pointer(PAnsiChar(SockIn)+sizeof(TTextRec)); // ignore Buffer[] (Delphi 2009+)
OpenFunc := @OpenSock;
Handle := -1;
end;
{$ifndef DELPHI5OROLDER}
SetLineBreakStyle(SockOut^,tlbsCRLF); // force e.g. for Linux platforms
{$endif}
Rewrite(SockOut^);
end;
procedure TCrtSocket.CloseSockIn;
begin
if (self<>nil) and (fSockIn<>nil) then begin
Freemem(fSockIn);
fSockIn := nil;
end;
end;
procedure TCrtSocket.CloseSockOut;
begin
if (self<>nil) and (fSockOut<>nil) then begin
Freemem(fSockOut);
fSockOut := nil;
end;
end;
procedure TCrtSocket.SockRecv(Buffer: pointer; Length: integer);
var read: integer;
begin
read := Length;
if not TrySockRecv(Buffer,read,{StopBeforeLength=}false) or (Length<>read) then
raise ECrtSocket.CreateFmt('SockRecv(%d) failure (read=%d)',[Length,read]);
end;
const
_CSP: array[TCrtSocketPending] of string[7] = ('ERROR','nodata','data');
function TCrtSocket.TrySockRecv(Buffer: pointer; var Length: integer;
StopBeforeLength: boolean): boolean;
var expected,read: PtrInt;
now, last, diff: Int64;
begin
result := false;
if (self<>nil) and (fSock>0) and (Buffer<>nil) and (Length>0) then begin
expected := Length;
Length := 0;
last := {$ifdef MSWINDOWS}GetTick64{$else}0{$endif};
repeat
read := expected-Length;
{$ifdef MSWINDOWS}
if fSecure.Initialized then
read := fSecure.Receive(fSock,Buffer,read) else
{$endif MSWINDOWS}
read := AsynchRecv(fSock,Buffer,read);
if read<=0 then begin // no more to read, or socket issue?
{$ifdef SYNCRTDEBUGLOW}
TSynLog.Add.Log(sllCustom2, 'TrySockRecv: sock=% AsynchRecv=% %',
[Sock,read,SocketErrorMessage],self);
{$endif}
if (read=0) or WSAIsFatalError then begin
Close; // connection broken or socket closed gracefully (read=0)
exit;
end;
if StopBeforeLength then
break;
end else begin
inc(fBytesIn,read);
inc(Length,read);
if StopBeforeLength or (Length=expected) then
break; // good enough for now
inc(PByte(Buffer),read);
end;
now := GetTick64;
if (last=0) or (read>0) then // check timeout from unfinished read
last := now else begin
diff := now-last;
if diff>=TimeOut then begin
{$ifdef SYNCRTDEBUGLOW}
TSynLog.Add.Log(sllCustom2, 'TrySockRecv: timeout (diff=%>%)',[diff,TimeOut],self);
{$endif}
exit; // identify read timeout as error
end;
if diff<100 then
SleepHiRes(0) else
SleepHiRes(1);
end;
until false;
result := true;
end;
end;
function TCrtSocket.SockReceivePending(TimeOutMS: integer): TCrtSocketPending;
var res: integer;
{$ifdef MSWINDOWS}
tv: TTimeVal;
fdset: TFDSet;
pending: integer;
{$ifdef SYNCRTDEBUGLOW}
time: TPrecisionTimer;
{$endif}
{$else}
p: TPollFD; // TFDSet limited to 1024 total sockets in POSIX -> use poll()
{$endif}
begin
if (self=nil) or (fSock<=0) then begin
{$ifdef SYNCRTDEBUGLOW}
TSynLog.Add.Log(sllCustom2, 'SockReceivePending: no Sock',self);
{$endif}
result := cspSocketError;
exit;
end;
{$ifdef MSWINDOWS}
{$ifdef SYNCRTDEBUGLOW} time.Start; {$endif}
fdset.fd_array[0] := fSock;
fdset.fd_count := 1;
tv.tv_sec := TimeOutMS div 1000;
tv.tv_usec := (TimeOutMS mod 1000)*1000;
pending := -1;
res := Select(fSock+1,@fdset,nil,nil,@tv);
if res<0 then
result := cspSocketError else
if (res=0) or (fdset.fd_count<>1) or (fdset.fd_array[0]<>fSock) then
result := cspNoData else
if IoctlSocket(fSock,FIONREAD,pending)=0 then
if pending>0 then
result := cspDataAvailable else
if TimeOutMS=0 then
result := cspNoData else begin
// https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-select#remarks
{$ifdef SYNCRTDEBUGLOW}
TSynLog.Add.Log(sllCustom3, 'SockReceivePending: sock=% closed gracefully?',[fSock],self);
{$endif}
result := cspSocketError;
end else
result := cspSocketError;
{$ifdef SYNCRTDEBUGLOW}
TSynLog.Add.Log(sllCustom2, 'SockReceivePending sock=% timeout=% fd_count=% fd_array[0]=% select=% result=% pending=% time=%',
[fSock, TimeOutMS, fdset.fd_count, fdset.fd_array[0], res, _CSP[result], pending, time.Stop], self);
{$endif}
{$else}
// https://moythreads.com/wordpress/2009/12/22/select-system-call-limitation
p.fd := fSock;
p.events := POLLIN;
p.revents := 0;
res := poll(@p,1,TimeOutMS);
if res<0 then
if WSAIsFatalError then
result := cspSocketError else
result := cspNoData else
if p.revents=POLLIN then
result := cspDataAvailable else
result := cspNoData;
{$endif}
end;
function TCrtSocket.LastLowSocketError: Integer;
begin
result := WSAGetLastError; // retrieved directly from Sockets API
end;
procedure TCrtSocket.SockRecvLn(out Line: SockString; CROnly: boolean);
procedure RecvLn(var Line: SockString);
var P: PAnsiChar;
LP, L: PtrInt;
tmp: array[0..1023] of AnsiChar; // avoid ReallocMem() every char
begin
P := @tmp;
Line := '';
repeat
SockRecv(P,1); // this is very slow under Windows -> use SockIn^ instead
if P^<>#13 then // at least NCSA 1.3 does send a #10 only -> ignore #13
if P^=#10 then begin
if Line='' then // get line
SetString(Line,tmp,P-tmp) else begin
LP := P-tmp; // append to already read chars
L := length(Line);
Setlength(Line,L+LP);
move(tmp,PByteArray(Line)[L],LP);
end;
exit;
end else
if P=@tmp[1023] then begin // tmp[] buffer full?
L := length(Line); // -> append to already read chars
Setlength(Line,L+1024);
move(tmp,PByteArray(Line)[L],1024);
P := tmp;
end else
inc(P);
until false;
end;
var c: byte;
L, Error: PtrInt;
begin
if CROnly then begin // slower but accurate version expecting #13 as line end
// SockIn^ expect either #10, either #13#10 -> a dedicated version is needed
repeat
SockRecv(@c,1); // this is slow but works
if c in [0,13] then
exit; // end of line
L := length(Line);
SetLength(Line,L+1);
PByteArray(Line)[L] := c;
until false;
end else
if SockIn<>nil then begin
{$I-}
readln(SockIn^,Line); // example: HTTP/1.0 200 OK
Error := ioresult;
if Error<>0 then
raise ECrtSocket.CreateFmt('SockRecvLn after %d chars',[length(Line)],Error);
{$I+}
end else
RecvLn(Line); // slow under Windows -> use SockIn^ instead
end;
procedure TCrtSocket.SockRecvLn;
var c: AnsiChar;
Error: integer;
begin
if SockIn<>nil then begin
{$I-}
readln(SockIn^);
Error := ioresult;
if Error<>0 then
raise ECrtSocket.Create('SockRecvLn',Error);
{$I+}
end else
repeat
SockRecv(@c,1);
until c=#10;
end;
function TCrtSocket.SockConnected: boolean;
var sin: TVarSin;
begin
result := (self<>nil) and (fSock>0) and (GetPeerName(fSock,sin)=0);
end;
function TCrtSocket.PeerAddress: SockString;
begin
IPText(PVarSin(@fPeerAddr)^,result);
end;
function TCrtSocket.PeerPort: integer;
begin
result := fPeerAddr.sin_port;
end;
function TCrtSocket.SockReceiveString: SockString;
var available, resultlen, read: integer;
begin
result := '';
if (self=nil) or (fSock<=0) then
exit;
resultlen := 0;
repeat
if (fSock<=0) or ((IOCtlSocket(fSock,FIONREAD,available)<>0) and WSAIsFatalError) then
exit; // raw socket error
if available=0 then // no data in the allowed timeout
if result='' then begin // wait till something
SleepHiRes(1);// some delay in infinite loop
continue;
end else
break; // return what we have
SetLength(result,resultlen+available); // append to result
read := available;
if not TrySockRecv(@PByteArray(result)[resultlen],read,{StopBeforeLength=}true) then begin
Close;
SetLength(result,resultlen);
exit;
end;
inc(resultlen,read);
if read<available then
SetLength(result,resultlen); // e.g. Read=0 may happen
SleepHiRes(0); // 10 microsecs on POSIX
until false;
end;
{ THttpClientSocket }
constructor THttpClientSocket.Create(aTimeOut: PtrInt);
begin
if aTimeOut=0 then
aTimeOut := HTTP_DEFAULT_RECEIVETIMEOUT;
inherited Create(aTimeOut);
fUserAgent := DefaultUserAgent(self);
end;
function THttpClientSocket.Delete(const url: SockString; KeepAlive: cardinal;
const header: SockString): integer;
begin
result := Request(url,'DELETE',KeepAlive,header,'','',false);
end;
function THttpClientSocket.Get(const url: SockString; KeepAlive: cardinal=0; const header: SockString=''): integer;
begin
result := Request(url,'GET',KeepAlive,header,'','',false);
end;
function AuthorizationBearer(const AuthToken: SockString): SockString;
begin
if AuthToken='' then
result := '' else
result := 'Authorization: Bearer '+AuthToken;
end;
function THttpClientSocket.GetAuth(const url, AuthToken: SockString; KeepAlive: cardinal=0): integer;
begin
result := Get(url,KeepAlive,AuthorizationBearer(AuthToken));
end;
function THttpClientSocket.Head(const url: SockString; KeepAlive: cardinal;
const header: SockString): integer;
begin
result := Request(url,'HEAD',KeepAlive,header,'','',false);
end;
function THttpClientSocket.Post(const url, Data, DataType: SockString; KeepAlive: cardinal;
const header: SockString): integer;
begin
result := Request(url,'POST',KeepAlive,header,Data,DataType,false);
end;
function THttpClientSocket.Put(const url, Data, DataType: SockString;
KeepAlive: cardinal; const header: SockString): integer;
begin
result := Request(url,'PUT',KeepAlive,header,Data,DataType,false);
end;
procedure THttpClientSocket.RequestSendHeader(const url, method: SockString);
begin
if fSock<=0 then
exit;
if SockIn=nil then // done once
CreateSockIn; // use SockIn by default if not already initialized: 2x faster
if TCPPrefix<>'' then
SockSend(TCPPrefix);
if (url='') or (url[1]<>'/') then
SockSend([method,' /',url,' HTTP/1.1']) else
SockSend([method,' ',url,' HTTP/1.1']);
if Port=DEFAULT_PORT[fTLS] then
SockSend(['Host: ',Server]) else
SockSend(['Host: ',Server,':',Port]);
SockSend(['Accept: */*'#13#10'User-Agent: ',UserAgent]);
end;
function THttpClientSocket.Request(const url, method: SockString;
KeepAlive: cardinal; const header, Data, DataType: SockString; retry: boolean): integer;
procedure DoRetry(Error: integer; const msg: SockString);
begin
{$ifdef SYNCRTDEBUGLOW} TSynLog.Add.Log(sllCustom2,
'Request: % socket=% DoRetry(%) retry=%',[msg,Sock,Error,BOOL_STR[retry]],self);
{$endif}
if retry then // retry once -> return error only if failed after retrial
result := Error else begin
Close; // close this connection
try
HeaderFlags := [];
OpenBind(Server,Port,false,-1,cslTcp,fTLS); // retry with a new socket
result := Request(url,method,KeepAlive,Header,Data,DataType,true);
except
on Exception do
result := Error;
end;
end;
end;
var P: PAnsiChar;
aData: SockString;
begin
if SockIn=nil then // done once
CreateSockIn; // use SockIn by default if not already initialized: 2x faster
Content := '';
if (connectionClose in HeaderFlags) or
(SockReceivePending(0)=cspSocketError) then begin
DoRetry(STATUS_NOTFOUND,'connection broken (kepepalive timeout or too many requests)');
exit;
end;
try
try
// send request - we use SockSend because writeln() is calling flush()
// -> all headers will be sent at once
RequestSendHeader(url,method);
if KeepAlive>0 then
SockSend(['Keep-Alive: ',KeepAlive,#13#10'Connection: Keep-Alive']) else
SockSend('Connection: Close');
aData := Data; // local var copy for Data to be compressed in-place
CompressDataAndWriteHeaders(DataType,aData);
if header<>'' then
SockSend(header);
if fCompressAcceptEncoding<>'' then
SockSend(fCompressAcceptEncoding);
SockSend; // send CRLF
SockSendFlush(aData); // flush all pending data to network
// get headers
if SockReceivePending(1000)=cspSocketError then begin
DoRetry(STATUS_NOTFOUND,'cspSocketError waiting for headers');
exit;
end;
SockRecvLn(Command); // will raise ECrtSocket on any error
if TCPPrefix<>'' then
if Command<>TCPPrefix then begin
result := STATUS_HTTPVERSIONNONSUPPORTED; // 505
exit;
end else
SockRecvLn(Command);
P := pointer(Command);
if IdemPChar(P,'HTTP/1.') then begin
result := GetCardinal(P+9); // get http numeric status code (200,404...)
if result=0 then begin
result := STATUS_HTTPVERSIONNONSUPPORTED;
exit;
end;
while result=100 do begin
repeat // 100 CONTINUE is just to be ignored client side
SockRecvLn(Command);
P := pointer(Command);
until IdemPChar(P,'HTTP/1.'); // ignore up to next command
result := GetCardinal(P+9);
end;
if P[7]='0' then
KeepAlive := 0; // HTTP/1.0 -> force connection close
end else begin // error on reading answer
DoRetry(STATUS_HTTPVERSIONNONSUPPORTED,Command); // 505=wrong format
exit;
end;
GetHeader(false); // read all other headers
if (result>=STATUS_SUCCESS) and (result<>STATUS_NOCONTENT) and
(result<>STATUS_NOTMODIFIED) and
(IdemPCharArray(pointer(method),['HEAD','OPTIONS'])<0) then
GetBody; // get content if necessary (HEAD or OPTIONS have no body)
except
on Exception do
DoRetry(STATUS_NOTFOUND,'Exception');
end;
finally
if KeepAlive=0 then
Close;
end;
end;
function Open(const aServer, aPort: SockString; aTLS: boolean): TCrtSocket;
begin
try
result := TCrtSocket.Open(aServer,aPort,cslTCP,10000,aTLS);
except
on ECrtSocket do
result := nil;
end;
end;
function OpenHttp(const aServer, aPort: SockString; aTLS: boolean;
aLayer: TCrtSocketLayer): THttpClientSocket;
begin
try
result := THttpClientSocket.Open(aServer,aPort,aLayer,0,aTLS); // HTTP_DEFAULT_RECEIVETIMEOUT
except
on ECrtSocket do
result := nil;
end;
end;
function OpenHttp(const aURI: SockString; aAddress: PSockString): THttpClientSocket;
var URI: TURI;
begin
result := nil;
if URI.From(aURI) then begin
result := OpenHttp(URI.Server,URI.Port,URI.Https,URI.Layer);
if aAddress <> nil then
aAddress^ := URI.Address;
end;
end;
function HttpGet(const server, port: SockString; const url: SockString;
const inHeaders: SockString; outHeaders: PSockString;
aLayer: TCrtSocketLayer): SockString;
var Http: THttpClientSocket;
begin
result := '';
Http := OpenHttp(server,port,false,aLayer);
if Http<>nil then
try
if Http.Get(url,0,inHeaders) in [STATUS_SUCCESS..STATUS_PARTIALCONTENT] then begin
result := Http.Content;
if outHeaders<>nil then
outHeaders^ := Http.HeaderGetText;
end;
finally
Http.Free;
end;
end;
function HttpGet(const aURI: SockString; outHeaders: PSockString; forceNotSocket: boolean;
outStatus: PInteger): SockString;
begin
result := HttpGet(aURI,'',outHeaders,forceNotSocket,outStatus);
end;
function HttpGet(const aURI: SockString; const inHeaders: SockString;
outHeaders: PSockString; forceNotSocket: boolean; outStatus: PInteger): SockString;
var URI: TURI;
begin
if URI.From(aURI) then
if URI.Https or forceNotSocket then
{$ifdef USEWININET}
result := TWinHTTP.Get(aURI,inHeaders,{weakCA=}true,outHeaders,outStatus) else
{$else}
{$ifdef USELIBCURL}
result := TCurlHTTP.Get(aURI,inHeaders,{weakCA=}true,outHeaders,outStatus) else
{$else}
raise ECrtSocket.CreateFmt('https is not supported by HttpGet(%s)',[aURI]) else
{$endif}
{$endif USEWININET}
result := HttpGet(URI.Server,URI.Port,URI.Address,inHeaders,outHeaders,URI.Layer) else
result := '';
{$ifdef LINUX_RAWDEBUGVOIDHTTPGET}
if result='' then
writeln('HttpGet returned VOID for ',URI.server,':',URI.Port,' ',URI.Address);
{$endif}
end;
function HttpGetAuth(const aURI, aAuthToken: SockString; outHeaders: PSockString;
forceNotSocket: boolean; outStatus: PInteger): SockString;
var status: integer;
begin
result := HttpGet(aURI,AuthorizationBearer(aAuthToken),outHeaders,forceNotSocket,@status);
if outStatus<>nil then
outStatus^ := status;
if not(status in [STATUS_SUCCESS..STATUS_PARTIALCONTENT]) then
result := '';
end;
function HttpPost(const server, port: SockString; const url, Data, DataType: SockString;
outData: PSockString; const auth: SockString): boolean;
var Http: THttpClientSocket;
begin
result := false;
Http := OpenHttp(server,port);
if Http<>nil then
try
result := Http.Post(url,Data,DataType,0,AuthorizationBearer(auth)) in
[STATUS_SUCCESS,STATUS_CREATED,STATUS_NOCONTENT];
if outdata<>nil then
outdata^ := Http.Content;
finally
Http.Free;
end;
end;
function HttpPut(const server, port: SockString; const url, Data, DataType: SockString;
outData: PSockString; const auth: SockString): boolean;
var Http: THttpClientSocket;
begin
result := false;
Http := OpenHttp(server,port);
if Http<>nil then
try
result := Http.Put(url,Data,DataType,0,AuthorizationBearer(auth)) in
[STATUS_SUCCESS,STATUS_CREATED,STATUS_NOCONTENT];
if outdata<>nil then
outdata^ := Http.Content;
finally
Http.Free;
end;
end;
function TSMTPConnection.FromText(const aText: SockString): boolean;
var u,h: SockString;
begin
if aText=SMTP_DEFAULT then begin
result := false;
exit;
end;
if Split(aText,'@',u,h) then begin
if not Split(u,':',User,Pass) then
User := u;
end else
h := aText;
if not Split(h,':',Host,Port) then begin
Host := h;
Port := '25';
end;
if (Host<>'') and (Host[1]='?') then
Host := '';
result := Host<>'';
end;
function SendEmail(const Server: TSMTPConnection; const From, CSVDest, Subject,
Text, Headers, TextCharSet: SockString; aTLS: boolean): boolean;
begin
result := SendEmail(Server.Host, From, CSVDest, Subject, Text, Headers,
Server.User, Server.Pass, Server.Port, TextCharSet,
(Server.Port = '465') or (Server.Port = '587'));
end;
function SendEmail(const Server, From, CSVDest, Subject, Text, Headers,
User, Pass, Port, TextCharSet: SockString; aTLS: boolean): boolean;
var TCP: TCrtSocket;
procedure Expect(const Answer: SockString);
var Res: SockString;
begin
repeat
readln(TCP.SockIn^,Res);
until (Length(Res)<4)or(Res[4]<>'-');
if not IdemPChar(pointer(Res),pointer(Answer)) then
raise ECrtSocket.Create(string(Res));
end;
procedure Exec(const Command, Answer: SockString);
begin
writeln(TCP.SockOut^,Command);
Expect(Answer)
end;
var P: PAnsiChar;
rec, ToList, head: SockString;
begin
result := false;
P := pointer(CSVDest);
if P=nil then exit;
TCP := Open(Server,Port,aTLS);
if TCP<>nil then
try
TCP.CreateSockIn; // we use SockIn and SockOut here
TCP.CreateSockOut;
Expect('220');
if (User<>'') and (Pass<>'') then begin
Exec('EHLO '+Server,'25');
Exec('AUTH LOGIN','334');
Exec(SockBase64Encode(User),'334');
Exec(SockBase64Encode(Pass),'235');
end else
Exec('HELO '+Server,'25');
writeln(TCP.SockOut^,'MAIL FROM:<',From,'>'); Expect('250');
repeat
GetNextItem(P,',',rec);
rec := Trim(rec);
if rec='' then continue;
if PosCh('<',rec)=0 then
rec := '<'+rec+'>';
Exec('RCPT TO:'+rec,'25');
if ToList='' then
ToList := #13#10'To: '+rec else
ToList := ToList+', '+rec;
until P=nil;
Exec('DATA','354');
head := trim(Headers);
if head<>'' then
head := head+#13#10;
writeln(TCP.SockOut^,'Subject: ',Subject,#13#10'From: ',From,ToList);
if TextCharSet='JSON' then
writeln(TCP.SockOut^,'Content-Type: application/json; charset=UTF-8')
else
writeln(TCP.SockOut^,'Content-Type: text/plain; charset=',TextCharSet);
writeln(TCP.SockOut^,'Content-Transfer-Encoding: 8bit'#13#10,head,#13#10,Text);
Exec('.','25');
writeln(TCP.SockOut^,'QUIT');
result := true;
finally
TCP.Free;
end;
end;
function IsAnsi7(const s: string): boolean;
var i: integer;
begin
result := false;
for i := 1 to length(s) do
if ord(s[i])>126 then
exit;
result := true;
end;
function SendEmailSubject(const Text: string): SockString;
var utf8: UTF8String;
begin
if IsAnsi7(Text) then
result := SockString(Text) else begin
utf8 := UTF8String(Text);
result := '=?UTF-8?B?'+SockBase64Encode(utf8);
end;
end;
{ THttpServerRequest }
constructor THttpServerRequest.Create(aServer: THttpServerGeneric;
aConnectionID: THttpServerConnectionID; aConnectionThread: TSynThread);
begin
inherited Create;
fServer := aServer;
fConnectionID := aConnectionID;
fConnectionThread := aConnectionThread;
end;
var
GlobalRequestID: integer;
procedure THttpServerRequest.Prepare(const aURL, aMethod, aInHeaders,
aInContent, aInContentType, aRemoteIP: SockString; aUseSSL: boolean);
var id: PInteger;
begin
if fServer=nil then
id := @GlobalRequestID else
id := @fServer.fCurrentRequestID;
fRequestID := InterLockedIncrement(id^);
if fRequestID=maxInt-2048 then // ensure no overflow (31-bit range)
id^ := 0;
fUseSSL := aUseSSL;
fURL := aURL;
fMethod := aMethod;
fRemoteIP := aRemoteIP;
if aRemoteIP<>'' then
if aInHeaders='' then
fInHeaders := 'RemoteIP: '+aRemoteIP else
fInHeaders := aInHeaders+#13#10'RemoteIP: '+aRemoteIP else
fInHeaders := aInHeaders;
fInContent := aInContent;
fInContentType := aInContentType;
fOutContent := '';
fOutContentType := '';
fOutCustomHeaders := '';
end;
procedure THttpServerRequest.AddInHeader(additionalHeader: SockString);
begin
additionalHeader := Trim(additionalHeader);
if additionalHeader<>'' then
if fInHeaders='' then
fInHeaders := additionalHeader else
fInHeaders := fInHeaders+#13#10+additionalHeader;
end;
{ TServerGeneric }
constructor TServerGeneric.Create(CreateSuspended: boolean;
OnStart,OnStop: TNotifyThreadEvent; const ProcessName: SockString);
begin
fProcessName := ProcessName;
fOnHttpThreadStart := OnStart;
SetOnTerminate(OnStop);
inherited Create(CreateSuspended);
end;
procedure TServerGeneric.NotifyThreadStart(Sender: TSynThread);
begin
if Sender=nil then
raise ECrtSocket.Create('NotifyThreadStart(nil)');
if Assigned(fOnHttpThreadStart) and not Assigned(Sender.fStartNotified) then begin
fOnHttpThreadStart(Sender);
Sender.fStartNotified := self;
end;
end;
procedure TServerGeneric.SetOnTerminate(const Event: TNotifyThreadEvent);
begin
fOnThreadTerminate := Event;
end;
{ THttpServerGeneric }
constructor THttpServerGeneric.Create(CreateSuspended: boolean;
OnStart,OnStop: TNotifyThreadEvent; const ProcessName: SockString);
begin
SetServerName('mORMot ('+XPOWEREDOS+')');
inherited Create(CreateSuspended,OnStart,OnStop,ProcessName);
end;
procedure THttpServerGeneric.RegisterCompress(aFunction: THttpSocketCompress;
aCompressMinSize: integer=1024);
begin
RegisterCompressFunc(fCompress,aFunction,fCompressAcceptEncoding,aCompressMinSize);
end;
procedure THttpServerGeneric.Shutdown;
begin
if self<>nil then
fShutdownInProgress := true;
end;
function THttpServerGeneric.Request(Ctxt: THttpServerRequest): cardinal;
begin
if (self=nil) or fShutdownInProgress then
result := STATUS_NOTFOUND else begin
NotifyThreadStart(Ctxt.ConnectionThread);
if Assigned(OnRequest) then
result := OnRequest(Ctxt) else
result := STATUS_NOTFOUND;
end;
end;
function THttpServerGeneric.Callback(Ctxt: THttpServerRequest; aNonBlocking: boolean): cardinal;
begin
raise ECrtSocket.CreateFmt('%s.Callback is not implemented: try to use '+
'another communication protocol, e.g. WebSockets',[ClassName]);
end;
procedure THttpServerGeneric.SetServerName(const aName: SockString);
begin
fServerName := aName;
end;
procedure THttpServerGeneric.SetOnRequest(const aRequest: TOnHttpServerRequest);
begin
fOnRequest := aRequest;
end;
procedure THttpServerGeneric.SetOnBeforeBody(const aEvent: TOnHttpServerBeforeBody);
begin
fOnBeforeBody := aEvent;
end;
procedure THttpServerGeneric.SetOnBeforeRequest(const aEvent: TOnHttpServerRequest);
begin
fOnBeforeRequest := aEvent;
end;
procedure THttpServerGeneric.SetOnAfterRequest(const aEvent: TOnHttpServerRequest);
begin
fOnAfterRequest := aEvent;
end;
procedure THttpServerGeneric.SetOnAfterResponse(
const aEvent: TOnHttpServerAfterResponse);
begin
fOnAfterResponse := aEvent;
end;
function THttpServerGeneric.DoBeforeRequest(Ctxt: THttpServerRequest): cardinal;
begin
if Assigned(fOnBeforeRequest) then
result := fOnBeforeRequest(Ctxt) else
result := 0;
end;
function THttpServerGeneric.DoAfterRequest(Ctxt: THttpServerRequest): cardinal;
begin
if Assigned(fOnAfterRequest) then
result := fOnAfterRequest(Ctxt) else
result := 0;
end;
procedure THttpServerGeneric.DoAfterResponse(Ctxt: THttpServerRequest;
const Code: cardinal);
begin
if Assigned(fOnAfterResponse) then
fOnAfterResponse(Ctxt, Code);
end;
procedure THttpServerGeneric.SetMaximumAllowedContentLength(aMax: cardinal);
begin
fMaximumAllowedContentLength := aMax;
end;
procedure THttpServerGeneric.SetRemoteIPHeader(const aHeader: SockString);
begin
fRemoteIPHeader := aHeader;
fRemoteIPHeaderUpper := UpperCase(aHeader);
end;
procedure THttpServerGeneric.SetRemoteConnIDHeader(const aHeader: SockString);
begin
fRemoteConnIDHeader := aHeader;
fRemoteConnIDHeaderUpper := UpperCase(aHeader);
end;
function THttpServerGeneric.NextConnectionID: integer;
begin
result := InterlockedIncrement(fCurrentConnectionID);
if result=maxInt-2048 then // paranoid 31-bit counter reset to ensure >0
fCurrentConnectionID := 0;
end;
{ THttpServer }
constructor THttpServer.Create(const aPort: SockString; OnStart,
OnStop: TNotifyThreadEvent; const ProcessName: SockString;
ServerThreadPoolCount: integer; KeepAliveTimeOut: integer;
HeadersUnFiltered: boolean; CreateSuspended: boolean);
begin
fSockPort := aPort;
fInternalHttpServerRespList := {$ifdef FPC}TFPList{$else}TList{$endif}.Create;
InitializeCriticalSection(fProcessCS);
fServerKeepAliveTimeOut := KeepAliveTimeOut; // 30 seconds by default
if fThreadPool<>nil then
fThreadPool.ContentionAbortDelay := 5000; // 5 seconds default
// event handlers set before inherited Create to be visible in childs
fOnHttpThreadStart := OnStart;
SetOnTerminate(OnStop);
if fThreadRespClass=nil then
fThreadRespClass := THttpServerResp;
if fSocketClass=nil then
fSocketClass := THttpServerSocket;
if ServerThreadPoolCount>0 then begin
fThreadPool := TSynThreadPoolTHttpServer.Create(self,ServerThreadPoolCount);
fHTTPQueueLength := 1000;
end;
fHeadersNotFiltered := HeadersUnFiltered;
inherited Create(CreateSuspended,OnStart,OnStop,ProcessName);
end;
function THttpServer.GetAPIVersion: string;
begin
result := Format('%s.%d',[WsaDataOnce.szDescription,WsaDataOnce.wVersion]);
end;
destructor THttpServer.Destroy;
var endtix: Int64;
i: integer;
resp: THttpServerResp;
begin
Terminate; // set Terminated := true for THttpServerResp.Execute
if fThreadPool<>nil then
fThreadPool.fTerminated := true; // notify background process
if (fExecuteState=esRunning) and (Sock<>nil) then begin
Sock.Close; // shutdown the socket to unlock Accept() in Execute
DirectShutdown(CallServer('127.0.0.1',Sock.Port,false,cslTCP,1));
end;
endtix := GetTick64+20000;
EnterCriticalSection(fProcessCS);
try
if fInternalHttpServerRespList<>nil then begin
for i := 0 to fInternalHttpServerRespList.Count-1 do begin
resp := fInternalHttpServerRespList.List[i];
resp.Terminate;
DirectShutdown(resp.fServerSock.Sock,{rdwr=}true);
end;
repeat // wait for all THttpServerResp.Execute to be finished
if (fInternalHttpServerRespList.Count=0) and (fExecuteState<>esRunning) then
break;
LeaveCriticalSection(fProcessCS);
SleepHiRes(100);
EnterCriticalSection(fProcessCS);
until GetTick64>endtix;
FreeAndNil(fInternalHttpServerRespList);
end;
finally
LeaveCriticalSection(fProcessCS);
FreeAndNil(fThreadPool); // release all associated threads and I/O completion
FreeAndNil(fSock);
inherited Destroy; // direct Thread abort, no wait till ended
DeleteCriticalSection(fProcessCS);
end;
end;
function THttpServer.GetStat(one: THttpServerSocketGetRequestResult): integer;
begin
result := fStats[one];
end;
function THttpServer.GetHTTPQueueLength: Cardinal;
begin
result := fHTTPQueueLength;
end;
procedure THttpServer.SetHTTPQueueLength(aValue: Cardinal);
begin
fHTTPQueueLength := aValue;
end;
procedure THttpServer.InternalHttpServerRespListAdd(resp: THttpServerResp);
begin
if (self=nil) or (fInternalHttpServerRespList=nil) or (resp=nil) then
exit;
EnterCriticalSection(fProcessCS);
try
fInternalHttpServerRespList.Add(resp);
finally
LeaveCriticalSection(fProcessCS);
end;
end;
procedure THttpServer.InternalHttpServerRespListRemove(resp: THttpServerResp);
var i: integer;
begin
if (self=nil) or (fInternalHttpServerRespList=nil) then
exit;
EnterCriticalSection(fProcessCS);
try
i := fInternalHttpServerRespList.IndexOf(resp);
if i>=0 then
fInternalHttpServerRespList.Delete(i);
finally
LeaveCriticalSection(fProcessCS);
end;
end;
function THttpServer.OnNginxAllowSend(Context: THttpServerRequest;
const LocalFileName: TFileName): boolean;
var match,i,f: PtrInt;
folder: ^TFileName;
begin
match := 0;
folder := pointer(fNginxSendFileFrom);
if LocalFileName<>'' then
for f := 1 to length(fNginxSendFileFrom) do begin
match := length(folder^);
for i := 1 to match do // case sensitive left search
if LocalFileName[i]<>folder^[i] then begin
match := 0;
break;
end;
if match<>0 then
break; // found matching folder
inc(folder);
end;
result := match<>0;
if not result then
exit; // no match -> manual send
delete(Context.fOutContent,1,match); // remove e.g. '/var/www'
Context.OutCustomHeaders := Trim(Context.OutCustomHeaders+#13#10+
'X-Accel-Redirect: '+Context.OutContent);
Context.OutContent := '';
end;
procedure THttpServer.NginxSendFileFrom(const FileNameLeftTrim: TFileName);
var n: PtrInt;
begin
n := length(fNginxSendFileFrom);
SetLength(fNginxSendFileFrom,n+1);
fNginxSendFileFrom[n] := FileNameLeftTrim;
fOnSendFile := OnNginxAllowSend;
end;
procedure THttpServer.WaitStarted(Seconds: integer);
var tix: Int64;
ok: boolean;
begin
tix := GetTick64 + Seconds * 1000; // never wait forever
repeat
EnterCriticalSection(fProcessCS);
ok := Terminated or (fExecuteState in [esRunning, esFinished]);
LeaveCriticalSection(fProcessCS);
if ok then
exit;
Sleep(1);
if GetTick64 > tix then
raise ECrtSocket.CreateFmt('%s.WaitStarted failed after %d seconds [%s]',
[ClassName,Seconds,fExecuteMessage]);
until false;
end;
{.$define MONOTHREAD}
// define this not to create a thread at every connection (not recommended)
procedure THttpServer.Execute;
var ClientSock: TSocket;
ClientSin: TVarSin;
ClientCrtSock: THttpServerSocket;
{$ifdef MONOTHREAD}
endtix: Int64;
{$endif}
begin
// THttpServerGeneric thread preparation: launch any OnHttpThreadStart event
fExecuteState := esBinding;
NotifyThreadStart(self);
// main server process loop
try
fSock := TCrtSocket.Bind(fSockPort); // BIND + LISTEN
{$ifdef LINUXNOTBSD}
// in case we started by systemd, listening socket is created by another process
// and do not interrupt while process got a signal. So we need to set a timeout to
// unblock accept() periodically and check we need terminations
if fSockPort = '' then // external socket
fSock.ReceiveTimeout := 1000; // unblock accept every second
{$endif}
fExecuteState := esRunning;
if fSock.Sock<=0 then // paranoid (Bind would have raise an exception)
raise ECrtSocket.Create('THttpServer.Execute: TCrtSocket.Bind failed');
while not Terminated do begin
ClientSock := Accept(Sock.Sock,ClientSin);
if ClientSock<=0 then
if Terminated then
break else begin
SleepHiRes(1); // failure (too many clients?) -> wait and retry
continue;
end;
if Terminated or (Sock=nil) then begin
DirectShutdown(ClientSock);
break; // don't accept input if server is down
end;
OnConnect;
{$ifdef MONOTHREAD}
ClientCrtSock := fSocketClass.Create(self);
try
ClientCrtSock.InitRequest(ClientSock);
endtix := fHeaderRetrieveAbortDelay;
if endtix>0 then
inc(endtix,GetTick64);
if ClientCrtSock.GetRequest({withbody=}true,endtix) in [grBodyReceived,grHeaderReceived] then
Process(ClientCrtSock,0,self);
OnDisconnect;
DirectShutdown(ClientSock);
finally
ClientCrtSock.Free;
end;
{$else}
if Assigned(fThreadPool) then begin
// use thread pool to process the request header, and probably its body
ClientCrtSock := fSocketClass.Create(self);
ClientCrtSock.AcceptRequest(ClientSock,@ClientSin);
if not fThreadPool.Push(pointer(PtrUInt(ClientCrtSock)),{waitoncontention=}true) then begin
// returned false if there is no idle thread in the pool, and queue is full
ClientCrtSock.Free; // will call DirectShutdown(ClientSock)
end;
end else
// default implementation creates one thread for each incoming socket
fThreadRespClass.Create(ClientSock,ClientSin,self);
{$endif MONOTHREAD}
end;
except
on E: Exception do // any exception would break and release the thread
fExecuteMessage := E.ClassName+' ['+E.Message+']';
end;
EnterCriticalSection(fProcessCS);
fExecuteState := esFinished;
LeaveCriticalSection(fProcessCS);
end;
procedure THttpServer.OnConnect;
begin
InterLockedIncrement(fServerConnectionCount);
InterLockedIncrement(fServerConnectionActive);
end;
procedure THttpServer.OnDisconnect;
begin
InterLockedDecrement(fServerConnectionActive);
end;
procedure THttpServer.Process(ClientSock: THttpServerSocket;
ConnectionID: THttpServerConnectionID; ConnectionThread: TSynThread);
var ctxt: THttpServerRequest;
P: PAnsiChar;
respsent: boolean;
Code, afterCode: cardinal;
s, reason: SockString;
ErrorMsg: string;
function SendResponse: boolean;
var
fs: TFileStream;
fn: TFileName;
begin
result := not Terminated; // true=success
if not result then
exit;
{$ifdef SYNCRTDEBUGLOW}
TSynLog.Add.Log(sllCustom2, 'SendResponse respsent=% code=%', [respsent,code], self);
{$endif}
respsent := true;
// handle case of direct sending of static file (as with http.sys)
if (ctxt.OutContent<>'') and (ctxt.OutContentType=HTTP_RESP_STATICFILE) then
try
ExtractNameValue(ctxt.fOutCustomHeaders,'CONTENT-TYPE:',ctxt.fOutContentType);
fn := {$ifdef UNICODE}UTF8ToUnicodeString{$else}Utf8ToAnsi{$endif}(ctxt.OutContent);
if not Assigned(fOnSendFile) or not fOnSendFile(ctxt,fn) then begin
fs := TFileStream.Create(fn,fmOpenRead or fmShareDenyNone);
try
SetString(ctxt.fOutContent,nil,fs.Size);
fs.Read(Pointer(ctxt.fOutContent)^,length(ctxt.fOutContent));
finally
fs.Free;
end;
end;
except
on E: Exception do begin // error reading or sending file
ErrorMsg := E.ClassName+': '+E.Message;
Code := STATUS_NOTFOUND;
result := false; // fatal error
end;
end;
if ctxt.OutContentType=HTTP_RESP_NORESPONSE then
ctxt.OutContentType := ''; // true HTTP always expects a response
// send response (multi-thread OK) at once
if (Code<STATUS_SUCCESS) or (ClientSock.Headers='') then
Code := STATUS_NOTFOUND;
reason := StatusCodeToReason(Code);
if ErrorMsg<>'' then begin
ctxt.OutCustomHeaders := '';
ctxt.OutContentType := 'text/html; charset=utf-8'; // create message to display
ctxt.OutContent := {$ifdef UNICODE}UTF8String{$else}UTF8Encode{$endif}(
format('<body style="font-family:verdana">'#10+
'<h1>%s Server Error %d</h1><hr><p>HTTP %d %s<p>%s<p><small>%s',
[ClassName,Code,Code,reason,HtmlEncodeString(ErrorMsg),fServerName]));
end;
// 1. send HTTP status command
if ClientSock.TCPPrefix<>'' then
ClientSock.SockSend(ClientSock.TCPPrefix);
if ClientSock.KeepAliveClient then
ClientSock.SockSend(['HTTP/1.1 ',Code,' ',reason]) else
ClientSock.SockSend(['HTTP/1.0 ',Code,' ',reason]);
// 2. send headers
// 2.1. custom headers from Request() method
P := pointer(ctxt.fOutCustomHeaders);
while P<>nil do begin
GetNextLine(P,s);
if s<>'' then begin // no void line (means headers ending)
ClientSock.SockSend(s);
if IdemPChar(pointer(s),'CONTENT-ENCODING:') then
integer(ClientSock.fCompressAcceptHeader) := 0; // custom encoding: don't compress
end;
end;
// 2.2. generic headers
ClientSock.SockSend([
{$ifndef NOXPOWEREDNAME}XPOWEREDNAME+': '+XPOWEREDVALUE+#13#10+{$endif}
'Server: ',fServerName]);
ClientSock.CompressDataAndWriteHeaders(ctxt.OutContentType,ctxt.fOutContent);
if ClientSock.KeepAliveClient then begin
if ClientSock.fCompressAcceptEncoding<>'' then
ClientSock.SockSend(ClientSock.fCompressAcceptEncoding);
ClientSock.SockSend('Connection: Keep-Alive'#13#10); // #13#10 -> end headers
end else
ClientSock.SockSend; // headers must end with a void line
// 3. sent HTTP body content (if any)
ClientSock.SockSendFlush(ctxt.OutContent); // flush all data to network
end;
begin
if (ClientSock=nil) or (ClientSock.Headers='') then
// we didn't get the request = socket read error
exit; // -> send will probably fail -> nothing to send back
if Terminated then
exit;
ctxt := THttpServerRequest.Create(self,ConnectionID,ConnectionThread);
try
respsent := false;
with ClientSock do
ctxt.Prepare(URL,Method,HeaderGetText(fRemoteIP),Content,ContentType,'',ClientSock.fTLS);
try
Code := DoBeforeRequest(ctxt);
{$ifdef SYNCRTDEBUGLOW}
TSynLog.Add.Log(sllCustom2, 'DoBeforeRequest=%', [code], self);
{$endif}
if Code>0 then
if not SendResponse or (Code<>STATUS_ACCEPTED) then
exit;
Code := Request(ctxt);
afterCode := DoAfterRequest(ctxt);
{$ifdef SYNCRTDEBUGLOW}
TSynLog.Add.Log(sllCustom2, 'Request=% DoAfterRequest=%', [code,afterCode], self);
{$endif}
if afterCode>0 then
Code := afterCode;
if respsent or SendResponse then
DoAfterResponse(ctxt, Code);
{$ifdef SYNCRTDEBUGLOW}
TSynLog.Add.Log(sllCustom2, 'DoAfterResponse respsent=% ErrorMsg=%', [respsent,ErrorMsg], self);
{$endif}
except
on E: Exception do
if not respsent then begin
ErrorMsg := E.ClassName+': '+E.Message;
Code := STATUS_SERVERERROR;
SendResponse;
end;
end;
finally
if Sock<>nil then begin // add transfert stats to main socket
EnterCriticalSection(fProcessCS);
inc(Sock.fBytesIn,ClientSock.BytesIn);
inc(Sock.fBytesOut,ClientSock.BytesOut);
LeaveCriticalSection(fProcessCS);
ClientSock.fBytesIn := 0;
ClientSock.fBytesOut := 0;
end;
ctxt.Free;
end;
end;
{ TSynThread }
constructor TSynThread.Create(CreateSuspended: boolean);
begin
{$ifdef FPC}
inherited Create(CreateSuspended,512*1024); // DefaultSizeStack=512KB
{$else}
inherited Create(CreateSuspended);
{$endif}
end;
function TSynThread.SleepOrTerminated(MS: cardinal): boolean;
var endtix: Int64;
begin
result := true; // notify Terminated
if Terminated then
exit;
if MS<32 then begin // smaller than GetTickCount resolution (under Windows)
SleepHiRes(MS);
if Terminated then
exit;
end else begin
endtix := GetTick64+MS;
repeat
SleepHiRes(10);
if Terminated then
exit;
until GetTick64>endtix;
end;
result := false; // abnormal delay expiration
end;
{$ifndef LVCL}
procedure TSynThread.DoTerminate;
begin
try
if Assigned(fStartNotified) and Assigned(fOnThreadTerminate) then begin
fOnThreadTerminate(self);
fStartNotified := nil;
end;
inherited DoTerminate; // call OnTerminate via Synchronize()
except // hardened: a closing thread should not jeopardize the whole project!
end;
end;
{$endif}
{$ifndef HASTTHREADSTART}
procedure TSynThread.Start;
begin
Resume;
end;
{$endif}
{ THttpServerResp }
constructor THttpServerResp.Create(aSock: TSocket; const aSin: TVarSin; aServer: THttpServer);
var c: THttpServerSocketClass;
begin
fClientSock := aSock;
fClientSin := aSin;
if aServer=nil then
c := THttpServerSocket else
c := aServer.fSocketClass;
Create(c.Create(aServer),aServer); // on Linux, Execute raises during Create
end;
constructor THttpServerResp.Create(aServerSock: THttpServerSocket; aServer: THttpServer);
begin
fServer := aServer;
fServerSock := aServerSock;
fOnThreadTerminate := fServer.fOnThreadTerminate;
fServer.InternalHttpServerRespListAdd(self);
fConnectionID := aServerSock.RemoteConnectionID;
if fConnectionID=0 then
fConnectionID := fServer.NextConnectionID; // fallback to 31-bit sequence
FreeOnTerminate := true;
inherited Create(false);
end;
procedure THttpServerResp.Execute;
procedure HandleRequestsProcess;
var keepaliveendtix,beforetix,headertix,tix: Int64;
pending: TCrtSocketPending;
res: THttpServerSocketGetRequestResult;
begin
{$ifdef SYNCRTDEBUGLOW} try {$endif}
try
repeat
beforetix := GetTick64;
keepaliveendtix := beforetix+fServer.ServerKeepAliveTimeOut;
repeat // within this loop, break=wait for next command, exit=quit
if (fServer=nil) or fServer.Terminated or (fServerSock=nil) then
exit; // server is down -> close connection
pending := fServerSock.SockReceivePending(50); // 50 ms timeout
if (fServer=nil) or fServer.Terminated then
exit; // server is down -> disconnect the client
{$ifdef SYNCRTDEBUGLOW}
TSynLog.Add.Log(sllCustom2, 'HandleRequestsProcess: sock=% pending=%',
[fServerSock.fSock, _CSP[pending]], self);
{$endif}
case pending of
cspSocketError:
exit; // socket error -> disconnect the client
cspNoData: begin
tix := GetTick64;
if tix>=keepaliveendtix then
exit; // reached keep alive time out -> close connection
if tix-beforetix<40 then begin
{$ifdef SYNCRTDEBUGLOW}
// getsockopt(fServerSock.fSock,SOL_SOCKET,SO_ERROR,@error,errorlen) returns 0 :(
TSynLog.Add.Log(sllCustom2, 'HandleRequestsProcess: sock=% LOWDELAY=%',
[fServerSock.fSock, tix-beforetix], self);
{$endif}
SleepHiRes(1); // seen only on Windows in practice
if (fServer=nil) or fServer.Terminated then
exit; // server is down -> disconnect the client
end;
beforetix := tix;
end;
cspDataAvailable: begin
// get request and headers
headertix := fServer.HeaderRetrieveAbortDelay;
if headertix>0 then
inc(headertix,beforetix);
res := fServerSock.GetRequest({withbody=}true,headertix);
if (fServer=nil) or fServer.Terminated then
exit; // server is down -> disconnect the client
InterLockedIncrement(fServer.fStats[res]);
case res of
grBodyReceived, grHeaderReceived: begin
if res=grBodyReceived then
InterlockedIncrement(fServer.fStats[grHeaderReceived]);
// calc answer and send response
fServer.Process(fServerSock,ConnectionID,self);
// keep connection only if necessary
if fServerSock.KeepAliveClient then
break else
exit;
end;
grOwned: begin
fServerSock := nil; // will be freed by new owner
exit;
end;
else // fServerSock connection was down or headers are not correct
exit;
end;
end;
end;
until false;
until false;
except
on E: Exception do
; // any exception will silently disconnect the client
end;
{$ifdef SYNCRTDEBUGLOW}
finally
TSynLog.Add.Log(sllCustom2, 'HandleRequestsProcess: close sock=%', [fServerSock.fSock], self);
end;
{$endif}
end;
var aSock: TSocket;
begin
fServer.NotifyThreadStart(self);
try
try
if fClientSock<>0 then begin
// direct call from incoming socket
aSock := fClientSock;
fClientSock := 0; // fServerSock owns fClientSock
fServerSock.AcceptRequest(aSock,@fClientSin);
if fServer<>nil then
HandleRequestsProcess;
end else begin
// call from TSynThreadPoolTHttpServer -> handle first request
if not fServerSock.fBodyRetrieved and
(IdemPCharArray(pointer(fServerSock.fMethod),['HEAD','OPTIONS'])<0) then
fServerSock.GetBody;
fServer.Process(fServerSock,ConnectionID,self);
if (fServer<>nil) and fServerSock.KeepAliveClient then
HandleRequestsProcess; // process further kept alive requests
end;
finally
try
if fServer<>nil then
try
fServer.OnDisconnect;
finally
fServer.InternalHttpServerRespListRemove(self);
fServer := nil;
end;
finally
FreeAndNil(fServerSock);
// if Destroy happens before fServerSock.GetRequest() in Execute below
DirectShutdown(fClientSock);
end;
end;
except
on Exception do
; // just ignore unexpected exceptions here, especially during clean-up
end;
end;
{ THttpSocket }
procedure THttpSocket.GetBody;
var Line: SockString; // 32 bits chunk length in hexa
LinePChar: array[0..31] of AnsiChar;
Len, LContent, Error: integer;
begin
fBodyRetrieved := true;
Content := '';
{$I-}
// direct read bytes, as indicated by Content-Length or Chunked
if transferChuked in HeaderFlags then begin // we ignore the Length
LContent := 0; // current read position in Content
repeat
if SockIn<>nil then begin
readln(SockIn^,LinePChar); // use of a static PChar is faster
Error := ioresult;
if Error<>0 then
raise ECrtSocket.Create('GetBody1',Error);
Len := HttpChunkToHex32(LinePChar); // get chunk length in hexa
end else begin
SockRecvLn(Line);
Len := HttpChunkToHex32(pointer(Line)); // get chunk length in hexa
end;
if Len=0 then begin // ignore next line (normally void)
SockRecvLn;
break;
end;
SetLength(Content,LContent+Len); // reserve memory space for this chunk
SockInRead(@PByteArray(Content)[LContent],Len) ; // append chunk data
inc(LContent,Len);
SockRecvLn; // ignore next #13#10
until false;
end else
if ContentLength>0 then begin
SetLength(Content,ContentLength); // not chuncked: direct read
SockInRead(pointer(Content),ContentLength); // works with SockIn=nil or not
end else
if (ContentLength<0) and IdemPChar(pointer(Command),'HTTP/1.0 200') then begin
// body = either Content-Length or Transfer-Encoding (HTTP/1.1 RFC 4.3)
if SockIn<>nil then // client loop for compatibility with old servers
while not eof(SockIn^) do begin
readln(SockIn^,Line);
if Content='' then
Content := Line else
Content := Content+#13#10+Line;
end;
ContentLength := length(Content); // update Content-Length
exit;
end;
// optionaly uncompress content
if cardinal(fContentCompress)<cardinal(length(fCompress)) then
if fCompress[fContentCompress].Func(Content,false)='' then
// invalid content
raise ECrtSocket.CreateFmt('%s uncompress',[fCompress[fContentCompress].Name]);
ContentLength := length(Content); // update Content-Length
{$ifdef SYNCRTDEBUGLOW}
TSynLog.Add.Log(sllCustom2,'GetBody sock=% pending=% sockin=% len=% %',
[fSock, SockInPending(0), PTextRec(SockIn)^.BufEnd-PTextRec(SockIn)^.bufpos,
ContentLength, LogEscapeFull(Content)], self);
{$endif}
if SockIn<>nil then begin
Error := ioresult;
if Error<>0 then
raise ECrtSocket.Create('GetBody2',Error);
end;
{$I+}
end;
procedure GetTrimmed(P: PAnsiChar; out result: SockString);
var B: PAnsiChar;
begin
while (P^>#0) and (P^<=' ') do inc(P);
B := P;
while P^<>#0 do inc(P);
while (P>B) and (P[-1]<=' ') do dec(P);
SetString(result,B,P-B);
end;
var
JSON_CONTENT_TYPE_VAR: SockString;
procedure THttpSocket.GetHeader(HeadersUnFiltered: boolean);
var s,c: SockString;
i, len: PtrInt;
err: integer;
P: PAnsiChar;
line: array[0..4095] of AnsiChar; // avoid most memory allocation
begin
HeaderFlags := [];
fBodyRetrieved := false;
fContentCompress := -1;
integer(fCompressAcceptHeader) := 0;
ContentType := '';
Upgrade := '';
ContentLength := -1;
ServerInternalState := 0;
fSndBufLen := 0; // SockSend() internal buffer is used when adding headers
repeat
P := @line;
if (SockIn<>nil) and not HeadersUnFiltered then begin
{$I-}
readln(SockIn^,line);
err := ioresult;
if err<>0 then
raise ECrtSocket.CreateFmt('%s.GetHeader',[ClassName],err);
{$I+}
if line[0]=#0 then
break; // HTTP headers end with a void line
end else begin
SockRecvLn(s);
if s = '' then
break;
P := pointer(s); // set P=nil below to store in Headers[]
end;
case IdemPCharArray(P,['CONTENT-', 'TRANSFER-ENCODING: CHUNKED', 'CONNECTION: ',
'ACCEPT-ENCODING:', 'UPGRADE:', 'SERVER-INTERNALSTATE:', 'X-POWERED-BY:']) of
0: case IdemPCharArray(P+8,['LENGTH:', 'TYPE:', 'ENCODING:']) of
0: ContentLength := GetCardinal(P+16);
1: begin
inc(P,13);
while P^=' ' do inc(P);
if IdemPChar(P,'APPLICATION/JSON') then
ContentType := JSON_CONTENT_TYPE_VAR else begin
GetTrimmed(P,ContentType);
if ContentType<>'' then
P := nil; // is searched by HEADER_CONTENT_TYPE_UPPER later on
end;
end;
2: if fCompress<>nil then begin
GetTrimmed(P+17,c);
for i := 0 to high(fCompress) do
if fCompress[i].Name=c then begin
fContentCompress := i;
break;
end;
end;
else P := nil;
end;
1: include(HeaderFlags,transferChuked);
2: case IdemPCharArray(P+12,['CLOSE','UPGRADE','KEEP-ALIVE']) of
0: include(HeaderFlags,connectionClose);
1: include(HeaderFlags,connectionUpgrade);
2: begin
include(HeaderFlags,connectionKeepAlive);
if P[22]=',' then begin
inc(P,23);
if P^=' ' then inc(P);
if IdemPChar(P,'UPGRADE') then
include(HeaderFlags,connectionUpgrade);
end;
end;
else P := nil;
end;
3: if fCompress<>nil then
fCompressAcceptHeader := ComputeContentEncoding(fCompress,P+16) else
P := nil;
4: GetTrimmed(P+8,Upgrade);
5: ServerInternalState := GetCardinal(P+21);
6: GetTrimmed(P+13,XPoweredBy);
else P := nil;
end;
if (P=nil) or HeadersUnFiltered then // only store meaningful headers
if s='' then begin
len := StrLen(line);
if len>SizeOf(line)-2 then
break; // avoid buffer overflow
PWord(@line[len])^ := 13+10 shl 8; // CR + LF
SockSend(@line,len+2);
end else
SockSend(s); // SockSend() internal buffer is used as temporary buffer
until false;
Headers := copy(fSndBuf, 1, fSndBufLen);
fSndBufLen := 0;
end;
procedure THttpSocket.HeaderAdd(const aValue: SockString);
begin
if aValue<>'' then
Headers := Headers+aValue+#13#10;
end;
procedure THttpSocket.HeaderSetText(const aText, aForcedContentType: SockString);
begin
if aText='' then
Headers := '' else
if aText[length(aText)-1]<>#10 then
Headers := aText+#13#10 else
Headers := aText;
if (aForcedContentType<>'') and
(ExistNameValue(pointer(aText),'CONTENT-TYPE:')=nil) then
Headers := Headers+'Content-Type: '+aForcedContentType+#13#10;
end;
function THttpSocket.HeaderGetText(const aRemoteIP: SockString): SockString;
begin
if (aRemoteIP<>'') and not(hasRemoteIP in HeaderFlags) then begin
Headers := Headers+'RemoteIP: '+aRemoteIP+#13#10;
include(HeaderFlags,hasRemoteIP);
end;
result := Headers;
end;
function THttpSocket.HeaderGetValue(const aUpperName: SockString): SockString;
begin
result := '';
GetHeaderValue(Headers,aUpperName,result);
end;
function THttpSocket.RegisterCompress(aFunction: THttpSocketCompress;
aCompressMinSize: integer): boolean;
begin
result := RegisterCompressFunc(fCompress,aFunction,fCompressAcceptEncoding,aCompressMinSize)<>'';
end;
procedure THttpSocket.CompressDataAndWriteHeaders(const OutContentType: SockString;
var OutContent: SockString);
var OutContentEncoding: SockString;
begin
if integer(fCompressAcceptHeader)<>0 then begin
OutContentEncoding := CompressDataAndGetHeaders(fCompressAcceptHeader,fCompress,
OutContentType,OutContent);
if OutContentEncoding<>'' then
SockSend(['Content-Encoding: ',OutContentEncoding]);
end;
SockSend(['Content-Length: ',length(OutContent)]); // needed even 0
if (OutContentType<>'') and (OutContentType<>HTTP_RESP_STATICFILE) then
SockSend(['Content-Type: ',OutContentType]);
end;
{ THttpServerSocket }
constructor THttpServerSocket.Create(aServer: THttpServer);
begin
inherited Create(5000);
if aServer<>nil then begin // nil e.g. from TRTSPOverHTTPServer
fServer := aServer;
fCompress := aServer.fCompress;
fCompressAcceptEncoding := aServer.fCompressAcceptEncoding;
fSocketLayer:=aServer.Sock.SocketLayer;
TCPPrefix := aServer.TCPPrefix;
end;
end;
function THttpServerSocket.GetRequest(withBody: boolean; headerMaxTix: Int64): THttpServerSocketGetRequestResult;
var P: PAnsiChar;
status: cardinal;
pending: integer;
reason, allheaders: SockString;
noheaderfilter: boolean;
begin
result := grError;
try
// use SockIn with 1KB buffer if not already initialized: 2x faster
CreateSockIn;
// abort now with no exception if socket is obviously broken
if fServer<>nil then begin
pending := SockInPending(100,{alsosocket=}true);
if (pending<0) or (fServer=nil) or fServer.Terminated then
exit;
noheaderfilter := fServer.HeadersNotFiltered;
end else
noheaderfilter := false;
// 1st line is command: 'GET /path HTTP/1.1' e.g.
SockRecvLn(Command);
if TCPPrefix<>'' then
if TCPPrefix<>Command then
exit else
SockRecvLn(Command);
P := pointer(Command);
if P=nil then
exit; // broken
GetNextItem(P,' ',fMethod); // 'GET'
GetNextItem(P,' ',fURL); // '/path'
fKeepAliveClient := ((fServer=nil) or (fServer.ServerKeepAliveTimeOut>0)) and
IdemPChar(P,'HTTP/1.1');
Content := '';
// get headers and content
GetHeader(noheaderfilter);
if fServer<>nil then begin // nil from TRTSPOverHTTPServer
if fServer.fRemoteIPHeaderUpper<>'' then
// real Internet IP (replace 127.0.0.1 from a proxy)
GetHeaderValue(Headers,fServer.fRemoteIPHeaderUpper,fRemoteIP);
if fServer.fRemoteConnIDHeaderUpper<>'' then begin
P := FindHeaderValue(pointer(Headers),fServer.fRemoteConnIDHeaderUpper);
if P<>nil then
fRemoteConnectionID := GetNextItemUInt64(P);
end;
end;
if connectionClose in HeaderFlags then
fKeepAliveClient := false;
if (ContentLength<0) and (KeepAliveClient or (fMethod = 'GET')) then
ContentLength := 0; // HTTP/1.1 and no content length -> no eof
if (headerMaxTix>0) and (GetTick64>headerMaxTix) then begin
result := grTimeout;
exit; // allow 10 sec for header -> DOS/TCPSYN Flood
end;
if fServer<>nil then begin
if (ContentLength>0) and (fServer.MaximumAllowedContentLength>0) and
(cardinal(ContentLength)>fServer.MaximumAllowedContentLength) then begin
SockSend('HTTP/1.0 413 Payload Too Large'#13#10#13#10'Rejected');
SockSendFlush('');
result := grOversizedPayload;
exit;
end;
if Assigned(fServer.OnBeforeBody) then begin
allheaders := HeaderGetText(fRemoteIP);
status := fServer.OnBeforeBody(fURL,fMethod,allheaders,ContentType,RemoteIP,ContentLength,false);
{$ifdef SYNCRTDEBUGLOW}
TSynLog.Add.Log(sllCustom2,'GetRequest sock=% OnBeforeBody=% Command=% Headers=%',
[fSock, status, LogEscapeFull(Command), LogEscapeFull(allheaders)], self);
TSynLog.Add.Log(sllCustom2,'GetRequest OnBeforeBody headers', TypeInfo(TSockStringDynArray), Headers, self);
{$endif}
if status<>STATUS_SUCCESS then begin
reason := StatusCodeToReason(status);
SockSend(['HTTP/1.0 ',status,' ',reason,#13#10#13#10,reason,' ', status]);
SockSendFlush('');
result := grRejected;
exit;
end;
end;
end;
if withBody and not (connectionUpgrade in HeaderFlags) then begin
if IdemPCharArray(pointer(fMethod),['HEAD','OPTIONS'])<0 then
GetBody;
result := grBodyReceived;
end else
result := grHeaderReceived;
except
on E: Exception do
result := grException;
end;
end;
procedure DirectShutdown(sock: TSocket; rdwr: boolean);
const SHUT_: array[boolean] of integer = (SHUT_RD, SHUT_RDWR);
begin
if sock<=0 then
exit;
{$ifdef LINUXNOTBSD}
// at last under Linux close() is enough. For example nginx don't call shutdown
if rdwr then
{$endif LINUXNOTBSD}
Shutdown(sock,SHUT_[rdwr]); // SHUT_RD doesn't unlock accept() on Linux
CloseSocket(sock); // SO_LINGER usually set to 5 or 10 seconds
end;
function AsynchSocket(sock: TSocket): boolean;
var nonblocking: integer;
begin
nonblocking := 1; // for both Windows and POSIX
if sock<=0 then
result := false else
result := IoctlSocket(sock, FIONBIO, nonblocking)=0;
end;
function AsynchRecv(sock: TSocket; buf: pointer; buflen: integer): integer;
begin
{$ifdef MSWINDOWS}
result := Recv(sock,buf,buflen,0);
{$else}
{$ifdef KYLIX3}
result := LibC.Recv(sock,buf^,buflen,0);
{$else}
result := fpRecv(sock,buf,buflen,0);
{$endif KYLIX3}
{$endif MSWINDOWS}
end;
function AsynchSend(sock: TSocket; buf: pointer; buflen: integer): integer;
begin
{$ifdef MSWINDOWS}
result := Send(sock,buf,buflen,MSG_NOSIGNAL);
{$else}
{$ifdef KYLIX3}
result := LibC.Send(sock,buf^,buflen,MSG_NOSIGNAL);
{$else}
result := fpSend(sock,buf,buflen,MSG_NOSIGNAL);
{$endif}
{$endif}
end;
{ ECrtSocket }
function GetRemoteIP(aClientSock: TSocket): SockString;
var Name: TVarSin;
begin
if GetPeerName(aClientSock,Name)=0 then
IPText(Name,result) else
result := '';
end;
function SocketErrorMessage(Error: integer): string;
begin
if Error=-1 then
Error := WSAGetLastError;
case Error of
WSAETIMEDOUT: result := 'WSAETIMEDOUT';
WSAENETDOWN: result := 'WSAENETDOWN';
WSATRY_AGAIN: result := 'WSATRY_AGAIN';
{$ifdef MSWINDOWS} // WSATRY_AGAIN=WSAEWOULDBLOCK on POSIX
WSAEWOULDBLOCK: result := 'WSAEWOULDBLOCK';
{$endif}
WSAECONNABORTED: result := 'WSAECONNABORTED';
WSAECONNRESET: result := 'WSAECONNRESET';
WSAEMFILE: result := 'WSAEMFILE';
else result := '';
end;
result := Format('%d %s %s',[Error,result,SysErrorMessage(Error)]);
end;
constructor ECrtSocket.Create(const Msg: string);
begin
Create(Msg,WSAGetLastError);
end;
constructor ECrtSocket.Create(const Msg: string; Error: integer);
begin
if Error=0 then
fLastError := WSAEWOULDBLOCK else // if unknown, probably a timeout
fLastError := abs(Error);
inherited CreateFmt('%s [%s]', [Msg,SocketErrorMessage(fLastError)]);
end;
constructor ECrtSocket.CreateFmt(const Msg: string; const Args: array of const; Error: integer);
begin
if Error<0 then
Error := WSAGetLastError;
Create(Format(Msg,Args),Error);
end;
{ TSynThreadPool }
const
// up to 256 * 2MB = 512MB of RAM for the TSynThreadPoolWorkThread stack
THREADPOOL_MAXTHREADS = 256;
// kept-alive or big HTTP requests will create a dedicated THttpServerResp
// - each thread reserves 2 MB of memory so it may break the server
// - keep the value to a decent number, to let resources be constrained up to 1GB
THREADPOOL_MAXWORKTHREADS = 512;
// if HTTP body length is bigger than 16 MB, creates a dedicated THttpServerResp
THREADPOOL_BIGBODYSIZE = 16*1024*1024;
constructor TSynThreadPool.Create(NumberOfThreads: Integer;
{$ifdef USE_WINIOCP}aOverlapHandle: THandle{$else}aQueuePendingContext: boolean{$endif});
var i: integer;
begin
if NumberOfThreads=0 then
NumberOfThreads := 1 else
if cardinal(NumberOfThreads)>THREADPOOL_MAXTHREADS then
NumberOfThreads := THREADPOOL_MAXTHREADS;
// create IO completion port to queue the HTTP requests
{$ifdef USE_WINIOCP}
fRequestQueue := CreateIoCompletionPort(aOverlapHandle, 0, 0, NumberOfThreads);
if fRequestQueue=INVALID_HANDLE_VALUE then begin
fRequestQueue := 0;
exit;
end;
{$else}
InitializeCriticalSection(fSafe);
fQueuePendingContext := aQueuePendingContext;
{$endif}
// now create the worker threads
fWorkThreadCount := NumberOfThreads;
SetLength(fWorkThread,fWorkThreadCount);
for i := 0 to fWorkThreadCount-1 do
fWorkThread[i] := TSynThreadPoolWorkThread.Create(Self);
end;
destructor TSynThreadPool.Destroy;
var i: integer;
endtix: Int64;
begin
fTerminated := true; // fWorkThread[].Execute will check this flag
try
// notify the threads we are shutting down
for i := 0 to fWorkThreadCount-1 do
{$ifdef USE_WINIOCP}
PostQueuedCompletionStatus(fRequestQueue,0,0,nil);
{$else}
fWorkThread[i].fEvent.SetEvent;
{$endif}
{$ifndef USE_WINIOCP}
// cleanup now any pending task (e.g. THttpServerSocket instance)
for i := 0 to fPendingContextCount-1 do
TaskAbort(fPendingContext[i]);
{$endif}
// wait for threads to finish, with 30 seconds TimeOut
endtix := GetTick64+30000;
while (fRunningThreads>0) and (GetTick64<endtix) do
SleepHiRes(5);
for i := 0 to fWorkThreadCount-1 do
fWorkThread[i].Free;
finally
{$ifdef USE_WINIOCP}
CloseHandle(fRequestQueue);
{$else}
DeleteCriticalSection(fSafe);
{$endif USE_WINIOCP}
end;
inherited Destroy;
end;
function TSynThreadPool.Push(aContext: pointer; aWaitOnContention: boolean): boolean;
{$ifdef USE_WINIOCP}
function Enqueue: boolean;
begin // IOCP has its own queue
result := PostQueuedCompletionStatus(fRequestQueue,0,0,aContext);
end;
{$else}
function Enqueue: boolean;
var i, n: integer;
found: TSynThreadPoolWorkThread;
thread: ^TSynThreadPoolWorkThread;
begin
result := false; // queue is full
found := nil;
EnterCriticalsection(fSafe);
try
thread := pointer(fWorkThread);
for i := 1 to fWorkThreadCount do
if thread^.fProcessingContext=nil then begin
found := thread^;
found.fProcessingContext := aContext;
result := true; // found one available thread
exit;
end else
inc(thread);
if not fQueuePendingContext then
exit;
n := fPendingContextCount;
if n+fWorkThreadCount>QueueLength then
exit; // too many connection limit reached (see QueueIsFull)
if n=length(fPendingContext) then
SetLength(fPendingContext,n+n shr 3+64);
fPendingContext[n] := aContext;
inc(fPendingContextCount);
result := true; // added in pending queue
finally
LeaveCriticalsection(fSafe);
if found<>nil then
found.fEvent.SetEvent; // rather notify outside of the fSafe lock
end;
end;
{$endif}
var tix, starttix, endtix: Int64;
begin
result := false;
if (self=nil) or fTerminated then
exit;
result := Enqueue;
if result then
exit;
inc(fContentionCount);
if (fContentionAbortDelay>0) and aWaitOnContention then begin
tix := GetTick64;
starttix := tix;
endtix := tix+fContentionAbortDelay; // default 5 sec
repeat // during this delay, no new connection is ACCEPTed
if tix-starttix<50 then // wait for an available slot in the queue
SleepHiRes(1) else
SleepHiRes(10);
tix := GetTick64;
if fTerminated then
exit;
if Enqueue then begin
result := true; // thread pool acquired or queued the client sock
break;
end;
until fTerminated or (tix>endtix);
inc(fContentionTime,tix-starttix);
end;
if not result then
inc(fContentionAbortCount);
end;
{$ifndef USE_WINIOCP}
function TSynThreadPool.GetPendingContextCount: integer;
begin
result := 0;
if (self=nil) or fTerminated or (fPendingContext=nil) then
exit;
EnterCriticalsection(fSafe);
try
result := fPendingContextCount;
finally
LeaveCriticalsection(fSafe);
end;
end;
function TSynThreadPool.QueueIsFull: boolean;
begin
result := fQueuePendingContext and
(GetPendingContextCount+fWorkThreadCount>QueueLength);
end;
function TSynThreadPool.PopPendingContext: pointer;
begin
result := nil;
if (self=nil) or fTerminated or (fPendingContext=nil) then
exit;
EnterCriticalsection(fSafe);
try
if fPendingContextCount>0 then begin
result := fPendingContext[0];
dec(fPendingContextCount);
Move(fPendingContext[1],fPendingContext[0],fPendingContextCount*SizeOf(pointer));
if fPendingContextCount=128 then
SetLength(fPendingContext,128); // small queue when congestion is resolved
end;
finally
LeaveCriticalsection(fSafe);
end;
end;
function TSynThreadPool.QueueLength: integer;
begin
result := 10000; // lazy high value
end;
{$endif USE_WINIOCP}
function TSynThreadPool.NeedStopOnIOError: boolean;
begin
result := True;
end;
procedure TSynThreadPool.TaskAbort(aContext: Pointer);
begin
end;
{ TSynThreadPoolWorkThread }
constructor TSynThreadPoolWorkThread.Create(Owner: TSynThreadPool);
begin
fOwner := Owner; // ensure it is set ASAP: on Linux, Execute raises immediately
fOnThreadTerminate := Owner.fOnThreadTerminate;
{$ifndef USE_WINIOCP}
fEvent := TEvent.Create(nil,false,false,'');
{$endif}
inherited Create(false);
end;
destructor TSynThreadPoolWorkThread.Destroy;
begin
inherited Destroy;
{$ifndef USE_WINIOCP}
fEvent.Free;
{$endif}
end;
{$ifdef USE_WINIOCP}
function GetQueuedCompletionStatus(CompletionPort: THandle;
var lpNumberOfBytesTransferred: DWORD; var lpCompletionKey: PtrUInt;
var lpOverlapped: pointer; dwMilliseconds: DWORD): BOOL; stdcall;
external kernel32; // redefine with an unique signature for all Delphi/FPC
{$endif}
procedure TSynThreadPoolWorkThread.DoTask(Context: pointer);
begin
try
fOwner.Task(Self,Context);
except
on Exception do // intercept any exception and let the thread continue
inc(fOwner.fExceptionsCount);
end;
end;
procedure TSynThreadPoolWorkThread.Execute;
var ctxt: pointer;
{$ifdef USE_WINIOCP}
dum1: DWORD;
dum2: PtrUInt;
{$endif}
begin
if fOwner<>nil then
try
fThreadNumber := InterlockedIncrement(fOwner.fRunningThreads);
NotifyThreadStart(self);
repeat
{$ifdef USE_WINIOCP}
if (not GetQueuedCompletionStatus(fOwner.fRequestQueue,dum1,dum2,ctxt,INFINITE) and
fOwner.NeedStopOnIOError) or fOwner.fTerminated then
break;
if ctxt<>nil then
DoTask(ctxt);
{$else}
fEvent.WaitFor(INFINITE);
if fOwner.fTerminated then
break;
EnterCriticalSection(fOwner.fSafe);
ctxt := fProcessingContext;
LeaveCriticalSection(fOwner.fSafe);
if ctxt<>nil then begin
repeat
DoTask(ctxt);
ctxt := fOwner.PopPendingContext; // unqueue any pending context
until ctxt=nil;
EnterCriticalSection(fOwner.fSafe);
fProcessingContext := nil; // indicates this thread is now available
LeaveCriticalSection(fOwner.fSafe);
end;
{$endif USE_WINIOCP}
until fOwner.fTerminated or Terminated;
finally
InterlockedDecrement(fOwner.fRunningThreads);
end;
end;
procedure TSynThreadPoolWorkThread.NotifyThreadStart(Sender: TSynThread);
begin
if Sender=nil then
raise ECrtSocket.Create('NotifyThreadStart(nil)');
{$ifdef FPC}
{$ifdef LINUX}
if fNotifyThreadStartName='' then begin
fNotifyThreadStartName := format('Pool%d-%4x',[fThreadNumber,PtrInt(fOwner)]);
SetUnixThreadName(fThreadID,fNotifyThreadStartName);
end;
{$endif}
{$endif}
if Assigned(fOwner.fOnThreadStart) and not Assigned(Sender.fStartNotified) then begin
fOwner.fOnThreadStart(Sender);
Sender.fStartNotified := self;
end;
end;
{ TSynThreadPoolTHttpServer }
constructor TSynThreadPoolTHttpServer.Create(Server: THttpServer; NumberOfThreads: Integer=32);
begin
fServer := Server;
fOnThreadTerminate := fServer.fOnThreadTerminate;
inherited Create(NumberOfThreads{$ifndef USE_WINIOCP},{queuepending=}true{$endif});
end;
{$ifndef USE_WINIOCP}
function TSynThreadPoolTHttpServer.QueueLength: integer;
begin
if fServer=nil then
result := 10000 else
result := fServer.fHTTPQueueLength;
end;
{$endif USE_WINIOCP}
procedure TSynThreadPoolTHttpServer.Task(aCaller: TSynThread; aContext: Pointer);
var ServerSock: THttpServerSocket;
headertix: Int64;
res: THttpServerSocketGetRequestResult;
begin
ServerSock := aContext;
try
if fServer.Terminated then
exit;
// get Header of incoming request in the thread pool
headertix := fServer.HeaderRetrieveAbortDelay;
if headertix>0 then
headertix := headertix+GetTick64;
res := ServerSock.GetRequest({withbody=}false,headertix);
if (fServer=nil) or fServer.Terminated then
exit;
InterlockedIncrement(fServer.fStats[res]);
case res of
grHeaderReceived: begin
// connection and header seem valid -> process request further
if (fServer.ServerKeepAliveTimeOut>0) and
(fServer.fInternalHttpServerRespList.Count<THREADPOOL_MAXWORKTHREADS) and
(ServerSock.KeepAliveClient or
(ServerSock.ContentLength>THREADPOOL_BIGBODYSIZE)) then begin
// HTTP/1.1 Keep Alive (including WebSockets) or posted data > 16 MB
// -> process in dedicated background thread
fServer.fThreadRespClass.Create(ServerSock,fServer);
ServerSock := nil; // THttpServerResp will own and free ServerSock
end else begin
// no Keep Alive = multi-connection -> process in the Thread Pool
if not (connectionUpgrade in ServerSock.HeaderFlags) and
(IdemPCharArray(pointer(ServerSock.Method),['HEAD','OPTIONS'])<0) then begin
ServerSock.GetBody; // we need to get it now
InterlockedIncrement(fServer.fStats[grBodyReceived]);
end;
// multi-connection -> process now
fServer.Process(ServerSock,ServerSock.RemoteConnectionID,aCaller);
fServer.OnDisconnect;
// no Shutdown here: will be done client-side
end;
end;
grOwned: // e.g. for asynchrounous WebSockets
ServerSock := nil; // to ignore FreeAndNil(ServerSock) below
end; // errors will close the connection
finally
FreeAndNil(ServerSock);
end;
end;
procedure TSynThreadPoolTHttpServer.TaskAbort(aContext: Pointer);
begin
THttpServerSocket(aContext).Free;
end;
{$ifdef MSWINDOWS}
{ ************ http.sys / HTTP API low-level direct access }
{$MINENUMSIZE 4}
{$A+}
{$ifdef FPC}
{$PACKRECORDS C}
{$endif}
type
// HTTP version used
HTTP_VERSION = packed record
MajorVersion: word;
MinorVersion: word;
end;
// the req* values identify Request Headers, and resp* Response Headers
THttpHeader = (
reqCacheControl,
reqConnection,
reqDate,
reqKeepAlive,
reqPragma,
reqTrailer,
reqTransferEncoding,
reqUpgrade,
reqVia,
reqWarning,
reqAllow,
reqContentLength,
reqContentType,
reqContentEncoding,
reqContentLanguage,
reqContentLocation,
reqContentMd5,
reqContentRange,
reqExpires,
reqLastModified,
reqAccept,
reqAcceptCharset,
reqAcceptEncoding,
reqAcceptLanguage,
reqAuthorization,
reqCookie,
reqExpect,
reqFrom,
reqHost,
reqIfMatch,
reqIfModifiedSince,
reqIfNoneMatch,
reqIfRange,
reqIfUnmodifiedSince,
reqMaxForwards,
reqProxyAuthorization,
reqReferrer,
reqRange,
reqTe,
reqTranslate,
reqUserAgent
{$ifdef DELPHI5OROLDER}
);
const // Delphi 5 does not support values overlapping for enums
respAcceptRanges = THttpHeader(20);
respAge = THttpHeader(21);
respEtag = THttpHeader(22);
respLocation = THttpHeader(23);
respProxyAuthenticate = THttpHeader(24);
respRetryAfter = THttpHeader(25);
respServer = THttpHeader(26);
respSetCookie = THttpHeader(27);
respVary = THttpHeader(28);
respWwwAuthenticate = THttpHeader(29);
type
{$else} ,
respAcceptRanges = 20,
respAge,
respEtag,
respLocation,
respProxyAuthenticate,
respRetryAfter,
respServer,
respSetCookie,
respVary,
respWwwAuthenticate);
{$endif}
THttpVerb = (
hvUnparsed,
hvUnknown,
hvInvalid,
hvOPTIONS,
hvGET,
hvHEAD,
hvPOST,
hvPUT,
hvDELETE,
hvTRACE,
hvCONNECT,
hvTRACK, // used by Microsoft Cluster Server for a non-logged trace
hvMOVE,
hvCOPY,
hvPROPFIND,
hvPROPPATCH,
hvMKCOL,
hvLOCK,
hvUNLOCK,
hvSEARCH,
hvMaximum );
THttpChunkType = (
hctFromMemory,
hctFromFileHandle,
hctFromFragmentCache);
THttpServiceConfigID = (
hscIPListenList,
hscSSLCertInfo,
hscUrlAclInfo,
hscMax);
THttpServiceConfigQueryType = (
hscQueryExact,
hscQueryNext,
hscQueryMax);
HTTP_URL_CONTEXT = HTTP_OPAQUE_ID;
HTTP_CONNECTION_ID = HTTP_OPAQUE_ID;
HTTP_RAW_CONNECTION_ID = HTTP_OPAQUE_ID;
// Pointers overlap and point into pFullUrl. nil if not present.
HTTP_COOKED_URL = record
FullUrlLength: word; // in bytes not including the #0
HostLength: word; // in bytes not including the #0
AbsPathLength: word; // in bytes not including the #0
QueryStringLength: word; // in bytes not including the #0
pFullUrl: PWideChar; // points to "http://hostname:port/abs/.../path?query"
pHost: PWideChar; // points to the first char in the hostname
pAbsPath: PWideChar; // Points to the 3rd '/' char
pQueryString: PWideChar; // Points to the 1st '?' char or #0
end;
HTTP_TRANSPORT_ADDRESS = record
pRemoteAddress: PSOCKADDR;
pLocalAddress: PSOCKADDR;
end;
HTTP_UNKNOWN_HEADER = record
NameLength: word; // in bytes not including the #0
RawValueLength: word; // in bytes not including the n#0
pName: PAnsiChar; // The header name (minus the ':' character)
pRawValue: PAnsiChar; // The header value
end;
PHTTP_UNKNOWN_HEADER = ^HTTP_UNKNOWN_HEADER;
HTTP_UNKNOWN_HEADERs = array of HTTP_UNKNOWN_HEADER;
HTTP_KNOWN_HEADER = record
RawValueLength: word; // in bytes not including the #0
pRawValue: PAnsiChar;
end;
PHTTP_KNOWN_HEADER = ^HTTP_KNOWN_HEADER;
HTTP_RESPONSE_HEADERS = record
// number of entries in the unknown HTTP headers array
UnknownHeaderCount: word;
// array of unknown HTTP headers
pUnknownHeaders: pointer;
// Reserved, must be 0
TrailerCount: word;
// Reserved, must be nil
pTrailers: pointer;
// Known headers
KnownHeaders: array[low(THttpHeader)..respWwwAuthenticate] of HTTP_KNOWN_HEADER;
end;
HTTP_REQUEST_HEADERS = record
// number of entries in the unknown HTTP headers array
UnknownHeaderCount: word;
// array of unknown HTTP headers
pUnknownHeaders: PHTTP_UNKNOWN_HEADER;
// Reserved, must be 0
TrailerCount: word;
// Reserved, must be nil
pTrailers: pointer;
// Known headers
KnownHeaders: array[low(THttpHeader)..reqUserAgent] of HTTP_KNOWN_HEADER;
end;
HTTP_BYTE_RANGE = record
StartingOffset: ULARGE_INTEGER;
Length: ULARGE_INTEGER;
end;
// we use 3 distinct HTTP_DATA_CHUNK_* records since variable records
// alignment is buggy/non compatible under Delphi XE3
HTTP_DATA_CHUNK_INMEMORY = record
DataChunkType: THttpChunkType; // always hctFromMemory
Reserved1: ULONG;
pBuffer: pointer;
BufferLength: ULONG;
Reserved2: ULONG;
Reserved3: ULONG;
end;
PHTTP_DATA_CHUNK_INMEMORY = ^HTTP_DATA_CHUNK_INMEMORY;
HTTP_DATA_CHUNK_FILEHANDLE = record
DataChunkType: THttpChunkType; // always hctFromFileHandle
ByteRange: HTTP_BYTE_RANGE;
FileHandle: THandle;
end;
HTTP_DATA_CHUNK_FRAGMENTCACHE = record
DataChunkType: THttpChunkType; // always hctFromFragmentCache
FragmentNameLength: word; // in bytes not including the #0
pFragmentName: PWideChar;
end;
HTTP_SSL_CLIENT_CERT_INFO = record
CertFlags: ULONG;
CertEncodedSize: ULONG;
pCertEncoded: PUCHAR;
Token: THandle;
CertDeniedByMapper: boolean;
end;
PHTTP_SSL_CLIENT_CERT_INFO = ^HTTP_SSL_CLIENT_CERT_INFO;
HTTP_SSL_INFO = record
ServerCertKeySize: word;
ConnectionKeySize: word;
ServerCertIssuerSize: ULONG;
ServerCertSubjectSize: ULONG;
pServerCertIssuer: PAnsiChar;
pServerCertSubject: PAnsiChar;
pClientCertInfo: PHTTP_SSL_CLIENT_CERT_INFO;
SslClientCertNegotiated: ULONG;
end;
PHTTP_SSL_INFO = ^HTTP_SSL_INFO;
HTTP_SERVICE_CONFIG_URLACL_KEY = record
pUrlPrefix: PWideChar;
end;
HTTP_SERVICE_CONFIG_URLACL_PARAM = record
pStringSecurityDescriptor: PWideChar;
end;
HTTP_SERVICE_CONFIG_URLACL_SET = record
KeyDesc: HTTP_SERVICE_CONFIG_URLACL_KEY;
ParamDesc: HTTP_SERVICE_CONFIG_URLACL_PARAM;
end;
HTTP_SERVICE_CONFIG_URLACL_QUERY = record
QueryDesc: THttpServiceConfigQueryType;
KeyDesc: HTTP_SERVICE_CONFIG_URLACL_KEY;
dwToken: DWORD;
end;
HTTP_REQUEST_INFO_TYPE = (
HttpRequestInfoTypeAuth,
HttpRequestInfoTypeChannelBind,
HttpRequestInfoTypeSslProtocol,
HttpRequestInfoTypeSslTokenBindingDraft,
HttpRequestInfoTypeSslTokenBinding,
HttpRequestInfoTypeRequestTiming,
HttpRequestInfoTypeTcpInfoV0,
HttpRequestInfoTypeRequestSizing,
HttpRequestInfoTypeQuicStats,
HttpRequestInfoTypeTcpInfoV1
);
// about Authentication in HTTP Version 2.0
// see https://msdn.microsoft.com/en-us/library/windows/desktop/aa364452
HTTP_AUTH_STATUS = (
HttpAuthStatusSuccess,
HttpAuthStatusNotAuthenticated,
HttpAuthStatusFailure
);
HTTP_REQUEST_AUTH_TYPE = (
HttpRequestAuthTypeNone,
HttpRequestAuthTypeBasic,
HttpRequestAuthTypeDigest,
HttpRequestAuthTypeNTLM,
HttpRequestAuthTypeNegotiate,
HttpRequestAuthTypeKerberos
);
SECURITY_STATUS = ULONG;
HTTP_REQUEST_AUTH_INFO = record
AuthStatus: HTTP_AUTH_STATUS;
SecStatus: SECURITY_STATUS;
Flags: ULONG;
AuthType: HTTP_REQUEST_AUTH_TYPE;
AccessToken: THandle;
ContextAttributes: ULONG;
PackedContextLength: ULONG;
PackedContextType: ULONG;
PackedContext: pointer;
MutualAuthDataLength: ULONG;
pMutualAuthData: PAnsiChar;
PackageNameLength: word;
pPackageName: LPWSTR;
end;
PHTTP_REQUEST_AUTH_INFO = ^HTTP_REQUEST_AUTH_INFO;
HTTP_REQUEST_INFO = record
InfoType: HTTP_REQUEST_INFO_TYPE;
InfoLength: ULONG;
pInfo: pointer;
end;
HTTP_REQUEST_INFOS = array[0..1000] of HTTP_REQUEST_INFO;
PHTTP_REQUEST_INFOS = ^HTTP_REQUEST_INFOS;
/// structure used to handle data associated with a specific request
HTTP_REQUEST = record
// either 0 (Only Header), either HTTP_RECEIVE_REQUEST_FLAG_COPY_BODY
Flags: cardinal;
// An identifier for the connection on which the request was received
ConnectionId: HTTP_CONNECTION_ID;
// A value used to identify the request when calling
// HttpReceiveRequestEntityBody, HttpSendHttpResponse, and/or
// HttpSendResponseEntityBody
RequestId: HTTP_REQUEST_ID;
// The context associated with the URL prefix
UrlContext: HTTP_URL_CONTEXT;
// The HTTP version number
Version: HTTP_VERSION;
// An HTTP verb associated with this request
Verb: THttpVerb;
// The length of the verb string if the Verb field is hvUnknown
// (in bytes not including the last #0)
UnknownVerbLength: word;
// The length of the raw (uncooked) URL (in bytes not including the last #0)
RawUrlLength: word;
// Pointer to the verb string if the Verb field is hvUnknown
pUnknownVerb: PAnsiChar;
// Pointer to the raw (uncooked) URL
pRawUrl: PAnsiChar;
// The canonicalized Unicode URL
CookedUrl: HTTP_COOKED_URL;
// Local and remote transport addresses for the connection
Address: HTTP_TRANSPORT_ADDRESS;
// The request headers.
Headers: HTTP_REQUEST_HEADERS;
// The total number of bytes received from network for this request
BytesReceived: ULONGLONG;
EntityChunkCount: word;
pEntityChunks: pointer;
RawConnectionId: HTTP_RAW_CONNECTION_ID;
// SSL connection information
pSslInfo: PHTTP_SSL_INFO;
{ beginning of HTTP_REQUEST_V2 structure - manual padding is needed :( }
{$ifdef CPU32}
padding: dword;
{$endif CPU32}
/// how many extended info about a specific request is available in v2
RequestInfoCount: word;
/// v2 trailing structure used to handle extended info about a specific request
pRequestInfo: PHTTP_REQUEST_INFOS;
end;
PHTTP_REQUEST = ^HTTP_REQUEST;
HTTP_RESPONSE_INFO_TYPE = (
HttpResponseInfoTypeMultipleKnownHeaders,
HttpResponseInfoTypeAuthenticationProperty,
HttpResponseInfoTypeQosProperty,
HttpResponseInfoTypeChannelBind
);
HTTP_RESPONSE_INFO = record
Typ: HTTP_RESPONSE_INFO_TYPE;
Length: ULONG;
pInfo: Pointer;
end;
PHTTP_RESPONSE_INFO = ^HTTP_RESPONSE_INFO;
/// structure as expected by HttpSendHttpResponse() API
HTTP_RESPONSE = object
public
Flags: cardinal;
// The raw HTTP protocol version number
Version: HTTP_VERSION;
// The HTTP status code (e.g., 200)
StatusCode: word;
// in bytes not including the '\0'
ReasonLength: word;
// The HTTP reason (e.g., "OK"). This MUST not contain non-ASCII characters
// (i.e., all chars must be in range 0x20-0x7E).
pReason: PAnsiChar;
// The response headers
Headers: HTTP_RESPONSE_HEADERS;
// number of elements in pEntityChunks[] array
EntityChunkCount: word;
// pEntityChunks points to an array of EntityChunkCount HTTP_DATA_CHUNK_*
pEntityChunks: pointer;
// contains the number of HTTP API 2.0 extended information
ResponseInfoCount: word;
// map the HTTP API 2.0 extended information
pResponseInfo: PHTTP_RESPONSE_INFO;
// will set both StatusCode and Reason
// - OutStatus is a temporary variable which will be field with the
// corresponding text
procedure SetStatus(code: integer; var OutStatus: SockString);
// will set the content of the reponse, and ContentType header
procedure SetContent(var DataChunk: HTTP_DATA_CHUNK_INMEMORY;
const Content: SockString; const ContentType: SockString='text/html');
/// will set all header values from lines
// - Content-Type/Content-Encoding/Location will be set in KnownHeaders[]
// - all other headers will be set in temp UnknownHeaders[]
procedure SetHeaders(P: PAnsiChar; var UnknownHeaders: HTTP_UNKNOWN_HEADERs);
/// add one header value to the internal headers
// - SetHeaders() method should have been called before to initialize the
// internal UnknownHeaders[] array
function AddCustomHeader(P: PAnsiChar; var UnknownHeaders: HTTP_UNKNOWN_HEADERs;
ForceCustomHeader: boolean): PAnsiChar;
end;
PHTTP_RESPONSE = ^HTTP_RESPONSE;
HTTP_PROPERTY_FLAGS = ULONG;
HTTP_ENABLED_STATE = (
HttpEnabledStateActive,
HttpEnabledStateInactive
);
PHTTP_ENABLED_STATE = ^HTTP_ENABLED_STATE;
HTTP_STATE_INFO = record
Flags: HTTP_PROPERTY_FLAGS;
State: HTTP_ENABLED_STATE;
end;
PHTTP_STATE_INFO = ^HTTP_STATE_INFO;
THTTP_503_RESPONSE_VERBOSITY = (
Http503ResponseVerbosityBasic,
Http503ResponseVerbosityLimited,
Http503ResponseVerbosityFull
);
PHTTP_503_RESPONSE_VERBOSITY = ^ THTTP_503_RESPONSE_VERBOSITY;
HTTP_QOS_SETTING_TYPE = (
HttpQosSettingTypeBandwidth,
HttpQosSettingTypeConnectionLimit,
HttpQosSettingTypeFlowRate // Windows Server 2008 R2 and Windows 7 only.
);
PHTTP_QOS_SETTING_TYPE = ^HTTP_QOS_SETTING_TYPE;
HTTP_QOS_SETTING_INFO = record
QosType: HTTP_QOS_SETTING_TYPE;
QosSetting: Pointer;
end;
PHTTP_QOS_SETTING_INFO = ^HTTP_QOS_SETTING_INFO;
HTTP_CONNECTION_LIMIT_INFO = record
Flags: HTTP_PROPERTY_FLAGS;
MaxConnections: ULONG;
end;
PHTTP_CONNECTION_LIMIT_INFO = ^HTTP_CONNECTION_LIMIT_INFO;
HTTP_BANDWIDTH_LIMIT_INFO = record
Flags: HTTP_PROPERTY_FLAGS;
MaxBandwidth: ULONG;
end;
PHTTP_BANDWIDTH_LIMIT_INFO = ^HTTP_BANDWIDTH_LIMIT_INFO;
HTTP_FLOWRATE_INFO = record
Flags: HTTP_PROPERTY_FLAGS;
MaxBandwidth: ULONG;
MaxPeakBandwidth: ULONG;
BurstSize: ULONG;
end;
PHTTP_FLOWRATE_INFO = ^HTTP_FLOWRATE_INFO;
const
HTTP_MIN_ALLOWED_BANDWIDTH_THROTTLING_RATE {:ULONG} = 1024;
HTTP_LIMIT_INFINITE {:ULONG} = ULONG(-1);
type
HTTP_SERVICE_CONFIG_TIMEOUT_KEY = (
IdleConnectionTimeout,
HeaderWaitTimeout
);
PHTTP_SERVICE_CONFIG_TIMEOUT_KEY = ^HTTP_SERVICE_CONFIG_TIMEOUT_KEY;
HTTP_SERVICE_CONFIG_TIMEOUT_PARAM = word;
PHTTP_SERVICE_CONFIG_TIMEOUT_PARAM = ^HTTP_SERVICE_CONFIG_TIMEOUT_PARAM;
HTTP_SERVICE_CONFIG_TIMEOUT_SET = record
KeyDesc: HTTP_SERVICE_CONFIG_TIMEOUT_KEY;
ParamDesc: HTTP_SERVICE_CONFIG_TIMEOUT_PARAM;
end;
PHTTP_SERVICE_CONFIG_TIMEOUT_SET = ^HTTP_SERVICE_CONFIG_TIMEOUT_SET;
HTTP_TIMEOUT_LIMIT_INFO = record
Flags: HTTP_PROPERTY_FLAGS;
EntityBody: word;
DrainEntityBody: word;
RequestQueue: word;
IdleConnection: word;
HeaderWait: word;
MinSendRate: cardinal;
end;
PHTTP_TIMEOUT_LIMIT_INFO = ^HTTP_TIMEOUT_LIMIT_INFO;
HTTP_LISTEN_ENDPOINT_INFO = record
Flags: HTTP_PROPERTY_FLAGS;
EnableSharing: boolean;
end;
PHTTP_LISTEN_ENDPOINT_INFO = ^HTTP_LISTEN_ENDPOINT_INFO;
HTTP_SERVER_AUTHENTICATION_DIGEST_PARAMS = record
DomainNameLength: word;
DomainName: PWideChar;
RealmLength: word;
Realm: PWideChar;
end;
PHTTP_SERVER_AUTHENTICATION_DIGEST_PARAMS = ^HTTP_SERVER_AUTHENTICATION_DIGEST_PARAMS;
HTTP_SERVER_AUTHENTICATION_BASIC_PARAMS = record
RealmLength: word;
Realm: PWideChar;
end;
PHTTP_SERVER_AUTHENTICATION_BASIC_PARAMS = ^HTTP_SERVER_AUTHENTICATION_BASIC_PARAMS;
const
HTTP_AUTH_ENABLE_BASIC = $00000001;
HTTP_AUTH_ENABLE_DIGEST = $00000002;
HTTP_AUTH_ENABLE_NTLM = $00000004;
HTTP_AUTH_ENABLE_NEGOTIATE = $00000008;
HTTP_AUTH_ENABLE_KERBEROS = $00000010;
HTTP_AUTH_ENABLE_ALL = $0000001F;
HTTP_AUTH_EX_FLAG_ENABLE_KERBEROS_CREDENTIAL_CACHING = $01;
HTTP_AUTH_EX_FLAG_CAPTURE_CREDENTIAL = $02;
type
HTTP_SERVER_AUTHENTICATION_INFO = record
Flags: HTTP_PROPERTY_FLAGS;
AuthSchemes: ULONG;
ReceiveMutualAuth: BYTEBOOL;
ReceiveContextHandle: BYTEBOOL;
DisableNTLMCredentialCaching: BYTEBOOL;
ExFlags: BYTE;
DigestParams: HTTP_SERVER_AUTHENTICATION_DIGEST_PARAMS;
BasicParams: HTTP_SERVER_AUTHENTICATION_BASIC_PARAMS;
end;
PHTTP_SERVER_AUTHENTICATION_INFO = ^HTTP_SERVER_AUTHENTICATION_INFO;
HTTP_SERVICE_BINDING_TYPE=(
HttpServiceBindingTypeNone,
HttpServiceBindingTypeW,
HttpServiceBindingTypeA
);
HTTP_SERVICE_BINDING_BASE = record
BindingType: HTTP_SERVICE_BINDING_TYPE;
end;
PHTTP_SERVICE_BINDING_BASE = ^HTTP_SERVICE_BINDING_BASE;
HTTP_SERVICE_BINDING_A = record
Base: HTTP_SERVICE_BINDING_BASE;
Buffer: PAnsiChar;
BufferSize: ULONG;
end;
PHTTP_SERVICE_BINDING_A = HTTP_SERVICE_BINDING_A;
HTTP_SERVICE_BINDING_W = record
Base: HTTP_SERVICE_BINDING_BASE;
Buffer: PWCHAR;
BufferSize: ULONG;
end;
PHTTP_SERVICE_BINDING_W = ^HTTP_SERVICE_BINDING_W;
HTTP_AUTHENTICATION_HARDENING_LEVELS = (
HttpAuthenticationHardeningLegacy,
HttpAuthenticationHardeningMedium,
HttpAuthenticationHardeningStrict
);
const
HTTP_CHANNEL_BIND_PROXY = $1;
HTTP_CHANNEL_BIND_PROXY_COHOSTING = $20;
HTTP_CHANNEL_BIND_NO_SERVICE_NAME_CHECK = $2;
HTTP_CHANNEL_BIND_DOTLESS_SERVICE = $4;
HTTP_CHANNEL_BIND_SECURE_CHANNEL_TOKEN = $8;
HTTP_CHANNEL_BIND_CLIENT_SERVICE = $10;
type
HTTP_CHANNEL_BIND_INFO = record
Hardening: HTTP_AUTHENTICATION_HARDENING_LEVELS;
Flags: ULONG;
ServiceNames: PHTTP_SERVICE_BINDING_BASE;
NumberOfServiceNames: ULONG;
end;
PHTTP_CHANNEL_BIND_INFO = ^HTTP_CHANNEL_BIND_INFO;
HTTP_REQUEST_CHANNEL_BIND_STATUS = record
ServiceName: PHTTP_SERVICE_BINDING_BASE;
ChannelToken: PUCHAR;
ChannelTokenSize: ULONG;
Flags: ULONG;
end;
PHTTP_REQUEST_CHANNEL_BIND_STATUS = ^HTTP_REQUEST_CHANNEL_BIND_STATUS;
const
// Logging option flags. When used in the logging configuration alters
// some default logging behaviour.
// HTTP_LOGGING_FLAG_LOCAL_TIME_ROLLOVER - This flag is used to change
// the log file rollover to happen by local time based. By default
// log file rollovers happen by GMT time.
HTTP_LOGGING_FLAG_LOCAL_TIME_ROLLOVER = 1;
// HTTP_LOGGING_FLAG_USE_UTF8_CONVERSION - When set the unicode fields
// will be converted to UTF8 multibytes when writting to the log
// files. When this flag is not present, the local code page
// conversion happens.
HTTP_LOGGING_FLAG_USE_UTF8_CONVERSION = 2;
// HTTP_LOGGING_FLAG_LOG_ERRORS_ONLY -
// HTTP_LOGGING_FLAG_LOG_SUCCESS_ONLY - These two flags are used to
// to do selective logging. If neither of them are present both
// types of requests will be logged. Only one these flags can be
// set at a time. They are mutually exclusive.
HTTP_LOGGING_FLAG_LOG_ERRORS_ONLY = 4;
HTTP_LOGGING_FLAG_LOG_SUCCESS_ONLY = 8;
// The known log fields recognized/supported by HTTPAPI. Following fields
// are used for W3C logging. Subset of them are also used for error logging
HTTP_LOG_FIELD_DATE = $00000001;
HTTP_LOG_FIELD_TIME = $00000002;
HTTP_LOG_FIELD_CLIENT_IP = $00000004;
HTTP_LOG_FIELD_USER_NAME = $00000008;
HTTP_LOG_FIELD_SITE_NAME = $00000010;
HTTP_LOG_FIELD_COMPUTER_NAME = $00000020;
HTTP_LOG_FIELD_SERVER_IP = $00000040;
HTTP_LOG_FIELD_METHOD = $00000080;
HTTP_LOG_FIELD_URI_STEM = $00000100;
HTTP_LOG_FIELD_URI_QUERY = $00000200;
HTTP_LOG_FIELD_STATUS = $00000400;
HTTP_LOG_FIELD_WIN32_STATUS = $00000800;
HTTP_LOG_FIELD_BYTES_SENT = $00001000;
HTTP_LOG_FIELD_BYTES_RECV = $00002000;
HTTP_LOG_FIELD_TIME_TAKEN = $00004000;
HTTP_LOG_FIELD_SERVER_PORT = $00008000;
HTTP_LOG_FIELD_USER_AGENT = $00010000;
HTTP_LOG_FIELD_COOKIE = $00020000;
HTTP_LOG_FIELD_REFERER = $00040000;
HTTP_LOG_FIELD_VERSION = $00080000;
HTTP_LOG_FIELD_HOST = $00100000;
HTTP_LOG_FIELD_SUB_STATUS = $00200000;
HTTP_ALL_NON_ERROR_LOG_FIELDS = HTTP_LOG_FIELD_SUB_STATUS*2-1;
// Fields that are used only for error logging
HTTP_LOG_FIELD_CLIENT_PORT = $00400000;
HTTP_LOG_FIELD_URI = $00800000;
HTTP_LOG_FIELD_SITE_ID = $01000000;
HTTP_LOG_FIELD_REASON = $02000000;
HTTP_LOG_FIELD_QUEUE_NAME = $04000000;
type
HTTP_LOGGING_TYPE = (
HttpLoggingTypeW3C,
HttpLoggingTypeIIS,
HttpLoggingTypeNCSA,
HttpLoggingTypeRaw
);
HTTP_LOGGING_ROLLOVER_TYPE = (
HttpLoggingRolloverSize,
HttpLoggingRolloverDaily,
HttpLoggingRolloverWeekly,
HttpLoggingRolloverMonthly,
HttpLoggingRolloverHourly
);
HTTP_LOGGING_INFO = record
Flags: HTTP_PROPERTY_FLAGS;
LoggingFlags: ULONG;
SoftwareName: PWideChar;
SoftwareNameLength: word;
DirectoryNameLength: word;
DirectoryName: PWideChar;
Format: HTTP_LOGGING_TYPE;
Fields: ULONG;
pExtFields: pointer;
NumOfExtFields: word;
MaxRecordSize: word;
RolloverType: HTTP_LOGGING_ROLLOVER_TYPE;
RolloverSize: ULONG;
pSecurityDescriptor: PSECURITY_DESCRIPTOR;
end;
PHTTP_LOGGING_INFO = ^HTTP_LOGGING_INFO;
HTTP_LOG_DATA_TYPE = (
HttpLogDataTypeFields
);
HTTP_LOG_DATA = record
Typ: HTTP_LOG_DATA_TYPE
end;
PHTTP_LOG_DATA = ^HTTP_LOG_DATA;
HTTP_LOG_FIELDS_DATA = record
Base: HTTP_LOG_DATA;
UserNameLength: word;
UriStemLength: word;
ClientIpLength: word;
ServerNameLength: word;
ServiceNameLength: word;
ServerIpLength: word;
MethodLength: word;
UriQueryLength: word;
HostLength: word;
UserAgentLength: word;
CookieLength: word;
ReferrerLength: word;
UserName: PWideChar;
UriStem: PWideChar;
ClientIp: PAnsiChar;
ServerName: PAnsiChar;
ServiceName: PAnsiChar;
ServerIp: PAnsiChar;
Method: PAnsiChar;
UriQuery: PAnsiChar;
Host: PAnsiChar;
UserAgent: PAnsiChar;
Cookie: PAnsiChar;
Referrer: PAnsiChar;
ServerPort: word;
ProtocolStatus: word;
Win32Status: ULONG;
MethodNum: THttpVerb;
SubStatus: word;
end;
PHTTP_LOG_FIELDS_DATA = ^HTTP_LOG_FIELDS_DATA;
HTTP_BINDING_INFO = record
Flags: HTTP_PROPERTY_FLAGS;
RequestQueueHandle: THandle;
end;
HTTP_PROTECTION_LEVEL_TYPE=(
HttpProtectionLevelUnrestricted,
HttpProtectionLevelEdgeRestricted,
HttpProtectionLevelRestricted
);
HTTP_PROTECTION_LEVEL_INFO = record
Flags: HTTP_PROPERTY_FLAGS;
Level: HTTP_PROTECTION_LEVEL_TYPE;
end;
PHTTP_PROTECTION_LEVEL_INFO = ^HTTP_PROTECTION_LEVEL_INFO;
const
HTTP_VERSION_UNKNOWN: HTTP_VERSION = (MajorVersion: 0; MinorVersion: 0);
HTTP_VERSION_0_9: HTTP_VERSION = (MajorVersion: 0; MinorVersion: 9);
HTTP_VERSION_1_0: HTTP_VERSION = (MajorVersion: 1; MinorVersion: 0);
HTTP_VERSION_1_1: HTTP_VERSION = (MajorVersion: 1; MinorVersion: 1);
/// error raised by HTTP API when the client disconnected (e.g. after timeout)
HTTPAPI_ERROR_NONEXISTENTCONNECTION = 1229;
// if set, available entity body is copied along with the request headers
// into pEntityChunks
HTTP_RECEIVE_REQUEST_FLAG_COPY_BODY = 1;
// there is more entity body to be read for this request
HTTP_REQUEST_FLAG_MORE_ENTITY_BODY_EXISTS = 1;
// initialization for applications that use the HTTP Server API
HTTP_INITIALIZE_SERVER = 1;
// initialization for applications that use the HTTP configuration functions
HTTP_INITIALIZE_CONFIG = 2;
// see http://msdn.microsoft.com/en-us/library/windows/desktop/aa364496
HTTP_RECEIVE_REQUEST_ENTITY_BODY_FLAG_FILL_BUFFER = 1;
// see http://msdn.microsoft.com/en-us/library/windows/desktop/aa364499
HTTP_SEND_RESPONSE_FLAG_DISCONNECT = $00000001;
HTTP_SEND_RESPONSE_FLAG_MORE_DATA = $00000002;
HTTP_SEND_RESPONSE_FLAG_BUFFER_DATA = $00000004;
HTTP_SEND_RESPONSE_FLAG_PROCESS_RANGES = $00000020;
HTTP_SEND_RESPONSE_FLAG_OPAQUE = $00000040;
// flag which can be used by HttpRemoveUrlFromUrlGroup()
HTTP_URL_FLAG_REMOVE_ALL = 1;
HTTP_KNOWNHEADERS: array[low(THttpHeader)..reqUserAgent] of string[19] = (
'Cache-Control','Connection','Date','Keep-Alive','Pragma','Trailer',
'Transfer-Encoding','Upgrade','Via','Warning','Allow','Content-Length',
'Content-Type','Content-Encoding','Content-Language','Content-Location',
'Content-MD5','Content-Range','Expires','Last-Modified','Accept',
'Accept-Charset','Accept-Encoding','Accept-Language','Authorization',
'Cookie','Expect','From','Host','If-Match','If-Modified-Since',
'If-None-Match','If-Range','If-Unmodified-Since','Max-Forwards',
'Proxy-Authorization','Referer','Range','TE','Translate','User-Agent');
REMOTEIP_HEADERLEN = 10;
REMOTEIP_HEADER: string[REMOTEIP_HEADERLEN] = 'RemoteIP: ';
function RetrieveHeaders(const Request: HTTP_REQUEST;
const RemoteIPHeadUp: SockString; out RemoteIP: SockString): SockString;
var i, L, Lip: integer;
H: THttpHeader;
P: PHTTP_UNKNOWN_HEADER;
D: PAnsiChar;
begin
assert(low(HTTP_KNOWNHEADERS)=low(Request.Headers.KnownHeaders));
assert(high(HTTP_KNOWNHEADERS)=high(Request.Headers.KnownHeaders));
// compute remote IP
L := length(RemoteIPHeadUp);
if L<>0 then begin
P := Request.Headers.pUnknownHeaders;
if P<>nil then
for i := 1 to Request.Headers.UnknownHeaderCount do
if (P^.NameLength=L) and IdemPChar(P^.pName,Pointer(RemoteIPHeadUp)) then begin
SetString(RemoteIP,p^.pRawValue,p^.RawValueLength);
break;
end else
inc(P);
end;
if (RemoteIP='') and (Request.Address.pRemoteAddress<>nil) then
IPText(PVarSin(Request.Address.pRemoteAddress)^,RemoteIP,RemoteIPLocalHostAsVoidInServers);
// compute headers length
Lip := length(RemoteIP);
if Lip<>0 then
L := (REMOTEIP_HEADERLEN+2)+Lip else
L := 0;
for H := low(HTTP_KNOWNHEADERS) to high(HTTP_KNOWNHEADERS) do
if Request.Headers.KnownHeaders[h].RawValueLength<>0 then
inc(L,Request.Headers.KnownHeaders[h].RawValueLength+ord(HTTP_KNOWNHEADERS[h][0])+4);
P := Request.Headers.pUnknownHeaders;
if P<>nil then
for i := 1 to Request.Headers.UnknownHeaderCount do begin
inc(L,P^.NameLength+P^.RawValueLength+4); // +4 for each ': '+#13#10
inc(P);
end;
// set headers content
SetString(result,nil,L);
D := pointer(result);
for H := low(HTTP_KNOWNHEADERS) to high(HTTP_KNOWNHEADERS) do
if Request.Headers.KnownHeaders[h].RawValueLength<>0 then begin
move(HTTP_KNOWNHEADERS[h][1],D^,ord(HTTP_KNOWNHEADERS[h][0]));
inc(D,ord(HTTP_KNOWNHEADERS[h][0]));
PWord(D)^ := ord(':')+ord(' ')shl 8;
inc(D,2);
move(Request.Headers.KnownHeaders[h].pRawValue^,D^,
Request.Headers.KnownHeaders[h].RawValueLength);
inc(D,Request.Headers.KnownHeaders[h].RawValueLength);
PWord(D)^ := 13+10 shl 8;
inc(D,2);
end;
P := Request.Headers.pUnknownHeaders;
if P<>nil then
for i := 1 to Request.Headers.UnknownHeaderCount do begin
move(P^.pName^,D^,P^.NameLength);
inc(D,P^.NameLength);
PWord(D)^ := ord(':')+ord(' ')shl 8;
inc(D,2);
move(P^.pRawValue^,D^,P^.RawValueLength);
inc(D,P^.RawValueLength);
inc(P);
PWord(D)^ := 13+10 shl 8;
inc(D,2);
end;
if Lip<>0 then begin
move(REMOTEIP_HEADER[1],D^,REMOTEIP_HEADERLEN);
inc(D,REMOTEIP_HEADERLEN);
move(pointer(RemoteIP)^,D^,Lip);
inc(D,Lip);
PWord(D)^ := 13+10 shl 8;
{$ifopt C+}
inc(D,2);
end;
assert(D-pointer(result)=L);
{$else}
end;
{$endif}
end;
type
HTTP_SERVER_PROPERTY = (
HttpServerAuthenticationProperty,
HttpServerLoggingProperty,
HttpServerQosProperty,
HttpServerTimeoutsProperty,
HttpServerQueueLengthProperty,
HttpServerStateProperty,
HttpServer503VerbosityProperty,
HttpServerBindingProperty,
HttpServerExtendedAuthenticationProperty,
HttpServerListenEndpointProperty,
HttpServerChannelBindProperty,
HttpServerProtectionLevelProperty
);
/// direct late-binding access to the HTTP API server 1.0 or 2.0
THttpAPI = packed record
/// access to the httpapi.dll loaded library
Module: THandle;
/// will be either 1.0 or 2.0, depending on the published .dll functions
Version: HTTP_VERSION;
/// The HttpInitialize function initializes the HTTP Server API driver, starts it,
// if it has not already been started, and allocates data structures for the
// calling application to support response-queue creation and other operations.
// Call this function before calling any other functions in the HTTP Server API.
Initialize: function(Version: HTTP_VERSION; Flags: cardinal;
pReserved: pointer=nil): HRESULT; stdcall;
/// The HttpTerminate function cleans up resources used by the HTTP Server API
// to process calls by an application. An application should call HttpTerminate
// once for every time it called HttpInitialize, with matching flag settings.
Terminate: function(Flags: cardinal;
Reserved: integer=0): HRESULT; stdcall;
/// The HttpCreateHttpHandle function creates an HTTP request queue for the
// calling application and returns a handle to it.
CreateHttpHandle: function(var ReqQueueHandle: THandle;
Reserved: integer=0): HRESULT; stdcall;
/// The HttpAddUrl function registers a given URL so that requests that match
// it are routed to a specified HTTP Server API request queue. An application
// can register multiple URLs to a single request queue using repeated calls to
// HttpAddUrl
// - a typical url prefix is 'http://+:80/vroot/', 'https://+:80/vroot/' or
// 'https://adatum.com:443/secure/database/' - here the '+' is called a
// Strong wildcard, i.e. will match every IP or server name
AddUrl: function(ReqQueueHandle: THandle; UrlPrefix: PWideChar;
Reserved: integer=0): HRESULT; stdcall;
/// Unregisters a specified URL, so that requests for it are no longer
// routed to a specified queue.
RemoveUrl: function(ReqQueueHandle: THandle; UrlPrefix: PWideChar): HRESULT; stdcall;
/// retrieves the next available HTTP request from the specified request queue
ReceiveHttpRequest: function(ReqQueueHandle: THandle; RequestId: HTTP_REQUEST_ID;
Flags: cardinal; var pRequestBuffer: HTTP_REQUEST; RequestBufferLength: ULONG;
var pBytesReceived: ULONG; pOverlapped: pointer=nil): HRESULT; stdcall;
/// sent the response to a specified HTTP request
// - pLogData optional parameter is handled since HTTP API 2.0
SendHttpResponse: function(ReqQueueHandle: THandle; RequestId: HTTP_REQUEST_ID;
Flags: integer; var pHttpResponse: HTTP_RESPONSE; pReserved1: pointer;
var pBytesSent: cardinal; pReserved2: pointer=nil; Reserved3: ULONG=0;
pOverlapped: pointer=nil; pLogData: PHTTP_LOG_DATA=nil): HRESULT; stdcall;
/// receives additional entity body data for a specified HTTP request
ReceiveRequestEntityBody: function(ReqQueueHandle: THandle; RequestId: HTTP_REQUEST_ID;
Flags: ULONG; pBuffer: pointer; BufferLength: cardinal; var pBytesReceived: cardinal;
pOverlapped: pointer=nil): HRESULT; stdcall;
/// sends entity-body data associated with an HTTP response.
SendResponseEntityBody: function(ReqQueueHandle: THandle; RequestId: HTTP_REQUEST_ID;
Flags: integer; EntityChunkCount: word; pEntityChunks: pointer; var pBytesSent: Cardinal;
pReserved1: Pointer=nil; pReserved2: Pointer=nil; pOverlapped: POverlapped=nil;
pLogData: PHTTP_LOG_DATA=nil): HRESULT; stdcall;
/// set specified data, such as IP addresses or SSL Certificates, from the
// HTTP Server API configuration store
SetServiceConfiguration: function(ServiceHandle: THandle;
ConfigId: THttpServiceConfigID; pConfigInformation: pointer;
ConfigInformationLength: ULONG; pOverlapped: pointer=nil): HRESULT; stdcall;
/// deletes specified data, such as IP addresses or SSL Certificates, from the
// HTTP Server API configuration store
DeleteServiceConfiguration: function(ServiceHandle: THandle;
ConfigId: THttpServiceConfigID; pConfigInformation: pointer;
ConfigInformationLength: ULONG; pOverlapped: pointer=nil): HRESULT; stdcall;
/// removes from the HTTP Server API cache associated with a given request
// queue all response fragments that have a name whose site portion matches
// a specified UrlPrefix
FlushResponseCache: function(ReqQueueHandle: THandle; pUrlPrefix: PWideChar; Flags: ULONG;
pOverlapped: POverlapped): ULONG; stdcall;
/// cancels a specified request
// - available only for HTTP API 2.0 (since Windows Vista / Server 2008)
CancelHttpRequest: function(ReqQueueHandle: THandle; RequestId: HTTP_REQUEST_ID;
pOverlapped: pointer = nil): HRESULT; stdcall;
/// creates a server session for the specified HTTP API version
// - available only for HTTP API 2.0 (since Windows Vista / Server 2008)
CreateServerSession: function(Version: HTTP_VERSION;
var ServerSessionId: HTTP_SERVER_SESSION_ID; Reserved: ULONG = 0): HRESULT; stdcall;
/// deletes the server session identified by the server session ID
// - available only for HTTP API 2.0 (since Windows Vista / Server 2008)
CloseServerSession: function(ServerSessionId: HTTP_SERVER_SESSION_ID): HRESULT; stdcall;
/// creates a new request queue or opens an existing request queue
// - available only for HTTP API 2.0 (since Windows Vista / Server 2008)
// - replaces the HTTP version 1.0 CreateHttpHandle() function
CreateRequestQueue: function(Version: HTTP_VERSION;
pName: PWideChar; pSecurityAttributes: Pointer;
Flags: ULONG; var ReqQueueHandle: THandle): HRESULT; stdcall;
/// sets a new server session property or modifies an existing property
// on the specified server session
// - available only for HTTP API 2.0 (since Windows Vista / Server 2008)
SetServerSessionProperty: function(ServerSessionId: HTTP_SERVER_SESSION_ID;
aProperty: HTTP_SERVER_PROPERTY; pPropertyInformation: Pointer;
PropertyInformationLength: ULONG): HRESULT; stdcall;
/// queries a server property on the specified server session
// - available only for HTTP API 2.0 (since Windows Vista / Server 2008)
QueryServerSessionProperty: function(ServerSessionId: HTTP_SERVER_SESSION_ID;
aProperty: HTTP_SERVER_PROPERTY; pPropertyInformation: Pointer;
PropertyInformationLength: ULONG; pReturnLength: PULONG = nil): HRESULT; stdcall;
/// creates a URL Group under the specified server session
// - available only for HTTP API 2.0 (since Windows Vista / Server 2008)
CreateUrlGroup: function(ServerSessionId: HTTP_SERVER_SESSION_ID;
var UrlGroupId: HTTP_URL_GROUP_ID; Reserved: ULONG = 0): HRESULT; stdcall;
/// closes the URL Group identified by the URL Group ID
// - this call also removes all of the URLs that are associated with
// the URL Group
// - available only for HTTP API 2.0 (since Windows Vista / Server 2008)
CloseUrlGroup: function(UrlGroupId: HTTP_URL_GROUP_ID): HRESULT; stdcall;
/// adds the specified URL to the URL Group identified by the URL Group ID
// - available only for HTTP API 2.0 (since Windows Vista / Server 2008)
// - this function replaces the HTTP version 1.0 AddUrl() function
AddUrlToUrlGroup: function(UrlGroupId: HTTP_URL_GROUP_ID;
pFullyQualifiedUrl: PWideChar; UrlContext: HTTP_URL_CONTEXT = 0;
Reserved: ULONG = 0): HRESULT; stdcall;
/// removes the specified URL from the group identified by the URL Group ID
// - this function removes one, or all, of the URLs from the group
// - available only for HTTP API 2.0 (since Windows Vista / Server 2008)
// - it replaces the HTTP version 1.0 RemoveUrl() function
RemoveUrlFromUrlGroup: function(UrlGroupId: HTTP_URL_GROUP_ID;
pFullyQualifiedUrl: PWideChar; Flags: ULONG): HRESULT; stdcall;
/// sets a new property or modifies an existing property on the specified
// URL Group
// - available only for HTTP API 2.0 (since Windows Vista / Server 2008)
SetUrlGroupProperty: function(UrlGroupId: HTTP_URL_GROUP_ID;
aProperty: HTTP_SERVER_PROPERTY; pPropertyInformation: Pointer;
PropertyInformationLength: ULONG): HRESULT; stdcall;
/// queries a property on the specified URL Group
// - available only for HTTP API 2.0 (since Windows Vista / Server 2008)
QueryUrlGroupProperty: function(UrlGroupId: HTTP_URL_GROUP_ID;
aProperty: HTTP_SERVER_PROPERTY; pPropertyInformation: Pointer;
PropertyInformationLength: ULONG; pReturnLength: PULONG = nil): HRESULT; stdcall;
/// sets a new property or modifies an existing property on the request
// queue identified by the specified handle
// - available only for HTTP API 2.0 (since Windows Vista / Server 2008)
SetRequestQueueProperty: function(ReqQueueHandle: THandle;
aProperty: HTTP_SERVER_PROPERTY; pPropertyInformation: Pointer;
PropertyInformationLength: ULONG; Reserved: ULONG; pReserved: Pointer): HRESULT; stdcall;
/// queries a property of the request queue identified by the
// specified handle
// - available only for HTTP API 2.0 (since Windows Vista / Server 2008)
QueryRequestQueueProperty: function(ReqQueueHandle: THandle;
aProperty: HTTP_SERVER_PROPERTY; pPropertyInformation: Pointer;
PropertyInformationLength: ULONG; Reserved: ULONG; pReturnLength: PULONG; pReserved: Pointer): HRESULT; stdcall;
end;
var
Http: THttpAPI;
type
THttpAPIs = (hInitialize,hTerminate,hCreateHttpHandle,
hAddUrl, hRemoveUrl, hReceiveHttpRequest,
hSendHttpResponse, hReceiveRequestEntityBody,
hResponseEntityBody,
hSetServiceConfiguration, hDeleteServiceConfiguration, hFlushResponseCache,
hCancelHttpRequest,
hCreateServerSession, hCloseServerSession,
hCreateRequestQueue,
hSetServerSessionProperty, hQueryServerSessionProperty,
hCreateUrlGroup, hCloseUrlGroup,
hAddUrlToUrlGroup, hRemoveUrlFromUrlGroup,
hSetUrlGroupProperty, hQueryUrlGroupProperty,
hSetRequestQueueProperty, hQueryRequestQueueProperty
);
const
hHttpApi2First = hCancelHttpRequest;
HttpNames: array[THttpAPIs] of PChar = (
'HttpInitialize','HttpTerminate','HttpCreateHttpHandle',
'HttpAddUrl', 'HttpRemoveUrl', 'HttpReceiveHttpRequest',
'HttpSendHttpResponse', 'HttpReceiveRequestEntityBody',
'HttpSendResponseEntityBody',
'HttpSetServiceConfiguration', 'HttpDeleteServiceConfiguration',
'HttpFlushResponseCache',
'HttpCancelHttpRequest',
'HttpCreateServerSession', 'HttpCloseServerSession',
'HttpCreateRequestQueue',
'HttpSetServerSessionProperty', 'HttpQueryServerSessionProperty',
'HttpCreateUrlGroup', 'HttpCloseUrlGroup',
'HttpAddUrlToUrlGroup', 'HttpRemoveUrlFromUrlGroup',
'HttpSetUrlGroupProperty', 'HttpQueryUrlGroupProperty',
'HttpSetRequestQueueProperty', 'HttpQueryRequestQueueProperty'
);
function RegURL(aRoot, aPort: SockString; Https: boolean;
aDomainName: SockString): SockUnicode;
const Prefix: array[boolean] of SockString = ('http://','https://');
begin
if aPort='' then
aPort := DEFAULT_PORT[Https];
aRoot := trim(aRoot);
aDomainName := trim(aDomainName);
if aDomainName='' then begin
result := '';
exit;
end;
if aRoot<>'' then begin
if aRoot[1]<>'/' then
insert('/',aRoot,1);
if aRoot[length(aRoot)]<>'/' then
aRoot := aRoot+'/';
end else
aRoot := '/'; // allow for instance 'http://*:2869/'
aRoot := Prefix[Https]+aDomainName+':'+aPort+aRoot;
result := SockUnicode(aRoot);
end;
const
HTTPAPI_DLL = 'httpapi.dll';
procedure HttpApiInitialize;
var api: THttpAPIs;
P: PPointer;
begin
if Http.Module<>0 then
exit; // already loaded
try
Http.Module := LoadLibrary(HTTPAPI_DLL);
Http.Version.MajorVersion := 2; // API 2.0 if all functions are available
if Http.Module<=255 then
raise ECrtSocket.CreateFmt('Unable to find %s',[HTTPAPI_DLL]);
P := @@Http.Initialize;
for api := low(api) to high(api) do begin
P^ := GetProcAddress(Http.Module,HttpNames[api]);
if P^=nil then
if api<hHttpApi2First then
raise ECrtSocket.CreateFmt('Unable to find %s() in %s',[HttpNames[api],HTTPAPI_DLL]) else
Http.Version.MajorVersion := 1; // e.g. Windows XP or Server 2003
inc(P);
end;
except
on E: Exception do begin
if Http.Module>255 then begin
FreeLibrary(Http.Module);
Http.Module := 0;
end;
raise;
end;
end;
end;
{ EHttpApiServer }
type
EHttpApiServer = class(ECrtSocket)
protected
fLastApi: THttpAPIs;
public
class procedure RaiseOnError(api: THttpAPIs; Error: integer);
constructor Create(api: THttpAPIs; Error: integer); reintroduce;
published
property LastApi: THttpAPIs read fLastApi;
end;
class procedure EHttpApiServer.RaiseOnError(api: THttpAPIs; Error: integer);
begin
if Error<>NO_ERROR then
raise self.Create(api,Error);
end;
constructor EHttpApiServer.Create(api: THttpAPIs; Error: integer);
begin
fLastError := Error;
fLastApi := api;
inherited CreateFmt('%s failed: %s (%d)',
[HttpNames[api],SysErrorMessagePerModule(Error,HTTPAPI_DLL),Error])
end;
{ THttpApiServer }
function THttpApiServer.AddUrl(const aRoot, aPort: SockString; Https: boolean;
const aDomainName: SockString; aRegisterURI: boolean; aContext: Int64): integer;
var uri: SockUnicode;
n: integer;
begin
result := -1;
if (Self=nil) or (fReqQueue=0) or (Http.Module=0) then
exit;
uri := RegURL(aRoot, aPort, Https, aDomainName);
if uri='' then
exit; // invalid parameters
if aRegisterURI then
AddUrlAuthorize(aRoot,aPort,Https,aDomainName);
if Http.Version.MajorVersion>1 then
result := Http.AddUrlToUrlGroup(fUrlGroupID,pointer(uri),aContext) else
result := Http.AddUrl(fReqQueue,pointer(uri));
if result=NO_ERROR then begin
n := length(fRegisteredUnicodeUrl);
SetLength(fRegisteredUnicodeUrl,n+1);
fRegisteredUnicodeUrl[n] := uri;
end;
end;
function THttpApiServer.RemoveUrl(const aRoot, aPort: SockString; Https: boolean;
const aDomainName: SockString): integer;
var uri: SockUnicode;
i,j,n: integer;
begin
result := -1;
if (Self=nil) or (fReqQueue=0) or (Http.Module=0) then
exit;
uri := RegURL(aRoot, aPort, Https, aDomainName);
if uri='' then
exit; // invalid parameters
n := High(fRegisteredUnicodeUrl);
for i := 0 to n do
if fRegisteredUnicodeUrl[i]=uri then begin
if Http.Version.MajorVersion>1 then
result := Http.RemoveUrlFromUrlGroup(fUrlGroupID,pointer(uri),0) else
result := Http.RemoveUrl(fReqQueue,pointer(uri));
if result<>0 then
exit; // shall be handled by caller
for j := i to n-1 do
fRegisteredUnicodeUrl[j] := fRegisteredUnicodeUrl[j+1];
SetLength(fRegisteredUnicodeUrl,n);
exit;
end;
end;
class function THttpApiServer.AddUrlAuthorize(const aRoot, aPort: SockString;
Https: boolean; const aDomainName: SockString; OnlyDelete: boolean): string;
const
/// will allow AddUrl() registration to everyone
// - 'GA' (GENERIC_ALL) to grant all access
// - 'S-1-1-0' defines a group that includes all users
HTTPADDURLSECDESC: PWideChar = 'D:(A;;GA;;;S-1-1-0)';
var prefix: SockUnicode;
Error: HRESULT;
Config: HTTP_SERVICE_CONFIG_URLACL_SET;
begin
try
HttpApiInitialize;
prefix := RegURL(aRoot, aPort, Https, aDomainName);
if prefix='' then
result := 'Invalid parameters' else begin
EHttpApiServer.RaiseOnError(hInitialize,Http.Initialize(
Http.Version,HTTP_INITIALIZE_CONFIG));
try
fillchar(Config,sizeof(Config),0);
Config.KeyDesc.pUrlPrefix := pointer(prefix);
// first delete any existing information
Error := Http.DeleteServiceConfiguration(0,hscUrlAclInfo,@Config,Sizeof(Config));
// then add authorization rule
if not OnlyDelete then begin
Config.KeyDesc.pUrlPrefix := pointer(prefix);
Config.ParamDesc.pStringSecurityDescriptor := HTTPADDURLSECDESC;
Error := Http.SetServiceConfiguration(0,hscUrlAclInfo,@Config,Sizeof(Config));
end;
if (Error<>NO_ERROR) and (Error<>ERROR_ALREADY_EXISTS) then
raise EHttpApiServer.Create(hSetServiceConfiguration,Error);
result := ''; // success
finally
Http.Terminate(HTTP_INITIALIZE_CONFIG);
end;
end;
except
on E: Exception do
result := E.Message;
end;
end;
type
THttpApiServerClass = class of THttpApiServer;
procedure THttpApiServer.Clone(ChildThreadCount: integer);
var i: integer;
begin
if (fReqQueue=0) or not Assigned(OnRequest) or (ChildThreadCount<=0) or (fClones<>nil) then
exit; // nothing to clone (need a queue and a process event)
if ChildThreadCount>256 then
ChildThreadCount := 256; // not worth adding
SetLength(fClones,ChildThreadCount);
for i := 0 to ChildThreadCount-1 do
fClones[i] := THttpApiServerClass(Self.ClassType).CreateClone(self);
end;
function THttpApiServer.GetAPIVersion: string;
begin
result := Format('HTTP API %d.%d',[Http.Version.MajorVersion,Http.Version.MinorVersion]);
end;
constructor THttpApiServer.Create(CreateSuspended: boolean; QueueName: SockUnicode;
OnStart,OnStop: TNotifyThreadEvent; const ProcessName: SockString);
var bindInfo: HTTP_BINDING_INFO;
begin
SetLength(fLogDataStorage,sizeof(HTTP_LOG_FIELDS_DATA)); // should be done 1st
inherited Create({suspended=}true,OnStart,OnStop,ProcessName);
HttpApiInitialize; // will raise an exception in case of failure
EHttpApiServer.RaiseOnError(hInitialize,
Http.Initialize(Http.Version,HTTP_INITIALIZE_SERVER));
if Http.Version.MajorVersion>1 then begin
EHttpApiServer.RaiseOnError(hCreateServerSession,Http.CreateServerSession(
Http.Version,fServerSessionID));
EHttpApiServer.RaiseOnError(hCreateUrlGroup,Http.CreateUrlGroup(
fServerSessionID,fUrlGroupID));
if QueueName='' then
BinToHexDisplayW(@fServerSessionID,SizeOf(fServerSessionID),QueueName);
EHttpApiServer.RaiseOnError(hCreateRequestQueue,Http.CreateRequestQueue(
Http.Version,pointer(QueueName),nil,0,fReqQueue));
bindInfo.Flags := 1;
bindInfo.RequestQueueHandle := FReqQueue;
EHttpApiServer.RaiseOnError(hSetUrlGroupProperty,Http.SetUrlGroupProperty(
fUrlGroupID,HttpServerBindingProperty,@bindInfo,SizeOf(bindInfo)));
end else
EHttpApiServer.RaiseOnError(hCreateHttpHandle,Http.CreateHttpHandle(fReqQueue));
fReceiveBufferSize := 1048576; // i.e. 1 MB
if not CreateSuspended then
Suspended := False;
end;
constructor THttpApiServer.CreateClone(From: THttpApiServer);
begin
SetLength(fLogDataStorage,sizeof(HTTP_LOG_FIELDS_DATA));
fOwner := From;
fReqQueue := From.fReqQueue;
fOnRequest := From.fOnRequest;
fOnBeforeBody := From.fOnBeforeBody;
fOnBeforeRequest := From.fOnBeforeRequest;
fOnAfterRequest := From.fOnAfterRequest;
fCanNotifyCallback := From.fCanNotifyCallback;
fCompress := From.fCompress;
fCompressAcceptEncoding := From.fCompressAcceptEncoding;
fReceiveBufferSize := From.fReceiveBufferSize;
if From.fLogData<>nil then
fLogData := pointer(fLogDataStorage);
SetServerName(From.fServerName);
SetRemoteIPHeader(From.RemoteIPHeader);
SetRemoteConnIDHeader(From.RemoteConnIDHeader);
fLoggingServiceName := From.fLoggingServiceName;
inherited Create(false,From.fOnHttpThreadStart,From.fOnThreadTerminate,From.ProcessName);
end;
procedure THttpApiServer.DestroyMainThread;
var i: PtrInt;
begin
if fReqQueue<>0 then begin
for i := 0 to length(fClones)-1 do
fClones[i].Terminate; // for CloseHandle() below to finish Execute
if Http.Version.MajorVersion>1 then begin
if fUrlGroupID<>0 then begin
Http.RemoveUrlFromUrlGroup(fUrlGroupID,nil,HTTP_URL_FLAG_REMOVE_ALL);
Http.CloseUrlGroup(fUrlGroupID);
fUrlGroupID := 0;
end;
CloseHandle(fReqQueue);
if fServerSessionID<>0 then begin
Http.CloseServerSession(fServerSessionID);
fServerSessionID := 0;
end;
end else begin
for i := 0 to high(fRegisteredUnicodeUrl) do
Http.RemoveUrl(fReqQueue,pointer(fRegisteredUnicodeUrl[i]));
CloseHandle(fReqQueue); // will break all THttpApiServer.Execute
end;
fReqQueue := 0;
{$ifdef FPC}
for i := 0 to length(fClones)-1 do
WaitForSingleObject(fClones[i].Handle,30000); // sometimes needed on FPC
{$endif FPC}
for i := 0 to length(fClones)-1 do
fClones[i].Free;
fClones := nil;
Http.Terminate(HTTP_INITIALIZE_SERVER);
end;
end;
destructor THttpApiServer.Destroy;
begin
Terminate; // for Execute to be notified about end of process
try
if (fOwner=nil) and (Http.Module<>0) then // fOwner<>nil for cloned threads
DestroyMainThread;
{$ifdef FPC}
WaitForSingleObject(Handle,30000); // wait the main Execute method on FPC
{$endif FPC}
finally
inherited Destroy;
end;
end;
procedure GetDomainUserNameFromToken(UserToken: THandle; var result: SockString);
var Buffer: array[0..511] of byte;
BufferSize, UserSize, DomainSize: DWORD;
UserInfo: PSIDAndAttributes;
NameUse: {$ifdef FPC}SID_NAME_USE{$else}Cardinal{$endif};
tmp: SockUnicode;
P: PWideChar;
begin
if not GetTokenInformation(UserToken,TokenUser,@Buffer,SizeOf(Buffer),BufferSize) then
exit;
UserInfo := @Buffer;
UserSize := 0;
DomainSize := 0;
LookupAccountSidW(nil,UserInfo^.Sid,nil,UserSize,nil,DomainSize,NameUse);
if (UserSize=0) or (DomainSize=0) then
exit;
SetLength(tmp,UserSize+DomainSize-1);
P := pointer(tmp);
if not LookupAccountSidW(nil,UserInfo^.Sid,P+DomainSize,UserSize,P,DomainSize,NameUse) then
exit;
P[DomainSize] := '\';
result := {$ifdef UNICODE}UTF8String{$else}UTF8Encode{$endif}(tmp);
end;
function THttpApiServer.GetSendResponseFlags(Ctxt: THttpServerRequest): Integer;
begin
result := 0;
end;
procedure THttpApiServer.Execute;
type
TVerbText = array[hvOPTIONS..pred(hvMaximum)] of SockString;
const
VERB_TEXT: TVerbText = (
'OPTIONS','GET','HEAD','POST','PUT','DELETE','TRACE','CONNECT','TRACK',
'MOVE','COPY','PROPFIND','PROPPATCH','MKCOL','LOCK','UNLOCK','SEARCH');
var Req: PHTTP_REQUEST;
ReqID: HTTP_REQUEST_ID;
ReqBuf, RespBuf, RemoteIP, RemoteConn: SockString;
ContentRange: shortstring;
i, L: integer;
P: PHTTP_UNKNOWN_HEADER;
flags, bytesRead, bytesSent: cardinal;
Err: HRESULT;
InCompressAccept: THttpSocketCompressSet;
InContentLength, InContentLengthChunk, InContentLengthRead: cardinal;
InContentEncoding, InAcceptEncoding, Range: SockString;
OutContentEncoding, OutStatus: SockString;
OutStatusCode, AfterStatusCode: Cardinal;
RespSent: boolean;
Context: THttpServerRequest;
FileHandle: THandle;
Resp: PHTTP_RESPONSE;
BufRead, R: PAnsiChar;
Heads: HTTP_UNKNOWN_HEADERs;
RangeStart, RangeLength: ULONGLONG;
OutContentLength: ULARGE_INTEGER;
DataChunkInMemory: HTTP_DATA_CHUNK_INMEMORY;
DataChunkFile: HTTP_DATA_CHUNK_FILEHANDLE;
CurrentLog: PHTTP_LOG_FIELDS_DATA;
Verbs: TVerbText; // to avoid memory allocation
procedure SendError(StatusCode: cardinal; const ErrorMsg: string; E: Exception=nil);
var Msg: string;
begin
try
Resp^.SetStatus(StatusCode,OutStatus);
CurrentLog^.ProtocolStatus := StatusCode;
Msg := format(
'<html><body style="font-family:verdana;"><h1>Server Error %d: %s</h1><p>',
[StatusCode,OutStatus]);
if E<>nil then
Msg := Msg+string(E.ClassName)+' Exception raised:<br>';
Resp^.SetContent(DataChunkInMemory,UTF8String(Msg)+HtmlEncode(
{$ifdef UNICODE}UTF8String{$else}UTF8Encode{$endif}(ErrorMsg))
{$ifndef NOXPOWEREDNAME}+'</p><p><small>'+XPOWEREDVALUE{$endif},
'text/html; charset=utf-8');
Http.SendHttpResponse(fReqQueue,
Req^.RequestId,0,Resp^,nil,bytesSent,nil,0,nil,fLogData);
except
on Exception do
; // ignore any HttpApi level errors here (client may crashed)
end;
end;
function SendResponse: boolean;
begin
result := not Terminated; // true=success
if not result then
exit;
RespSent := true;
Resp^.SetStatus(OutStatusCode,OutStatus);
if Terminated then
exit;
// update log information
if Http.Version.MajorVersion>=2 then
with Req^,CurrentLog^ do begin
MethodNum := Verb;
UriStemLength := CookedUrl.AbsPathLength;
UriStem := CookedUrl.pAbsPath;
with Headers.KnownHeaders[reqUserAgent] do begin
UserAgentLength := RawValueLength;
UserAgent := pRawValue;
end;
with Headers.KnownHeaders[reqHost] do begin
HostLength := RawValueLength;
Host := pRawValue;
end;
with Headers.KnownHeaders[reqReferrer] do begin
ReferrerLength := RawValueLength;
Referrer := pRawValue;
end;
ProtocolStatus := Resp^.StatusCode;
ClientIp := pointer(RemoteIP);
ClientIpLength := length(RemoteIP);
Method := pointer(Context.fMethod);
MethodLength := length(Context.fMethod);
UserName := pointer(Context.fAuthenticatedUser);
UserNameLength := Length(Context.fAuthenticatedUser);
end;
// send response
Resp^.Version := Req^.Version;
Resp^.SetHeaders(pointer(Context.OutCustomHeaders),Heads);
if fCompressAcceptEncoding<>'' then
Resp^.AddCustomHeader(pointer(fCompressAcceptEncoding),Heads,false);
with Resp^.Headers.KnownHeaders[respServer] do begin
pRawValue := pointer(fServerName);
RawValueLength := length(fServerName);
end;
if Context.OutContentType=HTTP_RESP_STATICFILE then begin
// response is file -> OutContent is UTF-8 file name to be served
FileHandle := FileOpen(
{$ifdef UNICODE}UTF8ToUnicodeString{$else}Utf8ToAnsi{$endif}(Context.OutContent),
fmOpenRead or fmShareDenyNone);
if PtrInt(FileHandle)<0 then begin
SendError(STATUS_NOTFOUND,SysErrorMessage(GetLastError));
result := false; // notify fatal error
end;
try // http.sys will serve then close the file from kernel
DataChunkFile.DataChunkType := hctFromFileHandle;
DataChunkFile.FileHandle := FileHandle;
flags := 0;
DataChunkFile.ByteRange.StartingOffset.QuadPart := 0;
Int64(DataChunkFile.ByteRange.Length.QuadPart) := -1; // to eof
with Req^.Headers.KnownHeaders[reqRange] do begin
if (RawValueLength>6) and IdemPChar(pRawValue,'BYTES=') and
(pRawValue[6] in ['0'..'9']) then begin
SetString(Range,pRawValue+6,RawValueLength-6); // need #0 end
R := pointer(Range);
RangeStart := GetNextItemUInt64(R);
if R^='-' then begin
OutContentLength.LowPart := GetFileSize(FileHandle,@OutContentLength.HighPart);
DataChunkFile.ByteRange.Length.QuadPart := OutContentLength.QuadPart-RangeStart;
inc(R);
flags := HTTP_SEND_RESPONSE_FLAG_PROCESS_RANGES;
DataChunkFile.ByteRange.StartingOffset.QuadPart := RangeStart;
if R^ in ['0'..'9'] then begin
RangeLength := GetNextItemUInt64(R)-RangeStart+1;
if RangeLength<DataChunkFile.ByteRange.Length.QuadPart then
// "bytes=0-499" -> start=0, len=500
DataChunkFile.ByteRange.Length.QuadPart := RangeLength;
end; // "bytes=1000-" -> start=1000, to eof)
ContentRange := 'Content-Range: bytes ';
AppendI64(RangeStart,ContentRange);
AppendChar('-',ContentRange);
AppendI64(RangeStart+DataChunkFile.ByteRange.Length.QuadPart-1,ContentRange);
AppendChar('/',ContentRange);
AppendI64(OutContentLength.QuadPart,ContentRange);
AppendChar(#0,ContentRange);
Resp^.AddCustomHeader(@ContentRange[1],Heads,false);
Resp^.SetStatus(STATUS_PARTIALCONTENT,OutStatus);
end;
end;
with Resp^.Headers.KnownHeaders[respAcceptRanges] do begin
pRawValue := 'bytes';
RawValueLength := 5;
end;
end;
Resp^.EntityChunkCount := 1;
Resp^.pEntityChunks := @DataChunkFile;
Http.SendHttpResponse(fReqQueue,
Req^.RequestId,flags,Resp^,nil,bytesSent,nil,0,nil,fLogData);
finally
FileClose(FileHandle);
end;
end else begin
// response is in OutContent -> send it from memory
if Context.OutContentType=HTTP_RESP_NORESPONSE then
Context.OutContentType := ''; // true HTTP always expects a response
if fCompress<>nil then begin
with Resp^.Headers.KnownHeaders[reqContentEncoding] do
if RawValueLength=0 then begin
// no previous encoding -> try if any compression
OutContentEncoding := CompressDataAndGetHeaders(InCompressAccept,
fCompress,Context.OutContentType,Context.fOutContent);
pRawValue := pointer(OutContentEncoding);
RawValueLength := length(OutContentEncoding);
end;
end;
Resp^.SetContent(DataChunkInMemory,Context.OutContent,Context.OutContentType);
EHttpApiServer.RaiseOnError(hSendHttpResponse,Http.SendHttpResponse(
fReqQueue,Req^.RequestId,getSendResponseFlags(Context),
Resp^,nil,bytesSent,nil,0,nil,fLogData));
end;
end;
begin
if Terminated then
exit;
Context := nil;
try
// THttpServerGeneric thread preparation: launch any OnHttpThreadStart event
NotifyThreadStart(self);
// reserve working buffers
SetLength(Heads,64);
SetLength(RespBuf,sizeof(Resp^));
Resp := pointer(RespBuf);
SetLength(ReqBuf,16384+sizeof(HTTP_REQUEST)); // space for Req^ + 16 KB of headers
Req := pointer(ReqBuf);
CurrentLog := pointer(fLogDataStorage);
Verbs := VERB_TEXT;
Context := THttpServerRequest.Create(self,0,self);
// main loop reusing a single Context instance for this thread
ReqID := 0;
Context.fServer := self;
repeat
Context.fInContent := ''; // release input/output body buffers ASAP
Context.fOutContent := '';
// Reset AuthenticationStatus & user between requests
Context.fAuthenticationStatus := hraNone;
Context.fAuthenticatedUser := '';
// retrieve next pending request, and read its headers
fillchar(Req^,sizeof(HTTP_REQUEST),0);
Err := Http.ReceiveHttpRequest(fReqQueue,ReqID,0,Req^,length(ReqBuf),bytesRead);
if Terminated then
break;
case Err of
NO_ERROR:
try
// parse method and headers
Context.fConnectionID := Req^.ConnectionId;
Context.fHttpApiRequest := Req;
SetString(Context.fFullURL,Req^.CookedUrl.pFullUrl,Req^.CookedUrl.FullUrlLength);
SetString(Context.fURL,Req^.pRawUrl,Req^.RawUrlLength);
if Req^.Verb in [low(Verbs)..high(Verbs)] then
Context.fMethod := Verbs[Req^.Verb] else
SetString(Context.fMethod,Req^.pUnknownVerb,Req^.UnknownVerbLength);
with Req^.Headers.KnownHeaders[reqContentType] do
SetString(Context.fInContentType,pRawValue,RawValueLength);
with Req^.Headers.KnownHeaders[reqAcceptEncoding] do
SetString(InAcceptEncoding,pRawValue,RawValueLength);
InCompressAccept := ComputeContentEncoding(fCompress,pointer(InAcceptEncoding));
Context.fUseSSL := Req^.pSslInfo<>nil;
Context.fInHeaders := RetrieveHeaders(Req^,fRemoteIPHeaderUpper,RemoteIP);
// compute remote connection ID
L := length(fRemoteConnIDHeaderUpper);
if L<>0 then begin
P := Req^.Headers.pUnknownHeaders;
if P<>nil then
for i := 1 to Req^.Headers.UnknownHeaderCount do
if (P^.NameLength=L) and
IdemPChar(P^.pName,Pointer(fRemoteConnIDHeaderUpper)) then begin
SetString(RemoteConn,p^.pRawValue,p^.RawValueLength); // need #0 end
R := pointer(RemoteConn);
Context.fConnectionID := GetNextItemUInt64(R);
break;
end else
inc(P);
end;
// retrieve any SetAuthenticationSchemes() information
if byte(fAuthenticationSchemes)<>0 then // set only with HTTP API 2.0
for i := 0 to Req^.RequestInfoCount-1 do
if Req^.pRequestInfo^[i].InfoType=HttpRequestInfoTypeAuth then
with PHTTP_REQUEST_AUTH_INFO(Req^.pRequestInfo^[i].pInfo)^ do
case AuthStatus of
HttpAuthStatusSuccess:
if AuthType>HttpRequestAuthTypeNone then begin
byte(Context.fAuthenticationStatus) := ord(AuthType)+1;
if AccessToken<>0 then begin
GetDomainUserNameFromToken(AccessToken,Context.fAuthenticatedUser);
// Per spec https://docs.microsoft.com/en-us/windows/win32/http/authentication-in-http-version-2-0
// AccessToken lifecycle is application responsability and should be closed after use
CloseHandle(AccessToken);
end;
end;
HttpAuthStatusFailure:
Context.fAuthenticationStatus := hraFailed;
end;
with Req^.Headers.KnownHeaders[reqContentLength] do
InContentLength := GetCardinal(pRawValue,pRawValue+RawValueLength);
if (InContentLength>0) and (MaximumAllowedContentLength>0) and
(InContentLength>MaximumAllowedContentLength) then begin
SendError(STATUS_PAYLOADTOOLARGE,'Rejected');
continue;
end;
if Assigned(OnBeforeBody) then begin
Err := OnBeforeBody(Context.URL,Context.Method,Context.InHeaders,
Context.InContentType,RemoteIP,InContentLength,Context.fUseSSL);
if Err<>STATUS_SUCCESS then begin
SendError(Err,'Rejected');
continue;
end;
end;
// retrieve body
if HTTP_REQUEST_FLAG_MORE_ENTITY_BODY_EXISTS and Req^.Flags<>0 then begin
with Req^.Headers.KnownHeaders[reqContentEncoding] do
SetString(InContentEncoding,pRawValue,RawValueLength);
if InContentLength<>0 then begin
SetLength(Context.fInContent,InContentLength);
BufRead := pointer(Context.InContent);
InContentLengthRead := 0;
repeat
BytesRead := 0;
if Http.Version.MajorVersion>1 then // speed optimization for Vista+
flags := HTTP_RECEIVE_REQUEST_ENTITY_BODY_FLAG_FILL_BUFFER else
flags := 0;
InContentLengthChunk := InContentLength-InContentLengthRead;
if (fReceiveBufferSize>=1024) and (InContentLengthChunk>fReceiveBufferSize) then
InContentLengthChunk := fReceiveBufferSize;
Err := Http.ReceiveRequestEntityBody(fReqQueue,Req^.RequestId,flags,
BufRead,InContentLengthChunk,BytesRead);
if Terminated then
exit;
inc(InContentLengthRead,BytesRead);
if Err=ERROR_HANDLE_EOF then begin
if InContentLengthRead<InContentLength then
SetLength(Context.fInContent,InContentLengthRead);
Err := NO_ERROR;
break; // should loop until returns ERROR_HANDLE_EOF
end;
if Err<>NO_ERROR then
break;
inc(BufRead,BytesRead);
until InContentLengthRead=InContentLength;
if Err<>NO_ERROR then begin
SendError(STATUS_NOTACCEPTABLE,SysErrorMessagePerModule(Err,HTTPAPI_DLL));
continue;
end;
if InContentEncoding<>'' then
for i := 0 to high(fCompress) do
if fCompress[i].Name=InContentEncoding then begin
fCompress[i].Func(Context.fInContent,false); // uncompress
break;
end;
end;
end;
try
// compute response
Context.OutContent := '';
Context.OutContentType := '';
Context.OutCustomHeaders := '';
fillchar(Resp^,sizeof(Resp^),0);
RespSent := false;
OutStatusCode := DoBeforeRequest(Context);
if OutStatusCode>0 then
if not SendResponse or (OutStatusCode<>STATUS_ACCEPTED) then
continue;
OutStatusCode := Request(Context);
AfterStatusCode := DoAfterRequest(Context);
if AfterStatusCode>0 then
OutStatusCode := AfterStatusCode;
// send response
if not RespSent then
if not SendResponse then
continue;
DoAfterResponse(Context, OutStatusCode);
except
on E: Exception do
// handle any exception raised during process: show must go on!
if not RespSent then
if not E.InheritsFrom(EHttpApiServer) or // ensure still connected
(EHttpApiServer(E).LastError<>HTTPAPI_ERROR_NONEXISTENTCONNECTION) then
SendError(STATUS_SERVERERROR,E.Message,E);
end;
finally
ReqId := 0; // reset Request ID to handle the next pending request
end;
ERROR_MORE_DATA: begin
// input buffer was too small to hold the request headers
// -> increase buffer size and call the API again
ReqID := Req^.RequestId;
SetLength(ReqBuf,bytesRead);
Req := pointer(ReqBuf);
end;
ERROR_CONNECTION_INVALID:
if ReqID=0 then
break else
// TCP connection was corrupted by the peer -> ignore + next request
ReqID := 0;
else break; // unhandled Err value
end;
until Terminated;
finally
Context.Free;
end;
end;
function THttpApiServer.GetHTTPQueueLength: Cardinal;
var returnLength: ULONG;
begin
if (Http.Version.MajorVersion<2) or (self=nil) then
result := 0 else begin
if fOwner<>nil then
self := fOwner;
if fReqQueue=0 then
result := 0 else
EHttpApiServer.RaiseOnError(hQueryRequestQueueProperty,
Http.QueryRequestQueueProperty(fReqQueue,HttpServerQueueLengthProperty,
@Result, sizeof(Result), 0, @returnLength, nil));
end;
end;
procedure THttpApiServer.SetHTTPQueueLength(aValue: Cardinal);
begin
if Http.Version.MajorVersion<2 then
raise EHttpApiServer.Create(hSetRequestQueueProperty, ERROR_OLD_WIN_VERSION);
if (self<>nil) and (fReqQueue<>0) then
EHttpApiServer.RaiseOnError(hSetRequestQueueProperty,
Http.SetRequestQueueProperty(fReqQueue,HttpServerQueueLengthProperty,
@aValue, sizeof(aValue), 0, nil));
end;
function THttpApiServer.GetRegisteredUrl: SockUnicode;
var i: integer;
begin
if fRegisteredUnicodeUrl=nil then
result := '' else
result := fRegisteredUnicodeUrl[0];
for i := 1 to high(fRegisteredUnicodeUrl) do
result := result+','+fRegisteredUnicodeUrl[i];
end;
function THttpApiServer.GetCloned: boolean;
begin
result := (fOwner<>nil);
end;
procedure THttpApiServer.SetMaxBandwidth(aValue: Cardinal);
var qosInfo: HTTP_QOS_SETTING_INFO;
limitInfo: HTTP_BANDWIDTH_LIMIT_INFO;
begin
if Http.Version.MajorVersion<2 then
raise EHttpApiServer.Create(hSetUrlGroupProperty,ERROR_OLD_WIN_VERSION);
if (self<>nil) and (fUrlGroupID<>0) then begin
if AValue=0 then
limitInfo.MaxBandwidth := HTTP_LIMIT_INFINITE else
if AValue<HTTP_MIN_ALLOWED_BANDWIDTH_THROTTLING_RATE then
limitInfo.MaxBandwidth := HTTP_MIN_ALLOWED_BANDWIDTH_THROTTLING_RATE else
limitInfo.MaxBandwidth := aValue;
limitInfo.Flags := 1;
qosInfo.QosType := HttpQosSettingTypeBandwidth;
qosInfo.QosSetting := @limitInfo;
EHttpApiServer.RaiseOnError(hSetServerSessionProperty,
Http.SetServerSessionProperty(fServerSessionID, HttpServerQosProperty,
@qosInfo, SizeOf(qosInfo)));
EHttpApiServer.RaiseOnError(hSetUrlGroupProperty,
Http.SetUrlGroupProperty(fUrlGroupID, HttpServerQosProperty,
@qosInfo, SizeOf(qosInfo)));
end;
end;
function THttpApiServer.GetMaxBandwidth: Cardinal;
var qosInfoGet: record
qosInfo: HTTP_QOS_SETTING_INFO;
limitInfo: HTTP_BANDWIDTH_LIMIT_INFO;
end;
begin
if (Http.Version.MajorVersion<2) or (self=nil) then begin
result := 0;
exit;
end;
if fOwner<>nil then
self := fOwner;
if fUrlGroupID=0 then begin
result := 0;
exit;
end;
qosInfoGet.qosInfo.QosType := HttpQosSettingTypeBandwidth;
qosInfoGet.qosInfo.QosSetting := @qosInfoGet.limitInfo;
EHttpApiServer.RaiseOnError(hQueryUrlGroupProperty,
Http.QueryUrlGroupProperty(fUrlGroupID, HttpServerQosProperty,
@qosInfoGet, SizeOf(qosInfoGet)));
Result := qosInfoGet.limitInfo.MaxBandwidth;
end;
function THttpApiServer.GetMaxConnections: Cardinal;
var qosInfoGet: record
qosInfo: HTTP_QOS_SETTING_INFO;
limitInfo: HTTP_CONNECTION_LIMIT_INFO;
end;
returnLength: ULONG;
begin
if (Http.Version.MajorVersion<2) or (self=nil) then begin
result := 0;
exit;
end;
if fOwner<>nil then
self := fOwner;
if fUrlGroupID=0 then begin
result := 0;
exit;
end;
qosInfoGet.qosInfo.QosType := HttpQosSettingTypeConnectionLimit;
qosInfoGet.qosInfo.QosSetting := @qosInfoGet.limitInfo;
EHttpApiServer.RaiseOnError(hQueryUrlGroupProperty,
Http.QueryUrlGroupProperty(fUrlGroupID, HttpServerQosProperty,
@qosInfoGet, SizeOf(qosInfoGet), @returnLength));
Result := qosInfoGet.limitInfo.MaxConnections;
end;
procedure THttpApiServer.SetMaxConnections(aValue: Cardinal);
var qosInfo: HTTP_QOS_SETTING_INFO;
limitInfo: HTTP_CONNECTION_LIMIT_INFO;
begin
if Http.Version.MajorVersion<2 then
raise EHttpApiServer.Create(hSetUrlGroupProperty, ERROR_OLD_WIN_VERSION);
if (self<>nil) and (fUrlGroupID<>0) then begin
if AValue = 0 then
limitInfo.MaxConnections := HTTP_LIMIT_INFINITE else
limitInfo.MaxConnections := aValue;
limitInfo.Flags := 1;
qosInfo.QosType := HttpQosSettingTypeConnectionLimit;
qosInfo.QosSetting := @limitInfo;
EHttpApiServer.RaiseOnError(hSetUrlGroupProperty,
Http.SetUrlGroupProperty(fUrlGroupID, HttpServerQosProperty,
@qosInfo, SizeOf(qosInfo)));
end;
end;
function THttpApiServer.HasAPI2: boolean;
begin
result := Http.Version.MajorVersion>=2;
end;
function THttpApiServer.GetLogging: boolean;
begin
result := (fLogData<>nil);
end;
procedure THttpApiServer.LogStart(const aLogFolder: TFileName;
aType: THttpApiLoggingType; const aSoftwareName: TFileName;
aRolloverType: THttpApiLoggingRollOver; aRolloverSize: cardinal;
aLogFields: THttpApiLogFields; aFlags: THttpApiLoggingFlags);
var logInfo : HTTP_LOGGING_INFO;
folder,software: SockUnicode;
begin
if (self=nil) or (fOwner<>nil) then
exit;
if Http.Version.MajorVersion<2 then
raise EHttpApiServer.Create(hSetUrlGroupProperty,ERROR_OLD_WIN_VERSION);
fLogData := nil; // disable any previous logging
fillchar(logInfo,SizeOf(logInfo),0);
logInfo.Flags := 1;
logInfo.LoggingFlags := byte(aFlags);
if aLogFolder='' then
raise EHttpApiServer.CreateFmt('LogStart(aLogFolder="")',[]);
if length(aLogFolder)>212 then
// http://msdn.microsoft.com/en-us/library/windows/desktop/aa364532
raise EHttpApiServer.CreateFmt('aLogFolder is too long for LogStart(%s)',[aLogFolder]);
folder := SockUnicode(aLogFolder);
software := SockUnicode(aSoftwareName);
logInfo.SoftwareNameLength := length(software)*2;
logInfo.SoftwareName := pointer(software);
logInfo.DirectoryNameLength := length(folder)*2;
logInfo.DirectoryName := pointer(folder);
logInfo.Format := HTTP_LOGGING_TYPE(aType);
if aType=hltNCSA then
aLogFields := [hlfDate..hlfSubStatus];
logInfo.Fields := integer(aLogFields);
logInfo.RolloverType := HTTP_LOGGING_ROLLOVER_TYPE(aRolloverType);
if aRolloverType=hlrSize then
logInfo.RolloverSize := aRolloverSize;
EHttpApiServer.RaiseOnError(hSetUrlGroupProperty,
Http.SetUrlGroupProperty(fUrlGroupID, HttpServerLoggingProperty,
@logInfo, SizeOf(logInfo)));
// on success, update the actual log memory structure
fLogData := pointer(fLogDataStorage);
end;
procedure THttpApiServer.RegisterCompress(aFunction: THttpSocketCompress;
aCompressMinSize: integer=1024);
var i: integer;
begin
inherited;
for i := 0 to length(fClones)-1 do
fClones[i].RegisterCompress(aFunction,aCompressMinSize);
end;
procedure THttpApiServer.SetOnTerminate(const Event: TNotifyThreadEvent);
var i: integer;
begin
inherited SetOnTerminate(Event);
if fOwner=nil then
for i := 0 to length(fClones)-1 do
fClones[i].OnHttpThreadTerminate := Event;
end;
procedure THttpApiServer.LogStop;
var i: integer;
begin
if (self=nil) or (fClones=nil) or (fLogData=nil) then
exit;
fLogData := nil;
for i := 0 to length(fClones)-1 do
fClones[i].fLogData := nil;
end;
procedure THttpApiServer.SetReceiveBufferSize(Value: cardinal);
var i: integer;
begin
fReceiveBufferSize := Value;
for i := 0 to length(fClones)-1 do
fClones[i].fReceiveBufferSize := Value;
end;
procedure THttpApiServer.SetServerName(const aName: SockString);
var i: integer;
begin
inherited SetServerName(aName);
with PHTTP_LOG_FIELDS_DATA(fLogDataStorage)^ do begin
ServerName := pointer(aName);
ServerNameLength := Length(aName);
end;
for i := 0 to length(fClones)-1 do
fClones[i].SetServerName(aName);
end;
procedure THttpApiServer.SetOnRequest(const aRequest: TOnHttpServerRequest);
var i: integer;
begin
inherited SetOnRequest(aRequest);
for i := 0 to length(fClones)-1 do
fClones[i].SetOnRequest(aRequest);
end;
procedure THttpApiServer.SetOnBeforeBody(const aEvent: TOnHttpServerBeforeBody);
var i: integer;
begin
inherited SetOnBeforeBody(aEvent);
for i := 0 to length(fClones)-1 do
fClones[i].SetOnBeforeBody(aEvent);
end;
procedure THttpApiServer.SetOnBeforeRequest(const aEvent: TOnHttpServerRequest);
var i: integer;
begin
inherited SetOnBeforeRequest(aEvent);
for i := 0 to length(fClones)-1 do
fClones[i].SetOnBeforeRequest(aEvent);
end;
procedure THttpApiServer.SetOnAfterRequest(const aEvent: TOnHttpServerRequest);
var i: integer;
begin
inherited SetOnAfterRequest(aEvent);
for i := 0 to length(fClones)-1 do
fClones[i].SetOnAfterRequest(aEvent);
end;
procedure THttpApiServer.SetOnAfterResponse(const aEvent: TOnHttpServerAfterResponse);
var i: integer;
begin
inherited SetOnAfterResponse(aEvent);
for i := 0 to length(fClones)-1 do
fClones[i].SetOnAfterResponse(aEvent);
end;
procedure THttpApiServer.SetMaximumAllowedContentLength(aMax: cardinal);
var i: integer;
begin
inherited SetMaximumAllowedContentLength(aMax);
for i := 0 to length(fClones)-1 do
fClones[i].SetMaximumAllowedContentLength(aMax);
end;
procedure THttpApiServer.SetRemoteIPHeader(const aHeader: SockString);
var i: integer;
begin
inherited SetRemoteIPHeader(aHeader);
for i := 0 to length(fClones)-1 do
fClones[i].SetRemoteIPHeader(aHeader);
end;
procedure THttpApiServer.SetRemoteConnIDHeader(const aHeader: SockString);
var i: integer;
begin
inherited SetRemoteConnIDHeader(aHeader);
for i := 0 to length(fClones)-1 do
fClones[i].SetRemoteConnIDHeader(aHeader);
end;
procedure THttpApiServer.SetLoggingServiceName(const aName: SockString);
begin
if self=nil then
exit;
fLoggingServiceName := aName;
PHTTP_LOG_FIELDS_DATA(fLogDataStorage)^.ServiceNameLength := Length(fLoggingServiceName);
PHTTP_LOG_FIELDS_DATA(fLogDataStorage)^.ServiceName := pointer(fLoggingServiceName);
end;
procedure THttpApiServer.SetAuthenticationSchemes(schemes: THttpApiRequestAuthentications;
const DomainName, Realm: SockUnicode);
var authInfo: HTTP_SERVER_AUTHENTICATION_INFO;
begin
if (self=nil) or (fOwner<>nil) then
exit;
if Http.Version.MajorVersion<2 then
raise EHttpApiServer.Create(hSetUrlGroupProperty,ERROR_OLD_WIN_VERSION);
fAuthenticationSchemes := schemes;
FillChar(authInfo,SizeOf(authInfo),0);
authInfo.Flags := 1;
authInfo.AuthSchemes := byte(schemes);
authInfo.ReceiveMutualAuth := true;
if haBasic in schemes then
with authInfo.BasicParams do begin
RealmLength := Length(Realm);
Realm := pointer(Realm);
end;
if haDigest in schemes then
with authInfo.DigestParams do begin
DomainNameLength := Length(DomainName);
DomainName := pointer(DomainName);
RealmLength := Length(Realm);
Realm := pointer(Realm);
end;
EHttpApiServer.RaiseOnError(hSetUrlGroupProperty,
Http.SetUrlGroupProperty(fUrlGroupID, HttpServerAuthenticationProperty,
@authInfo, SizeOf(authInfo)));
end;
procedure THttpApiServer.SetTimeOutLimits(aEntityBody, aDrainEntityBody,
aRequestQueue, aIdleConnection, aHeaderWait, aMinSendRate: cardinal);
var timeoutInfo: HTTP_TIMEOUT_LIMIT_INFO;
begin
if (self=nil) or (fOwner<>nil) then
exit;
if Http.Version.MajorVersion<2 then
raise EHttpApiServer.Create(hSetUrlGroupProperty,ERROR_OLD_WIN_VERSION);
FillChar(timeOutInfo,SizeOf(timeOutInfo),0);
timeoutInfo.Flags := 1;
timeoutInfo.EntityBody := aEntityBody;
timeoutInfo.DrainEntityBody := aDrainEntityBody;
timeoutInfo.RequestQueue := aRequestQueue;
timeoutInfo.IdleConnection := aIdleConnection;
timeoutInfo.HeaderWait := aHeaderWait;
timeoutInfo.MinSendRate := aMinSendRate;
EHttpApiServer.RaiseOnError(hSetUrlGroupProperty,
Http.SetUrlGroupProperty(fUrlGroupID, HttpServerTimeoutsProperty,
@timeoutInfo, SizeOf(timeoutInfo)));
end;
type
WEB_SOCKET_PROPERTY_TYPE = (
WEB_SOCKET_RECEIVE_BUFFER_SIZE_PROPERTY_TYPE, //0
WEB_SOCKET_SEND_BUFFER_SIZE_PROPERTY_TYPE,
WEB_SOCKET_DISABLE_MASKING_PROPERTY_TYPE,
WEB_SOCKET_ALLOCATED_BUFFER_PROPERTY_TYPE,
WEB_SOCKET_DISABLE_UTF8_VERIFICATION_PROPERTY_TYPE,
WEB_SOCKET_KEEPALIVE_INTERVAL_PROPERTY_TYPE,
WEB_SOCKET_SUPPORTED_VERSIONS_PROPERTY_TYPE
);
WEB_SOCKET_ACTION_QUEUE = Cardinal;
WEB_SOCKET_ACTION = (
WEB_SOCKET_NO_ACTION, //0
WEB_SOCKET_SEND_TO_NETWORK_ACTION,
WEB_SOCKET_INDICATE_SEND_COMPLETE_ACTION,
WEB_SOCKET_RECEIVE_FROM_NETWORK_ACTION,
WEB_SOCKET_INDICATE_RECEIVE_COMPLETE_ACTION
);
PWEB_SOCKET_ACTION = ^WEB_SOCKET_ACTION;
WEB_SOCKET_PROPERTY = record
PropType: WEB_SOCKET_PROPERTY_TYPE;
pvValue: Pointer;
ulValueSize: ULONG;
end;
PWEB_SOCKET_PROPERTY = ^WEB_SOCKET_PROPERTY;
WEB_SOCKET_HTTP_HEADER = record
pcName: PAnsiChar;
ulNameLength: ULONG;
pcValue: PAnsiChar;
ulValueLength: ULONG;
end;
PWEB_SOCKET_HTTP_HEADER = ^WEB_SOCKET_HTTP_HEADER;
WEB_SOCKET_HTTP_HEADER_ARR = array of WEB_SOCKET_HTTP_HEADER;
PWEB_SOCKET_BUFFER_DATA = ^WEB_SOCKET_BUFFER_DATA;
WEB_SOCKET_BUFFER_DATA = record
pbBuffer: PBYTE;
ulBufferLength: ULONG;
Reserved1: Word;
end;
WEB_SOCKET_BUFFER_CLOSE_STATUS = record
pbReason: PBYTE;
ulReasonLength: ULONG;
usStatus: WEB_SOCKET_CLOSE_STATUS;
end;
/// direct late-binding access to the WebSocket Protocol Component API functions
TWebSocketAPI = packed record
/// acces to the loaded library handle
LibraryHandle: THandle;
/// depends on Windows version
WebSocketEnabled: Boolean;
/// aborts a WebSocket session handle created by WebSocketCreateClientHandle
// or WebSocketCreateServerHandle
AbortHandle: procedure (hWebSocket: WEB_SOCKET_HANDLE); stdcall;
/// begins the client-side handshake
BeginClientHandshake: function (hWebSocket: WEB_SOCKET_HANDLE; pszSubprotocols: PAnsiChar;
ulSubprotocolCount: ULONG; pszExtensions: PAnsiChar; ulExtensionCount: ULONG;
const pInitialHeaders: PWEB_SOCKET_HTTP_HEADER; ulInitialHeaderCount: ULONG;
out pAdditionalHeaders: PWEB_SOCKET_HTTP_HEADER; out pulAdditionalHeaderCount: ULONG): HRESULT; stdcall;
/// begins the server-side handshake
BeginServerHandshake: function (hWebSocket: WEB_SOCKET_HANDLE; pszSubprotocolSelected: PAnsiChar;
pszExtensionSelected: PAnsiChar; ulExtensionSelectedCount: ULONG;
const pRequestHeaders: PWEB_SOCKET_HTTP_HEADER;
ulRequestHeaderCount: ULONG; out pResponseHeaders: PWEB_SOCKET_HTTP_HEADER;
out pulResponseHeaderCount: ULONG): HRESULT; stdcall;
/// completes an action started by WebSocketGetAction
CompleteAction: function (hWebSocket: WEB_SOCKET_HANDLE;
pvActionContext: Pointer; ulBytesTransferred: ULONG): HRESULT; stdcall;
/// creates a client-side WebSocket session handle
CreateClientHandle: function (const pProperties: PWEB_SOCKET_PROPERTY; ulPropertyCount: ULONG;
out phWebSocket: WEB_SOCKET_HANDLE): HRESULT; stdcall;
/// creates a server-side WebSocket session handle
CreateServerHandle: function (const pProperties: PWEB_SOCKET_PROPERTY; ulPropertyCount: ULONG;
out phWebSocket: WEB_SOCKET_HANDLE): HRESULT; stdcall;
/// deletes a WebSocket session handle created by WebSocketCreateClientHandle
// or WebSocketCreateServerHandle
DeleteHandle: procedure (hWebSocket: WEB_SOCKET_HANDLE); stdcall;
/// completes the client-side handshake
EndClientHandshake: function (hWebSocket: WEB_SOCKET_HANDLE;
const pResponseHeaders: PWEB_SOCKET_HTTP_HEADER;
ulReponseHeaderCount: ULONG; var pulSelectedExtensions: ULONG;
var pulSelectedExtensionCount: ULONG;
var pulSelectedSubprotocol: ULONG): HRESULT; stdcall;
/// completes the server-side handshake
EndServerHandshake: function (hWebSocket: WEB_SOCKET_HANDLE): HRESULT; stdcall;
/// returns an action from a call to WebSocketSend, WebSocketReceive or WebSocketCompleteAction
GetAction: function (hWebSocket: WEB_SOCKET_HANDLE; eActionQueue: WEB_SOCKET_ACTION_QUEUE;
pDataBuffers: Pointer {WEB_SOCKET_BUFFER_DATA}; var pulDataBufferCount: ULONG;
var pAction: WEB_SOCKET_ACTION;
var pBufferType: WEB_SOCKET_BUFFER_TYPE; var pvApplicationContext: Pointer;
var pvActionContext: Pointer): HRESULT; stdcall;
/// gets a single WebSocket property
GetGlobalProperty: function (eType: WEB_SOCKET_PROPERTY_TYPE;
pvValue: Pointer; var ulSize: ULONG): HRESULT ; stdcall;
/// adds a receive operation to the protocol component operation queue
Receive: function (hWebSocket: WEB_SOCKET_HANDLE; pBuffer: Pointer {PWEB_SOCKET_BUFFER_*};
pvContext: Pointer): HRESULT; stdcall;
/// adds a send operation to the protocol component operation queue
Send: function (hWebSocket: WEB_SOCKET_HANDLE; BufferType: WEB_SOCKET_BUFFER_TYPE;
pBuffer: Pointer {PWEB_SOCKET_BUFFER_*}; Context: Pointer): HRESULT; stdcall;
end;
/// identify each TWebSocketAPI late-binding API function
TWebSocketAPIs = (hAbortHandle, hBeginClientHandshake, hBeginServerHandshake,
hCompleteAction, hCreateClientHandle, hCreateServerHandle, hDeleteHandle,
hEndClientHandshake, hEndServerHandshake, hGetAction, hGetGlobalProperty,
hReceive, hSend
);
const
sProtocolHeader: SockString = 'SEC-WEBSOCKET-PROTOCOL';
function HttpSys2ToWebSocketHeaders(const aHttpHeaders: HTTP_REQUEST_HEADERS): WEB_SOCKET_HTTP_HEADER_ARR;
var headerCnt: Integer;
i, idx: PtrInt;
h: THttpHeader;
p: PHTTP_UNKNOWN_HEADER;
begin
headerCnt := 0;
for h := Low(HTTP_KNOWNHEADERS) to High(HTTP_KNOWNHEADERS) do
if aHttpHeaders.KnownHeaders[h].RawValueLength <> 0 then
inc(headerCnt);
p := aHttpHeaders.pUnknownHeaders;
if p<>nil then
inc(headerCnt, aHttpHeaders.UnknownHeaderCount);
SetLength(Result, headerCnt);
idx := 0;
for h := Low(HTTP_KNOWNHEADERS) to High(HTTP_KNOWNHEADERS) do
if aHttpHeaders.KnownHeaders[h].RawValueLength<>0 then begin
Result[idx].pcName := @HTTP_KNOWNHEADERS[h][1];
Result[idx].ulNameLength := ord(HTTP_KNOWNHEADERS[h][0]);
Result[idx].pcValue := aHttpHeaders.KnownHeaders[h].pRawValue;
Result[idx].ulValueLength := aHttpHeaders.KnownHeaders[h].RawValueLength;
inc(idx);
end;
p := aHttpHeaders.pUnknownHeaders;
if p<>nil then
for i := 1 to aHttpHeaders.UnknownHeaderCount do begin
Result[idx].pcName := p^.pName;
Result[idx].ulNameLength := p^.NameLength;
Result[idx].pcValue := p^.pRawValue;
Result[idx].ulValueLength := p^.RawValueLength;
inc(idx);
inc(p);
end;
end;
function WebSocketHeadersToSockString(const aHeaders: PWEB_SOCKET_HTTP_HEADER;
const aHeadersCount: Integer): SockString;
var i: Integer;
h: PWEB_SOCKET_HTTP_HEADER;
len: Integer;
d : PAnsiChar;
begin
len := 0;
h := aHeaders;
for i := 1 to aHeadersCount do begin
if h^.ulValueLength<>0 then
inc(len, h^.ulNameLength + h^.ulValueLength + 4);
inc(h);
end;
SetString(Result, nil, len);
d := Pointer(Result);
h := aHeaders;
for i := 1 to aHeadersCount do begin
if h^.ulValueLength<>0 then begin
Move(h^.pcName^, d^, h^.ulNameLength);
inc(d, h^.ulNameLength);
PWord(d)^ := Ord(':') + Ord(' ') shl 8;
inc(d, 2);
Move(h^.pcValue^, d^, h^.ulValueLength);
inc(d, h^.ulValueLength);
PWord(d)^ := 13 + 10 shl 8;
inc(d, 2);
end;
inc(h);
end;
Assert(d - Pointer(Result) = len);
end;
const
WEBSOCKET_DLL = 'websocket.dll';
WebSocketNames: array [TWebSocketAPIs] of PChar = (
'WebSocketAbortHandle',
'WebSocketBeginClientHandshake',
'WebSocketBeginServerHandshake',
'WebSocketCompleteAction',
'WebSocketCreateClientHandle',
'WebSocketCreateServerHandle',
'WebSocketDeleteHandle',
'WebSocketEndClientHandshake',
'WebSocketEndServerHandshake',
'WebSocketGetAction',
'WebSocketGetGlobalProperty',
'WebSocketReceive',
'WebSocketSend'
);
WEB_SOCKET_SEND_ACTION_QUEUE = $1;
WEB_SOCKET_RECEIVE_ACTION_QUEUE = $2;
WEB_SOCKET_ALL_ACTION_QUEUE = WEB_SOCKET_SEND_ACTION_QUEUE or WEB_SOCKET_RECEIVE_ACTION_QUEUE;
///Context ID of WebSocket URI group
WEB_SOCKET_URL_CONTEXT = 1;
var
WebSocketAPI: TWebSocketAPI;
procedure WebSocketApiInitialize;
var api: TWebSocketAPIs;
P: PPointer;
begin
if WebSocketAPI.LibraryHandle<>0 then
exit; // already loaded
WebSocketAPI.WebSocketEnabled := false;
WebSocketAPI.LibraryHandle := SafeLoadLibrary(WEBSOCKET_DLL);
if WebSocketAPI.LibraryHandle=0 then
exit;
P := @@WebSocketAPI.AbortHandle;
for api := low(api) to high(api) do begin
P^ := GetProcAddress(WebSocketAPI.LibraryHandle,WebSocketNames[api]);
if P^ = nil then begin
FreeLibrary(WebSocketAPI.LibraryHandle);
WebSocketAPI.LibraryHandle := 0;
exit;
end;
inc(P);
end;
WebSocketAPI.WebSocketEnabled := true;
end;
function WinHTTP_WebSocketEnabled: boolean;
begin
Result := WebSocketAPI.WebSocketEnabled;
end;
{ EWebSocketApi }
type
EWebSocketApi = class(ECrtSocket)
protected
fLastApi: TWebSocketAPIs;
public
class procedure RaiseOnError(api: TWebSocketAPIs; Error: integer);
constructor Create(api: TWebSocketAPIs; Error: integer); reintroduce;
published
property LastApi: TWebSocketAPIs read fLastApi;
end;
class procedure EWebSocketApi.RaiseOnError(api: TWebSocketAPIs; Error: integer);
begin
if Error<>NO_ERROR then
raise self.Create(api,Error);
end;
constructor EWebSocketApi.Create(api: TWebSocketAPIs; Error: integer);
begin
fLastError := Error;
fLastApi := api;
inherited CreateFmt('%s failed: %s (%d)',
[WebSocketNames[api],SysErrorMessagePerModule(Error,WEBSOCKET_DLL),Error])
end;
{ THttpApiWebSocketServerProtocol }
const WebSocketConnectionCapacity = 1000;
function THttpApiWebSocketServerProtocol.AddConnection(aConn: PHttpApiWebSocketConnection): Integer;
var i: integer;
begin
if fFirstEmptyConnectionIndex >= fConnectionsCapacity - 1 then begin
inc(fConnectionsCapacity, WebSocketConnectionCapacity);
ReallocMem(fConnections, fConnectionsCapacity * SizeOf(PHttpApiWebSocketConnection));
Fillchar(fConnections^[fConnectionsCapacity - WebSocketConnectionCapacity], WebSocketConnectionCapacity * SizeOf(PHttpApiWebSocketConnection), 0);
end;
if fFirstEmptyConnectionIndex >= fConnectionsCount then
fConnectionsCount := fFirstEmptyConnectionIndex + 1;
fConnections[fFirstEmptyConnectionIndex] := aConn;
Result := fFirstEmptyConnectionIndex;
for i := fFirstEmptyConnectionIndex + 1 to fConnectionsCount do begin
if fConnections[i] = nil then begin
fFirstEmptyConnectionIndex := i;
Break;
end;
end;
end;
function THttpApiWebSocketServerProtocol.Broadcast(
aBufferType: WEB_SOCKET_BUFFER_TYPE; aBuffer: Pointer;
aBufferSize: ULONG): boolean;
var i: integer;
begin
EnterCriticalSection(fSafe);
try
for i := 0 to fConnectionsCount - 1 do
if Assigned(fConnections[i]) then
fConnections[i].Send(aBufferType, aBuffer, aBufferSize);
finally
LeaveCriticalSection(fSafe);
end;
result := True;
end;
function THttpApiWebSocketServerProtocol.Close(index: Integer;
aStatus: WEB_SOCKET_CLOSE_STATUS; aBuffer: Pointer; aBufferSize: ULONG): boolean;
var conn: PHttpApiWebSocketConnection;
begin
Result := false;
if (index>=0) and (index<fConnectionsCount) then begin
conn := fConnections^[index];
if (conn<>nil) and (conn.fState = wsOpen) then begin
conn.Close(aStatus, aBuffer, aBufferSize);
result := True;
end;
end;
end;
constructor THttpApiWebSocketServerProtocol.Create(const aName: SockString;
aManualFragmentManagement: Boolean;
aServer: THttpApiWebSocketServer;
aOnAccept: THttpApiWebSocketServerOnAcceptEvent;
aOnMessage: THttpApiWebSocketServerOnMessageEvent;
aOnConnect: THttpApiWebSocketServerOnConnectEvent;
aOnDisconnect: THttpApiWebSocketServerOnDisconnectEvent;
aOnFragment: THttpApiWebSocketServerOnMessageEvent);
begin
if aManualFragmentManagement and not Assigned(aOnFragment) then
raise EWebSocketApi.CreateFmt('Error register WebSocket protocol. Protocol %s does not use buffer, ' + 'but OnFragment handler is not assigned', [aName]);
{$ifdef FPC}
InitCriticalSection(fSafe);
{$else}
InitializeCriticalSection(fSafe);
{$endif}
fPendingForClose := {$ifdef FPC}TFPList{$else}TList{$endif}.Create;
fName := aName;
fManualFragmentManagement := aManualFragmentManagement;
fServer := aServer;
fOnAccept := aOnAccept;
fOnMessage := aOnMessage;
fOnConnect := aOnConnect;
fOnDisconnect := aOnDisconnect;
fOnFragment := aOnFragment;
fConnectionsCapacity := WebSocketConnectionCapacity;
fConnectionsCount := 0;
fFirstEmptyConnectionIndex := 0;
GetMem(fConnections, fConnectionsCapacity * SizeOf(PHttpApiWebSocketConnection));
Fillchar(fConnections^, fConnectionsCapacity * SizeOf(PHttpApiWebSocketConnection), 0);
end;
destructor THttpApiWebSocketServerProtocol.Destroy;
var i: integer;
conn: PHttpApiWebSocketConnection;
begin
EnterCriticalSection(fSafe);
try
for i := 0 to fPendingForClose.Count-1 do begin
conn := fPendingForClose[i];
if Assigned(conn) then begin
conn.DoOnDisconnect();
conn.Disconnect();
Dispose(conn);
end;
end;
fPendingForClose.Free;
finally
LeaveCriticalSection(fSafe);
end;
{$IFDEF FPC}
DoneCriticalsection(fSafe);
{$ELSE}
DeleteCriticalSection(fSafe);
{$ENDIF}
FreeMem(fConnections, fConnectionsCapacity * SizeOf(PHttpApiWebSocketConnection));
fConnections := nil;
inherited;
end;
procedure THttpApiWebSocketServerProtocol.doShutdown;
var i: Integer;
conn: PHttpApiWebSocketConnection;
const sReason = 'Server shutdown';
begin
EnterCriticalSection(fSafe);
try
for i := 0 to fConnectionsCount - 1 do begin
conn := fConnections[i];
if Assigned(conn) then begin
RemoveConnection(i);
conn.fState := wsClosedByShutdown;
conn.fBuffer := sReason;
conn.fCloseStatus := WEB_SOCKET_ENDPOINT_UNAVAILABLE_CLOSE_STATUS;
conn.Close(WEB_SOCKET_ENDPOINT_UNAVAILABLE_CLOSE_STATUS, Pointer(conn.fBuffer), Length(conn.fBuffer));
// PostQueuedCompletionStatus(fServer.fThreadPoolServer.FRequestQueue, 0, 0, @conn.fOverlapped);
end;
end;
finally
LeaveCriticalSection(fSafe);
end;
end;
procedure THttpApiWebSocketServerProtocol.RemoveConnection(index: integer);
begin
fPendingForClose.Add(fConnections[index]);
fConnections[index] := nil;
if (fFirstEmptyConnectionIndex > index) then
fFirstEmptyConnectionIndex := index;
end;
function THttpApiWebSocketServerProtocol.Send(index: Integer;
aBufferType: WEB_SOCKET_BUFFER_TYPE; aBuffer: Pointer; aBufferSize: ULONG): boolean;
var conn: PHttpApiWebSocketConnection;
begin
result := false;
if (index>=0) and (index<fConnectionsCount) then begin
conn := fConnections^[index];
if (conn<>nil) and (conn.fState=wsOpen) then begin
conn.Send(aBufferType, aBuffer, aBufferSize);
result := True;
end;
end;
end;
{ THttpApiWebSocketConnection }
function THttpApiWebSocketConnection.TryAcceptConnection(aProtocol: THttpApiWebSocketServerProtocol;
Ctxt: THttpServerRequest; aNeedHeader: boolean): boolean;
var req: PHTTP_REQUEST;
wsRequestHeaders: WEB_SOCKET_HTTP_HEADER_ARR;
wsServerHeaders: PWEB_SOCKET_HTTP_HEADER;
wsServerHeadersCount: ULONG;
begin
fState := wsConnecting;
fBuffer := '';
fWSHandle := nil;
fLastActionContext := nil;
Fillchar(fOverlapped, SizeOf(fOverlapped), 0);
fProtocol := aProtocol;
req := PHTTP_REQUEST(Ctxt.HttpApiRequest);
fIndex := fProtocol.fFirstEmptyConnectionIndex;
fOpaqueHTTPRequestId := req^.RequestId;
if (fProtocol=nil) or (Assigned(fProtocol.OnAccept) and not fProtocol.OnAccept(Ctxt, Self)) then begin
result := False;
exit;
end;
EWebSocketApi.RaiseOnError(hCreateServerHandle, WebSocketAPI.CreateServerHandle(nil, 0, fWSHandle));
wsRequestHeaders := HttpSys2ToWebSocketHeaders(req^.Headers);
if aNeedHeader then
result := WebSocketAPI.BeginServerHandshake(fWSHandle, Pointer(fProtocol.name), nil, 0,
@wsRequestHeaders[0], Length(wsRequestHeaders), wsServerHeaders, wsServerHeadersCount) = S_OK else
result := WebSocketAPI.BeginServerHandshake(fWSHandle, nil, nil, 0,
@wsRequestHeaders[0], Length(wsRequestHeaders), wsServerHeaders, wsServerHeadersCount) = S_OK;
if result then
try
Ctxt.OutCustomHeaders := WebSocketHeadersToSockString(wsServerHeaders, wsServerHeadersCount);
finally
result := WebSocketAPI.EndServerHandshake(fWSHandle) = S_OK;
end;
if not Result then
Disconnect else
fLastReceiveTickCount := 0;
end;
procedure THttpApiWebSocketConnection.DoOnMessage(aBufferType: WEB_SOCKET_BUFFER_TYPE; aBuffer: Pointer; aBufferSize: ULONG);
procedure PushFragmentIntoBuffer;
var
l: Integer;
begin
l := Length(fBuffer);
SetLength(fBuffer, l + Integer(aBufferSize));
Move(aBuffer^, fBuffer[l + 1], aBufferSize);
end;
begin
if (fProtocol = nil) then
exit;
if (aBufferType=WEB_SOCKET_UTF8_FRAGMENT_BUFFER_TYPE) or
(aBufferType=WEB_SOCKET_BINARY_FRAGMENT_BUFFER_TYPE) then begin // Fragment
if not fProtocol.ManualFragmentManagement then
PushFragmentIntoBuffer;
if Assigned(fProtocol.OnFragment) then
fProtocol.OnFragment(self,aBufferType,aBuffer,aBufferSize);
end else begin // last Fragment
if Assigned(fProtocol.OnMessage) then begin
if fProtocol.ManualFragmentManagement then
fProtocol.OnMessage(self,aBufferType,aBuffer,aBufferSize) else begin
PushFragmentIntoBuffer;
fProtocol.OnMessage(self,aBufferType,Pointer(fBuffer),Length(fBuffer));
fBuffer := '';
end;
end;
end;
end;
procedure THttpApiWebSocketConnection.DoOnConnect;
begin
if (fProtocol<>nil) and Assigned(fProtocol.OnConnect) then
fProtocol.OnConnect(self);
end;
procedure THttpApiWebSocketConnection.DoOnDisconnect;
begin
if (fProtocol<>nil) and Assigned(fProtocol.OnDisconnect) then
fProtocol.OnDisconnect(self,fCloseStatus,Pointer(fBuffer),length(fBuffer));
end;
function THttpApiWebSocketConnection.ReadData(const WebsocketBufferData): integer;
var Err: HRESULT;
fBytesRead: cardinal;
aBuf: WEB_SOCKET_BUFFER_DATA absolute WebsocketBufferData;
begin
Result := 0;
if fWSHandle = nil then
exit;
Err := Http.ReceiveRequestEntityBody(fProtocol.fServer.FReqQueue, fOpaqueHTTPRequestId, 0,
aBuf.pbBuffer, aBuf.ulBufferLength, fBytesRead, @self.fOverlapped);
case Err of
// On page reload Safari do not send a WEB_SOCKET_INDICATE_RECEIVE_COMPLETE_ACTION
// with BufferType = WEB_SOCKET_CLOSE_BUFFER_TYPE, instead it send a dummy packet
// (WEB_SOCKET_RECEIVE_FROM_NETWORK_ACTION) and terminate socket
// see forum discussion https://synopse.info/forum/viewtopic.php?pid=27125
ERROR_HANDLE_EOF: Result := -1;
ERROR_IO_PENDING: ; //
NO_ERROR: ;//
else
// todo: close connection
end;
end;
procedure THttpApiWebSocketConnection.WriteData(const WebsocketBufferData);
var Err: HRESULT;
httpSendEntity: HTTP_DATA_CHUNK_INMEMORY;
bytesWrite: Cardinal;
aBuf: WEB_SOCKET_BUFFER_DATA absolute WebsocketBufferData;
begin
if fWSHandle = nil then
exit;
bytesWrite := 0;
httpSendEntity.DataChunkType := hctFromMemory;
httpSendEntity.pBuffer := aBuf.pbBuffer;
httpSendEntity.BufferLength := aBuf.ulBufferLength;
Err := Http.SendResponseEntityBody(fProtocol.fServer.FReqQueue,fOpaqueHTTPRequestId,
HTTP_SEND_RESPONSE_FLAG_BUFFER_DATA or HTTP_SEND_RESPONSE_FLAG_MORE_DATA,
1, @httpSendEntity, bytesWrite, nil, nil, @fProtocol.fServer.fSendOverlaped);
case Err of
ERROR_HANDLE_EOF: Disconnect;
ERROR_IO_PENDING: ; //
NO_ERROR: ;//
else
// todo: close connection
end;
end;
procedure THttpApiWebSocketConnection.CheckIsActive;
var elapsed: PtrInt;
const sCloseReason = 'Closed after ping timeout';
begin
if (fLastReceiveTickCount>0) and (fProtocol.fServer.fPingTimeout>0) then begin
elapsed := GetTick64-fLastReceiveTickCount;
if elapsed>2*fProtocol.fServer.PingTimeout*1000 then begin
fProtocol.RemoveConnection(fIndex);
fState := wsClosedByGuard;
fCloseStatus := WEB_SOCKET_ENDPOINT_UNAVAILABLE_CLOSE_STATUS;
fBuffer := sCloseReason;
PostQueuedCompletionStatus(
fProtocol.fServer.fThreadPoolServer.FRequestQueue, 0, 0, @fOverlapped);
end else
if elapsed>=fProtocol.fServer.PingTimeout * 1000 then
Ping;
end;
end;
procedure THttpApiWebSocketConnection.Disconnect;
var //Err: HRESULT; //todo: handle error
httpSendEntity: HTTP_DATA_CHUNK_INMEMORY;
bytesWrite: Cardinal;
begin
WebSocketAPI.AbortHandle(fWSHandle);
WebSocketAPI.DeleteHandle(fWSHandle);
fWSHandle := nil;
httpSendEntity.DataChunkType := hctFromMemory;
httpSendEntity.pBuffer := nil;
httpSendEntity.BufferLength := 0;
{Err :=} Http.SendResponseEntityBody(fProtocol.fServer.fReqQueue, fOpaqueHTTPRequestId,
HTTP_SEND_RESPONSE_FLAG_DISCONNECT, 1, @httpSendEntity, bytesWrite, nil, nil, nil);
end;
procedure THttpApiWebSocketConnection.BeforeRead;
begin
// if reading is in progress then try read messages else try receive new messages
if fState in [wsOpen, wsClosing] then begin
if Assigned(fLastActionContext) then begin
EWebSocketApi.RaiseOnError(hCompleteAction, WebSocketAPI.CompleteAction(
fWSHandle, fLastActionContext, fOverlapped.InternalHigh));
fLastActionContext := nil;
end else
EWebSocketApi.RaiseOnError(hReceive, WebSocketAPI.Receive(fWSHandle, nil, nil));
end else
raise EWebSocketApi.CreateFmt(
'THttpApiWebSocketConnection.BeforeRead state is not wsOpen', []);
end;
const
C_WEB_SOCKET_BUFFER_SIZE = 2;
type
TWebSocketBufferDataArr =
array [0 .. C_WEB_SOCKET_BUFFER_SIZE - 1] of WEB_SOCKET_BUFFER_DATA;
function THttpApiWebSocketConnection.ProcessActions(
ActionQueue: WEB_SOCKET_ACTION_QUEUE): boolean;
var ulDataBufferCount: ULONG;
Action: WEB_SOCKET_ACTION;
BufferType: WEB_SOCKET_BUFFER_TYPE;
ApplicationContext: Pointer;
ActionContext: Pointer;
i: integer;
Err: HRESULT;
Buffer: TWebSocketBufferDataArr;
procedure closeConnection();
begin
EnterCriticalSection(fProtocol.fSafe);
try
fProtocol.RemoveConnection(fIndex);
finally
LeaveCriticalSection(fProtocol.fSafe);
end;
EWebSocketApi.RaiseOnError(hCompleteAction, WebSocketAPI.CompleteAction(
fWSHandle, ActionContext, 0));
end;
begin
result := true;
repeat
ulDataBufferCount := Length(Buffer);
EWebSocketApi.RaiseOnError(hGetAction,
WebSocketAPI.GetAction(fWSHandle, ActionQueue, @Buffer[0], ulDataBufferCount,
Action, BufferType, ApplicationContext, ActionContext));
case Action of
WEB_SOCKET_NO_ACTION: ;
WEB_SOCKET_SEND_TO_NETWORK_ACTION: begin
for i := 0 to ulDataBufferCount - 1 do
WriteData(Buffer[i]);
if fWSHandle <> nil then begin
Err := WebSocketAPI.CompleteAction(fWSHandle, ActionContext, 0);
EWebSocketApi.RaiseOnError(hCompleteAction, Err);
end;
result := False;
exit;
end;
WEB_SOCKET_INDICATE_SEND_COMPLETE_ACTION: ;
WEB_SOCKET_RECEIVE_FROM_NETWORK_ACTION: begin
for i := 0 to ulDataBufferCount - 1 do
if (ReadData(Buffer[i])=-1) then begin
fState := wsClosedByClient;
fBuffer := '';
fCloseStatus := WEB_SOCKET_ENDPOINT_UNAVAILABLE_CLOSE_STATUS;
closeConnection();
end;
fLastActionContext := ActionContext;
result := False;
exit;
end;
WEB_SOCKET_INDICATE_RECEIVE_COMPLETE_ACTION: begin
fLastReceiveTickCount := GetTick64;
if BufferType = WEB_SOCKET_CLOSE_BUFFER_TYPE then begin
if fState = wsOpen then
fState := wsClosedByClient else
fState := wsClosedByServer;
SetString(fBuffer, PChar(Buffer[0].pbBuffer), Buffer[0].ulBufferLength);
fCloseStatus := Buffer[0].Reserved1;
closeConnection();
result := False;
exit;
end else if BufferType = WEB_SOCKET_PING_PONG_BUFFER_TYPE then begin
// todo: may be answer to client's ping
EWebSocketApi.RaiseOnError(hCompleteAction, WebSocketAPI.CompleteAction(
fWSHandle, ActionContext, 0));
exit;
end else if BufferType = WEB_SOCKET_UNSOLICITED_PONG_BUFFER_TYPE then begin
// todo: may be handle this situation
EWebSocketApi.RaiseOnError(hCompleteAction, WebSocketAPI.CompleteAction(
fWSHandle, ActionContext, 0));
exit;
end else begin
DoOnMessage(BufferType, Buffer[0].pbBuffer, Buffer[0].ulBufferLength);
EWebSocketApi.RaiseOnError(hCompleteAction, WebSocketAPI.CompleteAction(
fWSHandle, ActionContext, 0));
exit;
end;
end else
raise EWebSocketApi.CreateFmt('Invalid WebSocket action %d', [byte(Action)]);
end;
Err := WebSocketAPI.CompleteAction(fWSHandle, ActionContext, 0);
if ActionContext <> nil then
EWebSocketApi.RaiseOnError(hCompleteAction, Err);
until (Action = WEB_SOCKET_NO_ACTION);
end;
procedure THttpApiWebSocketConnection.InternalSend(aBufferType: WEB_SOCKET_BUFFER_TYPE;
WebsocketBufferData: pointer);
begin
EWebSocketApi.RaiseOnError(hSend, WebSocketAPI.Send(
fWSHandle, aBufferType, WebsocketBufferData, nil));
ProcessActions(WEB_SOCKET_SEND_ACTION_QUEUE);
end;
procedure THttpApiWebSocketConnection.Send(aBufferType: WEB_SOCKET_BUFFER_TYPE;
aBuffer: Pointer; aBufferSize: ULONG);
var wsSendBuf: WEB_SOCKET_BUFFER_DATA;
begin
if fState<>wsOpen then
exit;
wsSendBuf.pbBuffer := aBuffer;
wsSendBuf.ulBufferLength := aBufferSize;
InternalSend(aBufferType, @wsSendBuf);
end;
procedure THttpApiWebSocketConnection.Close(aStatus: WEB_SOCKET_CLOSE_STATUS;
aBuffer: Pointer; aBufferSize: ULONG);
var wsSendBuf: WEB_SOCKET_BUFFER_DATA;
begin
if fState=wsOpen then
fState := wsClosing;
wsSendBuf.pbBuffer := aBuffer;
wsSendBuf.ulBufferLength := aBufferSize;
wsSendBuf.Reserved1 := aStatus;
InternalSend(WEB_SOCKET_CLOSE_BUFFER_TYPE, @wsSendBuf);
end;
procedure THttpApiWebSocketConnection.Ping;
begin
InternalSend(WEB_SOCKET_PING_PONG_BUFFER_TYPE, nil);
end;
{ THttpApiWebSocketServer }
constructor THttpApiWebSocketServer.Create(CreateSuspended: Boolean;
aSocketThreadsCount, aPingTimeout: integer; QueueName: SockUnicode;
aOnWSThreadStart: TNotifyThreadEvent; aOnWSThreadTerminate: TNotifyThreadEvent);
begin
inherited Create(CreateSuspended, QueueName);
if not (WebSocketAPI.WebSocketEnabled) then
raise ECrtSocket.Create('WebSocket is not supported');
fPingTimeout := aPingTimeout;
if fPingTimeout>0 then
fGuard := TSynWebSocketGuard.Create(Self);
New(fRegisteredProtocols);
SetLength(fRegisteredProtocols^, 0);
FOnWSThreadStart := aOnWSThreadStart;
FOnWSThreadTerminate := aOnWSThreadTerminate;
fThreadPoolServer := TSynThreadPoolHttpApiWebSocketServer.Create(Self, aSocketThreadsCount);
end;
constructor THttpApiWebSocketServer.CreateClone(From: THttpApiServer);
var wsServer: THttpApiWebSocketServer absolute From;
begin
inherited CreateClone(From);
fThreadPoolServer := wsServer.fThreadPoolServer;
fPingTimeout := wsServer.fPingTimeout;
fRegisteredProtocols := wsServer.fRegisteredProtocols
end;
procedure THttpApiWebSocketServer.DestroyMainThread;
var i: PtrInt;
begin
fGuard.Free;
for i := 0 to Length(fRegisteredProtocols^) - 1 do
fRegisteredProtocols^[i].doShutdown;
FreeAndNil(fThreadPoolServer);
for i := 0 to Length(fRegisteredProtocols^) - 1 do
fRegisteredProtocols^[i].Free;
fRegisteredProtocols^ := nil;
Dispose(fRegisteredProtocols);
fRegisteredProtocols := nil;
inherited;
end;
procedure THttpApiWebSocketServer.DoAfterResponse(Ctxt: THttpServerRequest;
const Code: cardinal);
begin
if Assigned(fLastConnection) then
PostQueuedCompletionStatus(fThreadPoolServer.FRequestQueue, 0, 0,
@fLastConnection.fOverlapped);
inherited DoAfterResponse(Ctxt, Code);
end;
function THttpApiWebSocketServer.GetProtocol(index: integer): THttpApiWebSocketServerProtocol;
begin
if (index>=0) and (index<=Length(fRegisteredProtocols^)) then
result := fRegisteredProtocols^[index] else
result := nil;
end;
function THttpApiWebSocketServer.getProtocolsCount: Integer;
begin
if self=nil then
result := 0 else
result := Length(fRegisteredProtocols^);
end;
function THttpApiWebSocketServer.getSendResponseFlags(Ctxt: THttpServerRequest): Integer;
begin
if (PHTTP_REQUEST(Ctxt.HttpApiRequest)^.UrlContext=WEB_SOCKET_URL_CONTEXT) and
(fLastConnection<>nil) then
result := HTTP_SEND_RESPONSE_FLAG_OPAQUE or HTTP_SEND_RESPONSE_FLAG_MORE_DATA
or HTTP_SEND_RESPONSE_FLAG_BUFFER_DATA else
result := inherited getSendResponseFlags(Ctxt);
end;
function THttpApiWebSocketServer.UpgradeToWebSocket(Ctxt: THttpServerRequest): cardinal;
var Protocol: THttpApiWebSocketServerProtocol;
i, j: Integer;
p: PHTTP_UNKNOWN_HEADER;
ch, chB: PAnsiChar;
aName: SockString;
ProtocolHeaderFound: Boolean;
label protocolFound;
begin
result := 404;
Protocol := nil;
ProtocolHeaderFound := false;
p := PHTTP_REQUEST(Ctxt.HttpApiRequest)^.Headers.pUnknownHeaders;
for j := 1 to PHTTP_REQUEST(Ctxt.HttpApiRequest)^.Headers.UnknownHeaderCount do begin
if (p.NameLength=Length(sProtocolHeader)) and
IdemPChar(p.pName,Pointer(sProtocolHeader)) then begin
ProtocolHeaderFound := True;
for i := 0 to Length(fRegisteredProtocols^) - 1 do begin
ch := p.pRawValue;
while (ch-p.pRawValue)<P.RawValueLength do begin
while ((ch-p.pRawValue)<P.RawValueLength) and (ch^ in [',', ' ']) do inc(ch);
chB := ch;
while ((ch-p.pRawValue)<P.RawValueLength) and not (ch^ in [',']) do inc(ch);
SetString(aName, chB, ch - chB);
if aName = fRegisteredProtocols^[i].name then begin
Protocol := fRegisteredProtocols^[i];
goto protocolFound;
end;
end;
end;
end;
inc(p);
end;
if not ProtocolHeaderFound and (Protocol=nil) and (Length(fRegisteredProtocols^)=1) then
Protocol := fRegisteredProtocols^[0];
protocolFound:
if Protocol <> nil then begin
EnterCriticalSection(Protocol.fSafe);
try
New(fLastConnection);
if fLastConnection.TryAcceptConnection(Protocol,Ctxt,ProtocolHeaderFound) then begin
Protocol.AddConnection(fLastConnection);
result := 101
end else begin
Dispose(fLastConnection);
fLastConnection := nil;
result := 405;
end;
finally
LeaveCriticalSection(Protocol.fSafe);
end;
end;
end;
function THttpApiWebSocketServer.AddUrlWebSocket(const aRoot, aPort: SockString;
Https: boolean; const aDomainName: SockString; aRegisterURI: boolean): integer;
begin
result := AddUrl(aRoot, aPort, Https, aDomainName, aRegisterURI, WEB_SOCKET_URL_CONTEXT);
end;
procedure THttpApiWebSocketServer.RegisterProtocol(const aName: SockString;
aManualFragmentManagement: Boolean;
aOnAccept: THttpApiWebSocketServerOnAcceptEvent;
aOnMessage: THttpApiWebSocketServerOnMessageEvent;
aOnConnect: THttpApiWebSocketServerOnConnectEvent;
aOnDisconnect: THttpApiWebSocketServerOnDisconnectEvent;
aOnFragment: THttpApiWebSocketServerOnMessageEvent);
var protocol: THttpApiWebSocketServerProtocol;
begin
if self=nil then exit;
protocol := THttpApiWebSocketServerProtocol.Create(aName, aManualFragmentManagement,
Self, aOnAccept, aOnMessage, aOnConnect, aOnDisconnect, aOnFragment);
protocol.fIndex := length(fRegisteredProtocols^);
SetLength(fRegisteredProtocols^, protocol.fIndex + 1);
fRegisteredProtocols^[protocol.fIndex] := protocol;
end;
function THttpApiWebSocketServer.Request(Ctxt: THttpServerRequest): cardinal;
begin
if PHTTP_REQUEST(Ctxt.HttpApiRequest).UrlContext=WEB_SOCKET_URL_CONTEXT then
result := UpgradeToWebSocket(Ctxt)
else begin
result := inherited Request(Ctxt);
fLastConnection := nil;
end;
end;
procedure THttpApiWebSocketServer.SendServiceMessage;
begin
PostQueuedCompletionStatus(fThreadPoolServer.FRequestQueue, 0, 0, @fServiceOverlaped);
end;
procedure THttpApiWebSocketServer.SetOnWSThreadStart(
const Value: TNotifyThreadEvent);
begin
FOnWSThreadStart := Value;
end;
procedure THttpApiWebSocketServer.SetOnWSThreadTerminate(
const Value: TNotifyThreadEvent);
begin
FOnWSThreadTerminate := Value;
end;
{ TSynThreadPoolHttpApiWebSocketServer }
function TSynThreadPoolHttpApiWebSocketServer.NeedStopOnIOError: Boolean;
begin
// If connection closed by guard than ERROR_HANDLE_EOF or ERROR_OPERATION_ABORTED
// can be returned - Other connections must work normally
result := False;
end;
procedure TSynThreadPoolHttpApiWebSocketServer.OnThreadStart(Sender: TThread);
begin
if Assigned(fServer.OnWSThreadStart) then
fServer.OnWSThreadStart(Sender);
end;
procedure TSynThreadPoolHttpApiWebSocketServer.OnThreadTerminate(
Sender: TThread);
begin
if Assigned(fServer.OnWSThreadTerminate) then
fServer.OnWSThreadTerminate(Sender);
end;
procedure TSynThreadPoolHttpApiWebSocketServer.Task(aCaller: TSynThread; aContext: Pointer);
var conn: PHttpApiWebSocketConnection;
begin
if aContext=@fServer.fSendOverlaped then
exit;
if (aContext=@fServer.fServiceOverlaped) then begin
if Assigned(fServer.onServiceMessage) then
fServer.onServiceMessage;
exit;
end;
conn := PHttpApiWebSocketConnection(aContext);
if conn.fState=wsConnecting then begin
conn.fState := wsOpen;
conn.fLastReceiveTickCount := GetTick64;
conn.DoOnConnect();
end;
if conn.fState in [wsOpen, wsClosing] then
repeat
conn.BeforeRead;
until not conn.ProcessActions(WEB_SOCKET_RECEIVE_ACTION_QUEUE);
if conn.fState in [wsClosedByGuard] then
EWebSocketApi.RaiseOnError(hCompleteAction,
WebSocketAPI.CompleteAction(conn.fWSHandle, conn.fLastActionContext, 0));
if conn.fState in [wsClosedByClient,wsClosedByServer,wsClosedByGuard,wsClosedByShutdown] then begin
conn.DoOnDisconnect;
if conn.fState = wsClosedByClient then
conn.Close(conn.fCloseStatus, Pointer(conn.fBuffer), length(conn.fBuffer));
conn.Disconnect;
EnterCriticalSection(conn.Protocol.fSafe);
try
conn.Protocol.fPendingForClose.Remove(conn);
finally
LeaveCriticalSection(conn.Protocol.fSafe);
end;
Dispose(conn);
end;
end;
constructor TSynThreadPoolHttpApiWebSocketServer.Create(Server: THttpApiWebSocketServer;
NumberOfThreads: Integer);
begin
fServer := Server;
fOnThreadStart := OnThreadStart;
fOnThreadTerminate := OnThreadTerminate;
inherited Create(NumberOfThreads, Server.fReqQueue);
end;
{ TSynWebSocketGuard }
procedure TSynWebSocketGuard.Execute;
var i, j: Integer;
prot: THttpApiWebSocketServerProtocol;
begin
if fServer.fPingTimeout>0 then
while not Terminated do begin
if fServer<>nil then
for i := 0 to Length(fServer.fRegisteredProtocols^)-1 do begin
prot := fServer.fRegisteredProtocols^[i];
EnterCriticalSection(prot.fSafe);
try
for j := 0 to prot.fConnectionsCount - 1 do
if Assigned(prot.fConnections[j]) then
prot.fConnections[j].CheckIsActive;
finally
LeaveCriticalSection(prot.fSafe);
end;
end;
i := 0;
while not Terminated and (i<fServer.fPingTimeout) do begin
Sleep(1000);
inc(i);
end;
end
else
Terminate;
end;
constructor TSynWebSocketGuard.Create(Server: THttpApiWebSocketServer);
begin
fServer := Server;
inherited Create(false);
end;
{ HTTP_RESPONSE }
procedure HTTP_RESPONSE.SetContent(var DataChunk: HTTP_DATA_CHUNK_INMEMORY;
const Content, ContentType: SockString);
begin
fillchar(DataChunk,sizeof(DataChunk),0);
if ContentType<>'' then begin
Headers.KnownHeaders[reqContentType].RawValueLength := length(ContentType);
Headers.KnownHeaders[reqContentType].pRawValue := pointer(ContentType);
end;
if Content='' then
exit;
DataChunk.DataChunkType := hctFromMemory;
DataChunk.pBuffer := pointer(Content);
DataChunk.BufferLength := length(Content);
EntityChunkCount := 1;
pEntityChunks := @DataChunk;
end;
function HTTP_RESPONSE.AddCustomHeader(P: PAnsiChar; var UnknownHeaders: HTTP_UNKNOWN_HEADERs;
ForceCustomHeader: boolean): PAnsiChar;
const KNOWNHEADERS: array[reqCacheControl..respWwwAuthenticate] of PAnsiChar = (
'CACHE-CONTROL:','CONNECTION:','DATE:','KEEP-ALIVE:','PRAGMA:','TRAILER:',
'TRANSFER-ENCODING:','UPGRADE:','VIA:','WARNING:','ALLOW:','CONTENT-LENGTH:',
'CONTENT-TYPE:','CONTENT-ENCODING:','CONTENT-LANGUAGE:','CONTENT-LOCATION:',
'CONTENT-MD5:','CONTENT-RANGE:','EXPIRES:','LAST-MODIFIED:',
'ACCEPT-RANGES:','AGE:','ETAG:','LOCATION:','PROXY-AUTHENTICATE:',
'RETRY-AFTER:','SERVER:','SET-COOKIE:','VARY:','WWW-AUTHENTICATE:');
var UnknownName: PAnsiChar;
i: integer;
begin
if ForceCustomHeader then
i := -1 else
i := IdemPCharArray(P,KNOWNHEADERS);
// WebSockets need CONNECTION as unknown header
if (i>=0) and (THttpHeader(i)<>reqConnection) then
with Headers.KnownHeaders[THttpHeader(i)] do begin
while P^<>':' do inc(P);
inc(P); // jump ':'
while P^=' ' do inc(P);
pRawValue := P;
while P^>=' ' do inc(P);
RawValueLength := P-pRawValue;
end else begin
UnknownName := P;
while (P^>=' ') and (P^<>':') do inc(P);
if P^=':' then
with UnknownHeaders[Headers.UnknownHeaderCount] do begin
pName := UnknownName;
NameLength := P-pName;
repeat inc(P) until P^<>' ';
pRawValue := P;
while P^>=' ' do inc(P);
RawValueLength := P-pRawValue;
if Headers.UnknownHeaderCount=high(UnknownHeaders) then begin
SetLength(UnknownHeaders,Headers.UnknownHeaderCount+32);
Headers.pUnknownHeaders := pointer(UnknownHeaders);
end;
inc(Headers.UnknownHeaderCount);
end else
while P^>=' ' do inc(P);
end;
result := P;
end;
procedure HTTP_RESPONSE.SetHeaders(P: PAnsiChar; var UnknownHeaders: HTTP_UNKNOWN_HEADERs);
{$ifndef NOXPOWEREDNAME}
const XPN: PAnsiChar = XPOWEREDNAME;
XPV: PAnsiChar = XPOWEREDVALUE;
{$endif}
begin
Headers.pUnknownHeaders := pointer(UnknownHeaders);
{$ifdef NOXPOWEREDNAME}
Headers.UnknownHeaderCount := 0;
{$else}
with UnknownHeaders[0] do begin
pName := XPN;
NameLength := length(XPOWEREDNAME);
pRawValue := XPV;
RawValueLength := length(XPOWEREDVALUE);
end;
Headers.UnknownHeaderCount := 1;
{$endif}
if P<>nil then
repeat
while ord(P^) in [10,13] do inc(P);
if P^=#0 then
break;
P := AddCustomHeader(P,UnknownHeaders,false);
until false;
end;
procedure HTTP_RESPONSE.SetStatus(code: integer; var OutStatus: SockString);
begin
StatusCode := code;
OutStatus := StatusCodeToReason(code);
ReasonLength := length(OutStatus);
pReason := pointer(OutStatus);
end;
const
HTTP_LOG_FIELD_TEST_SUB_STATUS: THttpApiLogFields = [hlfSubStatus];
{$endif MSWINDOWS} // encapsulate whole http.sys / HTTP API process
{ THttpRequest }
function THttpRequest.RegisterCompress(aFunction: THttpSocketCompress;
aCompressMinSize: integer): boolean;
begin
result := RegisterCompressFunc(fCompress,aFunction,fCompressAcceptEncoding,aCompressMinSize)<>'';
end;
constructor THttpRequest.Create(const aServer, aPort: SockString;
aHttps: boolean; const aProxyName,aProxyByPass: SockString;
ConnectionTimeOut,SendTimeout,ReceiveTimeout: DWORD;
aLayer: TCrtSocketLayer);
begin
fLayer := aLayer;
if fLayer<>cslUNIX then begin
fPort := GetCardinal(pointer(aPort));
if fPort=0 then
if aHttps then
fPort := 443 else
fPort := 80;
end;
fServer := aServer;
fHttps := aHttps;
fProxyName := aProxyName;
fProxyByPass := aProxyByPass;
fExtendedOptions.UserAgent := DefaultUserAgent(self);
if ConnectionTimeOut=0 then
ConnectionTimeOut := HTTP_DEFAULT_CONNECTTIMEOUT;
if SendTimeout=0 then
SendTimeout := HTTP_DEFAULT_SENDTIMEOUT;
if ReceiveTimeout=0 then
ReceiveTimeout := HTTP_DEFAULT_RECEIVETIMEOUT;
InternalConnect(ConnectionTimeOut,SendTimeout,ReceiveTimeout); // raise an exception on error
end;
constructor THttpRequest.Create(const aURI, aProxyName,aProxyByPass: SockString;
ConnectionTimeOut,SendTimeout,ReceiveTimeout: DWORD; aIgnoreSSLCertificateErrors: boolean);
var URI: TURI;
begin
if not URI.From(aURI) then
raise ECrtSocket.CreateFmt('%.Create: invalid aURI=%', [ClassName, aURI]);
IgnoreSSLCertificateErrors := aIgnoreSSLCertificateErrors;
Create(URI.Server,URI.Port,URI.Https,aProxyName,aProxyByPass,
ConnectionTimeOut,SendTimeout,ReceiveTimeout,URI.Layer);
end;
class function THttpRequest.InternalREST(const url,method,data,header: SockString;
aIgnoreSSLCertificateErrors: boolean; outHeaders: PSockString;
outStatus: PInteger): SockString;
var URI: TURI;
oh: SockString;
status: integer;
begin
result := '';
with URI do
if From(url) then
try
with self.Create(Server,Port,Https,'','',0,0,0,Layer) do
try
IgnoreSSLCertificateErrors := aIgnoreSSLCertificateErrors;
status := Request(Address,method,0,header,data,'',oh,result);
if outStatus<>nil then
outStatus^ := status;
if outHeaders<>nil then
outHeaders^ := oh;
finally
Free;
end;
except
result := '';
end;
end;
class function THttpRequest.Get(const aURI,aHeader: SockString;
aIgnoreSSLCertificateErrors: boolean; outHeaders: PSockString;
outStatus: PInteger): SockString;
begin
result := InternalREST(aURI,'GET','',aHeader,
aIgnoreSSLCertificateErrors,outHeaders,outStatus);
end;
class function THttpRequest.Post(const aURI, aData, aHeader: SockString;
aIgnoreSSLCertificateErrors: boolean; outHeaders: PSockString;
outStatus: PInteger): SockString;
begin
result := InternalREST(aURI,'POST',aData,aHeader,
aIgnoreSSLCertificateErrors,outHeaders,outStatus);
end;
class function THttpRequest.Put(const aURI, aData, aHeader: SockString;
aIgnoreSSLCertificateErrors: boolean; outHeaders: PSockString;
outStatus: PInteger): SockString;
begin
result := InternalREST(aURI,'PUT',aData,aHeader,
aIgnoreSSLCertificateErrors,outHeaders,outStatus);
end;
class function THttpRequest.Delete(const aURI, aHeader: SockString;
aIgnoreSSLCertificateErrors: boolean; outHeaders: PSockString;
outStatus: PInteger): SockString;
begin
result := InternalREST(aURI,'DELETE','',aHeader,
aIgnoreSSLCertificateErrors,outHeaders,outStatus);
end;
function THttpRequest.Request(const url, method: SockString;
KeepAlive: cardinal; const InHeader, InData, InDataType: SockString;
out OutHeader, OutData: SockString): integer;
var aData, aDataEncoding, aAcceptEncoding, aURL: SockString;
i: integer;
begin
if (url='') or (url[1]<>'/') then
aURL := '/'+url else // need valid url according to the HTTP/1.1 RFC
aURL := url;
fKeepAlive := KeepAlive;
InternalCreateRequest(method,aURL); // should raise an exception on error
try
// common headers
InternalAddHeader(InHeader);
if InDataType<>'' then
InternalAddHeader(SockString('Content-Type: ')+InDataType);
// handle custom compression
aData := InData;
if integer(fCompressAcceptHeader)<>0 then begin
aDataEncoding := CompressDataAndGetHeaders(fCompressAcceptHeader,fCompress,
InDataType,aData);
if aDataEncoding<>'' then
InternalAddHeader(SockString('Content-Encoding: ')+aDataEncoding);
end;
if fCompressAcceptEncoding<>'' then
InternalAddHeader(fCompressAcceptEncoding);
// send request to remote server
InternalSendRequest(method, aData);
// retrieve status and headers
result := InternalRetrieveAnswer(OutHeader,aDataEncoding,aAcceptEncoding,OutData);
// handle incoming answer compression
if OutData<>'' then begin
if aDataEncoding<>'' then
for i := 0 to high(fCompress) do
with fCompress[i] do
if Name=aDataEncoding then
if Func(OutData,false)='' then
raise ECrtSocket.CreateFmt('%s uncompress',[Name]) else
break; // successfully uncompressed content
if aAcceptEncoding<>'' then
fCompressAcceptHeader := ComputeContentEncoding(fCompress,pointer(aAcceptEncoding));
end;
finally
InternalCloseRequest;
end;
end;
{$ifdef USEWININET}
{ ************ WinHttp / WinINet HTTP clients }
{ TWinHttpAPI }
const
// while reading an HTTP response, read it in blocks of this size. 8K for now
HTTP_RESP_BLOCK_SIZE = 8*1024;
function TWinHttpAPI.InternalRetrieveAnswer(
var Header, Encoding, AcceptEncoding, Data: SockString): integer;
var Bytes, ContentLength, Read: DWORD;
tmp: SockString;
begin // HTTP_QUERY* and WINHTTP_QUERY* do match -> common to TWinINet + TWinHTTP
result := InternalGetInfo32(HTTP_QUERY_STATUS_CODE);
Header := InternalGetInfo(HTTP_QUERY_RAW_HEADERS_CRLF);
Encoding := InternalGetInfo(HTTP_QUERY_CONTENT_ENCODING);
AcceptEncoding := InternalGetInfo(HTTP_QUERY_ACCEPT_ENCODING);
// retrieve received content (if any)
Read := 0;
ContentLength := InternalGetInfo32(HTTP_QUERY_CONTENT_LENGTH);
if Assigned(fOnDownload) then begin
// download per-chunk using calback event
Bytes := fOnDownloadChunkSize;
if Bytes<=0 then
Bytes := 65536; // 64KB seems fair enough by default
SetLength(tmp,Bytes);
repeat
Bytes := InternalQueryDataAvailable;
if Bytes=0 then
break;
if Integer(Bytes) > Length(tmp) then
SetLength(tmp, Bytes);
Bytes := InternalReadData(tmp,0,Bytes);
if Bytes=0 then
break;
inc(Read,Bytes);
if not fOnDownload(self,Read,ContentLength,Bytes,pointer(tmp)^) then
break; // returned false = aborted
if Assigned(fOnProgress) then
fOnProgress(self,Read,ContentLength);
until false;
end else
if ContentLength<>0 then begin
// optimized version reading "Content-Length: xxx" bytes
SetLength(Data,ContentLength);
repeat
Bytes := InternalQueryDataAvailable;
if Bytes=0 then begin
SetLength(Data,Read); // truncated content
break;
end;
Bytes := InternalReadData(Data,Read,Bytes);
if Bytes=0 then begin
SetLength(Data,Read); // truncated content
break;
end;
inc(Read,Bytes);
if Assigned(fOnProgress) then
fOnProgress(self,Read,ContentLength);
until Read=ContentLength;
end else begin
// Content-Length not set: read response in blocks of HTTP_RESP_BLOCK_SIZE
repeat
Bytes := InternalQueryDataAvailable;
if Bytes=0 then
break;
SetLength(Data,Read+Bytes{HTTP_RESP_BLOCK_SIZE});
Bytes := InternalReadData(Data,Read,Bytes);
if Bytes=0 then
break;
inc(Read,Bytes);
if Assigned(fOnProgress) then
fOnProgress(self,Read,ContentLength);
until false;
SetLength(Data,Read);
end;
end;
class function TWinHttpAPI.IsAvailable: boolean;
begin
result := true; // both WinINet and WinHTTP are statically linked
end;
{ EWinINet }
constructor EWinINet.Create;
var dwError, tmpLen: DWORD;
msg, tmp: string;
begin // see http://msdn.microsoft.com/en-us/library/windows/desktop/aa383884
fLastError := GetLastError;
msg := SysErrorMessagePerModule(fLastError,'wininet.dll');
if fLastError=ERROR_INTERNET_EXTENDED_ERROR then begin
InternetGetLastResponseInfo({$ifdef FPC}@{$endif}dwError,nil,tmpLen);
if tmpLen > 0 then begin
SetLength(tmp,tmpLen);
InternetGetLastResponseInfo({$ifdef FPC}@{$endif}dwError,PChar(tmp),tmpLen);
msg := msg+' ['+tmp+']';
end;
end;
inherited CreateFmt('%s (%d)',[msg,fLastError]);
end;
{ TWinINet }
destructor TWinINet.Destroy;
begin
if fConnection<>nil then
InternetCloseHandle(FConnection);
if fSession<>nil then
InternetCloseHandle(FSession);
inherited;
end;
procedure TWinINet.InternalAddHeader(const hdr: SockString);
begin
if (hdr<>'') and not HttpAddRequestHeadersA(fRequest,
Pointer(hdr), length(hdr), HTTP_ADDREQ_FLAG_COALESCE) then
raise EWinINet.Create;
end;
procedure TWinINet.InternalCloseRequest;
begin
if fRequest<>nil then begin
InternetCloseHandle(fRequest);
fRequest := nil;
end;
end;
procedure TWinINet.InternalConnect(ConnectionTimeOut,SendTimeout,ReceiveTimeout: DWORD);
var OpenType: integer;
begin
if fProxyName='' then
OpenType := INTERNET_OPEN_TYPE_PRECONFIG else
OpenType := INTERNET_OPEN_TYPE_PROXY;
fSession := InternetOpenA(Pointer(fExtendedOptions.UserAgent), OpenType,
pointer(fProxyName), pointer(fProxyByPass), 0);
if fSession=nil then
raise EWinINet.Create;
InternetSetOption(fConnection,INTERNET_OPTION_CONNECT_TIMEOUT,
@ConnectionTimeOut,SizeOf(ConnectionTimeOut));
InternetSetOption(fConnection,INTERNET_OPTION_SEND_TIMEOUT,
@SendTimeout,SizeOf(SendTimeout));
InternetSetOption(fConnection,INTERNET_OPTION_RECEIVE_TIMEOUT,
@ReceiveTimeout,SizeOf(ReceiveTimeout));
fConnection := InternetConnectA(fSession, pointer(fServer), fPort, nil, nil,
INTERNET_SERVICE_HTTP, 0, 0);
if fConnection=nil then
raise EWinINet.Create;
end;
function TWinINet.InternalGetInfo(Info: DWORD): SockString;
var dwSize, dwIndex: DWORD;
begin
result := '';
dwSize := 0;
dwIndex := 0;
if not HttpQueryInfoA(fRequest, Info, nil, dwSize, dwIndex) and
(GetLastError=ERROR_INSUFFICIENT_BUFFER) then begin
SetLength(result,dwSize-1);
if not HttpQueryInfoA(fRequest, Info, pointer(result), dwSize, dwIndex) then
result := '';
end;
end;
function TWinINet.InternalGetInfo32(Info: DWORD): DWORD;
var dwSize, dwIndex: DWORD;
begin
dwSize := sizeof(result);
dwIndex := 0;
Info := Info or HTTP_QUERY_FLAG_NUMBER;
if not HttpQueryInfoA(fRequest, Info, @result, dwSize, dwIndex) then
result := 0;
end;
function TWinINet.InternalQueryDataAvailable: DWORD;
begin
if not InternetQueryDataAvailable(fRequest, Result, 0, 0) then
raise EWinINet.Create;
end;
function TWinINet.InternalReadData(var Data: SockString; Read: integer;
Size: cardinal): cardinal;
begin
if not InternetReadFile(fRequest, @PByteArray(Data)[Read], Size, result) then
raise EWinINet.Create;
end;
procedure TWinINet.InternalCreateRequest(const aMethod,aURL: SockString);
const ALL_ACCEPT: array[0..1] of PAnsiChar = ('*/*',nil);
ACCEPT_TYPES: array[boolean] of PLPSTR = (@ALL_ACCEPT,nil);
var Flags: DWORD;
begin
Flags := INTERNET_FLAG_HYPERLINK or INTERNET_FLAG_PRAGMA_NOCACHE or
INTERNET_FLAG_RESYNCHRONIZE; // options for a true RESTful request
if fKeepAlive<>0 then
Flags := Flags or INTERNET_FLAG_KEEP_CONNECTION;
if fHttps then
Flags := Flags or INTERNET_FLAG_SECURE;
FRequest := HttpOpenRequestA(FConnection,Pointer(aMethod),Pointer(aURL),
nil,nil,ACCEPT_TYPES[fNoAllAccept],Flags,0);
if FRequest=nil then
raise EWinINet.Create;
end;
procedure TWinINet.InternalSendRequest(const aMethod,aData: SockString);
var
buff: TInternetBuffersA;
datapos, datalen, max, Bytes, BytesWritten: DWORD;
begin
datalen := length(aData);
if (datalen>0) and Assigned(fOnUpload) then begin
FillChar(buff,SizeOf(buff),0);
buff.dwStructSize := SizeOf(buff);
buff.dwBufferTotal := Length(aData);
if not HttpSendRequestExA(fRequest,@buff,nil,0,0) then
raise EWinINet.Create;
datapos := 0;
while datapos<datalen do begin
Bytes := fOnDownloadChunkSize;
if Bytes<=0 then
Bytes := 65536; // 64KB seems fair enough by default
max := datalen-datapos;
if Bytes>max then
Bytes := max;
if not InternetWriteFile(fRequest,@PByteArray(aData)[datapos],Bytes,BytesWritten) then
raise EWinINet.Create;
inc(datapos, BytesWritten);
if not fOnUpload(Self,datapos,datalen) then
raise EWinINet.CreateFmt('OnUpload Canceled %s',[aMethod]);
end;
if not HttpEndRequest(fRequest, nil, 0, 0) then
raise EWinINet.Create;
end else // blocking send with no callback
if not HttpSendRequestA(fRequest,nil,0,pointer(aData),length(aData)) then
raise EWinINet.Create;
end;
{ TWinHTTP }
const
winhttpdll = 'winhttp.dll';
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY = 0;
WINHTTP_ACCESS_TYPE_NO_PROXY = 1;
WINHTTP_ACCESS_TYPE_NAMED_PROXY = 3;
WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY = 4; // Windows 8.1 and newer
WINHTTP_FLAG_BYPASS_PROXY_CACHE = $00000100; // add "pragma: no-cache" request header
WINHTTP_FLAG_REFRESH = WINHTTP_FLAG_BYPASS_PROXY_CACHE;
WINHTTP_FLAG_SECURE = $00800000; // use SSL if applicable (HTTPS)
WINHTTP_ADDREQ_FLAG_COALESCE = $40000000;
WINHTTP_QUERY_FLAG_NUMBER = $20000000;
// taken from http://www.tek-tips.com/faqs.cfm?fid=7493
// status manifests for WinHttp status callback
WINHTTP_CALLBACK_STATUS_RESOLVING_NAME = $00000001;
WINHTTP_CALLBACK_STATUS_NAME_RESOLVED = $00000002;
WINHTTP_CALLBACK_STATUS_CONNECTING_TO_SERVER = $00000004;
WINHTTP_CALLBACK_STATUS_CONNECTED_TO_SERVER = $00000008;
WINHTTP_CALLBACK_STATUS_SENDING_REQUEST = $00000010;
WINHTTP_CALLBACK_STATUS_REQUEST_SENT = $00000020;
WINHTTP_CALLBACK_STATUS_RECEIVING_RESPONSE = $00000040;
WINHTTP_CALLBACK_STATUS_RESPONSE_RECEIVED = $00000080;
WINHTTP_CALLBACK_STATUS_CLOSING_CONNECTION = $00000100;
WINHTTP_CALLBACK_STATUS_CONNECTION_CLOSED = $00000200;
WINHTTP_CALLBACK_STATUS_HANDLE_CREATED = $00000400;
WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING = $00000800;
WINHTTP_CALLBACK_STATUS_DETECTING_PROXY = $00001000;
WINHTTP_CALLBACK_STATUS_REDIRECT = $00004000;
WINHTTP_CALLBACK_STATUS_INTERMEDIATE_RESPONSE = $00008000;
WINHTTP_CALLBACK_STATUS_SECURE_FAILURE = $00010000;
WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE = $00020000;
WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE = $00040000;
WINHTTP_CALLBACK_STATUS_READ_COMPLETE = $00080000;
WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE = $00100000;
WINHTTP_CALLBACK_STATUS_REQUEST_ERROR = $00200000;
WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE = $00400000;
WINHTTP_CALLBACK_FLAG_RESOLVE_NAME =
(WINHTTP_CALLBACK_STATUS_RESOLVING_NAME or WINHTTP_CALLBACK_STATUS_NAME_RESOLVED);
WINHTTP_CALLBACK_FLAG_CONNECT_TO_SERVER =
(WINHTTP_CALLBACK_STATUS_CONNECTING_TO_SERVER or
WINHTTP_CALLBACK_STATUS_CONNECTED_TO_SERVER);
WINHTTP_CALLBACK_FLAG_SEND_REQUEST =
(WINHTTP_CALLBACK_STATUS_SENDING_REQUEST or
WINHTTP_CALLBACK_STATUS_REQUEST_SENT);
WINHTTP_CALLBACK_FLAG_RECEIVE_RESPONSE =
(WINHTTP_CALLBACK_STATUS_RECEIVING_RESPONSE or
WINHTTP_CALLBACK_STATUS_RESPONSE_RECEIVED);
WINHTTP_CALLBACK_FLAG_CLOSE_CONNECTION =
(WINHTTP_CALLBACK_STATUS_CLOSING_CONNECTION or
WINHTTP_CALLBACK_STATUS_CONNECTION_CLOSED);
WINHTTP_CALLBACK_FLAG_HANDLES =
(WINHTTP_CALLBACK_STATUS_HANDLE_CREATED or
WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING);
WINHTTP_CALLBACK_FLAG_DETECTING_PROXY = WINHTTP_CALLBACK_STATUS_DETECTING_PROXY;
WINHTTP_CALLBACK_FLAG_REDIRECT = WINHTTP_CALLBACK_STATUS_REDIRECT;
WINHTTP_CALLBACK_FLAG_INTERMEDIATE_RESPONSE = WINHTTP_CALLBACK_STATUS_INTERMEDIATE_RESPONSE;
WINHTTP_CALLBACK_FLAG_SECURE_FAILURE = WINHTTP_CALLBACK_STATUS_SECURE_FAILURE;
WINHTTP_CALLBACK_FLAG_SENDREQUEST_COMPLETE = WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE;
WINHTTP_CALLBACK_FLAG_HEADERS_AVAILABLE = WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE;
WINHTTP_CALLBACK_FLAG_DATA_AVAILABLE = WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE;
WINHTTP_CALLBACK_FLAG_READ_COMPLETE = WINHTTP_CALLBACK_STATUS_READ_COMPLETE;
WINHTTP_CALLBACK_FLAG_WRITE_COMPLETE = WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE;
WINHTTP_CALLBACK_FLAG_REQUEST_ERROR = WINHTTP_CALLBACK_STATUS_REQUEST_ERROR;
WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS =
(WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE
or WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE
or WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE
or WINHTTP_CALLBACK_STATUS_READ_COMPLETE
or WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE
or WINHTTP_CALLBACK_STATUS_REQUEST_ERROR);
WINHTTP_CALLBACK_FLAG_ALL_NOTIFICATIONS = $ffffffff;
WINHTTP_FLAG_SECURE_PROTOCOL_SSL2 = $00000008;
WINHTTP_FLAG_SECURE_PROTOCOL_SSL3 = $00000020;
WINHTTP_FLAG_SECURE_PROTOCOL_TLS1 = $00000080;
// tls 1.1 & 1.2 const from here:
// https://github.com/nihon-tc/Rtest/blob/master/header/Microsoft%20SDKs/Windows/v7.0A/Include/winhttp.h
WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_1 = $00000200;
WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2 = $00000800;
// Sets an unsigned long integer value that specifies which secure protocols are acceptable.
// By default only SSL3 and TLS1 are enabled in Windows 7 and Windows 8.
// By default only SSL3, TLS1.0, TLS1.1, and TLS1.2 are enabled in Windows 8.1 and Windows 10.
WINHTTP_OPTION_SECURE_PROTOCOLS = 84;
// Instructs the stack to start a WebSocket handshake process with WinHttpSendRequest.
// This option takes no parameters.
WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET = 114;
// if the following value is returned by WinHttpSetStatusCallback, then
// probably an invalid (non-code) address was supplied for the callback
WINHTTP_INVALID_STATUS_CALLBACK = -1;
WINHTTP_OPTION_DISABLE_FEATURE = 63;
// values for WINHTTP_OPTION_DISABLE_FEATURE
WINHTTP_DISABLE_COOKIES = $00000001;
WINHTTP_DISABLE_REDIRECTS = $00000002;
WINHTTP_DISABLE_AUTHENTICATION = $00000004;
WINHTTP_DISABLE_KEEP_ALIVE = $00000008;
WINHTTP_OPTION_ENABLE_FEATURE = 79;
// values for WINHTTP_OPTION_ENABLE_FEATURE
WINHTTP_ENABLE_SSL_REVOCATION = $00000001;
WINHTTP_ENABLE_SSL_REVERT_IMPERSONATION = $00000002;
type
WINHTTP_STATUS_CALLBACK = procedure(hInternet: HINTERNET; dwContext: PDWORD;
dwInternetStatus: DWORD; lpvStatusInformation: pointer; dwStatusInformationLength: DWORD); stdcall;
PWINHTTP_STATUS_CALLBACK = ^WINHTTP_STATUS_CALLBACK;
/// direct late-binding access to the WinHTTP API
// - note: WebSocket* API calls require Windows 8 and later
TWinHTTPBinding = packed record
/// access to the winhttp.dll loaded library
LibraryHandle: THandle;
/// depends on the published .dll functions
WebSocketEnabled: Boolean;
/// Initializes an application's use of the WinHTTP functions.
Open: function(pwszUserAgent: PWideChar; dwAccessType: DWORD;
pwszProxyName, pwszProxyBypass: PWideChar; dwFlags: DWORD): HINTERNET; stdcall;
/// Sets up a callback function that WinHTTP can call as progress is made during an operation.
SetStatusCallback: function(hSession: HINTERNET;
lpfnInternetCallback: WINHTTP_STATUS_CALLBACK;
dwNotificationFlags: DWORD; dwReserved: PDWORD): WINHTTP_STATUS_CALLBACK; stdcall;
/// Specifies the initial target server of an HTTP request.
Connect: function(hSession: HINTERNET; pswzServerName: PWideChar;
nServerPort: INTERNET_PORT; dwReserved: DWORD): HINTERNET; stdcall;
/// Creates an HTTP request handle.
OpenRequest: function(hConnect: HINTERNET; pwszVerb: PWideChar;
pwszObjectName: PWideChar; pwszVersion: PWideChar; pwszReferer: PWideChar;
ppwszAcceptTypes: PLPWSTR; dwFlags: DWORD): HINTERNET; stdcall;
/// Closes a single HINTERNET handle.
CloseHandle: function(hInternet: HINTERNET): BOOL; stdcall;
/// Adds one or more HTTP request headers to the HTTP request handle.
AddRequestHeaders: function(hRequest: HINTERNET; pwszHeaders: PWideChar;
dwHeadersLength: DWORD; dwModifiers: DWORD): BOOL; stdcall;
/// Sends the specified request to the HTTP server.
SendRequest: function(hRequest: HINTERNET; pwszHeaders: PWideChar;
dwHeadersLength: DWORD; lpOptional: Pointer; dwOptionalLength: DWORD;
dwTotalLength: DWORD; dwContext: DWORD): BOOL; stdcall;
/// Ends an HTTP request that is initiated by WinHttpSendRequest.
ReceiveResponse: function(hRequest: HINTERNET;
lpReserved: Pointer): BOOL; stdcall;
/// Retrieves header information associated with an HTTP request.
QueryHeaders: function(hRequest: HINTERNET; dwInfoLevel: DWORD; pwszName: PWideChar;
lpBuffer: Pointer; var lpdwBufferLength, lpdwIndex: DWORD): BOOL; stdcall;
/// Returns the amount of data, in bytes, available to be read with WinHttpReadData.
QueryDataAvailable: function(hRequest: HINTERNET;
var lpdwNumberOfBytesAvailable: DWORD): BOOL; stdcall;
/// Reads data from a handle opened by the WinHttpOpenRequest function.
ReadData: function(hRequest: HINTERNET; lpBuffer: Pointer;
dwNumberOfBytesToRead: DWORD; var lpdwNumberOfBytesRead: DWORD): BOOL; stdcall;
/// Sets the various time-outs that are involved with HTTP transactions.
SetTimeouts: function(hInternet: HINTERNET; dwResolveTimeout: DWORD;
dwConnectTimeout: DWORD; dwSendTimeout: DWORD; dwReceiveTimeout: DWORD): BOOL; stdcall;
/// Sets an Internet option.
SetOption: function(hInternet: HINTERNET; dwOption: DWORD;
lpBuffer: Pointer; dwBufferLength: DWORD): BOOL; stdcall;
/// Passes the required authorization credentials to the server.
SetCredentials: function(hRequest: HINTERNET; AuthTargets: DWORD; AuthScheme: DWORD;
pwszUserName: PWideChar; pwszPassword: PWideChar; pAuthParams: Pointer) : BOOL; stdcall;
/// Completes a WebSocket handshake started by WinHttpSendRequest.
WebSocketCompleteUpgrade: function(hRequest: HINTERNET;
lpReserved: Pointer): HINTERNET; stdcall;
/// Closes a WebSocket connection.
WebSocketClose: function(hWebSocket: HINTERNET; usStatus: Word;
pvReason: Pointer; dwReasonLength: DWORD): DWORD; stdcall;
/// Retrieves the close status sent by a server
WebSocketQueryCloseStatus: function(hWebSocket: HINTERNET; out usStatus: Word;
pvReason: Pointer; dwReasonLength: DWORD; out dwReasonLengthConsumed: DWORD): DWORD; stdcall;
/// Sends data over a WebSocket connection.
WebSocketSend: function(hWebSocket: HINTERNET; eBufferType: WINHTTP_WEB_SOCKET_BUFFER_TYPE;
pvBuffer: Pointer; dwBufferLength: DWORD): DWORD; stdcall;
/// Receives data from a WebSocket connection.
WebSocketReceive: function(hWebSocket: HINTERNET; pvBuffer: Pointer; dwBufferLength: DWORD;
out dwBytesRead: DWORD; out eBufferType: WINHTTP_WEB_SOCKET_BUFFER_TYPE): DWORD; stdcall;
/// Writes data to a handle opened by the WinHttpOpenRequest function.
WriteData: function(hRequest: HINTERNET; lpBuffer: Pointer;
dwNumberOfBytesToWrite: DWORD; var lpdwNumberOfBytesWritten: DWORD): BOOL; stdcall;
end;
var
WinHttpAPI: TWinHTTPBinding;
type
TWinHttpAPIs = (hOpen, hSetStatusCallback, hConnect,
hOpenRequest, hCloseHandle, hAddRequestHeaders,
hSendRequest, hReceiveResponse, hQueryHeaders,
hQueryDataAvailable, hReadData, hSetTimeouts, hSetOption, hSetCredentials,
hWebSocketCompleteUpgrade, hWebSocketClose, hWebSocketQueryCloseStatus,
hWebSocketSend, hWebSocketReceive, hWriteData);
const
hWebSocketApiFirst = hWebSocketCompleteUpgrade;
const
WinHttpNames: array[TWinHttpAPIs] of PChar = (
'WinHttpOpen', 'WinHttpSetStatusCallback', 'WinHttpConnect',
'WinHttpOpenRequest', 'WinHttpCloseHandle', 'WinHttpAddRequestHeaders',
'WinHttpSendRequest', 'WinHttpReceiveResponse', 'WinHttpQueryHeaders',
'WinHttpQueryDataAvailable', 'WinHttpReadData', 'WinHttpSetTimeouts',
'WinHttpSetOption', 'WinHttpSetCredentials',
'WinHttpWebSocketCompleteUpgrade', 'WinHttpWebSocketClose',
'WinHttpWebSocketQueryCloseStatus', 'WinHttpWebSocketSend',
'WinHttpWebSocketReceive', 'WinHttpWriteData');
procedure WinHttpAPIInitialize;
var api: TWinHttpAPIs;
P: PPointer;
begin
if WinHttpAPI.LibraryHandle<>0 then
exit; // already loaded
WinHttpAPI.LibraryHandle := SafeLoadLibrary(winhttpdll);
WinHttpAPI.WebSocketEnabled := true; // WebSocketEnabled if all functions are available
if WinHttpAPI.LibraryHandle=0 then
raise ECrtSocket.CreateFmt('Unable to load library %s',[winhttpdll]);
P := @@WinHttpAPI.Open;
for api := low(api) to high(api) do begin
P^ := GetProcAddress(WinHttpAPI.LibraryHandle,WinHttpNames[api]);
if P^=nil then
if api<hWebSocketApiFirst then begin
FreeLibrary(WinHttpAPI.LibraryHandle);
WinHttpAPI.LibraryHandle := 0;
raise ECrtSocket.CreateFmt('Unable to find %s() in %s',[WinHttpNames[api], winhttpdll]);
end else
WinHttpAPI.WebSocketEnabled := false; // e.g. version is lower than Windows 8
inc(P);
end;
if WinHttpAPI.WebSocketEnabled then
WebSocketApiInitialize else
WebSocketAPI.WebSocketEnabled := false;
end;
destructor TWinHTTP.Destroy;
begin
if fConnection<>nil then
WinHttpAPI.CloseHandle(fConnection);
if fSession<>nil then
WinHttpAPI.CloseHandle(fSession);
inherited;
end;
procedure TWinHTTP.InternalAddHeader(const hdr: SockString);
begin
if (hdr<>'') and not WinHttpAPI.AddRequestHeaders(
FRequest, Pointer(Ansi7ToUnicode(hdr)), length(hdr), WINHTTP_ADDREQ_FLAG_COALESCE) then
RaiseLastModuleError(winhttpdll,EWinHTTP);
end;
procedure TWinHTTP.InternalCloseRequest;
begin
if fRequest<>nil then begin
WinHttpAPI.CloseHandle(fRequest);
FRequest := nil;
end;
end;
procedure WinHTTPSecurityErrorCallback(hInternet: HINTERNET; dwContext: PDWORD;
dwInternetStatus: DWORD; lpvStatusInformation: pointer; dwStatusInformationLength: DWORD); stdcall;
var err: string;
code: DWORD;
begin
code := PDWORD(lpvStatusInformation)^;
if code and $00000001<>0 then err := err+' WINHTTP_CALLBACK_STATUS_FLAG_CERT_REV_FAILED';
if code and $00000002<>0 then err := err+' WINHTTP_CALLBACK_STATUS_FLAG_INVALID_CERT';
if code and $00000004<>0 then err := err+' WINHTTP_CALLBACK_STATUS_FLAG_CERT_REVOKED';
if code and $00000008<>0 then err := err+' WINHTTP_CALLBACK_STATUS_FLAG_INVALID_CA';
if code and $00000010<>0 then err := err+' WINHTTP_CALLBACK_STATUS_FLAG_CERT_CN_INVALID';
if code and $00000020<>0 then err := err+' WINHTTP_CALLBACK_STATUS_FLAG_CERT_DATE_INVALID';
if code and $00000040<>0 then err := err+' WINHTTP_CALLBACK_STATUS_FLAG_CERT_WRONG_USAGE';
if code and $80000000<>0 then err := err+' WINHTTP_CALLBACK_STATUS_FLAG_SECURITY_CHANNEL_ERROR';
// in case lpvStatusInformation^=-2147483648 this is attempt to connect to
// non-https socket wrong port - perhaps must be 443?
raise EWinHTTP.CreateFmt('WinHTTP security error. Status %d, StatusInfo: %d ($%x%s)',
[dwInternetStatus, code, code, err]);
end;
{$ifndef UNICODE}
type
/// not defined in older Delphi versions
TOSVersionInfoEx = record
dwOSVersionInfoSize: DWORD;
dwMajorVersion: DWORD;
dwMinorVersion: DWORD;
dwBuildNumber: DWORD;
dwPlatformId: DWORD;
szCSDVersion: array[0..127] of char;
wServicePackMajor: WORD;
wServicePackMinor: WORD;
wSuiteMask: WORD;
wProductType: BYTE;
wReserved: BYTE;
end;
function GetVersionEx(var lpVersionInformation: TOSVersionInfoEx): BOOL; stdcall;
external kernel32 name 'GetVersionExA';
{$endif}
var // raw OS call, to avoid dependency to SynCommons.pas unit
OSVersionInfo: TOSVersionInfoEx;
function TWinHTTP.InternalGetProtocols: cardinal;
begin
// WINHTTP_FLAG_SECURE_PROTOCOL_SSL2 and WINHTTP_FLAG_SECURE_PROTOCOL_SSL3
// are unsafe, disabled at Windows level, therefore never supplied
result := WINHTTP_FLAG_SECURE_PROTOCOL_TLS1;
// Windows 7 and newer support TLS 1.1 & 1.2
if (OSVersionInfo.dwMajorVersion>6) or
((OSVersionInfo.dwMajorVersion=6) and (OSVersionInfo.dwMinorVersion>=1)) then
result := result or WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_1
or WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2;
end;
procedure TWinHTTP.InternalConnect(ConnectionTimeOut,SendTimeout,ReceiveTimeout: DWORD);
var OpenType: integer;
Callback: WINHTTP_STATUS_CALLBACK;
CallbackRes: PtrInt absolute Callback; // for FPC compatibility
protocols: DWORD;
begin
if OSVersionInfo.dwOSVersionInfoSize=0 then begin // API call once
OSVersionInfo.dwOSVersionInfoSize := sizeof(OSVersionInfo);
GetVersionEx(OSVersionInfo);
end;
if fProxyName='' then
if (OSVersionInfo.dwMajorVersion>6) or
((OSVersionInfo.dwMajorVersion=6) and (OSVersionInfo.dwMinorVersion>=3)) then
OpenType := WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY else // Windows 8.1 and newer
OpenType := WINHTTP_ACCESS_TYPE_NO_PROXY else
OpenType := WINHTTP_ACCESS_TYPE_NAMED_PROXY;
fSession := WinHttpAPI.Open(pointer(Ansi7ToUnicode(fExtendedOptions.UserAgent)),
OpenType, pointer(Ansi7ToUnicode(fProxyName)), pointer(Ansi7ToUnicode(fProxyByPass)), 0);
if fSession=nil then
RaiseLastModuleError(winhttpdll,EWinHTTP);
// cf. http://msdn.microsoft.com/en-us/library/windows/desktop/aa384116
if not WinHttpAPI.SetTimeouts(fSession,HTTP_DEFAULT_RESOLVETIMEOUT,
ConnectionTimeOut,SendTimeout,ReceiveTimeout) then
RaiseLastModuleError(winhttpdll,EWinHTTP);
if fHTTPS then begin
protocols := InternalGetProtocols;
if not WinHttpAPI.SetOption(fSession, WINHTTP_OPTION_SECURE_PROTOCOLS,
@protocols, SizeOf(protocols)) then
RaiseLastModuleError(winhttpdll,EWinHTTP);
Callback := WinHttpAPI.SetStatusCallback(fSession, WinHTTPSecurityErrorCallback,
WINHTTP_CALLBACK_FLAG_SECURE_FAILURE, nil);
if CallbackRes=WINHTTP_INVALID_STATUS_CALLBACK then
RaiseLastModuleError(winhttpdll,EWinHTTP);
end;
fConnection := WinHttpAPI.Connect(fSession, pointer(Ansi7ToUnicode(FServer)), fPort, 0);
if fConnection=nil then
RaiseLastModuleError(winhttpdll,EWinHTTP);
end;
function TWinHTTP.InternalGetInfo(Info: DWORD): SockString;
var dwSize, dwIndex: DWORD;
tmp: SockString;
i: integer;
begin
result := '';
dwSize := 0;
dwIndex := 0;
if not WinHttpAPI.QueryHeaders(fRequest, Info, nil, nil, dwSize, dwIndex) and
(GetLastError=ERROR_INSUFFICIENT_BUFFER) then begin
SetLength(tmp,dwSize);
if WinHttpAPI.QueryHeaders(fRequest, Info, nil, pointer(tmp), dwSize, dwIndex) then begin
dwSize := dwSize shr 1;
SetLength(result,dwSize);
for i := 0 to dwSize-1 do // fast ANSI 7 bit conversion
PByteArray(result)^[i] := PWordArray(tmp)^[i];
end;
end;
end;
function TWinHTTP.InternalGetInfo32(Info: DWORD): DWORD;
var dwSize, dwIndex: DWORD;
begin
dwSize := sizeof(result);
dwIndex := 0;
Info := Info or WINHTTP_QUERY_FLAG_NUMBER;
if not WinHttpAPI.QueryHeaders(fRequest, Info, nil, @result, dwSize, dwIndex) then
result := 0;
end;
function TWinHTTP.InternalQueryDataAvailable: DWORD;
begin
if not WinHttpAPI.QueryDataAvailable(fRequest, result) then
RaiseLastModuleError(winhttpdll,EWinHTTP);
end;
function TWinHTTP.InternalReadData(var Data: SockString; Read: integer; Size: cardinal): cardinal;
begin
if not WinHttpAPI.ReadData(fRequest, @PByteArray(Data)[Read], Size, result) then
RaiseLastModuleError(winhttpdll,EWinHTTP);
end;
procedure TWinHTTP.InternalCreateRequest(const aMethod,aURL: SockString);
const ALL_ACCEPT: array[0..1] of PWideChar = ('*/*',nil);
ACCEPT_TYPES: array[boolean] of PLPWSTR = (@ALL_ACCEPT,nil);
var Flags: DWORD;
begin
Flags := WINHTTP_FLAG_REFRESH; // options for a true RESTful request
if fHttps then
Flags := Flags or WINHTTP_FLAG_SECURE;
fRequest := WinHttpAPI.OpenRequest(fConnection,pointer(Ansi7ToUnicode(aMethod)),
pointer(Ansi7ToUnicode(aURL)),nil,nil,ACCEPT_TYPES[fNoAllAccept],Flags);
if fRequest=nil then
RaiseLastModuleError(winhttpdll,EWinHTTP);
if fKeepAlive = 0 then begin
Flags := WINHTTP_DISABLE_KEEP_ALIVE;
if not WinHttpAPI.SetOption(fRequest, WINHTTP_OPTION_DISABLE_FEATURE, @Flags, sizeOf(Flags)) then
RaiseLastModuleError(winhttpdll,EWinHTTP);
end;
end;
const
// from http://www.tek-tips.com/faqs.cfm?fid=7493
WINHTTP_OPTION_SECURITY_FLAGS = 31;
WINHTTP_OPTION_CLIENT_CERT_CONTEXT = $0000002F;
WINHTTP_NO_CLIENT_CERT_CONTEXT = $00000000;
ERROR_WINHTTP_CLIENT_AUTH_CERT_NEEDED = $00002F0C;
SECURITY_FLAG_IGNORE_UNKNOWN_CA = $00000100;
SECURITY_FLAG_IGNORE_CERT_DATE_INVALID = $00002000; // expired X509 Cert.
SECURITY_FLAG_IGNORE_CERT_CN_INVALID = $00001000; // bad common name in X509 Cert.
SECURITY_FLAG_IGNORE_CERT_WRONG_USAGE = $00000200;
SECURITY_FLAT_IGNORE_CERTIFICATES: DWORD =
SECURITY_FLAG_IGNORE_UNKNOWN_CA or
SECURITY_FLAG_IGNORE_CERT_DATE_INVALID or
SECURITY_FLAG_IGNORE_CERT_CN_INVALID or
SECURITY_FLAG_IGNORE_CERT_WRONG_USAGE;
WINHTTP_AUTH_TARGET_SERVER = 0;
WINHTTP_AUTH_TARGET_PROXY = 1;
WINHTTP_AUTH_SCHEME_BASIC = $00000001;
WINHTTP_AUTH_SCHEME_NTLM = $00000002;
WINHTTP_AUTH_SCHEME_PASSPORT = $00000004;
WINHTTP_AUTH_SCHEME_DIGEST = $00000008;
WINHTTP_AUTH_SCHEME_NEGOTIATE = $00000010;
procedure TWinHTTP.InternalSendRequest(const aMethod,aData: SockString);
function _SendRequest(L: DWORD): Boolean;
var Bytes, Current, Max, BytesWritten: DWORD;
begin
if Assigned(fOnUpload) and
(SameText(aMethod,'POST') or SameText(aMethod,'PUT')) then begin
result := WinHttpAPI.SendRequest(fRequest,nil,0,nil,0,L,0);
if result then begin
Current := 0;
while Current<L do begin
Bytes := fOnDownloadChunkSize;
if Bytes<=0 then
Bytes := 65536; // 64KB seems fair enough by default
Max := L-Current;
if Bytes>Max then
Bytes := Max;
if not WinHttpAPI.WriteData(fRequest, @PByteArray(aData)[Current],Bytes,BytesWritten) then
RaiseLastModuleError(winhttpdll,EWinHTTP);
inc(Current,BytesWritten);
if not fOnUpload(Self,Current,L) then
raise EWinHTTP.CreateFmt('OnUpload Canceled %s',[aMethod]);
end;
end;
end else
result := WinHttpAPI.SendRequest(fRequest,nil,0,pointer(aData),L,L,0);
end;
var L: integer;
winAuth: DWORD;
begin
with fExtendedOptions do
if AuthScheme<>wraNone then begin
case AuthScheme of
wraBasic: winAuth := WINHTTP_AUTH_SCHEME_BASIC;
wraDigest: winAuth := WINHTTP_AUTH_SCHEME_DIGEST;
wraNegotiate: winAuth := WINHTTP_AUTH_SCHEME_NEGOTIATE;
else raise EWinHTTP.CreateFmt('Unsupported AuthScheme=%d',[ord(AuthScheme)]);
end;
if not WinHttpAPI.SetCredentials(fRequest,WINHTTP_AUTH_TARGET_SERVER,
winAuth,pointer(AuthUserName),pointer(AuthPassword),nil) then
RaiseLastModuleError(winhttpdll,EWinHTTP);
end;
if fHTTPS and IgnoreSSLCertificateErrors then
if not WinHttpAPI.SetOption(fRequest, WINHTTP_OPTION_SECURITY_FLAGS,
@SECURITY_FLAT_IGNORE_CERTIFICATES, SizeOf(SECURITY_FLAT_IGNORE_CERTIFICATES)) then
RaiseLastModuleError(winhttpdll,EWinHTTP);
L := length(aData);
if not _SendRequest(L) or
not WinHttpAPI.ReceiveResponse(fRequest,nil) then
if fHTTPS and (GetLastError=ERROR_WINHTTP_CLIENT_AUTH_CERT_NEEDED) and
IgnoreSSLCertificateErrors then begin
if not WinHttpAPI.SetOption(fRequest,WINHTTP_OPTION_SECURITY_FLAGS,
@SECURITY_FLAT_IGNORE_CERTIFICATES,SizeOf(SECURITY_FLAT_IGNORE_CERTIFICATES)) then
RaiseLastModuleError(winhttpdll,EWinHTTP);
if not WinHttpAPI.SetOption(fRequest,WINHTTP_OPTION_CLIENT_CERT_CONTEXT,
pointer(WINHTTP_NO_CLIENT_CERT_CONTEXT),0) then
RaiseLastModuleError(winhttpdll,EWinHTTP);
if not _SendRequest(L) or
not WinHttpAPI.ReceiveResponse(fRequest,nil) then
RaiseLastModuleError(winhttpdll,EWinHTTP);
end
else RaiseLastModuleError(winhttpdll,EWinHTTP);
end;
{ TWinHTTPUpgradeable }
constructor TWinHTTPUpgradeable.Create(const aServer, aPort: SockString;
aHttps: boolean; const aProxyName, aProxyByPass: SockString;
ConnectionTimeOut, SendTimeout, ReceiveTimeout: DWORD;
aLayer: TCrtSocketLayer);
begin
inherited;
fSocket := nil;
end;
function TWinHTTPUpgradeable.InternalRetrieveAnswer(var Header, Encoding,
AcceptEncoding, Data: SockString): integer;
begin
result := inherited InternalRetrieveAnswer(Header, Encoding, AcceptEncoding, Data);
fSocket := WinHttpAPI.WebSocketCompleteUpgrade(fRequest, nil);
if fSocket=nil then
raise EWinHTTP.Create('Error upgrading socket');
end;
procedure TWinHTTPUpgradeable.InternalSendRequest(const aMethod,aData: SockString);
begin
if not WinHttpAPI.SetOption(fRequest,WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET,nil,0) then
raise EWinHTTP.Create('Error upgrading socket');
inherited;
end;
{ TWinHTTPWinSocketClient }
function TWinHTTPWebSocketClient.CheckSocket: Boolean;
begin
result := fSocket <> nil;
end;
function TWinHTTPWebSocketClient.CloseConnection(const aCloseReason: SockString): DWORD;
begin
if not CheckSocket then
result := ERROR_INVALID_HANDLE else
result := WinHttpAPI.WebSocketClose(fSocket, WEB_SOCKET_SUCCESS_CLOSE_STATUS, Pointer(aCloseReason), Length(aCloseReason));
if (Result = NO_ERROR) then
fSocket := nil;
end;
constructor TWinHTTPWebSocketClient.Create(const aServer, aPort: SockString;
aHttps: boolean; const url, aSubProtocol, aProxyName, aProxyByPass: SockString;
ConnectionTimeOut, SendTimeout, ReceiveTimeout: DWORD);
var _http: TWinHTTPUpgradeable;
inH, outH, outD: SockString;
begin
fSocket := nil;
_http := TWinHTTPUpgradeable.Create(aServer, aPort, aHttps, aProxyName, aProxyByPass,
ConnectionTimeOut, SendTimeout, ReceiveTimeout);
try
// WebSocketAPI.BeginClientHandshake()
if aSubProtocol <> '' then
inH := sProtocolHeader + ': '+aSubProtocol else
inH := '';
if _http.Request(url, 'GET', 0, inH, '', '', outH, outD) = 101 then
fSocket := _http.fSocket else
raise ECrtSocket.Create('WebSocketClient creation fail');
finally
_http.Free;
end;
end;
destructor TWinHTTPWebSocketClient.Destroy;
const CloseReason: SockString = 'object is destroyed';
var status: Word;
reason: SockString;
reasonLength: DWORD;
begin
if CheckSocket then begin // todo: check result
WinHttpAPI.WebSocketClose(fSocket, WEB_SOCKET_ABORTED_CLOSE_STATUS, Pointer(CloseReason), Length(CloseReason));
SetLength(reason, WEB_SOCKET_MAX_CLOSE_REASON_LENGTH);
WinHttpAPI.WebSocketQueryCloseStatus(fSocket, status, Pointer(reason),
WEB_SOCKET_MAX_CLOSE_REASON_LENGTH, reasonLength);
WinHttpAPI.CloseHandle(fSocket);
end;
inherited;
end;
function TWinHTTPWebSocketClient.Receive(aBuffer: pointer; aBufferLength: DWORD; out aBytesRead: DWORD; out aBufferType: WINHTTP_WEB_SOCKET_BUFFER_TYPE): DWORD;
begin
if not CheckSocket then
result := ERROR_INVALID_HANDLE else
result := WinHttpAPI.WebSocketReceive(fSocket, aBuffer, aBufferLength, aBytesRead, aBufferType);
end;
function TWinHTTPWebSocketClient.Send(aBufferType: WINHTTP_WEB_SOCKET_BUFFER_TYPE;
aBuffer: pointer; aBufferLength: DWORD): DWORD;
begin
if not CheckSocket then
result := ERROR_INVALID_HANDLE else
result := WinHttpAPI.WebSocketSend(fSocket, aBufferType, aBuffer, aBufferLength);
end;
{$endif USEWININET}
{$ifdef USELIBCURL}
{ ************ libcurl implementation }
{ TCurlHTTP }
procedure TCurlHTTP.InternalConnect(ConnectionTimeOut,SendTimeout,ReceiveTimeout: DWORD);
const HTTPS: array[boolean] of string = ('','s');
begin
if not IsAvailable then
raise ECrtSocket.CreateFmt('No available %s',[LIBCURL_DLL]);
fHandle := curl.easy_init;
if curl.globalShare <> nil then
curl.easy_setopt(fHandle,coShare,curl.globalShare);
curl.easy_setopt(fHandle,coConnectTimeoutMs,ConnectionTimeOut); // default=300 !
if SendTimeout<ReceiveTimeout then
SendTimeout := ReceiveTimeout;
if SendTimeout<>0 then // prevent send+receive forever
curl.easy_setopt(fHandle,coTimeoutMs,SendTimeout);
if fLayer=cslUNIX then
fRootURL := 'http://localhost' else // see CURLOPT_UNIX_SOCKET_PATH doc
fRootURL := AnsiString(Format('http%s://%s:%d',[HTTPS[fHttps],fServer,fPort]));
end;
destructor TCurlHTTP.Destroy;
begin
if fHandle<>nil then
curl.easy_cleanup(fHandle);
inherited;
end;
function TCurlHTTP.GetCACertFile: SockString;
begin
Result := fSSL.CACertFile;
end;
procedure TCurlHTTP.SetCACertFile(const aCertFile: SockString);
begin
fSSL.CACertFile := aCertFile;
end;
procedure TCurlHTTP.UseClientCertificate(
const aCertFile, aCACertFile, aKeyName, aPassPhrase: SockString);
begin
fSSL.CertFile := aCertFile;
fSSL.CACertFile := aCACertFile;
fSSL.KeyName := aKeyName;
fSSL.PassPhrase := aPassPhrase;
end;
procedure TCurlHTTP.InternalCreateRequest(const aMethod,aURL: SockString);
const CERT_PEM: SockString = 'PEM';
begin
fIn.URL := fRootURL+aURL;
curl.easy_setopt(fHandle,coFollowLocation,1); // url redirection (as TWinHTTP)
//curl.easy_setopt(fHandle,coTCPNoDelay,0); // disable Nagle
if fLayer=cslUNIX then
curl.easy_setopt(fHandle,coUnixSocketPath,pointer(fServer));
curl.easy_setopt(fHandle,coURL,pointer(fIn.URL));
if fProxyName<>'' then
curl.easy_setopt(fHandle,coProxy,pointer(fProxyName));
if fHttps then
if IgnoreSSLCertificateErrors then begin
curl.easy_setopt(fHandle,coSSLVerifyPeer,0);
curl.easy_setopt(fHandle,coSSLVerifyHost,0);
//curl.easy_setopt(fHandle,coProxySSLVerifyPeer,0);
//curl.easy_setopt(fHandle,coProxySSLVerifyHost,0);
end else begin
// see https://curl.haxx.se/libcurl/c/simplessl.html
if fSSL.CertFile<>'' then begin
curl.easy_setopt(fHandle,coSSLCertType,pointer(CERT_PEM));
curl.easy_setopt(fHandle,coSSLCert,pointer(fSSL.CertFile));
if fSSL.PassPhrase<>'' then
curl.easy_setopt(fHandle,coSSLCertPasswd,pointer(fSSL.PassPhrase));
curl.easy_setopt(fHandle,coSSLKeyType,nil);
curl.easy_setopt(fHandle,coSSLKey,pointer(fSSL.KeyName));
curl.easy_setopt(fHandle,coCAInfo,pointer(fSSL.CACertFile));
curl.easy_setopt(fHandle,coSSLVerifyPeer,1);
end
else if fSSL.CACertFile<>'' then
curl.easy_setopt(fHandle,coCAInfo,pointer(fSSL.CACertFile));
end;
curl.easy_setopt(fHandle,coUserAgent,pointer(fExtendedOptions.UserAgent));
curl.easy_setopt(fHandle,coWriteFunction,@CurlWriteRawByteString);
curl.easy_setopt(fHandle,coHeaderFunction,@CurlWriteRawByteString);
fIn.Method := UpperCase(aMethod);
if fIn.Method = '' then
fIn.Method := 'GET';
if fIn.Method = 'GET' then
fIn.Headers := nil else // disable Expect 100 continue in libcurl
fIn.Headers := curl.slist_append(nil,'Expect:');
Finalize(fOut);
end;
procedure TCurlHTTP.InternalAddHeader(const hdr: SockString);
var P: PAnsiChar;
s: SockString;
begin
P := pointer(hdr);
while P<>nil do begin
GetNextLine(P,s);
if s<>'' then // nil would reset the whole list
fIn.Headers := curl.slist_append(fIn.Headers,pointer(s));
end;
end;
class function TCurlHTTP.IsAvailable: boolean;
begin
Result := CurlIsAvailable;
end;
procedure TCurlHTTP.InternalSendRequest(const aMethod,aData: SockString);
begin // see http://curl.haxx.se/libcurl/c/CURLOPT_CUSTOMREQUEST.html
if fIn.Method='HEAD' then // the only verb what do not expect body in answer is HEAD
curl.easy_setopt(fHandle,coNoBody,1) else
curl.easy_setopt(fHandle,coNoBody,0);
curl.easy_setopt(fHandle,coCustomRequest,pointer(fIn.Method));
curl.easy_setopt(fHandle,coPostFields,pointer(aData));
curl.easy_setopt(fHandle,coPostFieldSize,length(aData));
curl.easy_setopt(fHandle,coHTTPHeader,fIn.Headers);
curl.easy_setopt(fHandle,coFile,@fOut.Data);
curl.easy_setopt(fHandle,coWriteHeader,@fOut.Header);
end;
function TCurlHTTP.InternalRetrieveAnswer(var Header, Encoding, AcceptEncoding,
Data: SockString): integer;
var res: TCurlResult;
P: PAnsiChar;
s: SockString;
i: integer;
rc: longint; // needed on Linux x86-64
begin
res := curl.easy_perform(fHandle);
if res<>crOK then
raise ECurlHTTP.CreateFmt('libcurl error %d (%s) on %s %s',
[ord(res), curl.easy_strerror(res), fIn.Method, fIn.URL]);
rc := 0;
curl.easy_getinfo(fHandle,ciResponseCode,rc);
result := rc;
Header := Trim(fOut.Header);
if IdemPChar(pointer(Header),'HTTP/') then begin
i := 6;
while Header[i]>=' ' do inc(i);
while ord(Header[i]) in [10,13] do inc(i);
system.Delete(Header,1,i-1); // trim leading 'HTTP/1.1 200 OK'#$D#$A
end;
P := pointer(Header);
while P<>nil do begin
GetNextLine(P,s);
if IdemPChar(pointer(s),'ACCEPT-ENCODING:') then
trimcopy(s,17,100,AcceptEncoding) else
if IdemPChar(pointer(s),'CONTENT-ENCODING:') then
trimcopy(s,18,100,Encoding);
end;
Data := fOut.Data;
end;
procedure TCurlHTTP.InternalCloseRequest;
begin
if fIn.Headers<>nil then begin
curl.slist_free_all(fIn.Headers);
fIn.Headers := nil;
end;
Finalize(fIn);
fIn.DataOffset := 0;
Finalize(fOut);
end;
{$endif USELIBCURL}
{ TSimpleHttpClient }
constructor TSimpleHttpClient.Create(aOnlyUseClientSocket: boolean);
begin
fOnlyUseClientSocket := aOnlyUseClientSocket;
inherited Create;
end;
destructor TSimpleHttpClient.Destroy;
begin
FreeAndNil(fHttp);
FreeAndNil(fHttps);
inherited Destroy;
end;
function TSimpleHttpClient.RawRequest(const Uri: TURI; const Method, Header,
Data, DataType: SockString; KeepAlive: cardinal): integer;
begin
result := 0;
if (Uri.Https or (Proxy <> '')) and not fOnlyUseClientSocket then
try
if (fHttps = nil) or (fHttps.Server <> Uri.Server) or
(integer(fHttps.Port) <> Uri.PortInt) then begin
FreeAndNil(fHttp);
FreeAndNil(fHttps); // need a new HTTPS connection
fHttps := MainHttpClass.Create(Uri.Server,Uri.Port,Uri.Https,Proxy,'',5000,5000,5000);
fHttps.IgnoreSSLCertificateErrors := fIgnoreSSLCertificateErrors;
if fUserAgent<>'' then
fHttps.UserAgent := fUserAgent;
end;
result := fHttps.Request(Uri.Address,Method,KeepAlive,
header,data,datatype,fHeaders,fBody);
if KeepAlive = 0 then
FreeAndNil(fHttps);
except
FreeAndNil(fHttps);
end
else
try
if (fHttp = nil) or (fHttp.Server <> Uri.Server) or
(fHttp.Port <> Uri.Port) or (connectionClose in fHttp.HeaderFlags) then begin
FreeAndNil(fHttps);
FreeAndNil(fHttp); // need a new HTTP connection
fHttp := THttpClientSocket.Open(Uri.Server,Uri.Port,cslTCP,5000,Uri.Https);
if fUserAgent<>'' then
fHttp.UserAgent := fUserAgent;
end;
if not fHttp.SockConnected then
exit else
result := fHttp.Request(Uri.Address,Method,KeepAlive,header,data,datatype,true);
fBody := fHttp.Content;
fHeaders := fHttp.HeaderGetText;
if KeepAlive = 0 then
FreeAndNil(fHttp);
except
FreeAndNil(fHttp);
end;
end;
function TSimpleHttpClient.Request(const uri,method,header,data,datatype: SockString;
keepalive: cardinal): integer;
var u: TURI;
begin
if u.From(uri) then
result := RawRequest(u,method,header,data,datatype,keepalive) else
result := STATUS_NOTFOUND;
end;
{ ************ socket polling for optimized multiple connections }
{ TPollSocketAbstract }
{.$define USEWSAPOLL}
// you may try it - but seems slightly SLOWER under Windows 7
function PollSocketClass: TPollSocketClass;
begin
{$ifdef LINUXNOTBSD}
result := TPollSocketEpoll; // the preferred way for our purpose
{$else}
{$ifdef MSWINDOWS}
{$ifdef USEWSAPOLL}
if Win32MajorVersion>=6 then // WSAPoll() not available before Vista
result := TPollSocketPoll else
{$endif USEWSAPOLL}
result := TPollSocketSelect; // Select() is FASTER than WSAPoll() :(
{$else}
result := TPollSocketPoll; // available on all POSIX systems
{$endif MSWINDOWS}
{$endif LINUXNOTBSD}
end;
constructor TPollSocketAbstract.Create;
begin
inherited Create;
end;
class function TPollSocketAbstract.New: TPollSocketAbstract;
begin
result := PollSocketClass.Create;
end;
{$ifdef MSWINDOWS}
{ TPollSocketSelect }
constructor TPollSocketSelect.Create;
begin
inherited Create;
fMaxSockets := FD_SETSIZE; // 64
end;
function TPollSocketSelect.Subscribe(socket: TSocket;
events: TPollSocketEvents; tag: TPollSocketTag): boolean;
begin
result := false;
if (self=nil) or (socket=0) or (byte(events)=0) or (fCount=fMaxSockets) then
exit;
if pseRead in events then
FD_SET(socket, fRead);
if pseWrite in events then
FD_SET(socket, fWrite);
fTag[fCount].socket := socket;
fTag[fCount].tag := tag;
inc(fCount);
if socket>fHighestSocket then
fHighestSocket := socket;
result := true;
end;
function TPollSocketSelect.Unsubscribe(socket: TSocket): boolean;
var i: integer;
begin
result := false;
if (self<>nil) and (socket<>0) then
for i := 0 to fCount-1 do
if fTag[i].socket=socket then begin
FD_CLR(socket,fRead);
FD_CLR(socket,fWrite);
dec(fCount);
if i<fCount then
move(fTag[i+1],fTag[i],(fCount-i)*sizeof(fTag[i]));
if fCount=0 then
fHighestSocket := 0;
result := true;
exit;
end;
end;
function TPollSocketSelect.WaitForModified(out results: TPollSocketResults;
timeoutMS: integer): integer;
var tv: TTimeVal;
rd,wr: TFDSet;
rdp,wrp: PFDSet;
ev: TPollSocketEvents;
i, pending: integer;
tmp: array[0..FD_SETSIZE-1] of TPollSocketResult;
begin
result := -1; // error
if (self=nil) or (fCount=0) then
exit;
if fRead.fd_count>0 then begin
rd := fRead;
rdp := @rd;
end else
rdp := nil;
if fWrite.fd_count>0 then begin
wr := fWrite;
wrp := @wr;
end else
wrp := nil;
tv.tv_sec := timeoutMS div 1000;
tv.tv_usec := (timeoutMS mod 1000)*1000;
result := Select(fHighestSocket+1,rdp,wrp,nil,@tv);
if result<=0 then
exit;
result := 0;
for i := 0 to fCount-1 do
with fTag[i] do begin
byte(ev) := 0;
if (rdp<>nil) and FD_ISSET(socket,rd) then begin
if (IoctlSocket(socket,FIONREAD,pending)=0) and (pending=0) then
// socket closed gracefully - see TCrtSocket.SockReceivePending
include(ev,pseClosed) else
include(ev,pseRead);
end;
if (wrp<>nil) and FD_ISSET(socket,wr) then
include(ev,pseWrite);
if byte(ev)<>0 then begin
tmp[result].events := ev;
tmp[result].tag := tag;
inc(result);
end;
end;
SetLength(results,result);
move(tmp,results[0],result*sizeof(tmp[0]));
end;
{$endif MSWINDOWS}
{ TPollSocketPoll }
constructor TPollSocketPoll.Create;
begin
inherited Create;
{$ifdef MSWINDOWS} // some practical values
fMaxSockets := 1024;
{$else}
fMaxSockets := 20000;
{$endif}
end;
function TPollSocketPoll.Subscribe(socket: TSocket;
events: TPollSocketEvents; tag: TPollSocketTag): boolean;
var i, n, e, fd: integer;
begin
result := false;
if (self=nil) or (socket=0) or (byte(events)=0) or (fCount=fMaxSockets) then
exit;
if pseRead in events then
e := POLLIN else
e := 0;
if pseWrite in events then
e := e or POLLOUT;
if fFDCount=fCount then begin // no void entry
for i := 0 to fFDCount-1 do
if fFD[i].fd=socket then // already subscribed
exit;
end else
for i := 0 to fFDCount-1 do begin
fd := fFD[i].fd;
if fd=socket then // already subscribed
exit else
if fd<0 then begin // found void entry
fTags[i] := tag;
with fFD[i] do begin
fd := socket;
events := e;
revents := 0;
end;
inc(fCount);
result := true;
exit;
end;
end;
if fFDCount=length(fFD) then begin // add new entry to the array
n := fFDCount+128+fFDCount shr 3;
if n>fMaxSockets then
n := fMaxSockets;
SetLength(fFD,n);
SetLength(fTags,n);
end;
with fFD[fFDCount] do begin
fd := socket;
events := e;
revents := 0;
end;
fTags[fFDCount] := tag;
inc(fFDCount);
inc(fCount);
result := true;
end;
procedure TPollSocketPoll.FDVacuum;
var n, i: integer;
begin
n := 0;
for i := 0 to fFDCount-1 do
if fFD[i].fd>0 then begin
if i<>n then begin
fFD[n] := fFD[i];
fTags[n] := fTags[i];
end;
inc(n);
end;
fFDCount := n;
end;
function TPollSocketPoll.Unsubscribe(socket: TSocket): boolean;
var i: integer;
begin
for i := 0 to fFDCount-1 do
if fFD[i].fd=socket then begin
fFD[i].fd := -1; // mark entry as void
dec(fCount);
if fCount<=fFDCount shr 1 then
FDVacuum; // avoid too many void entries
result := true;
exit;
end;
result := false;
end;
function TPollSocketPoll.WaitForModified(out results: TPollSocketResults;
timeoutMS: integer): integer;
var e: TPollSocketEvents;
i, ev, d: integer;
begin
result := -1; // error
if (self=nil) or (fCount=0) then
exit;
result := poll(pointer(fFD),fFDCount,timeoutMS);
if result<=0 then
exit;
SetLength(results,result);
d := 0;
for i := 0 to fFDCount-1 do
if fFD[i].fd>0 then begin
ev := fFD[i].revents;
if ev<>0 then begin
byte(e) := 0;
if ev and POLLIN<>0 then
include(e,pseRead);
if ev and POLLOUT<>0 then
include(e,pseWrite);
if ev and POLLERR<>0 then
include(e,pseError);
if ev and POLLHUP<>0 then
include(e,pseClosed);
results[d].events := e;
results[d].tag := fTags[i];
inc(d);
fFD[i].revents := 0; // reset result flags for reuse
end;
end;
if d<>result then
raise ECrtSocket.CreateFmt('TPollSocketPoll: result=%d d=%d',[result,d]);
end;
{$ifdef LINUXNOTBSD}
{ TPollSocketEpoll }
constructor TPollSocketEpoll.Create;
begin
inherited Create;
fEPFD := epoll_create($cafe);
fMaxSockets := 20000;
SetLength(fResults,fMaxSockets);
end;
destructor TPollSocketEpoll.Destroy;
begin
epoll_close(fEPFD);
inherited;
end;
function TPollSocketEpoll.Subscribe(socket: TSocket; events: TPollSocketEvents;
tag: TPollSocketTag): boolean;
var e: TEPollEvent;
begin
result := false;
if (self=nil) or (socket=0) or (socket=fEPFD) or
(byte(events)=0) or (fCount=fMaxSockets) then
exit;
e.data.ptr := pointer(tag);
if pseRead in events then
e.events := EPOLLIN else
e.events := 0;
if pseWrite in events then
e.events := e.events or EPOLLOUT;
// EPOLLERR and EPOLLHUP are always implicitly defined
result := epoll_ctl(fEPFD,EPOLL_CTL_ADD,socket,@e)=0;
if result then
inc(fCount);
end;
function TPollSocketEpoll.Unsubscribe(socket: TSocket): boolean;
var e: TEPollEvent; // should be there even if not used
begin
if (self=nil) or (socket=0) or (socket=fEPFD) then
result := false else begin
result := epoll_ctl(fEPFD,EPOLL_CTL_DEL,socket,@e)=0;
if result then
dec(fCount);
end;
end;
function TPollSocketEpoll.WaitForModified(out results: TPollSocketResults;
timeoutMS: integer): integer;
var e: TPollSocketEvents;
i, ev: integer;
begin
result := -1; // error
if (self=nil) or (fCount=0) then
exit;
result := epoll_wait(fEPFD,pointer(fResults),fMaxSockets,timeoutMS);
if result<=0 then
exit;
SetLength(results,result);
for i := 0 to result-1 do begin
ev := fResults[i].events;
byte(e) := 0;
if ev and EPOLLIN<>0 then
include(e,pseRead);
if ev and EPOLLOUT<>0 then
include(e,pseWrite);
if ev and EPOLLERR<>0 then
include(e,pseError);
if ev and EPOLLHUP<>0 then
include(e,pseClosed);
results[i].events := e;
results[i].tag := TPollSocketTag(fResults[i].data.ptr);
end;
end;
{$endif LINUXNOTBSD}
{ TPollSockets }
constructor TPollSockets.Create(aPollClass: TPollSocketClass=nil);
begin
inherited Create;
InitializeCriticalSection(fPendingLock);
InitializeCriticalSection(fPollLock);
if aPollClass=nil then
fPollClass := PollSocketClass else
fPollClass := aPollClass;
{$ifndef MSWINDOWS}
SetFileOpenLimit(GetFileOpenLimit(true)); // set soft limit to hard value
{$endif MSWINDOWS}
end;
destructor TPollSockets.Destroy;
var p: integer;
begin
for p := 0 to high(fPoll) do
fPoll[p].Free;
DeleteCriticalSection(fPendingLock);
DeleteCriticalSection(fPollLock);
inherited Destroy;
end;
function TPollSockets.Subscribe(socket: TSocket; tag: TPollSocketTag;
events: TPollSocketEvents): boolean;
var p,n: integer;
poll: TPollSocketAbstract;
begin
result := false;
if (self=nil) or (socket=0) or (events=[]) then
exit;
EnterCriticalSection(fPollLock);
try
poll := nil;
n := length(fPoll);
for p := 0 to n-1 do
if fPoll[p].Count<fPoll[p].MaxSockets then begin
poll := fPoll[p]; // stil some place in this poll instance
break;
end;
if poll=nil then begin
poll := fPollClass.Create;
SetLength(fPoll,n+1);
fPoll[n] := poll;
end;
result := poll.Subscribe(socket,events,tag);
if result then
inc(fCount);
finally
LeaveCriticalSection(fPollLock);
end;
end;
function TPollSockets.Unsubscribe(socket: TSocket; tag: TPollSocketTag): boolean;
var p: integer;
begin
result := false;
EnterCriticalSection(fPendingLock);
try
for p := fPendingIndex to high(fPending) do
if fPending[p].tag=tag then
byte(fPending[p].events) := 0; // tag to be ignored in future GetOne
finally
LeaveCriticalSection(fPendingLock);
end;
EnterCriticalSection(fPollLock);
try
for p := 0 to high(fPoll) do
if fPoll[p].Unsubscribe(socket) then begin
dec(fCount);
result := true;
exit;
end;
finally
LeaveCriticalSection(fPollLock);
end;
end;
function TPollSockets.GetOneWithinPending(out notif: TPollSocketResult): boolean;
var last,index: integer;
begin
result := false;
if fTerminated then
exit;
EnterCriticalSection(fPendingLock);
try
index := fPendingIndex;
last := high(fPending);
while index<=last do begin
notif := fPending[index]; // return notified events
if index<last then begin
inc(index);
fPendingIndex := index;
end else begin
fPending := nil;
fPendingIndex := 0;
end;
if byte(notif.events)<>0 then begin // void e.g. after Unsubscribe()
result := true;
exit;
end;
if fPending=nil then
break; // end of list
end;
finally
LeaveCriticalSection(fPendingLock);
end;
end;
function TPollSockets.GetOne(timeoutMS: integer; out notif: TPollSocketResult): boolean;
function PollAndSearchWithinPending(p: integer): boolean;
begin
if not fTerminated and
(fPoll[p].WaitForModified(fPending,{timeout=}0)>0) then begin
result := GetOneWithinPending(notif);
if result then
fPollIndex := p; // next call to continue from fPoll[fPollIndex+1]
end else
result := false;
end;
var p,n: integer;
elapsed,start: Int64;
begin
result := false;
byte(notif.events) := 0;
if (timeoutMS<0) or fTerminated then
exit;
start := {$ifdef MSWINDOWS}GetTick64{$else}0{$endif};
repeat
// non-blocking search within fPoll[]
EnterCriticalSection(fPollLock);
try
// check if some already notified as pending in fPoll[]
if GetOneWithinPending(notif) then
exit;
// calls fPoll[].WaitForModified({timeout=}0) to refresh pending state
n := length(fPoll);
if n>0 then begin
for p := fPollIndex+1 to n-1 do // search from fPollIndex = last found
if PollAndSearchWithinPending(p) then
exit;
for p := 0 to fPollIndex do // search from beginning up to fPollIndex
if PollAndSearchWithinPending(p) then
exit;
end;
finally
LeaveCriticalSection(fPollLock);
result := byte(notif.events)<>0; // exit would comes here and set result
end;
// wait a little for something to happen
if fTerminated or (timeoutMS=0) then
exit;
{$ifndef MSWINDOWS}
if start=0 then // measure time elapsed only if we wait
start := GetTick64 else {$endif} begin
elapsed := GetTick64-start; // allow multi-threaded process
if elapsed>timeoutMS then
exit else
if elapsed>300 then
SleepHiRes(50) else
if elapsed>50 then
SleepHiRes(10) else
SleepHiRes(1);
end;
until fTerminated;
end;
procedure TPollSockets.Terminate;
begin
if self<>nil then
fTerminated := true;
end;
{ TPollSocketsSlot }
function TPollSocketsSlot.Lock(writer: boolean): boolean;
begin
result := InterlockedIncrement(lockcounter[writer])=1;
if not result then
InterlockedDecrement(lockcounter[writer]);
end;
procedure TPollSocketsSlot.Unlock(writer: boolean);
begin
if @self<>nil then
InterlockedDecrement(lockcounter[writer]);
end;
function TPollSocketsSlot.TryLock(writer: boolean; timeoutMS: cardinal): boolean;
var endtix: Int64;
ms: integer;
begin
result := (@self<>nil) and (socket<>0);
if not result then
exit; // socket closed
result := Lock(writer);
if result or (timeoutMS=0) then
exit; // we acquired the slot, or we don't want to wait
endtix := GetTick64+timeoutMS; // never wait forever
ms := 0;
repeat
SleepHiRes(ms);
ms := ms xor 1; // 0,1,0,1,0,1...
if socket=0 then
exit; // no socket to lock for
result := Lock(writer);
if result then begin
result := socket<>0;
if not result then
UnLock(writer);
exit; // acquired or socket closed
end;
until GetTick64>=endtix;
end;
{ TPollAsynchSockets }
constructor TPollAsynchSockets.Create;
var c: TPollSocketClass;
begin
inherited Create;
c := PollSocketClass;
fRead := TPollSockets.Create(c);
{$ifdef LINUXNOTBSD}
c := TPollSocketPoll; // epoll is overkill for short-living writes
{$endif}
fWrite := TPollSockets.Create(c);
end;
destructor TPollAsynchSockets.Destroy;
begin
if not fRead.Terminated then
Terminate(5000);
inherited Destroy;
fRead.Free;
fWrite.Free;
end;
function TPollAsynchSockets.Start(connection: TObject): boolean;
var slot: PPollSocketsSlot;
begin
result := false;
if (fRead.Terminated) or (connection=nil) then
exit;
InterlockedIncrement(fProcessing);
try
slot := SlotFromConnection(connection);
if (slot=nil) or (slot.socket=0) then
exit;
if not AsynchSocket(slot.socket) then
exit; // we expect non-blocking mode on a real working socket
result := fRead.Subscribe(slot.socket,TPollSocketTag(connection),[pseRead]);
// now, ProcessRead will handle pseRead + pseError/pseClosed on this socket
finally
InterlockedDecrement(fProcessing);
end;
end;
function TPollAsynchSockets.Stop(connection: TObject): boolean;
var slot: PPollSocketsSlot;
sock: TSocket;
endtix: Int64;
lock: set of (r,w);
begin
result := false;
if fRead.Terminated or (connection=nil) then
exit;
InterlockedIncrement(fProcessing);
try
slot := SlotFromConnection(connection);
if slot=nil then
exit;
sock := slot.socket;
if sock<>0 then
try
slot.socket := 0; // notify ProcessRead/ProcessWrite to abort
slot.lastWSAError := WSAErrorAtShutdown(sock);
fRead.Unsubscribe(sock,TPollSocketTag(connection));
fWrite.Unsubscribe(sock,TPollSocketTag(connection));
result := true;
finally
DirectShutdown(sock);
endtix := GetTick64+10000;
lock := [];
repeat // acquire locks to avoid OnClose -> Connection.Free -> GPF
if not(r in lock) and slot.Lock(false) then
include(lock,r);
if not(w in lock) and slot.Lock(true) then
include(lock,w);
if lock=[r,w] then
break;
SleepHiRes(0); // 10 microsecs on POSIX
until GetTick64>=endtix;
end;
finally
InterlockedDecrement(fProcessing);
end;
end;
function TPollAsynchSockets.GetCount: integer;
begin
if self=nil then
result := 0 else
result := fRead.Count;
end;
procedure TPollAsynchSockets.Terminate(waitforMS: integer);
var endtix: Int64;
begin
fRead.Terminate;
fWrite.Terminate;
if waitforMS<=0 then
exit;
endtix := GetTick64+waitforMS;
repeat
SleepHiRes(1);
if fProcessing=0 then
break;
until GetTick64>endtix;
end;
function TPollAsynchSockets.WriteString(connection: TObject;
const data: SockString): boolean;
begin
if self=nil then
result := false else
result := Write(connection,pointer(data)^,length(data));
end;
procedure AppendData(var buf: SockString; const data; datalen: PtrInt);
var buflen: PtrInt;
begin
if datalen>0 then begin
buflen := length(buf);
SetLength(buf,buflen+datalen);
move(data,PByteArray(buf)^[buflen],datalen);
end;
end;
function TPollAsynchSockets.Write(connection: TObject;
const data; datalen, timeout: integer): boolean;
var tag: TPollSocketTag;
slot: PPollSocketsSlot;
P: PByte;
res,previous: integer;
begin
result := false;
if (datalen<=0) or (connection=nil) or fWrite.Terminated then
exit;
InterlockedIncrement(fProcessing);
try
tag := TPollSocketTag(connection);
slot := SlotFromConnection(connection);
if (slot=nil) or (slot.socket=0) then
exit;
if slot.TryLock(true,timeout) then // try and wait for another ProcessWrite
try
P := @data;
previous := length(slot.writebuf);
if (previous=0) and not (paoWritePollOnly in fOptions) then
repeat
// try to send now in non-blocking mode (works most of the time)
if fWrite.Terminated or (slot.socket=0) then
exit;
res := AsynchSend(slot.socket,P,datalen);
if slot.socket=0 then
exit; // Stop() called
if (res<0) and not WSAIsFatalError then
break; // fails now -> retry later in ProcessWrite
if res<=0 then
exit; // connection closed or broken -> abort
inc(fWriteCount);
inc(fWriteBytes,res);
dec(datalen,res);
if datalen=0 then begin
try // notify everything written
AfterWrite(connection);
result := true;
except
result := false;
end;
exit;
end;
inc(P,res);
until false;
// use fWrite output polling for the remaining data in ProcessWrite
AppendData(slot.writebuf,P^,datalen);
if previous>0 then // already subscribed
result := slot.socket<>0 else
if fWrite.Subscribe(slot.socket,tag,[pseWrite]) then
result := slot.socket<>0 else
slot.writebuf := ''; // subscription error -> abort
finally
slot.UnLock({writer=}true);
end;
finally
InterlockedDecrement(fProcessing);
end;
end;
procedure TPollAsynchSockets.ProcessRead(timeoutMS: integer);
var notif: TPollSocketResult;
connection: TObject;
slot: PPollSocketsSlot;
res,added: integer;
temp: array[0..$7fff] of byte; // read up to 32KB per chunk
procedure CloseConnection(withinreadlock: boolean);
begin
if withinreadlock then
slot.UnLock({writer=}false); // Stop() will try to acquire this lock
Stop(connection); // shutdown and set socket:=0 + acquire locks
try
OnClose(connection); // now safe to perform connection.Free
except
connection := nil; // user code may be unstable
end;
slot := nil; // ignore pseClosed and slot.Unlock(false)
end;
begin
if (self=nil) or fRead.Terminated then
exit;
InterlockedIncrement(fProcessing);
try
if not fRead.GetOne(timeoutMS,notif) then
exit;
connection := TObject(notif.tag);
slot := SlotFromConnection(connection);
if (slot=nil) or (slot.socket=0) then
exit;
if pseError in notif.events then
if not OnError(connection,notif.events) then begin // false = shutdown
CloseConnection({withinlock=}false);
exit;
end;
if pseRead in notif.events then begin
if slot.Lock({writer=}false) then // paranoid thread-safe read
try
added := 0;
repeat
if fRead.Terminated or (slot.socket=0) then
exit;
res := AsynchRecv(slot.socket,@temp,sizeof(temp));
if slot.socket=0 then
exit; // Stop() called
if (res<0) and not WSAIsFatalError then
break; // may block, try later
if res<=0 then begin
CloseConnection(true);
exit; // socket closed gracefully or unrecoverable error -> abort
end;
AppendData(slot.readbuf,temp,res);
inc(added,res);
until false;
if added>0 then
try
inc(fReadCount);
inc(fReadBytes,added);
if OnRead(connection)=sorClose then
CloseConnection(true);
except
CloseConnection(true); // force socket shutdown
end;
finally
slot.UnLock(false); // CloseConnection may set slot=nil
end;
end;
if (slot<>nil) and (slot.socket<>0) and (pseClosed in notif.events) then begin
CloseConnection(false);
exit;
end;
finally
InterlockedDecrement(fProcessing);
end;
end;
procedure TPollAsynchSockets.ProcessWrite(timeoutMS: integer);
var notif: TPollSocketResult;
connection: TObject;
slot: PPollSocketsSlot;
buf: PByte;
buflen,res,sent: integer;
begin
if (self=nil) or fWrite.Terminated then
exit;
InterlockedIncrement(fProcessing);
try
if not fWrite.GetOne(timeoutMS,notif) then
exit;
if notif.events<>[pseWrite] then
exit; // only try if we are sure the socket is writable and safe
connection := TObject(notif.tag);
slot := SlotFromConnection(connection);
if (slot=nil) or (slot.socket=0) then
exit;
if slot.Lock({writer=}true) then // paranoid check
try
buflen := length(slot.writebuf);
if buflen<>0 then begin
buf := pointer(slot.writebuf);
sent := 0;
repeat
if fWrite.Terminated or (slot.socket=0) then
exit;
res := AsynchSend(slot.socket,buf,buflen);
if slot.socket=0 then
exit; // Stop() called
if (res<0) and not WSAIsFatalError then
break; // may block, try later
if res<=0 then
exit; // socket closed gracefully or unrecoverable error -> abort
inc(fWriteCount);
inc(sent,res);
inc(buf,res);
dec(buflen,res);
until buflen=0;
inc(fWriteBytes,sent);
delete(slot.writebuf,1,sent);
end;
if slot.writebuf='' then begin // no data any more to be sent
fWrite.Unsubscribe(slot.socket,notif.tag);
try
AfterWrite(connection);
except
end;
end;
finally
slot.UnLock(true);
end;
finally
InterlockedDecrement(fProcessing);
end;
end;
var
_MainHttpClass: THttpRequestClass;
function MainHttpClass: THttpRequestClass;
begin
if _MainHttpClass = nil then begin
{$ifdef USEWININET}
_MainHttpClass := TWinHTTP;
{$else}
{$ifdef USELIBCURL}
_MainHttpClass := TCurlHTTP
{$else}
raise ECrtSocket.Create('No THttpRequest class known!');
{$endif}
{$endif}
end;
result := _MainHttpClass;
end;
procedure ReplaceMainHttpClass(aClass: THttpRequestClass);
begin
_MainHttpClass := aClass;
end;
procedure Initialize;
var i: integer;
begin
for i := 0 to high(NormToUpper) do
NormToUpper[i] := i;
for i := ord('a') to ord('z') do
dec(NormToUpper[i],32);
IP4local := '127.0.0.1'; // use var string with refcount=1 to avoid allocation
JSON_CONTENT_TYPE_VAR := 'application/json; charset=UTF-8';
{$ifdef MSWINDOWS}
Assert(
{$ifdef CPU64}
(sizeof(HTTP_REQUEST)=864) and
(sizeof(HTTP_SSL_INFO)=48) and
(sizeof(HTTP_DATA_CHUNK_INMEMORY)=32) and
(sizeof(HTTP_DATA_CHUNK_FILEHANDLE)=32) and
(sizeof(HTTP_REQUEST_HEADERS)=688) and
(sizeof(HTTP_RESPONSE_HEADERS)=512) and
(sizeof(HTTP_COOKED_URL)=40) and
(sizeof(HTTP_RESPONSE)=568) and
{$else}
(sizeof(HTTP_REQUEST)=472) and
(sizeof(HTTP_SSL_INFO)=28) and
(sizeof(HTTP_DATA_CHUNK_INMEMORY)=24) and
(sizeof(HTTP_DATA_CHUNK_FILEHANDLE)=32) and
(sizeof(HTTP_RESPONSE)=288) and
(sizeof(HTTP_REQUEST_HEADERS)=344) and
(sizeof(HTTP_RESPONSE_HEADERS)=256) and
(sizeof(HTTP_COOKED_URL)=24) and
{$endif CPU64}
(ord(reqUserAgent)=40) and
(ord(respLocation)=23) and (sizeof(THttpHeader)=4) and
(integer(HTTP_LOG_FIELD_TEST_SUB_STATUS)=HTTP_LOG_FIELD_SUB_STATUS));
GetTick64 := GetProcAddress(GetModuleHandle(kernel32),'GetTickCount64');
if not Assigned(GetTick64) then // fallback before Vista
GetTick64 := @GetTick64ForXP;
{$ifdef USEWININET}
FillChar(WinHttpAPI, SizeOf(WinHttpAPI), 0);
WinHttpAPIInitialize;
{$endif}
{$endif MSWINDOWS}
FillChar(WsaDataOnce,sizeof(WsaDataOnce),0);
if InitSocketInterface then
WSAStartup(WinsockLevel, WsaDataOnce);
end;
initialization
Initialize;
finalization
if WsaDataOnce.wVersion<>0 then
try
{$ifdef MSWINDOWS}
if Assigned(WSACleanup) then
WSACleanup;
{$endif}
finally
fillchar(WsaDataOnce,sizeof(WsaDataOnce),0);
end;
{$ifdef MSWINDOWS}
if Http.Module<>0 then begin
FreeLibrary(Http.Module);
Http.Module := 0;
end;
{$endif}
DestroySocketInterface;
end.