/// classes implementing HTTP/1.1 client and server protocol // - this unit is a part of the freeware Synopse framework, // licensed under a MPL/GPL/LGPL tri-license; version 1.9 unit SynCrtSock; { This file is part of Synopse SQLite3 database framework. Synopse SQLite3 database framework. Copyright (C) 2010 Arnaud Bouchez Synopse Informatique - http://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 SQLite3 database framework. The Initial Developer of the Original Code is Arnaud Bouchez. Portions created by the Initial Developer are Copyright (C) 2010 the Initial Developer. All Rights Reserved. Contributor(s): Alternatively, the contents of this file may be used under the terms of either the GNU General Public License Version 2 or later (the "GPL"), or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), in which case the provisions of the GPL or the LGPL are applicable instead of those above. If you wish to allow use of your version of this file only under the terms of either the GPL or the LGPL, and not to allow others to use your version of this file under the terms of the MPL, indicate your decision by deleting the provisions above and replace them with the notice and other provisions required by the GPL or the LGPL. If you do not delete the provisions above, a recipient may use your version of this file under the terms of any one of the MPL, the GPL or the LGPL. ***** END LICENSE BLOCK ***** TCP/IP and HTTP/1.1 Client and Server *************************************** Initial version: 2009 May, by Arnaud Bouchez Version 1.4 - February 8, 2010 - whole Synopse SQLite3 database framework released under the GNU Lesser General Public License version 3, instead of generic "Public Domain" - fix a bug happening when multiple HTTP connections were opened and closed in the same program Version 1.5 - March 1, 2010 - new generic unix implementation, using libc sockets, in SynLibcSock.pas Version 1.9 - avoid some GPF during client deconnection when the server shut down } interface {$ifdef WITHPHD} {$define DOS} {$define NOSERVER} {$endif} { $define DEBUG2} { $define DEBUG23} {$ifdef DEBUG2} {.$define DEBUG} {$endif} {.$define WITHCHECKSUM} // checksum of TCP/IP content - already done in TCP protocol -> not necessary {.$define WITHSERVERTIME} // hack of the HTTP protocol to automaticaly set the Client CMOS clock from the server {.$define COMPRESS} // the content of all HTTP request is GZiped before sending if client allows it // - usefull only over the Internet with tiny bandwidth // - content of 4803 bytes is compressed into 700, and time is 440 us instead of 220 us {.$define COMPRESS_GZIP} // by default, compression is done with the deflate algorithm // - enable it if you need the oldest GZip format, a bit slowest (due to crc32) uses {$ifdef DOS} // use DWPL TCP/IP stack Hardware, {$ifdef DEBUG} //{$ifdef WITHPHD}Hardware, {$endif} WDosIP, {$endif} Threads, WDosClock, WDosSock, {$endif} {$ifdef MSWINDOWS} Windows, SynWinSock, {$else} Types, LibC, {$endif} SysUtils, Classes; type /// ansi string used for byte storage of all data in this unit {$ifdef UNICODE} TSockData = RawByteString; {$else} TSockData = type AnsiString; {$endif} {$ifndef FPC} /// FPC 64 compatibility integer type PtrInt = integer; /// FPC 64 compatibility pointer type PPtrInt = ^PtrInt; {$endif} /// exception thrown by the classes of this unit ECrtSocket = class(Exception) public constructor Create(const Msg: string); end; TCrtSocketClass = class of TCrtSocket; /// the available available network transport layer // - either TCP/IP, UDP/IP or Unix sockets TCrtSocketLayer = (cslTCP, cslUDP, cslUNIX); /// Fast low-level Socket implementation // - direct access to the OS (Windows, Linux, DWPL) network layer // - use Open constructor to create a client to be connected to a server // - use Bind constructor to initialize a server // - use direct access to low level Windows, Linux or DWPL network layer // - use SockIn and SockOut (after CreateSock*) to read or write data // as with standard Delphi text files (see SendEmail implementation) // - if app is multi-threaded, use faster SockSend() instead of SockOut^ // for direct write access to the socket; but SockIn^ is much faster than // SockRecv() thanks to its internal buffer, even on multi-threaded app // (at least under Windows, it may be up to 10 times faster) // - but you can decide whatever to use none, one or both SockIn/SockOut // - our classes are much faster than the Indy or Synapse implementation TCrtSocket = class public /// initialized after Open() with socket Sock: TSocket; /// initialized after Open() with Server name Server: AnsiString; /// initialized after Open() with port number Port: AnsiString; /// after CreateSockIn, use Readln(SockIn,s) to read a line from the opened socket SockIn: ^TextFile; /// after CreateSockOut, use Writeln(SockOut,s) to send a line to the opened socket SockOut: ^TextFile; /// if higher than 0, read loop will wait for incoming data till // TimeOut milliseconds (default value is 10000) - used also in SockSend() TimeOut: cardinal; /// total bytes received BytesIn, /// total bytes sent BytesOut: cardinal; /// connect to aServer:aPort constructor Open(const aServer, aPort: AnsiString; aLayer: TCrtSocketLayer=cslTCP); /// bind to aPort constructor Bind(const aPort: AnsiString; aLayer: TCrtSocketLayer=cslTCP); /// raise an ECrtSocket exception on error (called by above constructors) procedure OpenBind(const aServer, aPort: AnsiString; doBind: boolean; aSock: integer=-1; aLayer: TCrtSocketLayer=cslTCP); /// 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 procedure CreateSockIn; /// 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; /// 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 // - can be used also without SockIn: it will call directly SockRecv() in such case function SockInRead(Content: PAnsiChar; Length: integer): integer; /// check the connection status of the socket function SockConnected: boolean; /// wait till some data is pending in the receiving queue within TimeOut milliseconds // - returns >0 = exact data count pending in, if there is some data to be read // - returns 0 if there is no data to be read // - returns <0 on any socket error function SockCanRead(aTimeOut: cardinal): integer; {$ifndef DOS} // DWPL doesn't implement select() for send :( /// wait till some data can be sent within TimeOut milliseconds // - returns >0 if data can be written // - returns 0 if data can not be written // - returns <0 on any socket error function SockCanWrite(aTimeOut: cardinal): integer; {$endif} /// simulate writeln() with direct use of Send(Sock, ..) // - usefull on multi-treaded environnement (as in THttpServer.Process) // - no temp buffer is used // - handle TSockData, ShortString, Char, Integer parameters // - raise ECrtSocket exception on socket error procedure SockSend(const Values: array of const); overload; /// simulate writeln() with a single line procedure SockSend(const Line: TSockData=''); overload; /// flush all pending data to be sent procedure SockSendFlush; /// 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); /// returns the socket input stream as a string // - specify the Max time to wait until some data is available for reading // - if the TimeOut parameter is 0, wait until something is available function SockReceiveString(TimeOut : integer = 300): TSockData; /// fill the Buffer with Length bytes // - use TimeOut milliseconds wait for incoming data // - bypass the SockIn^ buffers // - return false on any error, true on success function TrySockRecv(Buffer: pointer; Length: integer): boolean; /// call readln(SockIn^,Line) 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 procedure SockRecvLn(out Line: TSockData); 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; /// append P^ data into SndBuf (used by SockSend(), e.g.) // - call SockSendFlush to send it through the network via SndLow() procedure Snd(P: pointer; Len: integer); /// direct send data through network // - raise a ECrtSocket exception on any error // - bypass the SndBuf 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; /// 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 CR/CRLF is appened to the block procedure Write(const Data: TSockData); private SockInEof: boolean; /// updated by every Snd() SndBuf: TSockData; SndBufLen: integer; /// close and shutdown the connection (called from Destroy) procedure Close; end; /// parent of THttpClientSocket and THttpServerSocket classes // - contain properties for implementing the HTTP/1.1 protocol // - handle chunking of body content THttpSocket = class(TCrtSocket) protected /// true if the TRANSFER-ENCODING: CHUNKED was set in headers Chunked: boolean; {$ifdef COMPRESS} /// true if the GZIP protocol is inside the ACCEPT-ENCODING: header AcceptGZip: boolean; {$endif} procedure GetHeader; procedure GetBody; public /// 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: TSockData; /// will contain the header lines after a Request - use HeaderValue() to get one Headers: array of TSockData; /// will contain the data retrieved from the server, after the Request Content: TSockData; {$ifdef WITHCHECKSUM} /// same as HeaderValue('Content-Checksum'), but retrieved during Request // - if the communication is between a THttpServer and THttpClient, // the content is validated with a fast Adler32 checksum // - it's convenient for testing the custom DWPL TCP/IP stack ContentChecksum: cardinal; {$endif} /// same as HeaderValue('Content-Length'), but retrieved during Request ContentLength: integer; /// same as HeaderValue('Content-Type'), but retrieved during Request ContentType: TSockData; /// add an header entry, returning the just entered entry index in Headers[]s function HeaderAdd(const aValue: TSockData): integer; /// set all Header values at once, from CRLF delimited text procedure HeaderSetText(const aText: TSockData); /// get all Header values at once, as CRLF delimited text function HeaderGetText: TSockData; /// HeaderValue('Content-Type')='text/html', e.g. function HeaderValue(aName: TSockData): TSockData; end; {$ifndef NOSERVER} // without DWPL TCP/IP stack: server Thread /// HTTP/1.1 server class used by THttpServer main server Thread THttpServerSocket = class(THttpSocket) private {$ifdef WITHSERVERTIME} /// true if 'SERVER-TIME: ASK' in headers NeedServerTime: boolean; {$endif} public /// contains the method ('GET','POST'.. e.g.) after GetRequest() Method: TSockData; /// contains the URL ('/' e.g.) after GetRequest() URL: TSockData; /// 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") KeepAliveClient: boolean; /// main object function called after aClientSock := Accept + Create: // - get initialize the socket with the supplied accepted socket // - caller will then use the GetRequest method below to // get the request procedure InitRequest(aClientSock: TSocket); /// main object function called after aClientSock := Accept + Create: // - get Command, Method, URL and Headers // - get sent data in Content (if ContentLength<>0) procedure GetRequest; end; {$endif} /// 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) public /// 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 UserAgent: TSockData; /// after an Open(server,port), return 200 if OK, http status error otherwize - get // the page data in Content function Get(const url: TSockData; KeepAlive: cardinal=0; const header: TSockData=''): integer; /// after an Open(server,port), return 200 if OK, http status error otherwize - only // header is read from server: Content is always '', but Headers are set function Head(const url: TSockData; KeepAlive: cardinal=0; const header: TSockData=''): integer; /// after an Open(server,port), return 200,201,204 if OK, http status error otherwize function Post(const url, Data, DataType: TSockData; KeepAlive: cardinal=0; const header: TSockData=''): integer; /// after an Open(server,port), return 200,201,204 if OK, http status error otherwize function Put(const url, Data, DataType: TSockData; KeepAlive: cardinal=0; const header: TSockData=''): integer; /// after an Open(server,port), return 200,202,204 if OK, http status error otherwize function Delete(const url: TSockData; KeepAlive: cardinal=0; const header: TSockData=''): integer; /// low-level HTTP/1.1 request // - call by all REST methods above // - after an Open(server,port), return 200,202,204 if OK, http status error otherwize // - retry is false by caller, and will be recursively called with true to retry once function Request(const url, method: TSockData; KeepAlive: cardinal; const header, Data, DataType: TSockData; retry: boolean): integer; end; // without DWPL TCP/IP stack: {$ifndef NOSERVER} THttpServer = class; /// HTTP response Thread // - Execute procedure get the request and calculate the answer // - 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(TThread) protected fServer: THttpServer; fServerSock: THttpServerSocket; fClientSock: TSocket; /// main thread loop: read request from socket, send back answer procedure Execute; override; public /// initialize the response thread for the corresponding incoming socket constructor Create(aSock: TSocket; aServer: THttpServer); /// release used socket and memory destructor Destroy; override; end; /// main server Thread // - bind to a port and listen to incoming requests // - assign this requests to THttpServerResp threads // - 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 // - don't forget to use Free procedure when you are finished THttpServer = class(TThread) protected /// used to protect Process() call ProcessCS: TRTLCriticalSection; /// 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(var ClientSock: THttpServerSocket); virtual; public /// contains the main server 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 Sock: TCrtSocket; /// will contain the total number of connection to the server // - it's the global count since the server started ServerConnectionCount: cardinal; /// time, in milliseconds, for the HTTP.1/1 connections to be kept alive; // default is 3000 ms ServerKeepAliveTimeOut: cardinal; /// create a Server Thread, binded and listening on a port; // raise a EHttpServer exception if binding failed constructor Create(const aPort: AnsiString); /// release all memory and handlers destructor Destroy; override; /// override this function to customize your http server: // - input: ClientSock.Headers contains the headers for the client request // - output: ClientSock.Headers to be sent back to the client // - output: ClientSock.ContentType to 'text/html' or proper type // - output: Data contains the data to be sent back to the client; // note that 'Content-Length:' header will be filled by length(Data) // - output: cardinal result of the function is the HTTP error code (200 if OK, e.g.) // - this function is called thread-safe by the THttpServerResp threads, // protected by a Critical Section in Process() function Request(var ClientSock: THttpServerSocket; var Data: TSockData): cardinal; virtual; abstract; end; {$endif} /// create a TCrtSocket, returning nil on error // (usefull to easily catch socket error exception ECrtSocket) function Open(const aServer, aPort: AnsiString): TCrtSocket; /// create a THttpClientSocket, returning nil on error // (usefull to easily catch socket error exception ECrtSocket) function OpenHttp(const aServer, aPort: AnsiString): THttpClientSocket; /// retrieve the content of a web page, using the HTTP/1.1 protocol function HttpGet(const server, port: AnsiString; const url: TSockData): TSockData; /// send an email using the SMTP protocol // - retry true on success function SendEmail(const Server: AnsiString; const From, CSVDest, Subject, Text: TSockData; const Headers: TSockData=''; const User: TSockData=''; const Pass: TSockData=''; const Port: AnsiString='25'): boolean; {$ifndef DOS} /// retrieve the IP adress from a computer name function ResolveName(const Name: AnsiString): AnsiString; {$endif} {$ifndef COMPRESS} /// simple Adler32 checksum implementation function Adler32(Adler: cardinal; p: pByte; Count: Integer): cardinal; {$endif} /// Base64 encoding of a string function Base64Encode(const s: TSockData): TSockData; /// Base64 decoding of a string function Base64Decode(const s: TSockData): TSockData; {$ifdef Win32} /// 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: AnsiString): TSockData; {$endif} implementation {$ifdef COMPRESS} uses SynZip; {$endif} {$ifndef COMPRESS} function Adler32(Adler: cardinal; p: pByte; Count: Integer): cardinal; // simple Adler32 checksum implementation // not that if COMPRESS is defined, we will use its faster adler32() asm var s1, s2: cardinal; i, n: integer; begin s1 := LongRec(Adler).Lo; s2 := LongRec(Adler).Hi; while Count>0 do begin if Count<5552 then n := Count else n := 5552; for i := 1 to n do begin inc(s1,p^); inc(p); inc(s2,s1); end; s1 := s1 mod 65521; s2 := s2 mod 65521; dec(Count,n); end; result := word(s1)+cardinal(word(s2)) shl 16; end; {$endif} function Hex2Dec(c: AnsiChar): byte; begin case c of 'A'..'Z': result := Ord(c) - (Ord('A') - 10); 'a'..'z': result := Ord(c) - (Ord('a') - 10); '0'..'9': result := Ord(c) - Ord('0'); else result := 255; end; end; // Base64 string encoding function Base64Encode(const s: TSockData): TSockData; 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 Base64Decode(const s: TSockData): TSockData; var i, j, len: integer; sp, rp: PAnsiChar; c, ch: integer; begin result:= ''; len := length(s); if (len = 0) or (len mod 4 <> 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[0] := AnsiChar(c shr 16); rp[1] := AnsiChar(c shr 8); rp[2] := AnsiChar(c); inc(rp,3); inc(sp,4); end; end; {$ifndef DOS} function ResolveName(const Name: AnsiString): AnsiString; var l: TStringList; begin l := TStringList.Create; try // use AF_INET+PF_INET instead of AF_UNSPEC+PF_UNSPEC: IP6 is buggy! ResolveNameToIP(Name, AF_INET, PF_INET, SOCK_STREAM, l); if l.Count=0 then result := Name else result := AnsiString(l[0]); finally l.Free; end; end; {$endif} function CallServer(const Server, Port: AnsiString; doBind: boolean; aLayer: TCrtSocketLayer): TSocket; {$ifdef DOS} var SockAddr: TSockAddr; ServerIP: u_long; err, ServerPort: Integer; H: PHostEnt; ConnTimeout: Integer; begin result := -1; // mark error // Check Params val(Port,ServerPort,err); if err<>0 then exit; // Resolve Server IP ServerIP := inet_addr(pointer(Server)); if ServerIP = INADDR_ANY then begin // Doesn't seem to be an IP. Let's try to resolve it with DNS. H := gethostbyname(pointer(Server)); ServerIP := u_long(PInAddr(H^.h_addr^)^); if ServerIP = INADDR_NONE then exit; end; // Create socket to connect to the Server result := socket(PF_INET, SOCK_STREAM, IPPROTO_IP); if result<=0 then begin {$ifdef DEBUG} system.write('[PressAKey]'); readkey; {$endif} {$ifdef DEBUG2} system.Writeln('socket() error: ',WSAGetLastError);{$endif} exit; end; // Set Connect() timeout to 10 seconds (default is 5 seconds) ConnTimeout := 10000; SetSockOpt(result, SOL_SOCKET, SO_CONNTIMEOUT, ConnTimeout, SizeOf(ConnTimeout)); // Establish connection SockAddr.sin_family := PF_INET; SockAddr.sin_addr.S_addr := ServerIP; SockAddr.sin_port := Swap(ServerPort); if bind then begin // call bind if (WDosSock.Bind(result, SockAddr, SizeOf(SockAddr))<>0) or (Listen(result, SOMAXCONN)<>0) then begin CloseSocket(result); result := -1; end; end else if Connect(result, SockAddr, SizeOf(SockAddr))<>0 then begin {$ifdef DEBUG} system.write('[PressAKey]'); readkey; {$endif} {$ifdef DEBUG2} system.Writeln('connect() error: ',WSAGetLastError);{$endif} CloseSocket(result); result := -1; end; end; {$else} // not DOS: var Sin: TVarSin; IP: AnsiString; li: TLinger; SOCK_TYPE, IPPROTO: integer; {$ifndef MSWINDOWS} serveraddr: sockaddr; {$endif} begin result := -1; case aLayer of cslTCP: begin SOCK_TYPE := SOCK_STREAM; IPPROTO := IPPROTO_TCP; end; cslUDP: begin SOCK_TYPE := SOCK_DGRAM; IPPROTO := IPPROTO_UDP; end; cslUNIX: begin {$ifdef MSWINDOWS} exit; // not handled under Win32 {$else} // special version for UNIX sockets result := socket(AF_UNIX,SOCK_STREAM,0); if result<0 then exit; if doBind then begin fillchar(serveraddr,sizeof(serveraddr),0); http://publib.boulder.ibm.com/infocenter/iseries/v5r3/index.jsp?topic=/rzab6/rzab6uafunix.htm serveraddr. if (bind(result,@serveraddr,sizeof(serveraddr))<0) or (listen(result,SOMAXCONN)<0) then begin close(sd); result := -1; end; end; exit; {$endif} end; else exit; // make this stupid compiler happy end; IP := ResolveName(Server); // use AF_INET+PF_INET instead of AF_UNSPEC+PF_UNSPEC: IP6 is buggy! if SetVarSin(Sin, IP, Port, AF_INET, PF_INET, SOCK_TYPE, true)<>0 then exit; result := Socket(integer(Sin.AddressFamily), SOCK_TYPE, IPPROTO); if result=-1 then exit; if doBind then begin // Sockket should remain open for 1 second after a closesocket() call li.l_onoff := Ord(true); li.l_linger := 1; SetSockOpt(result, SOL_SOCKET, SO_LINGER, @li, SizeOf(li)); // bind and listen to this port if (Bind(result, Sin)<>0) or ((aLayer<>cslUDP) and (Listen(result, SOMAXCONN)<>0)) then begin CloseSocket(result); result := -1; end; end else if Connect(result,Sin)<>0 then begin CloseSocket(result); result := -1; end; end; {$endif} function OutputSock(var F: TTextRec): integer; var Index, Size: integer; Sock: TCRTSocket absolute F.Handle; begin if F.BufPos<>0 then begin result := -1; // on socket error -> raise ioresult error if (Sock=nil) or (Sock.Sock=-1) then exit; // file closed Index := 0; repeat Size := Send(Sock.Sock, @F.BufPtr[Index], F.BufPos, 0); if Size<=0 then exit; inc(Sock.BytesOut, Size); dec(F.BufPos,Size); inc(Index,Size); until F.BufPos=0; end; result := 0; // no error 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 absolute F.Handle; begin F.BufEnd := 0; F.BufPos := 0; result := -1; // on socket error -> raise ioresult error if (Sock=nil) or (Sock.Sock=-1) then exit; // file closed = no socket -> error if Sock.TimeOut<>0 then begin // wait for pending data? Size := Sock.SockCanRead(Sock.TimeOut); // Size>0 if some data pending in if Size<=0 then begin // socket error or time out Sock.SockInEof := true; // mark end of SockIn if Size<0 then // socket error result := -WSAGetLastError; // update ioresult and raise {$I+} exception exit; end; IOCtlSocket(Sock.Sock, FIONREAD, Size); // get exact count if Size>integer(F.BufSize) then Size := F.BufSize; end else Size := F.BufSize; Size := Recv(Sock.Sock, F.BufPtr, Size, 0); // Recv() may return Size=0 if no data is pending, but no TCP/IP error if Size>=0 then begin F.BufEnd := Size; inc(Sock.BytesIn, Size); result := 0; // no error end else begin Sock.SockInEof := true; // error -> mark end of SockIn result := -WSAGetLastError; // result <0 will update ioresult and raise an exception if {$I+} end; end; function CloseSock(var F: TTextRec): integer; var Sock: TCRTSocket absolute F.Handle; begin if Sock<>nil then Sock.Close; Sock := 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 } constructor TCrtSocket.Bind(const aPort: AnsiString; aLayer: TCrtSocketLayer=cslTCP); begin OpenBind('0.0.0.0',aPort,true,-1,aLayer); // raise an ECrtSocket exception on error end; constructor TCrtSocket.Open(const aServer, aPort: AnsiString; aLayer: TCrtSocketLayer); begin TimeOut := 5000; // default read timeout is 5 sec OpenBind(aServer,aPort,false,-1,aLayer); // raise an ECrtSocket exception on error end; procedure TCrtSocket.Close; begin if (SockIn<>nil) or (SockOut<>nil) then begin ioresult; // reset ioresult value if SockIn/SockOut were used if SockIn<>nil then begin TTextRec(SockIn^).BufPos := 0; // reset input buffer TTextRec(SockIn^).BufEnd := 0; end; if SockOut<>nil then begin TTextRec(SockOut^).BufPos := 0; // reset output buffer TTextRec(SockOut^).BufEnd := 0; end; end; if Sock=-1 then exit; // no opened connection to close Shutdown(Sock,1); CloseSocket(Sock); Sock := -1; // don't change Server or Port, since may try to reconnect end; procedure TCrtSocket.OpenBind(const aServer, aPort: AnsiString; doBind: boolean; aSock: integer=-1; aLayer: TCrtSocketLayer=cslTCP); begin if aPort='' then Port := '80' else // default port is 80 (HTTP) Port := aPort; if aSock<0 then Sock := CallServer(aServer,Port,doBind,aLayer) else // OPEN or BIND Sock := aSock; // ACCEPT mode -> socket is already created by caller if Sock=-1 then raise ECrtSocket.CreateFmt('Socket Creation error on %s:%s (%d)', [aServer,Port,WSAGetLastError]); Server := aServer; end; const CRLF: array[0..1] of AnsiChar = (#13,#10); 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: Snd(@VString^[1], pByte(VString)^); vtAnsiString: Snd(VAnsiString, length(TSockData(VAnsiString))); {$ifdef UNICODE} vtUnicodeString: begin tmp := shortstring(UnicodeString(VUnicodeString)); // convert into ansi (max length 255) Snd(@tmp[1],length(tmp)); end; {$endif} vtPChar: Snd(VPChar, StrLen(VPChar)); vtChar: Snd(@VChar, 1); vtWideChar: Snd(@VWideChar,1); // only ansi value vtInteger: begin Str(VInteger,tmp); Snd(@tmp[1],length(tmp)); end; end; Snd(@CRLF, 2); end; procedure TCrtSocket.SockSend(const Line: TSockData); begin if Line<>'' then Snd(pointer(Line),length(Line)); Snd(@CRLF, 2); end; procedure TCrtSocket.SockSendFlush; begin if SndBufLen=0 then exit; SndLow(pointer(SndBuf), SndBufLen); SndBufLen := 0; end; procedure TCrtSocket.SndLow(P: pointer; Len: integer); begin if not TrySndLow(P,Len) then raise ECrtSocket.Create('SndLow'); end; function TCrtSocket.TrySndLow(P: pointer; Len: integer): boolean; var SentLen: integer; begin result := false; if (self=nil) or (Len<=0) or (P=nil) then exit; {$ifndef DOS} if (TimeOut<>0) and (SockCanWrite(TimeOut)<=0) then exit; {$endif} repeat SentLen := Send(Sock, P, Len, 0); if SentLen<0 then exit; dec(Len,SentLen); if Len<=0 then break; inc(BytesOut,SentLen); inc(cardinal(P),SentLen); until false; result := true; end; procedure TCrtSocket.Write(const Data: TSockData); begin SndLow(pointer(Data),length(Data)); end; function TCrtSocket.SockCanRead(aTimeOut: cardinal): integer; {$ifdef DOS} // DWPL select() don't implement TimeOut -> do it here var TimeOut: Int64; begin TimeOut := GetMSCounter+aTimeOut; repeat IOCtlSocket(Sock, FIONREAD, result); if result>0 then // there is some data pending in exit; Yield; until GetMSCounter>TimeOut; result := 0; // time out end; {$else} var TimeV: TTimeVal; FDSet: TFDSet; begin // init select() parameters TimeV.tv_usec := (aTimeout mod 1024) * 1024; TimeV.tv_sec := aTimeout div 1024; FDSet.fd_count := 1; // is changed by select() -> recreate each time FDSet.fd_array[0] := Sock; // ask if any data pending in result := select(Sock+1, @FDSet, nil, nil, @TimeV); end; {$endif} {$ifndef DOS} // DWPL doesn't implement select() for send :( // -> we won't use this function with DWPL: it always allows to send data, // since the socket is connected function TCrtSocket.SockCanWrite(aTimeOut: cardinal): integer; var TimeV: TTimeVal; FDSet: TFDSet; begin // init select() parameters TimeV.tv_usec := (aTimeout mod 1024) * 1024; TimeV.tv_sec := aTimeout div 1024; FDSet.fd_count := 1; // is changed by select() -> recreate each time FDSet.fd_array[0] := Sock; // ask if any data can be written result := select(Sock+1, nil, @FDSet, nil, @TimeV); end; {$endif} function TCrtSocket.SockInRead(Content: PAnsiChar; Length: integer): integer; // read Length bytes from SockIn^ buffer + Sock if necessary begin // get data from SockIn buffer, if any (faster than ReadChar) if SockIn<>nil then with TTextRec(SockIn^) do begin result := BufEnd-BufPos; if result>0 then begin if result>Length then result := Length; move(BufPtr[BufPos],Content^,result); inc(BufPos,result); inc(Content,result); dec(Length,result); end; end else result := 0; // direct receiving of the triming bytes from socket if Length>0 then begin SockRecv(Content,Length); inc(result,Length); end; end; destructor TCrtSocket.Destroy; begin Close; if SockIn<>nil then Freemem(SockIn); if SockOut<>nil then Freemem(SockOut); inherited; end; procedure TCrtSocket.Snd(P: pointer; Len: integer); begin if Len<=0 then exit; if integer(SndBuf)=0 then if Len<2048 then // 2048 is about FASTMM4 small block size SetLength(SndBuf,2048) else SetLength(SndBuf,Len) else if Len+SndBufLen>pInteger(integer(SndBuf)-4)^ then SetLength(SndBuf,pInteger(integer(SndBuf)-4)^+Len+2048); move(P^,pointer(integer(SndBuf)+SndBufLen)^,Len); inc(SndBufLen,Len); end; const SOCKBUFSIZE = 1024; // big enough for headers (content will be read directly) procedure TCrtSocket.CreateSockIn; begin if SockIn<>nil then exit; // initialization already occured Getmem(SockIn,SOCKBUFSIZE+(sizeof(TTextRec)-sizeof(TTextBuf))); with TTextRec(SockIn^) do begin Handle := integer(self); Mode := fmClosed; BufSize := SOCKBUFSIZE; BufPtr := Buffer; OpenFunc := @OpenSock; end; SetLineBreakStyle(SockIn^,tlbsCRLF); // http does break lines with #13#10 Reset(SockIn^); end; procedure TCrtSocket.CreateSockOut; begin if SockOut<>nil then exit; // initialization already occured Getmem(SockOut,SOCKBUFSIZE+(sizeof(TTextRec)-sizeof(TTextBuf))); with TTextRec(SockOut^) do begin Handle := integer(self); Mode := fmClosed; BufSize := SOCKBUFSIZE; BufPtr := Buffer; OpenFunc := @OpenSock; end; SetLineBreakStyle(SockOut^,tlbsCRLF); Rewrite(SockOut^); end; procedure TCrtSocket.SockRecv(Buffer: pointer; Length: integer); begin if not TrySockRecv(Buffer,Length) then raise ECrtSocket.Create('SockRecv'); end; function TCrtSocket.TrySockRecv(Buffer: pointer; Length: integer): boolean; var Size: integer; begin result := false; if self=nil then exit; if (Buffer<>nil) and (Length>0) then repeat if TimeOut<>0 then begin // wait for pending data? Size := SockCanRead(TimeOut); // Size>0 if some data pending in if Size<=0 then // socket error or time out exit; end; Size := Recv(Sock, Buffer, Length, 0); if Size<=0 then exit; inc(BytesIn, Size); dec(Length,Size); inc(cardinal(Buffer),Size); until Length=0; result := true; end; procedure TCrtSocket.SockRecvLn(out Line: TSockData); procedure RecvLn(var Line: TSockData); var P: PAnsiChar; LP, L: integer; 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,pointer(integer(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,pointer(integer(Line)+L)^,1024); P := tmp; end else inc(P); until false; end; begin if SockIn<>nil then begin {$I-} readln(SockIn^,Line); // example: HTTP/1.0 200 OK if ioresult<>0 then raise ECrtSocket.Create('SockRecvLn'); {$I+} end else RecvLn(Line); // slow under Windows -> use SockIn^ instead end; procedure TCrtSocket.SockRecvLn; var c: AnsiChar; begin if SockIn<>nil then begin {$I-} readln(SockIn^); if ioresult<>0 then raise ECrtSocket.Create('SockRecvLn'); {$I+} end else repeat SockRecv(@c,1); until c=#10; end; function TCrtSocket.SockConnected: boolean; {$ifdef DOS} var SockAddr: TSockAddr; x: integer; begin x := sizeof(SockAddr); result := GetPeerName(Sock, SockAddr, x)=0; end; {$else} var Sin: TVarSin; begin result := GetPeerName(Sock,Sin)=0; end; {$endif} function TCrtSocket.SockReceiveString(TimeOut: integer): TSockData; var Size, L, Read: integer; begin result := ''; if self=nil then exit; if (TimeOut<>0) and (SockCanRead(TimeOut)<=0) then exit; L := 0; repeat sleep(1); if IOCtlSocket(Sock, FIONREAD, Size)<>0 then // get exact count exit; if Size=0 then if (TimeOut=0) and (result='') then begin // TimeOut=0 -> wait till something SockCanRead(100); // 100 ms delay in infinite loop continue; end else break; SetLength(result,L+Size); // append to result Read := recv(Sock,PAnsiChar(pointer(result))+L,Size,0); inc(L,Read); if Read<Size then SetLength(result,L); // e.g. Read=0 may happen until false; end; { THttpClientSocket } function THttpClientSocket.Delete(const url: TSockData; KeepAlive: cardinal; const header: TSockData): integer; begin result := Request(url,'DELETE',KeepAlive,header,'','',false); end; function THttpClientSocket.Get(const url: TSockData; KeepAlive: cardinal=0; const header: TSockData=''): integer; begin result := Request(url,'GET',KeepAlive,header,'','',false); end; function THttpClientSocket.Head(const url: TSockData; KeepAlive: cardinal; const header: TSockData): integer; begin result := Request(url,'HEAD',KeepAlive,header,'','',false); end; function THttpClientSocket.Post(const url, Data, DataType: TSockData; KeepAlive: cardinal; const header: TSockData): integer; begin result := Request(url,'POST',KeepAlive,header,Data,DataType,false); end; function THttpClientSocket.Put(const url, Data, DataType: TSockData; KeepAlive: cardinal; const header: TSockData): integer; begin result := Request(url,'PUT',KeepAlive,header,Data,DataType,false); end; {$ifndef WITHPHD} function IdemPChar(p, up: pAnsiChar): boolean; // if the beginning of p^ is same as up^ (ignore case - up^ must be already Upper) var c: AnsiChar; begin result := false; if (p=nil) or (up=nil) then exit; while up^<>#0 do begin c := p^; if up^<>c then if c in ['a'..'z'] then begin dec(c,32); if up^<>c then exit; end else exit; inc(up); inc(p); end; result := true; end; {$endif} function GetNextItem(var P: PAnsiChar; Sep: AnsiChar = ','): TSockData; // 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; {$ifdef UNICODE} // rewrite some functions to avoid unncessary ansi<->unicode conversion function Trim(const S: TSockData): TSockData; 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 // RawByteString code page for Delphi 2009/2010 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; function UpperCase(const S: TSockData): TSockData; procedure Upper(Source, Dest: PAnsiChar; L: cardinal); var Ch: AnsiChar; // this sub-call is shorter and faster than 1 plain proc begin repeat Ch := Source^; if (Ch >= 'a') and (Ch <= 'z') then dec(Ch, 32); Dest^ := Ch; dec(L); inc(Source); inc(Dest); until L=0; end; var L: cardinal; begin result := ''; L := Length(S); if L=0 then exit; SetLength(result, L); Upper(pointer(S),pointer(result),L); end; {$endif} function GetCardinal(P: PAnsiChar): cardinal; 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; {$ifdef DOS} var CMOSClockSet: boolean = false; {$endif} function THttpClientSocket.Request(const url, method: TSockData; KeepAlive: cardinal; const Header, Data, DataType: TSockData; retry: boolean): integer; procedure DoRetry(Error: integer); begin if retry then // retry once -> return error if already retried result := Error else begin Close; // close this connection try OpenBind(Server,Port,false); // then retry this request with a new socket result := Request(url,method,KeepAlive,Header,Data,DataType,true); except on Exception do result := Error; end; end; end; var DataLen: integer; P: PAnsiChar; begin {$ifdef WIN32} if SockIn=nil then // done once CreateSockIn; // use SockIn by default if not already initialized: 2x faster {$endif} Content := ''; {$ifdef DEBUG2}system.write(#13#10,method,' ',url); if Retry then system.Write(' RETRY');{$endif} if Sock=-1 then DoRetry(404) else // socket closed (e.g. KeepAlive=0) -> reconnect try try {$ifdef DEBUG23}system.write(' Send');{$endif} // send request - we use SockSend because writeln() is calling flush() // -> all header will be sent at once DataLen := length(Data); SockSend([method, ' ', url, ' HTTP/1.1'#13#10+ 'Accept: */*'#13#10'Host: ', Server]); if UserAgent='' then SockSend('User-Agent: Mozilla/4.0 (compatible; MSIE 5.5; Windows; FREE)') else SockSend(['User-Agent: ',UserAgent]); // for POST/PUT: always put content byte count to be sent (even 0) SockSend(['Content-Length: ', DataLen]); {$ifdef WITHCHECKSUM} if DataLen<>0 then SockSend(['Content-Checksum: ', IntToHex(Adler32(0,pointer(Data),DataLen),1)]); {$endif} if DataType<>'' then SockSend(['Content-Type: ',DataType]); if KeepAlive>0 then SockSend(['Keep-Alive: ',KeepAlive,#13#10'Connection: Keep-Alive']) else SockSend('Connection: Close'); if header<>'' then SockSend(header); {$ifdef WITHSERVERTIME} {$ifdef DOS} if not CMOSClockSet then SockSend('SERVER-TIME: ASK'); {$endif} {$endif} SockSend; // send CRLF {$ifdef DEBUG23} SndBuf[SndBufLen+1] := #0; system.Writeln(#13#10'HeaderOut ',PAnsiChar(SndBuf));{$endif} SockSendFlush; // flush all pending data (i.e. headers) to network if DataLen<>0 then // for POST and PUT methods: content to be sent SndLow(pointer(Data),DataLen); // no CRLF at the end of data {$ifdef DEBUG23}system.write('OK ');{$endif} // get headers SockRecvLn(Command); // will raise ECrtSocket on any error {$ifdef DEBUG23}system.write(Command);{$endif} P := pointer(Command); if IdemPChar(P,'HTTP/1.') then begin if P[7]='0' then KeepAlive := 0; // HTTP/1.0 -> force connection close inc(P,9); result := GetCardinal(P); // get http numeric status code if result=0 then begin result := 505; exit; end; end else begin // error on reading answer DoRetry(505); // 505=wrong format exit; end; GetHeader; // read all other headers {$ifdef DEBUG23}system.write('OK Body');{$endif} if not IdemPChar(pointer(method),'HEAD') then GetBody; // get content if necessary (not HEAD method) except on Exception do DoRetry(404); end; finally if KeepAlive=0 then Close; end; end; function Open(const aServer, aPort: AnsiString): TCrtSocket; begin try result := TCrtSocket.Open(aServer,aPort); except on ECrtSocket do result := nil; end; end; function OpenHttp(const aServer, aPort: AnsiString): THttpClientSocket; begin try result := THttpClientSocket.Open(aServer,aPort); except on ECrtSocket do result := nil; end; end; function HttpGet(const server, port: AnsiString; const url: TSockData): TSockData; var Http: THttpClientSocket; begin result := ''; Http := OpenHttp(server,port); if Http<>nil then try if Http.Get(url)=200 then result := Http.Content; finally Http.Free; end; end; function SendEmail(const Server: AnsiString; const From, CSVDest, Subject, Text: TSockData; const Headers: TSockData=''; const User: TSockData=''; const Pass: TSockData=''; const Port: AnsiString='25'): boolean; var TCP: TCrtSocket; procedure Expect(const Answer: TSockData); var Res: TSockData; begin repeat readln(TCP.SockIn^,Res); until (Length(Res)<4)or(Res[4]<>'-'); if not IdemPChar(pointer(Res),pointer(Answer)) then raise Exception.Create(string(Res)); end; procedure Exec(const Command, Answer: TSockData); begin writeln(TCP.SockOut^,Command); Expect(Answer) end; var P: PAnsiChar; rec, ToList: TSockData; begin result := false; P := pointer(CSVDest); if P=nil then exit; TCP := Open(Server, Port); 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(Base64Encode(User),'334'); Exec(Base64Encode(Pass),'235'); end else Exec('HELO '+Server,'25'); writeln(TCP.SockOut^,'MAIL FROM:<',From,'>'); Expect('250'); ToList := 'To: '; repeat rec := trim(GetNextItem(P)); if rec='' then continue; if pos(TSockData('<'),rec)=0 then rec := '<'+rec+'>'; Exec('RCPT TO:'+rec,'25'); ToList := ToList+rec+', '; until P=nil; Exec('DATA','354'); writeln(TCP.SockOut^,'Subject: ',Subject,#13#10, ToList,#13#10'Content-Type: text/plain; charset=ISO-8859-1'#13#10+ 'Content-Transfer-Encoding: 8bit'#13#10, Headers,#13#10#13#10,Text); Exec('.','25'); writeln(TCP.SockOut^,'QUIT'); result := true; finally TCP.Free; end; end; {$ifndef DOS} // without DWPL TCP/IP stack: server Thread var WsaDataOnce: TWSADATA; {$endif} {$ifndef NOSERVER} { THttpServer } constructor THttpServer.Create(const aPort: AnsiString); var aSock: TCrtSocket; begin aSock := TCrtSocket.Bind(aPort); // BIND + LISTEN inherited Create(false); Sock := aSock; InitializeCriticalSection(ProcessCS); ServerKeepAliveTimeOut := 3000; // HTTP.1/1 KeepAlive is 3 seconds by default end; destructor THttpServer.Destroy; begin Terminate; // THttpServerResp.Execute expects Terminated if we reached here // ThreadID := 0 -> don't call WaitFor in Destroy: Execute.Accept() is blocking {$ifdef Linux} pthread_detach(ThreadID); // manualy do it here {$endif} PInteger(@ThreadID)^ := 0; // trick to access a read-only property inherited Destroy; // direct Thread abort, no wait till ended DeleteCriticalSection(ProcessCS); FreeAndNil(Sock); end; {.$define MONOTHREAD} // define this not to create a thread at every connection procedure THttpServer.Execute; var ClientSock: TSocket; Sin: TVarSin; {$ifdef MONOTHREAD} ClientCrtSock: THttpServerSocket; {$endif} begin if Sock.Sock>0 then while not Terminated do begin ClientSock := Accept(Sock.Sock,Sin); if Terminated or (Sock=nil) then begin Shutdown(ClientSock,1); CloseSocket(ClientSock); break; // don't accept input if server is down end; OnConnect; {$ifdef MONOTHREAD} ClientCrtSock := THttpServerSocket.Create; try ClientCrtSock.GetRequest(ClientSock); Process(ClientCrtSock); OnDisconnect; finally ClientCrtSock.Free; // call CloseSocket(ClientSock) end; {$else} THttpServerResp.Create(ClientSock, self); {$endif} end; end; function NowToDosDateTime: integer; var SystemTime: TSystemTime; begin GetLocalTime(SystemTime); with SystemTime do begin LongRec(result).Lo := (wSecond shr 1) or (wMinute shl 5) or (wHour shl 11); LongRec(result).Hi := wDay or (wMonth shl 5) or ((wYear - 1980) shl 9); end; end; procedure THttpServer.OnConnect; begin inc(ServerConnectionCount); end; procedure THttpServer.OnDisconnect; begin // nothing to do by default end; {$ifdef COMPRESS} const GZIP_LEVEL = 1; // 6 is standard, but 1 is enough and faster // w/deflate e.g (uS/bytes for 4803 bytes in): // 1=140/700 2=146/707 3=157/709 5=250/696 6=280/698 7=380/690 9=420/690 {.$define PERF} function GZCompress(const Source: TSockData): TSockData; {$ifdef COMPRESS_GZIP} // performance note: both compression have the same overhead, // about 110us for 425 bytes of data const gzheader : array [0..2] of cardinal = ($88B1F,0,0); var L: integer; P: PAnsiChar; {$ifdef PERF}T: TPrecisionTimer;{$endif} begin {$ifdef PERF}T.Start;{$endif} L := length(Source); SetLength(result,L+128+L shr 3); // maximum possible memory required P := pointer(result); move(gzheader,P^,10); inc(P,10); inc(P,CompressMem(pointer(Source),P,L,length(result)-20,GZIP_LEVEL)); PCardinal(P)^ := crc32(0,pointer(source),L); inc(P,4); PCardinal(P)^ := L; inc(P,4); SetLength(result,P-pointer(result)); {$ifdef PERF}T.Stop; writeln(L,'->',length(result),':',T.Time);{$endif} end; {$else} var strm: TZStream; {$ifdef PERF}T: TPrecisionTimer;{$endif} begin {$ifdef PERF}T.Start;{$endif} strm.Init; strm.next_in := pointer(Source); strm.avail_in := length(Source); SetLength(result,strm.avail_in+256+strm.avail_in shr 3); // max mem required strm.next_out := pointer(result); strm.avail_out := length(result); // +MAX_WBITS below = encode in deflate format // Z_HUFFMAN_ONLY instead of Z_DEFAULT_STRATEGY is slowest and bad if deflateInit2_(strm, GZIP_LEVEL, Z_DEFLATED, +MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY, ZLIB_VERSION, sizeof(strm))>=0 then try Check(deflate(strm,Z_FINISH),[Z_STREAM_END]); finally deflateEnd(strm); end; SetLength(result,strm.total_out); {$ifdef PERF}T.Stop; writeln(strm.total_in,'->',strm.total_out,':',T.Time);{$endif} end; {$endif} {$endif} procedure THttpServer.Process(var ClientSock: THttpServerSocket); var Data: TSockData; DataLen: cardinal; Code: cardinal; i: integer; s: TSockData; begin if ClientSock.Headers=nil then // we didn't get the request = socket read error exit; // -> send will probably fail -> nothing to send back if Terminated then exit; // calc answer (unique thread) try EnterCriticalSection(ProcessCS); // calculation thread protection Code := Request(ClientSock,Data); finally LeaveCriticalSection(ProcessCS); end; if Terminated then exit; // send response (multi-thread OK) at once if (Code<200) or (ClientSock.Headers=nil) then Code := 404; if not(Code in [200,201]) and (Data='') then begin Setlength(ClientSock.Headers,0); ClientSock.ContentType := 'text/html'; // create message to display Data := TSockData(ClassName+' Server Error '+IntToStr(Code) +'<hr>'+StringReplace(string(s),#13#10,'<br>',[rfReplaceAll])); end; // 1. send HTTP status command if ClientSock.KeepAliveClient then ClientSock.SockSend(['HTTP/1.1 ',Code,' OK']) else ClientSock.SockSend(['HTTP/1.0 ',Code,' OK']); // 2. send headers for i := 0 to high(ClientSock.Headers) do begin s := ClientSock.Headers[i]; if s<>'' then begin // no void line (means headers ending) ClientSock.SockSend(s); {$ifdef COMPRESS} if IdemPChar(pointer(s),'CONTENT-ENCODING:') then ClientSock.AcceptGZip := false; // custom encoding: don't GZIP {$endif} end; end; ClientSock.SockSend(['X-Powered-By: SynCrtSock http://synopse.info'#13#10+ 'Server: ',ClassName]); {$ifdef COMPRESS} if ClientSock.AcceptGZip and (Data<>'') then begin s := UpperCase(ClientSock.ContentType); if (s<>'') and (length(Data)+ClientSock.SndBufLen>1024) and (IdemPChar(pointer(s),'TEXT/') or (pos('/JSON',string(s))>0)) then begin // update header ClientSock.SockSend('Content-Encoding: '+ {$ifdef COMPRESS_GZIP}'gzip'{$else}'deflate'{$endif}); // on the fly gzip of the content Data := GZCompress(Data); end else ClientSock.AcceptGZip := false; end; {$endif} DataLen := length(Data); ClientSock.SockSend(['Content-Length: ',DataLen]); // need size, even 0 if Data<>'' then begin if ClientSock.ContentType<>'' then ClientSock.SockSend(['Content-Type: ',ClientSock.ContentType]); {$ifdef WITHCHECKSUM} ClientSock.SockSend(['Content-Checksum: ', IntToHex(Adler32(0,pointer(Data),DataLen),1)]); {$endif} end; {$ifdef WITHSERVERTIME} if ClientSock.NeedServerTime then ClientSock.SockSend(['Server-Time: ',IntToHex(NowToDosDateTime,1)]); {$endif} if ClientSock.KeepAliveClient then ClientSock.SockSend('Connection: Keep-Alive'#13#10) else ClientSock.SockSend; // headers end with a void line ClientSock.SockSendFlush; // flush all pending data (i.e. headers) to network // 3. sent HTTP body content (if any) if Data<>'' then // no CRLF at the end of data ClientSock.SndLow(pointer(Data),DataLen); // direct send to socket end; { THttpServerResp } constructor THttpServerResp.Create(aSock: TSocket; aServer: THttpServer); begin inherited Create(false); FreeOnTerminate := true; fServer := aServer; fClientSock := aSock; fServerSock := THttpServerSocket.Create; end; destructor THttpServerResp.Destroy; begin fServerSock.Free; if fClientSock<>0 then begin // if Destroy happens before fServerSock.GetRequest() in Execute below Shutdown(fClientSock,1); CloseSocket(fClientSock); end; inherited Destroy; end; procedure THttpServerResp.Execute; var c: char; Loop, LoopEnd, LoopMax: integer; aSock: TSocket; const LOOPWAIT = 64; // ms sleep beetwen connections label S; begin aSock := fClientSock; fClientSock := 0; // mark no need to Shutdown and close fClientSock fServerSock.InitRequest(aSock); // now fClientSock is in fServerSock LoopEnd := fServer.ServerKeepAliveTimeOut div LOOPWAIT; LoopMax := LoopEnd+10; Loop := 0; goto S; repeat // get request and headers fServerSock.GetRequest; // calc answer and send response fServer.Process(fServerSock); Loop := 0; if not fServerSock.KeepAliveClient then break; repeat // KeepAlive persistent connection S: if not fServerSock.SockConnected then break; if fServer.Terminated then exit; if (fServerSock.SockCanRead(LOOPWAIT)>0) and (Recv(fServerSock.Sock,@c,1,MSG_PEEK)>0) then Loop := LoopMax; inc(Loop); until Loop>=LoopEnd; // reached ServerKeepAliveTimeOut? until Loop<LoopMax; if fServer<>nil then fServer.OnDisconnect; if (fServer<>nil) and (fServer.Sock<>nil) then try EnterCriticalSection(fServer.ProcessCS); // fServer thread protection inc(fServer.Sock.BytesIn,fServerSock.BytesIn); inc(fServer.Sock.BytesOut,fServerSock.BytesOut); finally LeaveCriticalSection(fServer.ProcessCS); end; end; {$endif} { THttpSocket } function PCharToHex32(p: PAnsiChar): cardinal; var v0,v1: byte; begin result := 0; if p<>nil then begin while p^=' ' do inc(p); repeat v0 := Hex2Dec(p[0]); if v0=255 then break; // not in '0'..'9','a'..'f' v1 := Hex2Dec(p[1]); inc(p); if v1=255 then begin result := (result shl 4)+v0; // only one char left break; end; v0 := v0 shl 4; result := result shl 8; inc(v0,v1); inc(p); inc(result,v0); until false; end; end; procedure THttpSocket.GetBody; var Line: TSockData; // 32 bits chunk length in hexa LinePChar: array[0..31] of AnsiChar; Len, LContent: integer; begin {$ifdef DEBUG23}system.writeln('GetBody ContentLength=',ContentLength);{$endif} Content := ''; {$I-} // direct read bytes, as indicated by Content-Length or Chunked if Chunked 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 if ioresult<>0 then raise ECrtSocket.Create('GetBody1'); Len := PCharToHex32(LinePChar); // get chunk length in hexa end else begin SockRecvLn(Line); Len := PCharToHex32(pointer(Line)); // get chunk length in hexa end; if Len=0 then begin // ignore next line (normaly void) SockRecvLn; break; end; SetLength(Content,LContent+Len); // reserve memory space for this chunk SockInRead(pointer(integer(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 then begin // ContentLength=-1 if no Content-Length // no Content-Length nor Chunked header -> read until eof() if SockIn<>nil then 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; ContentLength := length(Content); // update Content-Length {$ifdef WITHCHECKSUM} if (Content<>'') and (ContentChecksum<>0) then if ContentChecksum<>Adler32(0,pointer(Content),ContentLength) then raise ECrtSocket.Create('CHECKSUM'); {$endif} if (SockIn<>nil) and (ioresult<>0) then raise ECrtSocket.Create('GetBody2'); {$I+} end; procedure THttpSocket.GetHeader; var s: TSockData; {$ifdef COMPRESS} P: PAnsiChar; {$endif} n: integer; begin ContentType := ''; ContentLength := -1; {$ifdef WITHCHECKSUM} ContentChecksum := 0; {$endif} {$ifdef COMPRESS} AcceptGZip := false; {$endif} Chunked := false; n := 0; repeat SockRecvLn(s); if s='' then break; // headers end with a void line if length(Headers)<=n then SetLength(Headers,n+10); Headers[n] := s; inc(n); {$ifdef DEBUG23}system.Writeln(ClassName,'.HeaderIn ',s);{$endif} if IdemPChar(pointer(s),'CONTENT-LENGTH:') then ContentLength := GetCardinal(pointer(PtrInt(s)+16)) else if IdemPChar(pointer(s),'CONTENT-TYPE:') then ContentType := trim(copy(s,14,128)) else {$ifdef WITHCHECKSUM} if IdemPChar(pointer(s),'CONTENT-CHECKSUM:') then ContentChecksum := PCharToHex32(@s[18]) else {$endif} {$ifdef WITHSERVERTIME} {$ifdef DOS} if not CMOSClockSet and IdemPChar(pointer(s),'SERVER-TIME:') then begin CMOSClockSet := true; SetSystemTime(PCharToHex32(@s[13])); end else {$endif} {$endif} if IdemPChar(pointer(s),'TRANSFER-ENCODING: CHUNKED') then Chunked := true else {$ifdef COMPRESS} if IdemPChar(pointer(s),'ACCEPT-ENCODING:') then begin P := pointer(s); inc(P,16); repeat while P^ in [' ',','] do inc(P); if PInteger(P)^ and $dfdfdfdf= {$ifdef COMPRESS_GZIP} ord('G')+ord('Z')shl 8+ord('I')shl 16+ord('P')shl 24 then begin {$else} ord('D')+ord('E')shl 8+ord('F')shl 16+ord('L')shl 24 then begin {$endif} AcceptGZip := true; break; end; while not (P^ in [',',#0]) do inc(P); until P^=#0; end; {$endif} until false; SetLength(Headers,n); end; function THttpSocket.HeaderAdd(const aValue: TSockData): integer; begin result := length(Headers); SetLength(Headers,result+1); Headers[result] := aValue; end; procedure THttpSocket.HeaderSetText(const aText: TSockData); var P, PDeb: PAnsiChar; n: integer; begin P := pointer(aText); n := 0; if P<>nil then repeat PDeb := P; while P^>#13 do inc(P); if PDeb<>P then begin // add any not void line if length(Headers)<=n then SetLength(Headers,n+10); SetString(Headers[n],PDeb,P-PDeb); inc(n); end; while (P^=#13) or (P^=#10) do inc(P); until P^=#0; SetLength(Headers,n); end; function THttpSocket.HeaderGetText: TSockData; var i,V,L,n: PtrInt; P: PAnsiChar; begin // much faster than for i := 0 to Count-1 do result := result+Headers[i]+#13#10; result := ''; n := length(Headers); if n=0 then exit; L := n*2; // #13#10 size dec(n); for i := 0 to n do if PtrInt(Headers[i])<>0 then inc(L,PPtrInt(PtrInt(Headers[i])-4)^); // fast add length(List[i]) SetLength(result,L); P := pointer(result); for i := 0 to n do begin V := PtrInt(Headers[i]); if V<>0 then begin L := PPtrInt(V-4)^; // L := length(List[i]) move(pointer(V)^,P^,L); inc(P,L); end; PWord(P)^ := 13+10 shl 8; inc(P,2); end; end; function THttpSocket.HeaderValue(aName: TSockData): TSockData; var i: integer; begin if Headers<>nil then begin aName := UpperCase(aName)+':'; for i := 0 to high(Headers) do if IdemPChar(pointer(Headers[i]),pointer(aName)) then begin result := trim(copy(Headers[i],length(aName)+1,maxInt)); exit; end; end; result := ''; end; {$ifndef NOSERVER} // without DWPL TCP/IP stack: server Thread { THttpServerSocket } procedure THttpServerSocket.InitRequest(aClientSock: TSocket); begin {$ifdef WIN32} CreateSockIn; // use SockIn by default if not already initialized: 2x faster {$endif} OpenBind('','',false,aClientSock); // open aClientSock for reading end; procedure THttpServerSocket.GetRequest; var P: PAnsiChar; i: integer; begin if not SockConnected then exit; // 1st line is command: 'GET /path HTTP/1.1' e.g. SockRecvLn(Command); P := pointer(Command); Method := GetNextItem(P,' '); // 'GET' URL := GetNextItem(P,' '); // '/path' KeepAliveClient := IdemPChar(P,'HTTP/1.1'); {$ifdef WITHSERVERTIME} NeedServerTime := false; {$endif} Content := ''; // get headers and content GetHeader; if (ContentLength<0) and KeepAliveClient then ContentLength := 0; // HTTP/1.1 and no content length -> no eof for i := 0 to high(Headers) do {$ifdef WITHSERVERTIME} if IdemPChar(pointer(Headers[i]),'SERVER-TIME: ASK') then NeedServerTime := true else {$endif} if IdemPChar(pointer(Headers[i]),'CONNECTION: CLOSE') then KeepAliveClient := false; GetBody; end; {$endif} { ECrtSocket } constructor ECrtSocket.Create(const Msg: string); begin inherited CreateFmt('%s %d',[Msg,WSAGetLastError]); end; {$ifdef Win32} function GetRemoteMacAddress(const IP: AnsiString): TSockData; // implements http://msdn.microsoft.com/en-us/library/aa366358(VS.85).aspx type TSendARP = function(DestIp: DWORD; srcIP: DWORD; pMacAddr: pointer; PhyAddrLen: Pointer): DWORD; stdcall; const HexChars: array[0..15] of AnsiChar = '0123456789ABCDEF'; var dwRemoteIP: DWORD; PhyAddrLen: Longword; pMacAddr : array [0..7] of byte; I: integer; P: PAnsiChar; SendARPLibHandle: THandle; SendARP: TSendARP; begin result := ''; SendARPLibHandle := LoadLibrary('iphlpapi.dll'); if SendARPLibHandle<>0 then try SendARP := GetProcAddress(SendARPLibHandle,'SendARP'); if @SendARP=nil then exit; // we are not under 2K or later 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 begin SetLength(result,12); P := pointer(result); for i := 0 to 5 do begin P[0] := HexChars[pMacAddr[i] shr 4]; P[1] := HexChars[pMacAddr[i] and $F]; inc(P,2); end; end; end; end; finally FreeLibrary(SendARPLibHandle); end; end; {$endif} initialization {$ifdef DOS} // Network and TCPIP initialization must appears in the program main: // InitNetwork(TNE2000Adapter.Create($220)); e.g. (BUGGY) // InitNetwork(TPacketDriverAdapter.Create); e.g. with uses WDosPktDrv; asm mov ah,2ah int 21h cmp cx,2008 jb @1 mov CMOSClockSet,true // CMOS in 21th century -> don't get time from server @1:end; {$else} if InitSocketInterface then WSAStartup(WinsockLevel, WsaDataOnce) else fillchar(WsaDataOnce,sizeof(WsaDataOnce),0); {$endif} finalization {$ifndef DOS} if WsaDataOnce.wVersion<>0 then WSACleanup; DestroySocketInterface; {$endif} end.