{ Copyright (C) 2002-2014 Massimo Melina (www.rejetto.com) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA HTTP Server Lib ==== TO DO * upload bandwidth control (can it be done without multi-threading?) } {$I- } unit HSlib; interface uses OverbyteIcsWSocket, classes, messages, winprocs, forms, extctrls, sysutils, system.contnrs, strUtils, winsock, inifiles, types; const VERSION = '2.11.0'; type ThttpSrv=class; ThttpConn=class; ThttpMethod=( HM_UNK, HM_GET, HM_POST, HM_HEAD ); ThttpEvent=( HE_OPEN, // server is listening HE_CLOSE, // server does not listen anymore HE_CONNECTED, // a client just connected HE_DISCONNECTED, // client communication terminated HE_GOT, // other peer sent sth HE_SENT, // we sent sth HE_REQUESTING, // a possible new request starts here HE_GOT_HEADER, // header part was fully received HE_REQUESTED, // a full request has been submitted HE_STREAM_READY, // reply stream ready HE_REPLIED, // the reply has been sent HE_POST_FILE, // new file is posted HE_POST_MORE_FILE, // more data has come for the previous file HE_POST_END_FILE, // last file done HE_POST_VARS, // variables are available HE_POST_VAR, // single variable available HE_POST_END, // POST section terminated HE_LAST_BYTE_DONE, // useful to count full downloads HE_CANT_OPEN_FILE // error ); ThttpConnState=( HCS_IDLE, // connected but idle HCS_REQUESTING, // getting request HCS_POSTING, // getting post data HCS_REPLYING, // a reply is pending HCS_REPLYING_HEADER, // sending header HCS_REPLYING_BODY, // sending body HCS_DISCONNECTED // disconnected ); ThttpReplyMode=( HRM_REPLY, // reply header+body HRM_REPLY_HEADER, // reply header only HRM_DENY, // answer a deny code HRM_UNAUTHORIZED, // bad user/pwd HRM_NOT_FOUND, // answer a not-found code HRM_BAD_REQUEST, // answer a bad-request code HRM_INTERNAL_ERROR, // answer an internal-error code HRM_CLOSE, // close connection with no reply HRM_IGNORE, // does nothing, connection remains open HRM_METHOD_NOT_ALLOWED, // answer a method-not-allowed code HRM_REDIRECT, // redirection to another URL HRM_OVERLOAD, // server is overloaded, retry later HRM_TOO_LARGE, // the request has exceeded the max length allowed HRM_MOVED, // moved permanently to another url HRM_NOT_MODIFIED // use the one in your cache, client ); ThttpReply = record mode: ThttpReplyMode; header: ansistring; // full raw header (optional) contentType: ansistring; // ContentType header (optional) additionalHeaders: ansistring; // these are appended to predefined headers (opt) bodyMode :( RBM_FILE, // variable body specifies a file RBM_STRING, // variable body specifies byte content RBM_STREAM // refer to bodyStream ); body: ansistring; // specifies reply body according to bodyMode bodyFile: string; bodyStream: Tstream; // note: the stream is automatically freed firstByte, lastByte: int64; // body interval for partial replies (206) realm, // this will appear in the authentication dialog reason, // customized reason phrase url: string; // used for redirections resumeForbidden: boolean; end; ThttpRequest = record full: ansistring; // the raw request, byte by byte method: ThttpMethod; url: ansistring; ver: ansistring; firstByte, lastByte: int64; // body interval for partial requests headers, cookies: ThashedStringList; user,pwd: string; end; ThttpPost = record length: int64; // multipart form-data length boundary, // multipart form-data boundary header, // contextual header data: ansistring; // misc data varname, // post variable name filename: string; // name of posted file mode: (PM_NONE, PM_URLENCODED, PM_MULTIPART); end; TspeedLimiter = class { connections can be bound to a limiter. The limiter is a common limited { resource (the bandwidth) that is consumed. } protected P_maxSpeed: integer; // this is the limit we set. MAXINT means disabled. procedure setMaxSpeed(v:integer); public availableBandwidth: integer; // this is the resource itself property maxSpeed: integer read P_maxSpeed write setMaxSpeed; constructor create(max:integer=MAXINT); end; ThttpConn = class protected srv: ThttpSrv; // reference to the server stream: Tstream; P_address: string; P_port: string; brecvd: int64; // bytes received from the client bsent: int64; // bytes sent to the client bsent_body: int64; // bytes sent to the client (current body only) bsent_bodies: int64; // bytes sent to the client (for all bodies) P_requestCount: integer; P_destroying: boolean; // destroying is in progress P_sndBuf: integer; persistent: boolean; disconnecting: boolean; // disconnected() has been called lockCount: integer; // prevent freeing of the object dontFulFil: boolean; firstPostFile: boolean; lastPostItemPos, FbytesPostedLastItem: int64; // post handling inBoundaries: boolean; // we are between form-data boundaries postDataReceived: int64; // bytes received in post data // used to calculate actual speed lastBsent, lastBrecvd: int64; lastSpeedTime: int64; P_speedOut, P_speedIn: real; buffer: ansistring; // internal buffer for incoming data // event handlers procedure disconnected(Sender: TObject; Error: Word); procedure dataavailable(Sender: TObject; Error: Word); procedure senddata(sender:Tobject; bytes:integer); procedure datasent(sender:Tobject; error:word); function fullBodySize():int64; function partialBodySize():int64; function sendNextChunk(max:integer=MAXINT):integer; function getBytesToSend():int64; function getBytesToPost():int64; function getBytesGot():int64; procedure notify(ev:ThttpEvent); procedure tryNotify(ev:ThttpEvent); procedure calculateSpeed(); procedure sendheader(h:ansistring=''); function replyHeader_mode(mode:ThttpReplyMode):ansistring; function replyHeader_code(code:integer):ansistring; function getDontFree():boolean; procedure processInputBuffer(); procedure clearRequest(); procedure clearReply(); procedure setSndbuf(v:integer); public sock: Twsocket; // client-server communication socket state: ThttpConnState; // what is doing now with this request: ThttpRequest; // it requests reply: ThttpReply; // we serve post: ThttpPost; // it posts data: pointer; // user data paused: boolean; // while (not paused) do senddata() eventData: ansistring; ignoreSpeedLimit: boolean; limiters: TobjectList; // every connection can be bound to a number of TspeedLimiter constructor create(server:ThttpSrv; acceptingSock:Twsocket); destructor Destroy; override; procedure disconnect(); procedure addHeader(s:ansistring; overwrite:boolean=TRUE); // set an additional header line. If overwrite=false will always append. function setHeaderIfNone(s:ansistring):boolean; // set header if not already existing procedure removeHeader(name:ansistring); function getHeader(h:ansistring):string; // extract the value associated to the specified header field function getHeaderA(h:ansistring):ansistring; // extract the value associated to the specified header field function getCookie(k:string):string; procedure setCookie(k, v:string; pairs:array of string; extra:string=''); procedure delCookie(k:string); function getBuffer():ansistring; function initInputStream():boolean; property address:string read P_address; // other peer ip address property port:string read P_port; // other peer port property requestCount:integer read P_requestCount; property bytesToSend:int64 read getBytesToSend; property bytesToPost:int64 read getBytesToPost; property bytesSent:int64 read bsent_bodies; property bytesSentLastItem:int64 read bsent_body; property bytesPartial:int64 read partialBodySize; property bytesFullBody:int64 read fullBodySize; property bytesGot:int64 read getBytesGot; property bytesPosted:int64 read postDataReceived; property bytesPostedLastItem:int64 read FbytesPostedLastItem; property speedIn:real read P_speedIn; // (bytes_recvd/s) property speedOut:real read P_speedOut; // (bytes_sent/s) property disconnectedByServer:boolean read disconnecting; property destroying:boolean read P_destroying; property dontFree:boolean read getDontFree; property getLockCount:integer read lockCount; property sndBuf:integer read P_sndBuf write setSndBuf; end; ThttpSrv = class protected timer: Ttimer; lockTimerevent: boolean; lastHertz: Tdatetime; P_port: string; P_autoFree: boolean; P_speedIn, P_speedOut: real; bsent, brecvd: int64; procedure setPort(v:string); function getActive():boolean; procedure setActive(v:boolean); procedure connected(Sender: TObject; Error: Word); procedure disconnected(Sender: TObject; Error: Word); procedure bgexception(Sender: TObject; E:Exception; var CanClose:Boolean); procedure setAutoFree(v:boolean); procedure notify(ev:ThttpEvent; conn:ThttpConn); procedure hertzEvent(); procedure timerEvent(sender:Tobject); procedure calculateSpeed(); procedure processDisconnecting(); public sock, sock6: Twsocket; // listening socket conns, // full list of connected clients disconnecting, // list of pending disconnections offlines, // disconnected clients to be freed q, // clients waiting for data to be sent limiters: TobjectList; data: pointer; // user data persistentConnections: boolean; // if FALSE disconnect clients after they're served onEvent: procedure(event:ThttpEvent; conn:ThttpConn) of object; constructor create(); overload; destructor Destroy(); override; property active:boolean read getActive write setActive; // r we listening? property port:string read P_port write setPort; property bytesSent:int64 read bsent; property bytesReceived:int64 read brecvd; property speedIn:real read P_speedIn; // (bytes_recvd/s) property speedOut:real read P_speedOut; // (bytes_sent/s) property autoFreeDisconnectedClients: boolean read P_autoFree write setAutoFree; function start(onAddress:string='*'):boolean; // returns true if all is ok procedure stop(); procedure disconnectAll(wait:boolean=FALSE); procedure freeConnList(l:TobjectList); end; const TIMER_HZ = 100; MINIMUM_CHUNK_SIZE = 2*1024; MAXIMUM_CHUNK_SIZE = 1024*1024; HRM2CODE: array [ThttpReplyMode] of integer = (200, 200, 403, 401, 404, 400, 500, 0, 0, 405, 302, 503, 413, 301, 304 ); METHOD2STR: array [ThttpMethod] of ansistring = ('UNK','GET','POST','HEAD'); HRM2STR: array [ThttpReplyMode] of ansistring = ('Head+Body', 'Head only', 'Deny', 'Unauthorized', 'Not found', 'Bad request', 'Internal error', 'Close', 'Ignore', 'Unallowed method', 'Redirect', 'Overload', 'Request too large', 'Moved permanently', 'Not Modified'); { split S in position where SS is found, the first part is returned the second part following SS is left in S } function chop(ss:string; var s:string):string; overload; function chop(ss:ansistring; var s:ansistring):ansistring; overload; // same as before, but separator is I function chop(i:integer; var s:string):string; overload; // same as before, but specifying separator length function chop(i, l:integer; var s:string):string; overload; function chop(i, l:integer; var s:ansistring):ansistring; overload; // same as chop(lineterminator, s) function chopLine(var s:string):string; overload; // decode/decode url function decodeURL(url:ansistring; utf8:boolean=TRUE):string; function encodeURL(url:string; nonascii:boolean=TRUE; spaces:boolean=TRUE; htmlEncoding:boolean=FALSE):string; // returns true if address is not suitable for the internet function isLocalIP(ip:string):boolean; // base64 encoding function base64encode(s:ansistring):ansistring; function base64decode(s:ansistring):ansistring; // an ip address where we are listening function getIP():string; // ensure a string ends with a specific string procedure includeTrailingString(var s:string; ss:string); // gets unicode code for specified character function charToUnicode(c:char):dword; // this version of pos() is able to skip the pattern if inside quotes function nonQuotedPos(ss, s:string; ofs:integer=1; quote:string='"'; unquote:string='"'):integer; // case insensitive version function ipos(ss, s:string; ofs:integer=1):integer; overload; implementation uses Windows, ansistrings; const CRLF = #13#10; HEADER_LIMITER: ansistring = CRLF+CRLF; MAX_REQUEST_LENGTH = 64*1024; MAX_INPUT_BUFFER_LENGTH = 256*1024; // used as body content when the user did not specify any HRM2BODY: array [ThttpReplyMode] of string = ( '200 - OK', '200 - OK (header only)', '403 - You are not allowed to access this file', '401 - You are not authorized to access this file', '404 - File not found', '400 - Bad request', '500 - Internal server error', '', '', '405 - Method not allowed', '
302 - Redirection to %url%', '503 - Server is overloaded, retry later', '413 - The request has exceeded the max length allowed', '301 - Moved permanently to %url%', '' // RFC2616: The 304 response MUST NOT contain a message-body ); var freq: int64; procedure includeTrailingString(var s:string; ss:string); overload; begin if copy(s, length(s)-length(ss)+1, length(ss)) <> ss then s:=s+ss end; procedure includeTrailingString(var s:ansistring; ss:ansistring); overload; begin if copy(s, length(s)-length(ss)+1, length(ss)) <> ss then s:=s+ss end; function charToUnicode(c:char):dword; begin stringToWideChar(c,@result,4) end; function isLocalIP(ip:string):boolean; var r: record d,c,b,a:byte end; begin if ip = '::1' then exit(TRUE); dword(r):=WSocket_ntohl(WSocket_inet_addr(ansiString(ip))); result:=(r.a in [0,10,23,127]) or (r.a = 192) and ((r.b = 168) or (r.b = 0) and (r.c = 2)) or (r.a = 169) and (r.b = 254) or (r.a = 172) and (r.b in [16..31]) end; // isLocalIP function ifThen(c:boolean; a:integer; b:integer=0):integer; overload; begin if c then result:=a else result:=b end; function min(a,b:integer):integer; begin if a < b then result:=a else result:=b end; function ipos(ss, s: string; ofs:integer=1):integer; overload; var rss, rs, rss1, p: pchar; l: integer; begin result:=0; l:=length(s); if (l < ofs) or (l = 0) or (ss = '') then exit; // every strange thing you may notice here is an optimization based on the produced asm ss:=uppercase(ss); rss1:=@ss[1]; rs:=@s[ofs]; for result:=ofs to l do begin rss:=rss1; p:=rs; while (rss^ <> #0) and (rss^ = upcase(p^)) do begin inc(rss); inc(p); end; if rss^ = #0 then exit; // we saw it all, and we saw it was good inc(rs); end; result:=0; end; // ipos function nonQuotedPos(ss, s:string; ofs:integer=1; quote:string='"'; unquote:string='"'):integer; var qpos: integer; begin repeat result:=posEx(ss, s, ofs); if result = 0 then exit; repeat qpos:=posEx(quote, s, ofs); if qpos = 0 then exit; // there's no quoting, our result will fit if qpos > result then exit; // the quoting doesn't affect the piece, accept the result ofs:=posEx(unquote, s, qpos+1); if ofs = 0 then exit; // it is not closed, we don't consider it quoting inc(ofs); until ofs > result; // this quoting was short, let's see if we have another until false; end; // nonQuotedPos // consider using TBase64Encoding.Base64.Encode() in unit netencoding function base64encode(s:ansistring):ansistring; const TABLE:ansistring='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; type Ttriple=array [0..2] of byte; var p: ^Ttriple; i: integer; begin result:=''; p:=@s[1]; for i:=1 to length(s) div 3 do begin result:=result+TABLE[1+p[0] shr 2] +TABLE[1+(p[0] and 3) shl 4+p[1] shr 4] +TABLE[1+(p[1] and 15) shl 2+p[2] shr 6] +TABLE[1+(p[2] and 63)]; inc(p); end; if length(s) mod 3 = 0 then exit; result:=result +TABLE[1+p[0] shr 2] +TABLE[1+(p[0] and 3) shl 4+p[1] shr 4]; if length(s) mod 3=1 then result:=result+'==' else result:=result+TABLE[1+(p[1] and 15) shl 2+p[2] shr 6]+'='; end; // base64encode function base64decode(s:ansistring):ansistring; function if_(cond:boolean; c:ansichar):ansistring; begin if cond then result:=c else result:='' end; const TABLE:array[#43..#122] of byte=( 62,0,0,0,63,52,53,54,55,56,57,58,59,60,61,0,0,0,0,0,0,0,0,1,2,3,4,5,6,7, 8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,0,0,0,0,0,0,26,27,28, 29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51); var i: integer; p1, p2: byte; begin result:=''; i:=1; while i <= length(s) do begin p1:=TABLE[s[i+1]]; p2:=TABLE[s[i+2]]; result:=result +ansichar(TABLE[s[i]] shl 2+p1 shr 4) +if_(s[i+2]<>'=', ansichar(p1 shl 4+p2 shr 2)) +if_(s[i+3]<>'=', ansichar(p2 shl 6+TABLE[s[i+3]])); inc(i,4); end; end; // base64decode function validUTF8(s:rawbytestring):boolean; var i, more, len: integer; c: byte; begin len:=length(s); i:=0; while i < len do begin inc(i); c:=ord(s[i]); if c < $80 then continue; if c >= $FE then exit(FALSE); if c >= $F0 then more:=3 else if c >= $E0 then more:=2 else if c >= $C0 then more:=1 else exit(FALSE); if i+more > len then exit(FALSE); while more > 0 do begin inc(i); c:=ord(s[i]); if (c < $80) or (c > $C0) then exit(FALSE); dec(more); end; end; result:=TRUE; end; // validUTF8 function decodeURL(url:ansistring; utf8:boolean=TRUE):string; var i, j: integer; begin j:=0; i:=0; while i