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 := 'Invalid Request

Invalid Request

'+aMessage+''; SetLength(fResponseData, Length(aMessage)); Move(aMessage[1], fResponseData[0], Length(aMessage)); SendResponse; end; procedure TIPAsyncContext.SendResponse; var lHeaderData: string; begin if fResponseSent then raise EIPSocketError.Create('Response already sent'); fResponseSent := true; if fDisconnected then begin if assigned(fOwner) and assigned(fOwner.fOnResponsefailed) then begin fOwner.fOnResponsefailed(fOwner, self); end; exit; end; fResponse.Headers.Values['Content-Length'] := IntToStr(Length(fResponseData)); lHeaderData := fResponse.ToString; if (Length(lHeaderData) > 4096) or (Length(fResponseData) = 0) then begin if Length(fTempBuf) < Length(lHeaderData) then begin if Length(lHeaderData) > 4096 then SetLength(fTempBuf, Length(lHeaderData)) else SetLength(fTempBuf, 4096); end; Move(lHeaderdata[1], fTempBuf[0], Length(lHeaderData)); fBodyOffset := 0; fConnection.BeginSend(@ftempBuf[0], 0, Length(lHeaderData), cbResponse); end else begin if Length(fTempBuf) < 4096 then SetLength(fTempBuf, 4096); Move(lHeaderdata[1], fTempBuf[0], Length(lHeaderData)); fBodyOffset := 4096 - Length(lHeaderData); if fBodyOffset > Length(fResponseData) then fBodyOffset := Length(fResponseData); Move(fResponseData[0], fTempBuf[Length(lHeaderData)], fBodyOffset); fconnection.BeginSend(@fTempBuf[0], 0, fBodyOffset + Length(lHeaderData), cbResponse); end; end; procedure TIPAsyncContext.cbResponse(sender: TIPBaseAsyncSocket); var lLen: Integer; begin if fConnection.EndSend <= 0 then begin cbDisconnect(sender); exit; end; lLen := 4096; if fBodyOffset+ lLen > Length(fResponseData) then lLen := Length(fResponseData) - fBodyOffset; if lLen <= 0 then begin if assigned(fOwner) and assigned(fOwner.fOnResponseSent) then fOwner.fOnResponseSent(fOwner, Self); Clear; // we're done fConnection.BeginReadLine(cbFirstLine); end else begin move(fResponseData[fBodyOffset], fTempBuf[0], lLen); fBodyOffset := fBodyOffset + lLen; fcOnnection.BeginSend(@fTempBuf[0], 0, lLen, cbResponse); end; end; function TIPAsyncContext.GetDisconnected: Boolean; begin result := fDisconnected; end; function TIPAsyncContext.GetOwner: TIPAsyncHttpServer; begin result := fOwner; end; function TIPAsyncContext.GetRequest: TIPHttpRequestHeaders; begin result := fRequest; end; function TIPAsyncContext.GetRequestData: TDynamicByteArray; begin result := fRequestData; end; function TIPAsyncContext.GetResponse: TIPHttpResponseHeaders; begin result := fREsponse; end; function TIPAsyncContext.GetResponseData: TDynamicByteArray; begin result := fResponseData; end; function TIPAsyncContext.GetResponseSent: Boolean; begin result := fResponseSent; end; function TIPAsyncContext.GetUserData: Pointer; begin result := fUserData; end; procedure TIPAsyncContext.SetRequestData(aData: TDynamicByteArray); begin fRequestData := aData; end; procedure TIPAsyncContext.SetResponseData(aData: TDynamicByteArray); begin fResponseData := aData; end; procedure TIPAsyncContext.SetUserData(aData: Pointer); begin fUserData := aDAta; end; function TIPAsyncContext.GetSelf: TIPAsyncContext; begin result := self; end; { TIPAsyncHttpServer } procedure TIPAsyncHttpServer.cbAccept(aSender: TIPBaseAsyncSocket); var lSock: TIPBaseAsyncSocket; lContext: IIPAsyncContext; begin try lSock := aSender.EndAccept; except on e: EIPSocketError do exit; // We're shutting down, exit now. end; lContext := TIPAsyncContext.Create(self, lSock); fClients.Add(lContext); aSender.BeginAccept(cbAccept); end; constructor TIPAsyncHttpServer.Create; begin inherited Create; fMaxPostData := DefaultMaxPostData; fMaxHeaderLines := 64; fBindV6 := false; fBindV4 := true; fPort := 80; fClients := TInterfaceList.Create; end; destructor TIPAsyncHttpServer.Destroy; begin Active := False; DisconnectClients; fClients.Free; inherited Destroy; end; procedure TIPAsyncHttpServer.DisconnectClients; var i: integer; ctx: IIPAsyncContext; begin fClients.Lock; try for i := 0 to fClients.Count -1 do begin ctx := IIPAsyncContext(fClients[i]); ctx.GetSelf.fOwner := nil; ctx.GetSelf.Disconnect; end; finally fClients.Unlock; end; fClients.Clear; end; procedure TIPAsyncHttpServer.SetActive(aValue: Boolean); begin if fActive = aValue then exit; fActive := aValue; if aValue then begin if fBindV4 then begin fSocket4 := TIPAsyncSocket.Create(rsmTCPIP4); if not assigned(fOnManualBind) then fSocket4.Bind('0.0.0.0', fPort); end; if fBindV6 then begin fSocket6 := TIPAsyncSocket.Create(rsmTCPIP6); if not assigned(fOnManualBind) then fSocket6.Bind('0.0.0.0', fPort); end; if assigned(fOnManualBind) then fOnManualBind(self); if assigned(fSocket4) then begin fSocket4.Listen(10); fSocket4.BeginAccept(cbAccept); end; if assigned(fSocket6) then begin fSocket6.Listen(10); fSocket6.BeginAccept(cbAccept); end; end else begin DisconnectClients; fSocket4.Free; fSocket6.Free; fSocket4 := nil; fSocket6 := nil; end; end; end.