13145 lines
483 KiB
ObjectPascal
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+'<';
|
|
'>': result := result+'>';
|
|
'&': result := result+'&';
|
|
'"': result := result+'"';
|
|
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+'<';
|
|
'>': result := result+'>';
|
|
'&': result := result+'&';
|
|
'"': result := result+'"';
|
|
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.
|