{ HTTP server and client components Copyright (C) 2006-2008 Micha Nelissen This library is Free software; you can redistribute it and/or modify it under the terms of the GNU Library 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 Library General Public License for more details. You should have received a Copy of the GNU Library General Public License along with This library; if not, Write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. This license has been modified. See file LICENSE.ADDON for more information. Should you find these sources without a LICENSE File, please contact me at ales@chello.sk } unit lhttp; {$mode objfpc}{$h+} {$inline on} interface uses classes, sysutils, lnet, levents, lhttputil, lstrbuffer; type TLHTTPMethod = (hmHead, hmGet, hmPost, hmUnknown); TLHTTPMethods = set of TLHTTPMethod; TLHTTPParameter = (hpConnection, hpContentLength, hpContentType, hpAccept, hpAcceptCharset, hpAcceptEncoding, hpAcceptLanguage, hpHost, hpFrom, hpReferer, hpUserAgent, hpRange, hpTransferEncoding, hpIfModifiedSince, hpIfUnmodifiedSince, hpCookie); TLHTTPStatus = (hsUnknown, hsOK, hsNoContent, hsMovedPermanently, hsFound, hsNotModified, hsBadRequest, hsForbidden, hsNotFound, hsPreconditionFailed, hsRequestTooLong, hsInternalError, hsNotImplemented, hsNotAllowed); TLHTTPTransferEncoding = (teIdentity, teChunked); TLHTTPClientError = (ceNone, ceMalformedStatusLine, ceVersionNotSupported, ceUnsupportedEncoding); const HTTPDisconnectStatuses = [hsBadRequest, hsRequestTooLong, hsForbidden, hsInternalError, hsNotAllowed]; HTTPMethodStrings: array[TLHTTPMethod] of string = ('HEAD', 'GET', 'POST', ''); HTTPParameterStrings: array[TLHTTPParameter] of string = ('CONNECTION', 'CONTENT-LENGTH', 'CONTENT-TYPE', 'ACCEPT', 'ACCEPT-CHARSET', 'ACCEPT-ENCODING', 'ACCEPT-LANGUAGE', 'HOST', 'FROM', 'REFERER', 'USER-AGENT', 'RANGE', 'TRANSFER-ENCODING', 'IF-MODIFIED-SINCE', 'IF-UNMODIFIED-SINCE', 'COOKIE'); HTTPStatusCodes: array[TLHTTPStatus] of dword = (0, 200, 204, 301, 302, 304, 400, 403, 404, 412, 414, 500, 501, 504); HTTPTexts: array[TLHTTPStatus] of string = ('', 'OK', 'No Content', 'Moved Permanently', 'Found', 'Not Modified', 'Bad Request', 'Forbidden', 'Not Found', 'Precondition Failed', 'Request Too Long', 'Internal Error', 'Method Not Implemented', 'Method Not Allowed'); HTTPDescriptions: array[TLHTTPStatus] of string = ( { hsUnknown } '', { hsOK } '', { hsNoContent } '', { hsMovedPermanently } '', { hsFound } '', { hsNotModified } '', { hsBadRequest } '
Your browser did a request this server did not understand.
'+#10+ ''+#10, { hsForbidden } 'You do not have permission to access this resource.
'+#10+ ''+#10, { hsNotFound } 'The requested URL was not found on this server.
'+#10+ ''+#10, { hsPreconditionFailed } 'The precondition on the request evaluated to false.
'+#10+ ''+#10, { hsRequestTooLong } 'Your browser did a request that was too long for this server to parse.
'+#10+ ''+#10, { hsInternalError } 'An error occurred while generating the content for this request.
'+#10+ ''+#10, { hsNotImplemented } 'The method used in the request is invalid.
'+#10+ ''+#10, { hsNotAllowed } 'The method used in the request is not allowed on the resource specified in the URL.
'+#10+ ''+#10); type TLHTTPSocket = class; TLHTTPConnection = class; TLHTTPClientSocket = class; PRequestInfo = ^TRequestInfo; TRequestInfo = record RequestType: TLHTTPMethod; DateTime: TDateTime; Method: pansichar; Argument: pansichar; QueryParams: pansichar; VersionStr: pansichar; Version: dword; end; PClientRequest = ^TClientRequest; TClientRequest = record Method: TLHTTPMethod; URI: string; QueryParams: string; RangeStart: qword; RangeEnd: qword; end; PClientResponse = ^TClientResponse; TClientResponse = record Status: TLHTTPStatus; Version: dword; Reason: string; end; PHeaderOutInfo = ^THeaderOutInfo; THeaderOutInfo = record ContentLength: integer; TransferEncoding: TLHTTPTransferEncoding; ExtraHeaders: TStringBuffer; Version: dword; end; PResponseInfo = ^TResponseInfo; TResponseInfo = record Status: TLHTTPStatus; ContentType: Ansistring; ContentCharset: Ansistring; LastModified: TDateTime; end; TWriteBlockStatus = (wsPendingData, wsWaitingData, wsDone); TWriteBlockMethod = function: TWriteBlockStatus of object; TOutputItem = class(TObject) protected FBuffer: PAnsiChar; FBufferPos: integer; FBufferSize: integer; FBufferOffset: integer; FOutputPending: boolean; FEof: boolean; FPrev: TOutputItem; FNext: TOutputItem; FPrevDelayFree: TOutputItem; FNextDelayFree: TOutputItem; FSocket: TLHTTPSocket; FWriteBlock: TWriteBlockMethod; procedure DoneInput; virtual; function HandleInput(ABuffer: PAnsiChar; ASize: integer): integer; virtual; function WriteBlock: TWriteBlockStatus; virtual; public constructor Create(ASocket: TLHTTPSocket); destructor Destroy; override; procedure LogError(const AMessage: string); property Socket: TLHTTPSocket read FSocket; end; TProcMethod = procedure of object; TBufferOutput = class(TOutputItem) protected FPrepareBuffer: TProcMethod; FFinishBuffer: TProcMethod; FBufferMemSize: integer; procedure PrepareBuffer; procedure PrepareChunk; procedure FinishBuffer; procedure FinishChunk; procedure SelectChunked; procedure SelectBuffered; procedure SelectPlain; procedure PrependBufferOutput(MinBufferSize: integer); procedure PrependStreamOutput(AStream: TStream; AFree: boolean); function FillBuffer: TWriteBlockStatus; virtual; abstract; function WriteChunk: TWriteBlockStatus; function WriteBuffer: TWriteBlockStatus; function WritePlain: TWriteBlockStatus; function WriteBlock: TWriteBlockStatus; override; public constructor Create(ASocket: TLHTTPSocket); destructor Destroy; override; procedure Add(ABuf: pointer; ASize: integer); procedure Add(const AStr: string); procedure Add(AStream: TStream; AQueue: boolean = false; AFree: boolean = true); end; TMemoryOutput = class(TOutputItem) protected FFreeBuffer: boolean; public constructor Create(ASocket: TLHTTPSocket; ABuffer: pointer; ABufferOffset, ABufferSize: integer; AFreeBuffer: boolean); destructor Destroy; override; end; TStreamOutput = class(TBufferOutput) protected FStream: TStream; FFreeStream: boolean; FStreamSize: integer; function FillBuffer: TWriteBlockStatus; override; public constructor Create(ASocket: TLHTTPSocket; AStream: TStream; AFreeStream: boolean); destructor Destroy; override; end; TMemoryStreamOutput = class(TOutputItem) protected FFreeStream: boolean; FStream: TMemoryStream; function WriteBlock: TWriteBlockStatus; override; public constructor Create(ASocket: TLHTTPSocket; AStream: TMemoryStream; AFreeStream: boolean); destructor Destroy; override; end; TChunkState = (csInitial, csData, csDataEnd, csTrailer, csFinished); TLHTTPParameterArray = array[TLHTTPParameter] of PAnsiChar; TParseBufferMethod = function: boolean of object; TLInputEvent = function(ASocket: TLHTTPClientSocket; ABuffer: PAnsiChar; ASize: integer): integer of object; TLCanWriteEvent = procedure(ASocket: TLHTTPClientSocket; var OutputEof: TWriteBlockStatus) of object; TLHTTPClientEvent = procedure(ASocket: TLHTTPClientSocket) of object; TLHTTPConnection = class(TLTcp) protected procedure CanSendEvent(aSocket: TLHandle); override; procedure LogAccess(const AMessage: string); virtual; procedure ReceiveEvent(aSocket: TLHandle); override; public destructor Destroy; override; end; TLHTTPSocket = class(TLSocket) protected FBuffer: PAnsiChar; FBufferPos: PAnsiChar; FBufferEnd: PAnsiChar; FBufferSize: integer; FRequestBuffer: PAnsiChar; FRequestPos: PAnsiChar; FRequestInputDone: boolean; FRequestHeaderDone: boolean; FOutputDone: boolean; FInputRemaining: integer; FChunkState: TChunkState; FCurrentInput: TOutputItem; FCurrentOutput: TOutputItem; FLastOutput: TOutputItem; FKeepAlive: boolean; FParseBuffer: TParseBufferMethod; FParameters: TLHTTPParameterArray; FDelayFreeItems: TOutputItem; procedure AddContentLength(ALength: integer); virtual; abstract; function CalcAvailableBufferSpace: integer; procedure DelayFree(AOutputItem: TOutputItem); procedure DoneBuffer(AOutput: TBufferOutput); virtual; procedure FreeDelayFreeItems; procedure LogAccess(const AMessage: string); virtual; procedure LogMessage; virtual; procedure FlushRequest; virtual; procedure PackRequestBuffer; procedure PackInputBuffer; function ParseRequest: boolean; function ParseEntityPlain: boolean; function ParseEntityChunked: boolean; procedure ParseLine(pLineEnd: PAnsiChar); virtual; procedure ParseParameterLine(pLineEnd: PAnsiChar); function ProcessEncoding: boolean; procedure ProcessHeaders; virtual; abstract; procedure RelocateVariable(var AVar: PAnsiChar); procedure RelocateVariables; virtual; procedure ResetDefaults; virtual; function SetupEncoding(AOutputItem: TBufferOutput; AHeaderOut: PHeaderOutInfo): boolean; procedure WriteError(AStatus: TLHTTPStatus); virtual; public constructor Create; override; destructor Destroy; override; procedure AddToOutput(AOutputItem: TOutputItem); procedure Disconnect(const Forced: Boolean = True); override; procedure PrependOutput(ANewItem, AItem: TOutputItem); procedure RemoveOutput(AOutputItem: TOutputItem); procedure HandleReceive; function ParseBuffer: boolean; procedure WriteBlock; property Parameters: TLHTTPParameterArray read FParameters; end; { http server } TSetupEncodingState = (seNone, seWaitHeaders, seStartHeaders); TLHTTPServerSocket = class(TLHTTPSocket) protected FLogMessage: TStringBuffer; FSetupEncodingState: TSetupEncodingState; procedure AddContentLength(ALength: integer); override; procedure DoneBuffer(AOutput: TBufferOutput); override; procedure FlushRequest; override; function HandleURI: TOutputItem; virtual; procedure LogAccess(const AMessage: string); override; procedure LogMessage; override; procedure RelocateVariables; override; procedure ResetDefaults; override; procedure ParseLine(pLineEnd: PAnsiChar); override; procedure ParseRequestLine(pLineEnd: PAnsiChar); function PrepareResponse(AOutputItem: TOutputItem; ACustomErrorMessage: boolean): boolean; procedure ProcessHeaders; override; procedure WriteError(AStatus: TLHTTPStatus); override; procedure WriteHeaders(AHeaderResponse, ADataResponse: TOutputItem); public FHeaderOut: THeaderOutInfo; FRequestInfo: TRequestInfo; FResponseInfo: TResponseInfo; constructor Create; override; destructor Destroy; override; function SetupEncoding(AOutputItem: TBufferOutput): boolean; procedure StartMemoryResponse(AOutputItem: TMemoryOutput; ACustomErrorMessage: boolean = false); procedure StartResponse(AOutputItem: TBufferOutput; ACustomErrorMessage: boolean = false); end; TURIHandler = class(TObject) private FNext: TURIHandler; FMethods: TLHTTPMethods; protected function HandleURI(ASocket: TLHTTPServerSocket): TOutputItem; virtual; abstract; procedure RegisterWithEventer(AEventer: TLEventer); virtual; public constructor Create; property Methods: TLHTTPMethods read FMethods write FMethods; end; TLAccessEvent = procedure(AMessage: string) of object; TLHTTPServer = class(TLHTTPConnection) protected FHandlerList: TURIHandler; FLogMessageTZString: string; FServerSoftware: string; FOnAccess: TLAccessEvent; function HandleURI(ASocket: TLHTTPServerSocket): TOutputItem; protected procedure LogAccess(const AMessage: string); override; procedure RegisterWithEventer; override; public constructor Create(AOwner: TComponent); override; procedure RegisterHandler(AHandler: TURIHandler); property ServerSoftware: string read FServerSoftware write FServerSoftware; property OnAccess: TLAccessEvent read FOnAccess write FOnAccess; end; { http client } TLHTTPClientSocket = class(TLHTTPSocket) protected FRequest: PClientRequest; FResponse: PClientResponse; FHeaderOut: PHeaderOutInfo; FError: TLHTTPClientError; procedure AddContentLength(ALength: integer); override; function GetResponseReason: string; function GetResponseStatus: TLHTTPStatus; procedure Cancel(AError: TLHTTPClientError); procedure ParseLine(pLineEnd: PAnsiChar); override; procedure ParseStatusLine(pLineEnd: PAnsiChar); procedure ProcessHeaders; override; procedure ResetDefaults; override; public constructor Create; override; destructor Destroy; override; procedure SendRequest; property Error: TLHTTPClientError read FError write FError; property Response: PClientResponse read FResponse; property ResponseReason: string read GetResponseReason; property ResponseStatus: TLHTTPStatus read GetResponseStatus; end; TLHTTPClientState = (hcsIdle, hcsWaiting, hcsReceiving); TLHTTPClient = class(TLHTTPConnection) protected FRequest: TClientRequest; FResponse: TClientResponse; FHeaderOut: THeaderOutInfo; FState: TLHTTPClientState; FPendingResponses: integer; FOutputEof: boolean; FOnCanWrite: TLCanWriteEvent; FOnDoneInput: TLHTTPClientEvent; FOnInput: TLInputEvent; FOnProcessHeaders: TLHTTPClientEvent; procedure ConnectEvent(aSocket: TLHandle); override; procedure DoDoneInput(ASocket: TLHTTPClientSocket); function DoHandleInput(ASocket: TLHTTPClientSocket; ABuffer: PAnsiChar; ASize: integer): integer; procedure DoProcessHeaders(ASocket: TLHTTPClientSocket); function DoWriteBlock(ASocket: TLHTTPClientSocket): TWriteBlockStatus; function InitSocket(aSocket: TLSocket): TLSocket; override; procedure InternalSendRequest; public constructor Create(AOwner: TComponent); override; destructor Destroy; override; procedure AddExtraHeader(const AHeader: string); procedure AddCookie(const AName, AValue: string; const APath: string = ''; const ADomain: string = ''; const AVersion: string = '0'); procedure ResetRange; procedure SendRequest; property ContentLength: integer read FHeaderOut.ContentLength write FHeaderOut.ContentLength; property Method: TLHTTPMethod read FRequest.Method write FRequest.Method; property PendingResponses: integer read FPendingResponses; property RangeStart: qword read FRequest.RangeStart write FRequest.RangeStart; property RangeEnd: qword read FRequest.RangeEnd write FRequest.RangeEnd; property Request: TClientRequest read FRequest; property State: TLHTTPClientState read FState; property URI: string read FRequest.URI write FRequest.URI; property Response: TClientResponse read FResponse; property OnCanWrite: TLCanWriteEvent read FOnCanWrite write FOnCanWrite; property OnDoneInput: TLHTTPClientEvent read FOnDoneInput write FOnDoneInput; property OnInput: TLInputEvent read FOnInput write FOnInput; property OnProcessHeaders: TLHTTPClientEvent read FOnProcessHeaders write FOnProcessHeaders; end; implementation uses lCommon; const RequestBufferSize = 1024; DataBufferSize = 16*1024; BufferEmptyToWriteStatus: array[boolean] of TWriteBlockStatus = (wsPendingData, wsDone); EofToWriteStatus: array[boolean] of TWriteBlockStatus = (wsWaitingData, wsDone); { helper functions } function TrySingleDigit(ADigit: char; out OutDigit: byte): boolean; begin Result := (ord(ADigit) >= ord('0')) and (ord(ADigit) <= ord('9')); if not Result then exit; OutDigit := ord(ADigit) - ord('0'); end; function HTTPVersionCheck(AStr, AStrEnd: PAnsiChar; out AVersion: dword): boolean; var lMajorVersion, lMinorVersion: byte; begin Result := ((AStrEnd-AStr) = 8) and CompareMem(AStr, PAnsiChar('HTTP/'), 5) and TrySingleDigit(AStr[5], lMajorVersion) and (AStr[6] = '.') and TrySingleDigit(AStr[7], lMinorVersion); AVersion := lMajorVersion * 10 + lMinorVersion; end; function CodeToHTTPStatus(ACode: dword): TLHTTPStatus; begin for Result := Low(TLHTTPStatus) to High(TLHTTPStatus) do if HTTPStatusCodes[Result] = ACode then exit; Result := hsUnknown; end; const HexDigits: array[0..15] of char = '0123456789ABCDEF'; function HexReverse(AValue: dword; ABuffer: PAnsiChar): integer; begin Result := 0; repeat ABuffer^ := HexDigits[AValue and $F]; AValue := AValue shr 4; Dec(ABuffer); Inc(Result); until AValue = 0; end; procedure HexToInt(ABuffer: PAnsiChar; out AValue: dword; out ACode: integer); var Val, Incr: dword; Start: PAnsiChar; begin Val := 0; ACode := 0; Start := ABuffer; while ABuffer^ <> #0 do begin if (ABuffer^ >= '0') and (ABuffer^ <= '9') then Incr := ord(ABuffer^) - ord('0') else if (ABuffer^ >= 'A') and (ABuffer^ <= 'F') then Incr := ord(ABuffer^) - ord('A') + 10 else if (ABuffer^ >= 'a') and (ABuffer^ <= 'f') then Incr := ord(ABuffer^) - ord('a') + 10 else begin ACode := ABuffer - Start + 1; break; end; Val := (Val shl 4) + Incr; Inc(ABuffer); end; AValue := Val; end; function EscapeCookie(const AInput: string): string; begin Result := StringReplace(AInput, ';', '%3B', [rfReplaceAll]); end; { TURIHandler } constructor TURIHandler.Create; begin FMethods := [hmHead, hmGet, hmPost]; end; procedure TURIHandler.RegisterWithEventer(AEventer: TLEventer); begin end; { TOutputItem } constructor TOutputItem.Create(ASocket: TLHTTPSocket); begin FSocket := ASocket; inherited Create; end; destructor TOutputItem.Destroy; begin if FSocket.FCurrentInput = Self then FSocket.FCurrentInput := nil; if FPrevDelayFree = nil then FSocket.FDelayFreeItems := FNextDelayFree else FPrevDelayFree.FNextDelayFree := FNextDelayFree; if FNextDelayFree <> nil then FNextDelayFree.FPrevDelayFree := FPrevDelayFree; inherited; end; procedure TOutputItem.DoneInput; begin end; function TOutputItem.HandleInput(ABuffer: PAnsiChar; ASize: integer): integer; begin { discard input } Result := ASize; end; procedure TOutputItem.LogError(const AMessage: string); begin FSocket.LogError(AMessage, 0); end; function TOutputItem.WriteBlock: TWriteBlockStatus; var lWritten: integer; begin if FOutputPending then begin if FBufferSize > FBufferPos then begin lWritten := FSocket.Send(FBuffer[FBufferPos], FBufferSize-FBufferPos); Inc(FBufferPos, lWritten); end; FOutputPending := FBufferPos < FBufferSize; Result := BufferEmptyToWriteStatus[not FOutputPending]; end else Result := EofToWriteStatus[FEof]; end; const ReserveChunkBytes = 12; constructor TBufferOutput.Create(ASocket: TLHTTPSocket); begin inherited; GetMem(FBuffer, DataBufferSize); FWriteBlock := @WritePlain; FPrepareBuffer := @PrepareBuffer; FFinishBuffer := @FinishBuffer; FBufferMemSize := DataBufferSize; end; destructor TBufferOutput.Destroy; begin inherited; FreeMem(FBuffer); end; procedure TBufferOutput.Add(ABuf: pointer; ASize: integer); var copySize: integer; begin repeat copySize := FBufferSize-FBufferPos; if copySize > ASize then copySize := ASize; Move(ABuf^, FBuffer[FBufferPos], copySize); Inc(FBufferPos, copySize); Dec(ASize, copySize); if ASize = 0 then break; PrependBufferOutput(ASize); until false; end; procedure TBufferOutput.Add(const AStr: string); begin Add(PAnsiChar(AStr), Length(AStr)); end; procedure TBufferOutput.PrependStreamOutput(AStream: TStream; AFree: boolean); begin if AStream is TMemoryStream then FSocket.PrependOutput(TMemoryStreamOutput.Create(FSocket, TMemoryStream(AStream), AFree), Self) else FSocket.PrependOutput(TStreamOutput.Create(FSocket, AStream, AFree), Self); end; procedure TBufferOutput.Add(AStream: TStream; AQueue: boolean = false; AFree: boolean = true); var size, copySize: integer; begin size := AStream.Size - AStream.Position; repeat copySize := FBufferSize-FBufferPos; if copySize > size then copySize := size; AStream.Read(FBuffer[FBufferPos], copySize); Inc(FBufferPos, copySize); Dec(size, copySize); if size = 0 then break; if AQueue then begin PrependBufferOutput(0); PrependStreamOutput(AStream, AFree); end else begin PrependBufferOutput(size); end; until false; end; procedure TBufferOutput.PrepareChunk; begin { 12 bytes for starting space, 7 bytes to end: