{ Web server component, built on the HTTP server component

  Copyright (C) 2006 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, 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 lwebserver;

{$mode objfpc}{$h+}
{$inline on}

interface

uses
  sysutils, classes, lhttp, lhttputil, lmimetypes, levents,
  lprocess, process, lfastcgi, fastcgi;

type
  TLMultipartParameter = (mpContentType, mpContentDisposition, mpContentTransferEncoding,
    mpContentID, mpContentDescription);
  TLMultipartState = (msStart, msBodypartHeader, msBodypartData);

const
  URIParamSepChar: char = '&';
  CookieSepChar: char = ';';
  FormURLContentType: pchar = 'application/x-www-form-urlencoded';
  MultipartContentType: pchar = 'multipart/form-data';
  MPParameterStrings: array[TLMultipartParameter] of string =
    ('Content-Type', 'Content-Disposition', 'Content-Transfer-Encoding',
     'Content-ID', 'Content-Discription');

type
  TDocumentHandler = class;
  TFileHandler = class;

  TFileOutput = class(TBufferOutput)
  protected
    FFile: file;

    function GetSize: integer;
    function FillBuffer: TWriteBlockStatus; override;
  public
    constructor Create(ASocket: TLHTTPSocket);
    destructor Destroy; override;

    function  Open(const AFileName: string): boolean;

    property Size: integer read GetSize;
  end;

  TCGIOutput = class(TBufferOutput)
  protected
    FParsePos: pchar;
    FReadPos: integer;
    FParsingHeaders: boolean;
   
    procedure AddEnvironment(const AName, AValue: string); virtual; abstract;
    procedure AddHTTPParam(const AName: string; AParam: TLHTTPParameter);
    function  ParseHeaders: boolean;
    procedure CGIOutputError; virtual; abstract;
    procedure WriteCGIBlock;
    function  WriteCGIData: TWriteBlockStatus; virtual; abstract;
  public
    FDocumentRoot: string;
    FExtraPath: string;
    FEnvPath: string;
    FScriptFileName: string;
    FScriptName: string;

    constructor Create(ASocket: TLHTTPSocket);
    destructor Destroy; override;

    function  FillBuffer: TWriteBlockStatus; override;
    procedure StartRequest; virtual;
  end;

  TSimpleCGIOutput = class(TCGIOutput)
  protected
    FProcess: TLProcess;

    procedure AddEnvironment(const AName, AValue: string); override;
    procedure CGIProcNeedInput(AHandle: TLHandle);
    procedure CGIProcHasOutput(AHandle: TLHandle);
    procedure CGIProcHasStderr(AHandle: TLHandle);
    procedure DoneInput; override;
    function  HandleInput(ABuffer: pchar; ASize: integer): integer; override;
    procedure CGIOutputError; override;
    function  WriteCGIData: TWriteBlockStatus; override;
  public
    constructor Create(ASocket: TLHTTPSocket);
    destructor Destroy; override;

    procedure  StartRequest; override;

    property Process: TLProcess read FProcess;
  end;

  TFastCGIOutput = class(TCGIOutput)
  protected
    FRequest: TLFastCGIRequest;

    procedure AddEnvironment(const AName, AValue: string); override;
    procedure CGIOutputError; override;
    procedure DoneInput; override;
    procedure RequestEnd(ARequest: TLFastCGIRequest);
    procedure RequestNeedInput(ARequest: TLFastCGIRequest);
    procedure RequestHasOutput(ARequest: TLFastCGIRequest);
    procedure RequestHasStderr(ARequest: TLFastCGIRequest);
    function  HandleInput(ABuffer: pchar; ASize: integer): integer; override;
    function  WriteCGIData: TWriteBlockStatus; override;
    function  WriteBlock: TWriteBlockStatus; override;
  public
    constructor Create(ASocket: TLHTTPSocket);
    destructor Destroy; override;

    procedure StartRequest; override;

    property Request: TLFastCGIRequest read FRequest write FRequest;
  end;

  TCGIHandler = class(TURIHandler)
  protected
    function HandleURI(ASocket: TLHTTPServerSocket): TOutputItem; override;
  public
    FCGIRoot: string;
    FEnvPath: string;
    FDocumentRoot: string;
    FScriptPathPrefix: string;
  end;

  TDocumentRequest = record
    Socket: TLHTTPServerSocket;
    Document: string;
    URIPath: string;
    ExtraPath: string;
    Info: TSearchRec;
    InfoValid: boolean;
  end;

  TDocumentHandler = class(TObject)
  private
    FNext: TDocumentHandler;
  protected
    FFileHandler: TFileHandler;

    procedure RegisterWithEventer(AEventer: TLEventer); virtual;
  public
    function HandleDocument(const ARequest: TDocumentRequest): TOutputItem; virtual; abstract;

    property FileHandler: TFileHandler read FFileHandler;
  end;

  { TFileHandler }

  TFileHandler = class(TURIHandler)
  protected
    FDocHandlerList: TDocumentHandler;
    FDirIndexList: TStrings;
    FMimeTypeFile: string;

    procedure SetMimeTypeFile(const AValue: string);
    function HandleFile(const ARequest: TDocumentRequest): TOutputItem;
    function HandleURI(ASocket: TLHTTPServerSocket): TOutputItem; override;
    procedure RegisterWithEventer(AEventer: TLEventer); override;
  public
    DocumentRoot: string;

    constructor Create;
    destructor Destroy; override;
    
    procedure RegisterHandler(AHandler: TDocumentHandler);

    property DirIndexList: TStrings read FDirIndexList;
    property MimeTypeFile: string read FMimeTypeFile write SetMimeTypeFile;
  end;

  TPHPCGIHandler = class(TDocumentHandler)
  protected
    FAppName: string;
    FEnvPath: string;
  public
    function HandleDocument(const ARequest: TDocumentRequest): TOutputItem; override;

    property AppName: string read FAppName write FAppName;
    property EnvPath: string read FEnvPath write FEnvPath;
  end;

  TPHPFastCGIHandler = class(TDocumentHandler)
  protected
    FPool: TLFastCGIPool;
    FEnvPath: string;

    function  GetAppEnv: string;
    function  GetAppName: string;
    function  GetHost: string;
    function  GetPort: integer;
    procedure RegisterWithEventer(AEventer: TLEventer); override;
    procedure SetAppEnv(NewEnv: string);
    procedure SetAppName(NewName: string);
    procedure SetHost(NewHost: string);
    procedure SetPort(NewPort: integer);
  public
    constructor Create;
    destructor Destroy; override;

    function HandleDocument(const ARequest: TDocumentRequest): TOutputItem; override;

    property AppEnv: string read GetAppEnv write SetAppEnv;
    property AppName: string read GetAppName write SetAppName;
    property EnvPath: string read FEnvPath write FEnvPath;
    property Host: string read GetHost write SetHost;
    property Pool: TLFastCGIPool read FPool;
    property Port: integer read GetPort write SetPort;
  end;

  { Forms }

  TFormOutput = class;

  TFillBufferEvent = procedure(AFormOutput: TFormOutput; var AStatus: TWriteBlockStatus);
  THandleInputMethod = function(ABuffer: pchar; ASize: integer): integer of object;

  TFormOutput = class(TBufferOutput)
  protected
    FBoundary: pchar;
    FRequestVars: TStrings;
    FMPParameters: array[TLMultipartParameter] of pchar;
    FMPState: TLMultipartState;
    FOnExtraHeaders: TNotifyEvent;
    FOnFillBuffer: TFillBufferEvent;
    FHandleInput: THandleInputMethod;

    procedure DoneInput; override;
    function  FillBuffer: TWriteBlockStatus; override;
    function  FindBoundary(ABuffer: pchar): pchar;
    function  HandleInput(ABuffer: pchar; ASize: integer): integer; override;
    function  HandleInputDiscard(ABuffer: pchar; ASize: integer): integer;
    function  HandleInputFormURL(ABuffer: pchar; ASize: integer): integer;
    function  HandleInputMultipart(ABuffer: pchar; ASize: integer): integer;
    procedure ParseMultipartHeader(ABuffer, ALineEnd: pchar);
  public
    constructor Create(ASocket: TLHTTPSocket);
    destructor Destroy; override;

    function AddVariables(Variables: pchar; ASize: integer; SepChar: char): integer;
    procedure DeleteCookie(const AName: string; const APath: string = '/'; 
        const ADomain: string = '');
    procedure SetCookie(const AName, AValue: string; const AExpires: TDateTime; 
        const APath: string = '/'; const ADomain: string = '');

    property OnExtraHeaders: TNotifyEvent read FOnExtraHeaders write FOnExtraHeaders;
    property OnFillBuffer: TFillBufferEvent read FOnFillBuffer write FOnFillBuffer;
  end;

  THandleURIEvent = function(ASocket: TLHTTPServerSocket): TFormOutput;

  TFormHandler = class(TURIHandler)
  protected
    FOnHandleURI: THandleURIEvent;

    function HandleURI(ASocket: TLHTTPServerSocket): TOutputItem; override;
    procedure SelectMultipart(AFormOutput: TFormOutput; AContentType: pchar);
  public
    property OnHandleURI: THandleURIEvent read FOnHandleURI write FOnHandleURI;
  end;

var
  EnableWriteln: Boolean = True;

implementation

uses
  lstrbuffer;

{ Example handlers }

const
  InputBufferEmptyToWriteStatus: array[boolean] of TWriteBlockStatus =
    (wsPendingData, wsWaitingData);
  
procedure InternalWrite(const s: string);
begin
  if EnableWriteln then
    Writeln(s);
end;

procedure TDocumentHandler.RegisterWithEventer(AEventer: TLEventer);
begin
end;

function TCGIHandler.HandleURI(ASocket: TLHTTPServerSocket): TOutputItem;
var
  lOutput: TSimpleCGIOutput;
  lExecPath: string;
begin
  if StrLComp(ASocket.FRequestInfo.Argument, PChar(FScriptPathPrefix),
      Length(FScriptPathPrefix)) = 0 then
  begin
    lOutput := TSimpleCGIOutput.Create(ASocket);
    lOutput.FDocumentRoot := FDocumentRoot;
    lOutput.FEnvPath := FEnvPath;
    lOutput.Process.CurrentDirectory := FCGIRoot;
    lExecPath := ASocket.FRequestInfo.Argument+Length(FScriptPathPrefix);
    DoDirSeparators(lExecPath);
    lExecPath := FCGIRoot+lExecPath;
    if SeparatePath(lExecPath, lOutput.FExtraPath, faAnyFile and not faDirectory) then
    begin
      lOutput.Process.CommandLine := lExecPath;
      lOutput.FScriptFileName := lExecPath;
      lOutput.FScriptName := Copy(lExecPath, Length(FCGIRoot), 
        Length(lExecPath)-Length(FCGIRoot)+1);
      lOutput.StartRequest;
    end else
      ASocket.FResponseInfo.Status := hsNotFound;
    Result := lOutput;
  end else
    Result := nil;
end;

constructor TFileHandler.Create;
begin
  inherited;

  FDirIndexList := TStringList.Create;
end;

destructor TFileHandler.Destroy;
begin
  FreeAndNil(FDirIndexList);

  inherited;
end;

procedure TFileHandler.RegisterWithEventer(AEventer: TLEventer);
var
  lHandler: TDocumentHandler;
begin
  lHandler := FDocHandlerList;
  while lHandler <> nil do
  begin
    lHandler.RegisterWithEventer(AEventer);
    lHandler := lHandler.FNext;
  end;
end;

procedure TFileHandler.SetMimeTypeFile(const AValue: string);
begin
  FMimeTypeFile:=AValue;
  InitMimeList(aValue);
end;

function TFileHandler.HandleFile(const ARequest: TDocumentRequest): TOutputItem;
var
  lFileOutput: TFileOutput;
  lReqInfo: PRequestInfo;
  lRespInfo: PResponseInfo;
  lHeaderOut: PHeaderOutInfo;
  lIndex: integer;
begin
  Result := nil;
  if ARequest.InfoValid then
  begin
    lReqInfo := @ARequest.Socket.FRequestInfo;
    lRespInfo := @ARequest.Socket.FResponseInfo;
    lHeaderOut := @ARequest.Socket.FHeaderOut;
    if not (lReqInfo^.RequestType in [hmHead, hmGet]) then
    begin
      lRespInfo^.Status := hsNotAllowed;
    end else begin
      lFileOutput := TFileOutput.Create(ARequest.Socket);
      if lFileOutput.Open(ARequest.Document) then
      begin
        lRespInfo^.Status := hsOK;
        lHeaderOut^.ContentLength := ARequest.Info.Size;
        lRespInfo^.LastModified := LocalTimeToGMT(FileDateToDateTime(ARequest.Info.Time));
        lIndex := MimeList.IndexOf(ExtractFileExt(ARequest.Document));
        if lIndex >= 0 then
          lRespInfo^.ContentType := TStringObject(MimeList.Objects[lIndex]).Str;
        Result := lFileOutput;
        ARequest.Socket.StartResponse(lFileOutput);
      end else
        lFileOutput.Free;
    end;
  end;
end;

function TFileHandler.HandleURI(ASocket: TLHTTPServerSocket): TOutputItem;
var
  lDocRequest: TDocumentRequest;
  lHandler: TDocumentHandler;
  lTempDoc: string;
  lDirIndexFound: boolean;
  I: integer;
begin
  Result := nil;
  lDocRequest.Socket := ASocket;
  lDocRequest.URIPath := ASocket.FRequestInfo.Argument;
  lDocRequest.Document := lDocRequest.URIPath;
  DoDirSeparators(LDocRequest.Document);
  lDocRequest.Document := IncludeTrailingPathDelimiter(DocumentRoot)
    + lDocRequest.Document;
  lDocRequest.InfoValid := SeparatePath(lDocRequest.Document,lDocRequest.ExtraPath, 
    faAnyFile, @lDocRequest.Info);
  if not lDocRequest.InfoValid then
    exit;
  if (lDocRequest.Info.Attr and faDirectory) <> 0 then
  begin
    lDirIndexFound := false;
    { if non-trivial ExtraPath, then it's not a pure directory request, so do
      not show default directory document }
    if lDocRequest.ExtraPath = PathDelim then
    begin
      lDocRequest.Document := IncludeTrailingPathDelimiter(lDocRequest.Document);
      for I := 0 to FDirIndexList.Count - 1 do
      begin
        lTempDoc := lDocRequest.Document + FDirIndexList.Strings[I];
        lDocRequest.InfoValid := FindFirst(lTempDoc, 
          faAnyFile and not faDirectory, lDocRequest.Info) = 0;
        FindClose(lDocRequest.Info);
        if lDocRequest.InfoValid and ((lDocRequest.Info.Attr and faDirectory) = 0) then
        begin
          lDocRequest.Document := lTempDoc;
          lDirIndexFound := true;
          break;
        end;
      end;
    end;
    { requested a directory, but no source to show }
    if not lDirIndexFound then exit;
  end;

  lHandler := FDocHandlerList;
  while lHandler <> nil do
  begin
    Result := lHandler.HandleDocument(lDocRequest);
    if Result <> nil then exit;
    if ASocket.FResponseInfo.Status <> hsOK then exit;
    lHandler := lHandler.FNext;
  end;

  { no dynamic handler, see if it's a plain file }
  Result := HandleFile(lDocRequest);
end;

procedure TFileHandler.RegisterHandler(AHandler: TDocumentHandler);
begin
  if AHandler = nil then exit;
  AHandler.FFileHandler := Self;
  AHandler.FNext := FDocHandlerList;
  FDocHandlerList := AHandler;
end;

function TPHPCGIHandler.HandleDocument(const ARequest: TDocumentRequest): TOutputItem;
var
  lOutput: TSimpleCGIOutput;
begin
  if ExtractFileExt(ARequest.Document) = '.php' then
  begin
    lOutput := TSimpleCGIOutput.Create(ARequest.Socket);
    lOutput.FDocumentRoot := FFileHandler.DocumentRoot;
    lOutput.Process.CommandLine := FAppName;
    lOutput.FScriptName := ARequest.URIPath;
    lOutput.FScriptFileName := ARequest.Document;
    lOutput.FExtraPath := ARequest.ExtraPath;
    lOutput.FEnvPath := FEnvPath;
    lOutput.StartRequest;
    Result := lOutput;
  end else
    Result := nil;
end;

constructor TPHPFastCGIHandler.Create;
begin
  inherited;
  FPool := TLFastCGIPool.Create;
end;

destructor TPHPFastCGIHandler.Destroy;
begin
  inherited;
  FPool.Free;
end;

function  TPHPFastCGIHandler.GetAppEnv: string;
begin
  Result := FPool.AppEnv;
end;

function  TPHPFastCGIHandler.GetAppName: string;
begin
  Result := FPool.AppName;
end;

function  TPHPFastCGIHandler.GetHost: string;
begin
  Result := FPool.Host;
end;

function  TPHPFastCGIHandler.GetPort: integer;
begin
  Result := FPool.Port;
end;

procedure TPHPFastCGIHandler.SetAppEnv(NewEnv: string);
begin
  FPool.AppEnv := NewEnv;
end;

procedure TPHPFastCGIHandler.SetAppName(NewName: string);
begin
  FPool.AppName := NewName;
end;

procedure TPHPFastCGIHandler.SetHost(NewHost: string);
begin
  FPool.Host := NewHost;
end;

procedure TPHPFastCGIHandler.SetPort(NewPort: integer);
begin
  FPool.Port := NewPort;
end;

procedure TPHPFastCGIHandler.RegisterWithEventer(AEventer: TLEventer);
begin
  FPool.Eventer := AEventer;
end;

function TPHPFastCGIHandler.HandleDocument(const ARequest: TDocumentRequest): TOutputItem;
var
  lOutput: TFastCGIOutput;
  fcgiRequest: TLFastCGIRequest;
begin
  if ExtractFileExt(ARequest.Document) = '.php' then
  begin
    fcgiRequest := FPool.BeginRequest(FCGI_RESPONDER);
    if fcgiRequest <> nil then
    begin
      lOutput := TFastCGIOutput.Create(ARequest.Socket);
      lOutput.FDocumentRoot := FFileHandler.DocumentRoot;
      lOutput.FScriptName := ARequest.URIPath;
      lOutput.FScriptFileName := ARequest.Document;
      lOutput.FExtraPath := ARequest.ExtraPath;
      lOutput.FEnvPath := FEnvPath;
      lOutput.Request := fcgiRequest;
      ARequest.Socket.SetupEncoding(lOutput);
      lOutput.StartRequest;
      Result := lOutput;
    end else begin
      ARequest.Socket.FResponseInfo.Status := hsInternalError;
      ARequest.Socket.StartResponse(nil);
      Result := nil;
    end;
  end else
    Result := nil;
end;

{ Output Items }

constructor TFileOutput.Create(ASocket: TLHTTPSocket);
begin
  inherited;
  FEof := true;
end;

destructor TFileOutput.Destroy;
begin
  inherited;
  
  if not FEof then
    Close(FFile);
end;

function TFileOutput.Open(const AFileName: string): boolean;
begin
  {$I-}
  FileMode := 0;
  Assign(FFile, AFileName);
  Reset(FFile,1);
  {$I+}
  Result := IOResult = 0;
  FEof := false;
end;

function TFileOutput.GetSize: integer; inline;
begin
  Result := FileSize(FFile);
end;

function TFileOutput.FillBuffer: TWriteBlockStatus;
var
  lRead: integer;
begin
  if FEof then 
    exit(wsDone);
  BlockRead(FFile, FBuffer[FBufferPos], FBufferSize-FBufferPos, lRead);
  Inc(FBufferPos, lRead);
  if lRead = 0 then
  begin
    { EOF reached }
    Close(FFile);
    exit(wsDone);
  end;
  Result := wsPendingData;
end;

constructor TCGIOutput.Create(ASocket: TLHTTPSocket);
begin
  inherited;
end;

destructor TCGIOutput.Destroy;
begin
  inherited;
end;

procedure TCGIOutput.AddHTTPParam(const AName: string; AParam: TLHTTPParameter);
var
  lValue: pchar;
begin
  lValue := FSocket.Parameters[AParam];
  if lValue = nil then exit;
  AddEnvironment(AName, lValue);
end;

procedure TCGIOutput.StartRequest;
var
  lServerSocket: TLHTTPServerSocket absolute FSocket;
  tempStr: string;
begin
{
  FProcess.Environment.Add('SERVER_ADDR=');
  FProcess.Environment.Add('SERVER_ADMIN=');
  FProcess.Environment.Add('SERVER_NAME=');
  FProcess.Environment.Add('SERVER_PORT=');
}
  tempStr := TLHTTPServer(lServerSocket.Creator).ServerSoftware;
  if Length(tempStr) > 0 then
    AddEnvironment('SERVER_SOFTWARE', tempStr);

  AddEnvironment('GATEWAY_INTERFACE', 'CGI/1.1'); 
  AddEnvironment('SERVER_PROTOCOL', lServerSocket.FRequestInfo.VersionStr);
  AddEnvironment('REQUEST_METHOD', lServerSocket.FRequestInfo.Method);
  AddEnvironment('REQUEST_URI', '/'+lServerSocket.FRequestInfo.Argument);

  if Length(FExtraPath) > 0 then
  begin
    AddEnvironment('PATH_INFO', FExtraPath);
    { do not set PATH_TRANSLATED: bug in PHP }
//    AddEnvironment('PATH_TRANSLATED', DocumentRoot+FExtraPath);
  end;

  AddEnvironment('SCRIPT_NAME', FScriptName);
  AddEnvironment('SCRIPT_FILENAME', FScriptFileName);
  
  AddEnvironment('QUERY_STRING', lServerSocket.FRequestInfo.QueryParams);
  AddHTTPParam('CONTENT_TYPE', hpContentType);
  AddHTTPParam('CONTENT_LENGTH', hpContentLength);

  AddEnvironment('REMOTE_ADDR', FSocket.PeerAddress);
  AddEnvironment('REMOTE_PORT', IntToStr(FSocket.LocalPort));

  { used when user has authenticated in some way to server }
//  AddEnvironment('AUTH_TYPE='+...);
//  AddEnvironment('REMOTE_USER='+...);
  
  AddEnvironment('DOCUMENT_ROOT', FDocumentRoot);
  AddEnvironment('REDIRECT_STATUS', '200');
  AddHTTPParam('HTTP_HOST', hpHost);
  AddHTTPParam('HTTP_COOKIE', hpCookie);
  AddHTTPParam('HTTP_CONNECTION', hpConnection);
  AddHTTPParam('HTTP_REFERER', hpReferer);
  AddHTTPParam('HTTP_USER_AGENT', hpUserAgent);
  AddHTTPParam('HTTP_ACCEPT', hpAccept);
  AddEnvironment('PATH', FEnvPath);

  FParsingHeaders := true;
  FReadPos := FBufferPos;
  FParsePos := FBuffer+FReadPos;
end;

function  TCGIOutput.ParseHeaders: boolean;
var
  lHttpStatus: TLHTTPStatus;
  iEnd, lCode: integer;
  lStatus, lLength: dword;
  pLineEnd, pNextLine, pValue: pchar;
  lServerSocket: TLHTTPServerSocket absolute FSocket;

  procedure AddExtraHeader;
  begin
    AppendString(lServerSocket.FHeaderOut.ExtraHeaders, 
      FParsePos + ': ' + pValue + #13#10);
  end;

begin
  repeat
    iEnd := IndexByte(FParsePos^, @FBuffer[FReadPos]-FParsePos, 10);
    if iEnd = -1 then exit(false);
    pNextLine := FParsePos+iEnd+1;
    if (iEnd > 0) and (FParsePos[iEnd-1] = #13) then
      dec(iEnd);
    pLineEnd := FParsePos+iEnd;
    pLineEnd^ := #0;
    if pLineEnd = FParsePos then
    begin
      { empty line signals end of headers }
      FParsingHeaders := false;
      FBufferOffset := pNextLine-FBuffer;
      FBufferPos := FReadPos;
      FReadPos := 0;
      lServerSocket.StartResponse(Self, true);
      exit(false);
    end;
    iEnd := IndexByte(FParsePos^, iEnd, ord(':'));
    if (iEnd = -1) or (FParsePos[iEnd+1] <> ' ') then
      break;
    FParsePos[iEnd] := #0;
    pValue := FParsePos+iEnd+2;
    if StrIComp(FParsePos, 'Content-type') = 0 then
    begin
      lServerSocket.FResponseInfo.ContentType := pValue;
    end else 
    if StrIComp(FParsePos, 'Location') = 0 then
    begin
      if StrLIComp(pValue, 'http://', 7) = 0 then
      begin
        lServerSocket.FResponseInfo.Status := hsMovedPermanently;
        { add location header as-is to response }
        AddExtraHeader;
      end else
        InternalWrite('WARNING: unimplemented ''Location'' response received from CGI script');
    end else 
    if StrIComp(FParsePos, 'Status') = 0 then
    begin
      { sometimes we get '<status code> space <reason>' }
      iEnd := IndexByte(pValue^, pLineEnd-pValue, ord(' '));
      if iEnd <> -1 then
        pValue[iEnd] := #0;
      Val(pValue, lStatus, lCode);
      if lCode <> 0 then
        break;
      for lHttpStatus := Low(TLHTTPStatus) to High(TLHTTPStatus) do
        if HTTPStatusCodes[lHttpStatus] = lStatus then
          lServerSocket.FResponseInfo.Status := lHttpStatus;
    end else
    if StrIComp(FParsePos, 'Content-Length') = 0 then
    begin
      Val(pValue, lLength, lCode);
      if lCode <> 0 then
        break;
      lServerSocket.FHeaderOut.ContentLength := lLength;
    end else
    if StrIComp(FParsePos, 'Last-Modified') = 0 then
    begin
      if not TryHTTPDateStrToDateTime(pValue, 
          lServerSocket.FResponseInfo.LastModified) then
        InternalWrite('WARNING: unable to parse last-modified string from CGI script: ' + pValue);
    end else
      AddExtraHeader;
    FParsePos := pNextLine;
  until false;

  { error happened }
  lServerSocket.FResponseInfo.Status := hsInternalError;
  exit(true);
end;

function TCGIOutput.FillBuffer: TWriteBlockStatus;
begin
  if not FParsingHeaders then
    FReadPos := FBufferPos;
  Result := WriteCGIData;
  if FParsingHeaders then
  begin
    if ParseHeaders then
    begin
      { error while parsing }
      FEof := true;
      exit(wsDone);
    end;
  end else
    FBufferPos := FReadPos;
end;

procedure TCGIOutput.WriteCGIBlock;
begin
  { CGI process has output pending, we can write a block to socket }
  if FParsingHeaders then
  begin
    if (FillBuffer = wsDone) and FParsingHeaders then
    begin
      { still parsing headers ? something's wrong }
      FParsingHeaders := false;
      CGIOutputError;
      TLHTTPServerSocket(FSocket).StartResponse(Self);
    end;
  end;
  if not FParsingHeaders then
    FSocket.WriteBlock;
end;

{ TSimpleCGIOutput }

constructor TSimpleCGIOutput.Create(ASocket: TLHTTPSocket);
begin
  inherited;
  FProcess := TLProcess.Create(nil);
  FProcess.Options := FProcess.Options + [poUsePipes];
  FProcess.OnNeedInput := @CGIProcNeedInput;
  FProcess.OnHasOutput := @CGIProcHasOutput;
  FProcess.OnHasStderr := @CGIProcHasStderr;
end;

destructor TSimpleCGIOutput.Destroy;
begin
  inherited;
  FProcess.Free;
end;

function TSimpleCGIOutput.WriteCGIData: TWriteBlockStatus;
var
  lRead: integer;
begin
  lRead := FProcess.Output.Read(FBuffer[FReadPos], FBufferSize-FReadPos);
  if lRead = 0 then exit(wsDone);
  Inc(FReadPos, lRead);
  Result := InputBufferEmptyToWriteStatus[lRead = 0];
end;

procedure TSimpleCGIOutput.AddEnvironment(const AName, AValue: string);
begin
  FProcess.Environment.Add(AName+'='+AValue);
end;

procedure TSimpleCGIOutput.DoneInput;
begin
  FProcess.CloseInput;
end;

function TSimpleCGIOutput.HandleInput(ABuffer: pchar; ASize: integer): integer;
begin
  if ASize > 0 then
    Result := FProcess.Input.Write(ABuffer^, ASize)
  else
    Result := 0;
  FProcess.InputEvent.IgnoreWrite := ASize = 0;
end;

procedure TSimpleCGIOutput.StartRequest;
begin
  inherited;
  
  FProcess.Eventer := FSocket.Eventer;
  FProcess.Execute;
end;

procedure TSimpleCGIOutput.CGIOutputError;
var
  ServerSocket: TLHTTPServerSocket absolute FSocket;
begin
  if FProcess.ExitStatus = 127 then
    ServerSocket.FResponseInfo.Status := hsNotFound
  else
    ServerSocket.FResponseInfo.Status := hsInternalError;
end;

procedure TSimpleCGIOutput.CGIProcNeedInput(AHandle: TLHandle);
begin
  FProcess.InputEvent.IgnoreWrite := true;
  FSocket.ParseBuffer;
end;

procedure TSimpleCGIOutput.CGIProcHasOutput(AHandle: TLHandle);
begin
  WriteCGIBlock;
end;

procedure TSimpleCGIOutput.CGIProcHasStderr(AHandle: TLHandle);
var
  lBuf: array[0..1023] of char;
  lRead: integer;
begin
  lRead := FProcess.Stderr.Read(lBuf, sizeof(lBuf)-1);
  lBuf[lRead] := #0;
  write(pchar(@lBuf[0]));
end;

{ TFastCGIOutput }

constructor TFastCGIOutput.Create(ASocket: TLHTTPSocket);
begin
  inherited;
end;

destructor TFastCGIOutput.Destroy;
begin
  if FRequest <> nil then
  begin
    FRequest.OnInput := nil;
    FRequest.OnOutput := nil;
    FRequest.OnStderr := nil;
    FRequest.OnEndRequest := nil;
    FRequest.AbortRequest;
  end;
  inherited;
end;

procedure TFastCGIOutput.AddEnvironment(const AName, AValue: string);
begin
  FRequest.SendParam(AName, AValue);
end;

procedure TFastCGIOutput.CGIOutputError;
begin
  TLHTTPServerSocket(FSocket).FResponseInfo.Status := hsInternalError;
end;

procedure TFastCGIOutput.DoneInput;
begin
  if FRequest <> nil then
    FRequest.DoneInput;
end;

procedure TFastCGIOutput.RequestEnd(ARequest: TLFastCGIRequest);
begin
  FRequest.OnEndRequest := nil;
  FRequest.OnInput := nil;
  FRequest.OnOutput := nil;
  FRequest := nil;
  { trigger final write, to flush output to socket }
  WriteCGIBlock;
end;

procedure TFastCGIOutput.RequestNeedInput(ARequest: TLFastCGIRequest);
begin
  FSocket.ParseBuffer;
end;

procedure TFastCGIOutput.RequestHasOutput(ARequest: TLFastCGIRequest);
begin
  WriteCGIBlock;
end;

procedure TFastCGIOutput.RequestHasStderr(ARequest: TLFastCGIRequest);
var
  lBuf: array[0..1023] of char;
  lRead: integer;
begin
  lRead := ARequest.Get(lBuf, sizeof(lBuf)-1);
  lBuf[lRead] := #0;
  write(pchar(@lBuf[0]));
end;

function  TFastCGIOutput.HandleInput(ABuffer: pchar; ASize: integer): integer;
begin
  Result := FRequest.SendInput(ABuffer, ASize);
end;

function  TFastCGIOutput.WriteCGIData: TWriteBlockStatus;
var
  lRead: integer;
begin
  if FRequest = nil then exit(wsDone);
  if FRequest.OutputDone then exit(wsDone);
  lRead := FRequest.Get(@FBuffer[FReadPos], FBufferSize-FReadPos);
  Inc(FReadPos, lRead);
  Result := InputBufferEmptyToWriteStatus[lRead = 0];
end;

function  TFastCGIOutput.WriteBlock: TWriteBlockStatus;
begin
  if (FRequest <> nil) and FRequest.OutputPending then
  begin
    FRequest.ParseClientBuffer;
    Result := wsWaitingData;
  end else
    Result := inherited;
end;

procedure TFastCGIOutput.StartRequest;
begin
  FRequest.OnEndRequest := @RequestEnd;
  FRequest.OnInput := @RequestNeedInput;
  FRequest.OnOutput := @RequestHasOutput;
  FRequest.OnStderr := @RequestHasStderr;
  inherited;
  FRequest.DoneParams;
end;

{ TFormOutput } 

constructor TFormOutput.Create(ASocket: TLHTTPSocket);
begin
  inherited;
  FRequestVars := TStringList.Create;
end;

destructor TFormOutput.Destroy;
var
  I: integer;
  tmpObj: TObject;
begin
  for I := 0 to FRequestVars.Count - 1 do
  begin
    tmpObj := FRequestVars.Objects[I];
    Finalize(string(tmpObj));
    FRequestVars.Objects[I] := nil;
  end;
  FRequestVars.Free;
  inherited;
end;

function TFormOutput.AddVariables(Variables: pchar; ASize: integer; SepChar: char): integer;
var
  varname, sep, next: pchar;
  strName, strValue: string;
  tmpObj: TObject;
  i: integer;
begin
  if Variables = nil then
    exit(0);
  if ASize = -1 then
    ASize := StrLen(Variables);
  varname := Variables;
  repeat
    sep := varname + IndexChar(varname^, ASize, '=');
    if sep < varname then
      break;
    dec(ASize, sep-varname);
    next := sep + IndexChar(sep^, ASize, SepChar);
    if next < sep then
    begin
      next := sep + ASize;
      ASize := 0;
    end else
      dec(ASize, next+1-sep);
    if sep > varname then
    begin
      setlength(strName, sep-varname);
      move(varname[0], strName[1], sep-varname);
      setlength(strValue, next-sep-1);
      move(sep[1], strValue[1], next-sep-1);
      i := FRequestVars.Add(strName);
      tmpObj := nil;
      string(tmpObj) := strValue;
      FRequestVars.Objects[i] := tmpObj; 
    end;
    varname := next+1;
  until false;
  Result := ASize;
end;

procedure TFormOutput.DoneInput;
begin
  if Assigned(FOnExtraHeaders) then
    FOnExtraHeaders(Self);
  TLHTTPServerSocket(FSocket).StartResponse(Self);
end;

function TFormOutput.HandleInputFormURL(ABuffer: pchar; ASize: integer): integer;
begin
  Result := ASize-AddVariables(ABuffer, ASize, URIParamSepChar)
end;

procedure TFormOutput.ParseMultipartHeader(ABuffer, ALineEnd: pchar);
var
  I: TLMultipartParameter;
  len: integer;
begin
  for I := Low(TLMultipartParameter) to High(TLMultipartParameter) do
  begin
    len := Length(MPParameterStrings[I]);
    if ABuffer+len >= ALineEnd then
      continue;
    if (ABuffer[len] = ':')
      and (StrLIComp(ABuffer, PChar(MPParameterStrings[I]), len) = 0) then
    begin
      Inc(ABuffer, len+2);
      repeat
        if ABuffer = ALineEnd then exit;
        if ABuffer^ <> ' ' then break;
        inc(ABuffer);
      until false;
      FMPParameters[I] := ABuffer;
      if I = mpContentType then
      begin
        repeat
          if ABuffer = ALineEnd then exit;
          if ABuffer = ';' then break;
          inc(ABuffer);
        until false;

      end;
      break;
    end;
  end;
end;

function TFormOutput.FindBoundary(ABuffer: pchar): pchar;
begin
  {$warning TODO}
  Result := nil;
end;

function TFormOutput.HandleInputMultipart(ABuffer: pchar; ASize: integer): integer;
var
  pos, next, endline: pchar;
begin
  pos := ABuffer;
  repeat
    case FMPState of
      msStart:
      begin
        { discard until first boundary }
        next := FindBoundary(pos);
        if next = nil then
          exit(ASize);
        FMPState := msBodypartHeader;
      end;
      msBodypartHeader:
      begin
        endline := pos + IndexChar(pos, ASize, #10);
        if endline < pos then
          exit(pos-ABuffer);
        next := endline+1;
        if (endline > pos) and ((endline-1)^ = #13) then
          dec(endline);
        endline^ := #0;
        if endline > pos then
          ParseMultipartHeader(pos, endline)
        else
          FMPState := msBodypartData;
      end;
      msBodypartData:
      begin
        { decode based on content-transfer-encoding ? }
        { CRLF before boundary, belongs to boundary, not data! }
        next := FindBoundary(ABuffer);
      end;
    else
      exit(ASize);
    end;
    dec(ASize, next-pos);
    pos := next;
  until false;
end;

function TFormOutput.HandleInputDiscard(ABuffer: pchar; ASize: integer): integer;
begin
  Result := ASize;
end;

function TFormOutput.HandleInput(ABuffer: pchar; ASize: integer): integer;
begin
  Result := FHandleInput(ABuffer, ASize);
end;

function TFormOutput.FillBuffer: TWriteBlockStatus;
begin
  Result := wsDone;
  if Assigned(FOnFillBuffer) then
    FOnFillBuffer(Self, Result);
end;

procedure TFormOutput.DeleteCookie(const AName: string; const APath: string = '/'; 
  const ADomain: string = '');
begin
  { cookies expire when expires is in the past, duh }
  SetCookie(AName, '', Now - 7.0, APath, ADomain);
end;

procedure TFormOutput.SetCookie(const AName, AValue: string; const AExpires: TDateTime;
  const APath: string = '/'; const ADomain: string = '');
var
  headers: PStringBuffer;
begin
  headers := @TLHTTPServerSocket(FSocket).FHeaderOut.ExtraHeaders;
  AppendString(headers^, 'Set-Cookie: ' + HTTPEncode(AName) + '=' + HTTPEncode(AValue));
  AppendString(headers^, ';path=' + APath + ';expires=' + FormatDateTime(HTTPDateFormat, AExpires));
  if Length(ADomain) > 0 then
  begin
    AppendString(headers^, ';domain=');
    AppendString(headers^, ADomain);
  end;
  AppendString(headers^, #13#10);
end;

{ TFormHandler }

procedure TFormHandler.SelectMultipart(AFormOutput: TFormOutput; AContentType: pchar);
var
  boundary, endquote: pchar;
begin
  boundary := StrScan(AContentType, '=');
  if boundary <> nil then
  begin
    Inc(boundary);
    if boundary^ = '"' then
    begin
      Inc(boundary);
      endquote := StrScan(boundary, '"');
      if endquote <> nil then
        endquote^ := #0;
    end;
  end;

  AFormOutput.FBoundary := boundary;
  AFormOutput.FHandleInput := @AFormOutput.HandleInputMultipart;
end;

function TFormHandler.HandleURI(ASocket: TLHTTPServerSocket): TOutputItem;
var
  newFormOutput: TFormOutput;
  contentType: pchar;
begin
  if not Assigned(FOnHandleURI) then
    exit(nil);

  newFormOutput := FOnHandleURI(ASocket);
  if newFormOutput = nil then
    exit(nil);

  newFormOutput.AddVariables(ASocket.FRequestInfo.QueryParams, -1, URIParamSepChar);
  newFormOutput.AddVariables(ASocket.Parameters[hpCookie], -1, CookieSepChar);
  contentType := TLHTTPServerSocket(ASocket).Parameters[hpContentType];
  if StrIComp(contentType, FormURLContentType) = 0 then
    newFormOutput.FHandleInput := @newFormOutput.HandleInputFormURL
  else if StrIComp(contentType, MultipartContentType) = 0 then
    SelectMultipart(newFormOutput, contentType)
  else
    newFormOutput.FHandleInput := @newFormOutput.HandleInputDiscard;

  Result := newFormOutput;
end;

end.
