unit uIPAsyncHttpServer; interface {$I RemObjects.inc} uses SysUtils, Classes, uIPAsyncSocket, uIPHttpHeaders; const DefaultMaxPostData = 1024 * 1024 * 2; // 2 mb type TIPAsyncHttpServer = class; TIPAsyncContext = class; IIPAsyncContext = interface ['{83983C87-A9ED-47F2-9352-8C61455325A0}'] function GetOwner: TIPAsyncHttpServer; function GetRequest: TIPHttpRequestHeaders; function GetResponse: TIPHttpResponseHeaders; function GetRequestData: TDynamicByteArray; procedure SetRequestData(aData: TDynamicByteArray); function GetResponseData: TDynamicByteArray; procedure SetResponseData(aData: TDynamicByteArray); function GetResponseSent: Boolean; function GetUserData: Pointer; procedure SetUserData(aData: Pointer); function GetDisconnected: Boolean; property Owner: TIPAsyncHttpServer read GetOwner; property Request: TIPHttpRequestHeaders read GetRequest; property Response: TIPHttpResponseHeaders read GetResponse; property RequestData: TDynamicByteArray read GetRequestData write SetRequestData; property ResponseData: TDynamicByteArray read GetResponseData write SetResponseData; property ResponseSent: Boolean read GetResponseSent; property UserData: Pointer read GetUserData write SetUserData; property Disconnected: Boolean read GetDisconnected; procedure SendResponse; function GetSelf: TIPAsyncContext; end; TIPAsyncContext = class(TInterfacedObject, IIPAsyncContext) private fOwner: TIPAsyncHttpServer; fRequest: TIPHttpRequestHeaders; fResponse: TIPHttpResponseHeaders; fRequestData: TDynamicByteArray; fResponseData: TDynamicByteArray; fResponseSent: Boolean; fConnection: TIPBaseAsyncSocket; fUserData: Pointer; fTempBuf: TDynamicByteArray; fBodyOffset: Integer; fDisconnected: Boolean; procedure cbDisconnect(sender: TIPBaseAsyncSocket); procedure cbFirstLine(sender: TIPBaseAsyncSocket); procedure cbHeaderLine(sender: TIPBaseAsyncSocket); procedure cbPostData(sender: TIPBaseAsyncSocket); procedure cbResponse(sender: TIPBaseAsyncSocket); procedure SendInvalidRequest(aMessage: string = ''); function GetDisconnected: Boolean; function GetOwner: TIPAsyncHttpServer; function GetRequest: TIPHttpRequestHeaders; function GetRequestData: TDynamicByteArray; function GetResponse: TIPHttpResponseHeaders; function GetResponseData: TDynamicByteArray; function GetResponseSent: Boolean; function GetUserData: Pointer; procedure SetRequestData(aData: TDynamicByteArray); procedure SetResponseData(aData: TDynamicByteArray); procedure SetUserData(aData: Pointer); public constructor Create(aOwner: TIPAsyncHttpServer; aConnection: TIPBaseAsyncSocket); destructor Destroy; override; procedure Clear; procedure Disconnect; property Owner: TIPAsyncHttpServer read fOwner; property Request: TIPHttpRequestHeaders read fRequest; property Response: TIPHttpResponseHeaders read fResponse; property RequestData: TDynamicByteArray read fRequestData write fRequestData; property ResponseData: TDynamicByteArray read fResponseData write fResponseData; property ResponseSent: Boolean read fResponseSent; property UserData: Pointer read fUserData write fUserData; property Disconnected: Boolean read fDisconnected; procedure SendResponse; function GetSelf: TIPAsyncContext; end; TIPAsyncHttpRequestHandler = procedure (Sender: TObject; aContext: IIPAsyncContext) of Object; TIPAsyncHttpServer = class(TObject) private fOnManualBind: TNotifyEvent; fOnBeforeData, fOnBeforeCleanupContext, fOnRequest: TIPAsyncHttpRequestHandler; fOnResponseSent, fOnResponsefailed: TIPAsyncHttpRequestHandler; fMaxPostData: Integer; fMaxHeaderLines: Integer; fActive: Boolean; fSocket4, fSocket6: TIPBaseAsyncSocket; fBindV4, fBindV6: Boolean; fPort: Integer; fClients: TInterfaceList; procedure SetActive(aValue: Boolean); procedure cbAccept(aSender: TIPBaseAsyncSocket); procedure DisconnectClients; public constructor Create; destructor Destroy; override; property Socket4: TIPBaseAsyncSocket read fSocket4; property Socket6: TIPBaseAsyncSocket read fSocket6; property Port: Integer read fPort write fPort default 80; property BindV4: Boolean read fBindV4 write fBindV4 default true; property BindV6: Boolean read fBindV6 write fBindV6 default false; property MaxHeaderLines: Integer read fMaxHeaderLines write fMaxHeaderLines default 64; property Active: Boolean read fActive write SetActive; property MaxPostData: Integer read fMaxPostData write fMaxPostData default DefaultMaxPostData; property OnManualBind: TNotifyEvent read fOnManualBind write fOnManualBind; property OnBeforeData: TIPAsyncHttpRequestHandler read fOnBeforeData write fOnBeforeData; property OnRequest: TIPAsyncHttpRequestHandler read fOnrequest write fOnrequest; property OnBeforeCleanupContext: TIPAsyncHttpRequestHandler read fOnBeforeCleanupContext write fOnBeforeCleanupContext; property OnResponseSent: TIPAsyncHttpRequestHandler read fOnResponseSent write fOnResponseSent; property OnResponseFailed: TIPAsyncHttpRequestHandler read fOnResponsefailed write fOnResponsefailed; end; implementation uses Math; { TIPAsyncContext } procedure TIPAsyncContext.cbDisconnect(sender: TIPBaseAsyncSocket); var lConn: TIPBaseAsyncSocket; begin fDisconnected := true; if assigned(fOwner) and assigned(fOwner.fOnBeforeCleanupContext) then fOwner.fOnBeforeCleanupContext(fOwner, Self); lConn := fConnection; fConnection := nil; if lConn <> nil then begin lConn.EndDisconnect; lConn.Free; end; if assigned(fOwner) then begin fOwner.fClients.Remove(IIPAsyncContext(self)); end; end; procedure TIPAsyncContext.cbFirstLine(sender: TIPBaseAsyncSocket); var lLine: string; lIdx: Integer; begin if not sender.EndReadLine(lLine) then begin cbDisconnect(sender); exit; end; lIdx := pos(' ', lLine); if lIdx = 0 then begin SendInvalidRequest; exit; end; fRequest.Method := copy(lLine, 1, lIdx-1); Delete(lLine, 1, lIdx); lIdx := pos(' ', lLine); if lIdx = 0 then begin SendInvalidRequest; exit; end; fRequest.Path := copy(lLine, 1, lIdx -1); fRequest.Version := UpperCase(copy(lLine, lIdx+1, MaxInt)); if (fRequest.Method <> 'GET') and (fRequest.Method <> 'POST') then begin SendInvalidRequest; exit; end; sender.BeginReadLine(cbHeaderLine); end; procedure TIPAsyncContext.cbHeaderLine(sender: TIPBaseAsyncSocket); var lLine: string; i: Integer; lName, lVal: string; begin if not Sender.EndReadLine(lLine) then begin cbDisconnect(sender); exit; end; if lLine <> '' then begin if assigned(fOwner) and (fRequest.Headers.Count > fOwner.MaxHeaderLines) then begin SendInvalidRequest; exit; end; i := Pos(':', lLine); if i = 0 then begin SendInvalidRequest; exit; end; lName := Trim(copy(lLine, 1, i-1)); lVal := trim(Copy(lLine, i+1, MaxInt)); fRequest.Headers.Add(lName+'='+lVal); sender.BeginReadLine(cbHeaderLine); end else begin if fRequest.Method = 'POST' then begin try if assigned(fOwner) and assigned(fowner.fOnBeforeData) then fOwner.fOnBeforeData(fOwner, Self); except SendInvalidRequest; // we cannot let exceptions escape to the caller thread. exit; end; if fRequest.ContentLength > fOwner.fMaxPostData then begin SendInvalidRequest('Content-Length too large'); exit; end; SetLength(fRequestData, fRequest.ContentLength); sender.BeginReceive(@fRequestData[0], 0, Length(fRequestData), cbPostData); end else begin try if assigned(fOwner) and assigned(fOwner.fOnRequest) then begin fOwner.fOnRequest(fOwner, Self); end else begin SendInvalidRequest('Request handler not assigned'); exit; end; except SendInvalidRequest; exit; end; end; end; end; procedure TIPAsyncContext.cbPostData(sender: TIPBaseAsyncSocket); begin if sender.EndReceive < Length(fRequestData) then begin cbDisconnect(sender); // we got disconnected when there's less data. When buffered is true it won't return until all data is there. exit; end; try if assigned(fOwner) and assigned(fOwner.fOnRequest) then begin fOwner.fOnRequest(fOwner, Self); end else begin SendInvalidRequest('Request handler not assigned'); exit; end; except SendInvalidRequest; exit; end; end; procedure TIPAsyncContext.Clear; begin if assigned(fOwner) and assigned(fOwner.fOnBeforeCleanupContext) then fOwner.fOnBeforeCleanupContext(fOwner, Self); fRequest.Clear; fResponse.Clear; fRequestData := nil; fResponseData := nil; fResponseSent := false; end; constructor TIPAsyncContext.Create(aOwner: TIPAsyncHttpServer; aConnection: TIPBaseAsyncSocket); begin fOwner := aOwner; fConnection := aConnection; fConnection.MaxLineLength := 8096; fConnection.OnDisconnected := cbDisconnect; fRequest := TIPHttpRequestHeaders.Create; fResponse := TIPHttpResponseHeaders.Create; fconnection.Buffered := true; fConnection.IdleTimeout := 180; // 3min fConnection.OnTimeout := cbDisconnect; fConnection.BeginReadLine(cbFirstLine); end; destructor TIPAsyncContext.Destroy; begin inc(FRefCount); Clear; Dec(FRefCount); fRequest.Free; fResponse.Free; inherited Destroy; end; procedure TIPAsyncContext.Disconnect; begin fDisconnected := true; if fResponseSent then begin if assigned(fOwner) and assigned(fOwner.fOnResponsefailed) then fOwner.fOnResponsefailed(fOwner, Self); end; if assigned(fConnection) then fConnection.BeginDisconnect(cbDisconnect); end; procedure TIPAsyncContext.SendInvalidRequest(aMessage: string = ''); begin fResponse.Code := 500; fResponse.Reason := 'Internal Server Error'; fResponse.Headers.VAlues['Content-Type'] := 'text/html'; aMessage := '