1{ Web server component, built on the HTTP server component
2
3  Copyright (C) 2006-2008 Micha Nelissen
4
5  This library is Free software; you can redistribute it and/or modify it
6  under the terms of the GNU Library General Public License as published by
7  the Free Software Foundation; either version 2 of the License, or (at your
8  option) any later version.
9
10  This program is diStributed in the hope that it will be useful, but WITHOUT
11  ANY WARRANTY; withOut even the implied warranty of MERCHANTABILITY or
12  FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License
13  for more details.
14
15  You should have received a Copy of the GNU Library General Public License
16  along with This library; if not, Write to the Free Software Foundation,
17  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
19  This license has been modified. See file LICENSE.ADDON for more information.
20  Should you find these sources without a LICENSE File, please contact
21  me at ales@chello.sk
22}
23
24unit lwebserver;
25
26{$mode objfpc}{$h+}
27{$inline on}
28
29interface
30
31uses
32  sysutils, classes, lhttp, lhttputil, lmimetypes, levents,
33  lprocess, process, lfastcgi, fastcgi_base;
34
35type
36  TLMultipartParameter = (mpContentType, mpContentDisposition, mpContentTransferEncoding,
37    mpContentID, mpContentDescription);
38  TLMultipartState = (msStart, msBodypartHeader, msBodypartData);
39
40const
41  URIParamSepChar: char = '&';
42  CookieSepChar: char = ';';
43  FormURLContentType: pchar = 'application/x-www-form-urlencoded';
44  MultipartContentType: pchar = 'multipart/form-data';
45  MPParameterStrings: array[TLMultipartParameter] of string =
46    ('Content-Type', 'Content-Disposition', 'Content-Transfer-Encoding',
47     'Content-ID', 'Content-Discription');
48
49type
50  TDocumentHandler = class;
51  TFileHandler = class;
52
53  TFileOutput = class(TBufferOutput)
54  protected
55    FFile: file;
56
57    function GetSize: integer;
58    function FillBuffer: TWriteBlockStatus; override;
59  public
60    constructor Create(ASocket: TLHTTPSocket);
61    destructor Destroy; override;
62
63    function  Open(const AFileName: string): boolean;
64
65    property Size: integer read GetSize;
66  end;
67
68  TCGIOutput = class(TBufferOutput)
69  protected
70    FParsePos: pchar;
71    FReadPos: integer;
72    FParsingHeaders: boolean;
73
74    procedure AddEnvironment(const AName, AValue: string); virtual; abstract;
75    procedure AddHTTPParam(const AName: string; AParam: TLHTTPParameter);
76    function  ParseHeaders: boolean;
77    procedure CGIOutputError; virtual; abstract;
78    procedure WriteCGIBlock;
79    function  WriteCGIData: TWriteBlockStatus; virtual; abstract;
80  public
81    FDocumentRoot: string;
82    FExtraPath: string;
83    FEnvPath: string;
84    FScriptFileName: string;
85    FScriptName: string;
86
87    constructor Create(ASocket: TLHTTPSocket);
88    destructor Destroy; override;
89
90    function  FillBuffer: TWriteBlockStatus; override;
91    procedure StartRequest; virtual;
92  end;
93
94  TSimpleCGIOutput = class(TCGIOutput)
95  protected
96    FProcess: TLProcess;
97
98    procedure AddEnvironment(const AName, AValue: string); override;
99    procedure CGIProcNeedInput(AHandle: TLHandle);
100    procedure CGIProcHasOutput(AHandle: TLHandle);
101    procedure CGIProcHasStderr(AHandle: TLHandle);
102    procedure DoneInput; override;
103    function  HandleInput(ABuffer: pchar; ASize: integer): integer; override;
104    procedure CGIOutputError; override;
105    function  WriteCGIData: TWriteBlockStatus; override;
106  public
107    constructor Create(ASocket: TLHTTPSocket);
108    destructor Destroy; override;
109
110    procedure  StartRequest; override;
111
112    property Process: TLProcess read FProcess;
113  end;
114
115  TFastCGIOutput = class(TCGIOutput)
116  protected
117    FRequest: TLFastCGIRequest;
118
119    procedure AddEnvironment(const AName, AValue: string); override;
120    procedure CGIOutputError; override;
121    procedure DoneInput; override;
122    procedure RequestEnd(ARequest: TLFastCGIRequest);
123    procedure RequestNeedInput(ARequest: TLFastCGIRequest);
124    procedure RequestHasOutput(ARequest: TLFastCGIRequest);
125    procedure RequestHasStderr(ARequest: TLFastCGIRequest);
126    function  HandleInput(ABuffer: pchar; ASize: integer): integer; override;
127    function  WriteCGIData: TWriteBlockStatus; override;
128    function  WriteBlock: TWriteBlockStatus; override;
129  public
130    constructor Create(ASocket: TLHTTPSocket);
131    destructor Destroy; override;
132
133    procedure StartRequest; override;
134
135    property Request: TLFastCGIRequest read FRequest write FRequest;
136  end;
137
138  TCGIHandler = class(TURIHandler)
139  protected
140    function HandleURI(ASocket: TLHTTPServerSocket): TOutputItem; override;
141  public
142    FCGIRoot: string;
143    FEnvPath: string;
144    FDocumentRoot: string;
145    FScriptPathPrefix: string;
146  end;
147
148  TDocumentRequest = record
149    Socket: TLHTTPServerSocket;
150    Document: string;
151    URIPath: string;
152    ExtraPath: string;
153    Info: TSearchRec;
154    InfoValid: boolean;
155  end;
156
157  TDocumentHandler = class(TObject)
158  private
159    FNext: TDocumentHandler;
160  protected
161    FFileHandler: TFileHandler;
162
163    procedure RegisterWithEventer(AEventer: TLEventer); virtual;
164  public
165    function HandleDocument(const ARequest: TDocumentRequest): TOutputItem; virtual; abstract;
166
167    property FileHandler: TFileHandler read FFileHandler;
168  end;
169
170  { TFileHandler }
171
172  TFileHandler = class(TURIHandler)
173  protected
174    FDocHandlerList: TDocumentHandler;
175    FDirIndexList: TStrings;
176    FMimeTypeFile: string;
177
178    procedure SetMimeTypeFile(const AValue: string);
179    function HandleFile(const ARequest: TDocumentRequest): TOutputItem;
180    function HandleURI(ASocket: TLHTTPServerSocket): TOutputItem; override;
181    procedure RegisterWithEventer(AEventer: TLEventer); override;
182  public
183    DocumentRoot: string;
184
185    constructor Create;
186    destructor Destroy; override;
187
188    procedure RegisterHandler(AHandler: TDocumentHandler);
189
190    property DirIndexList: TStrings read FDirIndexList;
191    property MimeTypeFile: string read FMimeTypeFile write SetMimeTypeFile;
192  end;
193
194  TPHPCGIHandler = class(TDocumentHandler)
195  protected
196    FAppName: string;
197    FEnvPath: string;
198  public
199    function HandleDocument(const ARequest: TDocumentRequest): TOutputItem; override;
200
201    property AppName: string read FAppName write FAppName;
202    property EnvPath: string read FEnvPath write FEnvPath;
203  end;
204
205  TPHPFastCGIHandler = class(TDocumentHandler)
206  protected
207    FPool: TLFastCGIPool;
208    FEnvPath: string;
209
210    function  GetAppEnv: string;
211    function  GetAppName: string;
212    function  GetHost: string;
213    function  GetPort: integer;
214    procedure RegisterWithEventer(AEventer: TLEventer); override;
215    procedure SetAppEnv(NewEnv: string);
216    procedure SetAppName(NewName: string);
217    procedure SetHost(NewHost: string);
218    procedure SetPort(NewPort: integer);
219  public
220    constructor Create;
221    destructor Destroy; override;
222
223    function HandleDocument(const ARequest: TDocumentRequest): TOutputItem; override;
224
225    property AppEnv: string read GetAppEnv write SetAppEnv;
226    property AppName: string read GetAppName write SetAppName;
227    property EnvPath: string read FEnvPath write FEnvPath;
228    property Host: string read GetHost write SetHost;
229    property Pool: TLFastCGIPool read FPool;
230    property Port: integer read GetPort write SetPort;
231  end;
232
233  { Forms }
234
235  TFormOutput = class;
236
237  TFillBufferEvent = procedure(AFormOutput: TFormOutput; var AStatus: TWriteBlockStatus);
238  THandleInputMethod = function(ABuffer: pchar; ASize: integer): integer of object;
239
240  TFormOutput = class(TBufferOutput)
241  protected
242    FBoundary: pchar;
243    FRequestVars: TStrings;
244    FMPParameters: array[TLMultipartParameter] of pchar;
245    FMPState: TLMultipartState;
246    FOnExtraHeaders: TNotifyEvent;
247    FOnFillBuffer: TFillBufferEvent;
248    FHandleInput: THandleInputMethod;
249
250    procedure DoneInput; override;
251    function  FillBuffer: TWriteBlockStatus; override;
252    function  FindBoundary(ABuffer: pchar): pchar;
253    function  HandleInput(ABuffer: pchar; ASize: integer): integer; override;
254    function  HandleInputDiscard(ABuffer: pchar; ASize: integer): integer;
255    function  HandleInputFormURL(ABuffer: pchar; ASize: integer): integer;
256    function  HandleInputMultipart(ABuffer: pchar; ASize: integer): integer;
257    procedure ParseMultipartHeader(ABuffer, ALineEnd: pchar);
258  public
259    constructor Create(ASocket: TLHTTPSocket);
260    destructor Destroy; override;
261
262    function AddVariables(Variables: pchar; ASize: integer; SepChar: char): integer;
263    procedure DeleteCookie(const AName: string; const APath: string = '/';
264        const ADomain: string = '');
265    procedure SetCookie(const AName, AValue: string; const AExpires: TDateTime;
266        const APath: string = '/'; const ADomain: string = '');
267
268    property OnExtraHeaders: TNotifyEvent read FOnExtraHeaders write FOnExtraHeaders;
269    property OnFillBuffer: TFillBufferEvent read FOnFillBuffer write FOnFillBuffer;
270  end;
271
272  THandleURIEvent = function(ASocket: TLHTTPServerSocket): TFormOutput;
273
274  TFormHandler = class(TURIHandler)
275  protected
276    FOnHandleURI: THandleURIEvent;
277
278    function HandleURI(ASocket: TLHTTPServerSocket): TOutputItem; override;
279    procedure SelectMultipart(AFormOutput: TFormOutput; AContentType: pchar);
280  public
281    property OnHandleURI: THandleURIEvent read FOnHandleURI write FOnHandleURI;
282  end;
283
284var
285  EnableWriteln: Boolean = True;
286
287implementation
288
289uses
290  lstrbuffer;
291
292{ Example handlers }
293
294const
295  InputBufferEmptyToWriteStatus: array[boolean] of TWriteBlockStatus =
296    (wsPendingData, wsWaitingData);
297
298procedure InternalWrite(const s: string);
299begin
300  if EnableWriteln then
301    Writeln(s);
302end;
303
304procedure TDocumentHandler.RegisterWithEventer(AEventer: TLEventer);
305begin
306end;
307
308function TCGIHandler.HandleURI(ASocket: TLHTTPServerSocket): TOutputItem;
309var
310  lOutput: TSimpleCGIOutput;
311  lExecPath: string;
312begin
313  if StrLComp(ASocket.FRequestInfo.Argument, PChar(FScriptPathPrefix),
314      Length(FScriptPathPrefix)) = 0 then
315  begin
316    lOutput := TSimpleCGIOutput.Create(ASocket);
317    lOutput.FDocumentRoot := FDocumentRoot;
318    lOutput.FEnvPath := FEnvPath;
319    lOutput.Process.CurrentDirectory := FCGIRoot;
320    lExecPath := ASocket.FRequestInfo.Argument+Length(FScriptPathPrefix);
321    DoDirSeparators(lExecPath);
322    lExecPath := FCGIRoot+lExecPath;
323    if SeparatePath(lExecPath, lOutput.FExtraPath, faAnyFile and not faDirectory) then
324    begin
325      lOutput.Process.CommandLine := lExecPath;
326      lOutput.FScriptFileName := lExecPath;
327      lOutput.FScriptName := Copy(lExecPath, Length(FCGIRoot),
328        Length(lExecPath)-Length(FCGIRoot)+1);
329      lOutput.StartRequest;
330    end else
331      ASocket.FResponseInfo.Status := hsNotFound;
332    Result := lOutput;
333  end else
334    Result := nil;
335end;
336
337constructor TFileHandler.Create;
338begin
339  inherited;
340
341  FDirIndexList := TStringList.Create;
342end;
343
344destructor TFileHandler.Destroy;
345begin
346  FreeAndNil(FDirIndexList);
347
348  inherited;
349end;
350
351procedure TFileHandler.RegisterWithEventer(AEventer: TLEventer);
352var
353  lHandler: TDocumentHandler;
354begin
355  lHandler := FDocHandlerList;
356  while lHandler <> nil do
357  begin
358    lHandler.RegisterWithEventer(AEventer);
359    lHandler := lHandler.FNext;
360  end;
361end;
362
363procedure TFileHandler.SetMimeTypeFile(const AValue: string);
364begin
365  FMimeTypeFile:=AValue;
366  InitMimeList(aValue);
367end;
368
369function TFileHandler.HandleFile(const ARequest: TDocumentRequest): TOutputItem;
370var
371  lFileOutput: TFileOutput;
372  lReqInfo: PRequestInfo;
373  lRespInfo: PResponseInfo;
374  lHeaderOut: PHeaderOutInfo;
375  lIndex: integer;
376begin
377  Result := nil;
378  if ARequest.InfoValid then
379  begin
380    lReqInfo := @ARequest.Socket.FRequestInfo;
381    lRespInfo := @ARequest.Socket.FResponseInfo;
382    lHeaderOut := @ARequest.Socket.FHeaderOut;
383    if not (lReqInfo^.RequestType in [hmHead, hmGet]) then
384    begin
385      lRespInfo^.Status := hsNotAllowed;
386    end else begin
387      lFileOutput := TFileOutput.Create(ARequest.Socket);
388      if lFileOutput.Open(ARequest.Document) then
389      begin
390        lRespInfo^.Status := hsOK;
391        lHeaderOut^.ContentLength := ARequest.Info.Size;
392        lRespInfo^.LastModified := LocalTimeToGMT(FileDateToDateTime(ARequest.Info.Time));
393        lIndex := MimeList.IndexOf(ExtractFileExt(ARequest.Document));
394        if lIndex >= 0 then
395          lRespInfo^.ContentType := TStringObject(MimeList.Objects[lIndex]).Str;
396        Result := lFileOutput;
397        ARequest.Socket.StartResponse(lFileOutput);
398      end else
399        lFileOutput.Free;
400    end;
401  end;
402end;
403
404function TFileHandler.HandleURI(ASocket: TLHTTPServerSocket): TOutputItem;
405var
406  lDocRequest: TDocumentRequest;
407  lHandler: TDocumentHandler;
408  lTempDoc: string;
409  lDirIndexFound: boolean;
410  I: integer;
411begin
412  Result := nil;
413  lDocRequest.Socket := ASocket;
414  lDocRequest.URIPath := ASocket.FRequestInfo.Argument;
415  lDocRequest.Document := lDocRequest.URIPath;
416  DoDirSeparators(LDocRequest.Document);
417  lDocRequest.Document := IncludeTrailingPathDelimiter(DocumentRoot)
418    + lDocRequest.Document;
419  lDocRequest.InfoValid := SeparatePath(lDocRequest.Document,lDocRequest.ExtraPath,
420    faAnyFile, @lDocRequest.Info);
421  if not lDocRequest.InfoValid then
422    exit;
423  if (lDocRequest.Info.Attr and faDirectory) <> 0 then
424  begin
425    lDirIndexFound := false;
426    { if non-trivial ExtraPath, then it's not a pure directory request, so do
427      not show default directory document }
428    if lDocRequest.ExtraPath = PathDelim then
429    begin
430      lDocRequest.Document := IncludeTrailingPathDelimiter(lDocRequest.Document);
431      for I := 0 to FDirIndexList.Count - 1 do
432      begin
433        lTempDoc := lDocRequest.Document + FDirIndexList.Strings[I];
434        lDocRequest.InfoValid := FindFirst(lTempDoc,
435          faAnyFile and not faDirectory, lDocRequest.Info) = 0;
436        FindClose(lDocRequest.Info);
437        if lDocRequest.InfoValid and ((lDocRequest.Info.Attr and faDirectory) = 0) then
438        begin
439          lDocRequest.Document := lTempDoc;
440          lDirIndexFound := true;
441          break;
442        end;
443      end;
444    end;
445    { requested a directory, but no source to show }
446    if not lDirIndexFound then exit;
447  end;
448
449  lHandler := FDocHandlerList;
450  while lHandler <> nil do
451  begin
452    Result := lHandler.HandleDocument(lDocRequest);
453    if Result <> nil then exit;
454    if ASocket.FResponseInfo.Status <> hsOK then exit;
455    lHandler := lHandler.FNext;
456  end;
457
458  { no dynamic handler, see if it's a plain file }
459  Result := HandleFile(lDocRequest);
460end;
461
462procedure TFileHandler.RegisterHandler(AHandler: TDocumentHandler);
463begin
464  if AHandler = nil then exit;
465  AHandler.FFileHandler := Self;
466  AHandler.FNext := FDocHandlerList;
467  FDocHandlerList := AHandler;
468end;
469
470function TPHPCGIHandler.HandleDocument(const ARequest: TDocumentRequest): TOutputItem;
471var
472  lOutput: TSimpleCGIOutput;
473begin
474  if ExtractFileExt(ARequest.Document) = '.php' then
475  begin
476    lOutput := TSimpleCGIOutput.Create(ARequest.Socket);
477    lOutput.FDocumentRoot := FFileHandler.DocumentRoot;
478    lOutput.Process.CommandLine := FAppName;
479    lOutput.FScriptName := ARequest.URIPath;
480    lOutput.FScriptFileName := ARequest.Document;
481    lOutput.FExtraPath := ARequest.ExtraPath;
482    lOutput.FEnvPath := FEnvPath;
483    lOutput.StartRequest;
484    Result := lOutput;
485  end else
486    Result := nil;
487end;
488
489constructor TPHPFastCGIHandler.Create;
490begin
491  inherited;
492  FPool := TLFastCGIPool.Create;
493end;
494
495destructor TPHPFastCGIHandler.Destroy;
496begin
497  inherited;
498  FPool.Free;
499end;
500
501function  TPHPFastCGIHandler.GetAppEnv: string;
502begin
503  Result := FPool.AppEnv;
504end;
505
506function  TPHPFastCGIHandler.GetAppName: string;
507begin
508  Result := FPool.AppName;
509end;
510
511function  TPHPFastCGIHandler.GetHost: string;
512begin
513  Result := FPool.Host;
514end;
515
516function  TPHPFastCGIHandler.GetPort: integer;
517begin
518  Result := FPool.Port;
519end;
520
521procedure TPHPFastCGIHandler.SetAppEnv(NewEnv: string);
522begin
523  FPool.AppEnv := NewEnv;
524end;
525
526procedure TPHPFastCGIHandler.SetAppName(NewName: string);
527begin
528  FPool.AppName := NewName;
529end;
530
531procedure TPHPFastCGIHandler.SetHost(NewHost: string);
532begin
533  FPool.Host := NewHost;
534end;
535
536procedure TPHPFastCGIHandler.SetPort(NewPort: integer);
537begin
538  FPool.Port := NewPort;
539end;
540
541procedure TPHPFastCGIHandler.RegisterWithEventer(AEventer: TLEventer);
542begin
543  FPool.Eventer := AEventer;
544end;
545
546function TPHPFastCGIHandler.HandleDocument(const ARequest: TDocumentRequest): TOutputItem;
547var
548  lOutput: TFastCGIOutput;
549  fcgiRequest: TLFastCGIRequest;
550begin
551  if ExtractFileExt(ARequest.Document) = '.php' then
552  begin
553    fcgiRequest := FPool.BeginRequest(FCGI_RESPONDER);
554    if fcgiRequest <> nil then
555    begin
556      lOutput := TFastCGIOutput.Create(ARequest.Socket);
557      lOutput.FDocumentRoot := FFileHandler.DocumentRoot;
558      lOutput.FScriptName := ARequest.URIPath;
559      lOutput.FScriptFileName := ARequest.Document;
560      lOutput.FExtraPath := ARequest.ExtraPath;
561      lOutput.FEnvPath := FEnvPath;
562      lOutput.Request := fcgiRequest;
563      ARequest.Socket.SetupEncoding(lOutput);
564      lOutput.StartRequest;
565      Result := lOutput;
566    end else begin
567      ARequest.Socket.FResponseInfo.Status := hsInternalError;
568      ARequest.Socket.StartResponse(nil);
569      Result := nil;
570    end;
571  end else
572    Result := nil;
573end;
574
575{ Output Items }
576
577constructor TFileOutput.Create(ASocket: TLHTTPSocket);
578begin
579  inherited;
580  FEof := true;
581end;
582
583destructor TFileOutput.Destroy;
584begin
585  inherited;
586
587  if not FEof then
588    Close(FFile);
589end;
590
591function TFileOutput.Open(const AFileName: string): boolean;
592begin
593  {$I-}
594  FileMode := 0;
595  Assign(FFile, AFileName);
596  Reset(FFile,1);
597  {$I+}
598  Result := IOResult = 0;
599  FEof := false;
600end;
601
602function TFileOutput.GetSize: integer; inline;
603begin
604  Result := FileSize(FFile);
605end;
606
607function TFileOutput.FillBuffer: TWriteBlockStatus;
608var
609  lRead: integer;
610begin
611  if FEof then
612    exit(wsDone);
613  BlockRead(FFile, FBuffer[FBufferPos], FBufferSize-FBufferPos, lRead);
614  Inc(FBufferPos, lRead);
615  if lRead = 0 then
616  begin
617    { EOF reached }
618    Close(FFile);
619    exit(wsDone);
620  end;
621  Result := wsPendingData;
622end;
623
624constructor TCGIOutput.Create(ASocket: TLHTTPSocket);
625begin
626  inherited;
627end;
628
629destructor TCGIOutput.Destroy;
630begin
631  inherited;
632end;
633
634procedure TCGIOutput.AddHTTPParam(const AName: string; AParam: TLHTTPParameter);
635var
636  lValue: pchar;
637begin
638  lValue := FSocket.Parameters[AParam];
639  if lValue = nil then exit;
640  AddEnvironment(AName, lValue);
641end;
642
643procedure TCGIOutput.StartRequest;
644var
645  lServerSocket: TLHTTPServerSocket;
646  tempStr: string;
647begin
648  lServerSocket := TLHTTPServerSocket(FSocket);
649{
650  FProcess.Environment.Add('SERVER_ADDR=');
651  FProcess.Environment.Add('SERVER_ADMIN=');
652  FProcess.Environment.Add('SERVER_NAME=');
653  FProcess.Environment.Add('SERVER_PORT=');
654}
655  Self := nil;
656  tempStr := TLHTTPServer(lServerSocket.Creator).ServerSoftware;
657  if Length(tempStr) > 0 then
658    AddEnvironment('SERVER_SOFTWARE', tempStr);
659
660  AddEnvironment('GATEWAY_INTERFACE', 'CGI/1.1');
661  AddEnvironment('SERVER_PROTOCOL', lServerSocket.FRequestInfo.VersionStr);
662  AddEnvironment('REQUEST_METHOD', lServerSocket.FRequestInfo.Method);
663  AddEnvironment('REQUEST_URI', '/'+lServerSocket.FRequestInfo.Argument);
664
665  if Length(FExtraPath) > 0 then
666  begin
667    AddEnvironment('PATH_INFO', FExtraPath);
668    { do not set PATH_TRANSLATED: bug in PHP }
669//    AddEnvironment('PATH_TRANSLATED', DocumentRoot+FExtraPath);
670  end;
671
672  AddEnvironment('SCRIPT_NAME', FScriptName);
673  AddEnvironment('SCRIPT_FILENAME', FScriptFileName);
674
675  AddEnvironment('QUERY_STRING', lServerSocket.FRequestInfo.QueryParams);
676  AddHTTPParam('CONTENT_TYPE', hpContentType);
677  AddHTTPParam('CONTENT_LENGTH', hpContentLength);
678
679  AddEnvironment('REMOTE_ADDR', FSocket.PeerAddress);
680  AddEnvironment('REMOTE_PORT', IntToStr(FSocket.LocalPort));
681
682  { used when user has authenticated in some way to server }
683//  AddEnvironment('AUTH_TYPE='+...);
684//  AddEnvironment('REMOTE_USER='+...);
685
686  AddEnvironment('DOCUMENT_ROOT', FDocumentRoot);
687  AddEnvironment('REDIRECT_STATUS', '200');
688  AddHTTPParam('HTTP_HOST', hpHost);
689  AddHTTPParam('HTTP_COOKIE', hpCookie);
690  AddHTTPParam('HTTP_CONNECTION', hpConnection);
691  AddHTTPParam('HTTP_REFERER', hpReferer);
692  AddHTTPParam('HTTP_USER_AGENT', hpUserAgent);
693  AddHTTPParam('HTTP_ACCEPT', hpAccept);
694  AddEnvironment('PATH', FEnvPath);
695
696  FParsingHeaders := true;
697  FReadPos := FBufferPos;
698  FParsePos := FBuffer+FReadPos;
699end;
700
701function  TCGIOutput.ParseHeaders: boolean;
702var
703  lHttpStatus: TLHTTPStatus;
704  iEnd, lCode: integer;
705  lStatus, lLength: dword;
706  pLineEnd, pNextLine, pValue: pchar;
707  lServerSocket: TLHTTPServerSocket;
708
709  procedure AddExtraHeader;
710  begin
711    AppendString(lServerSocket.FHeaderOut.ExtraHeaders,
712      FParsePos + ': ' + pValue + #13#10);
713  end;
714
715begin
716  lServerSocket := TLHTTPServerSocket(FSocket);
717  repeat
718    iEnd := IndexByte(FParsePos^, @FBuffer[FReadPos]-FParsePos, 10);
719    if iEnd = -1 then exit(false);
720    pNextLine := FParsePos+iEnd+1;
721    if (iEnd > 0) and (FParsePos[iEnd-1] = #13) then
722      dec(iEnd);
723    pLineEnd := FParsePos+iEnd;
724    pLineEnd^ := #0;
725    if pLineEnd = FParsePos then
726    begin
727      { empty line signals end of headers }
728      FParsingHeaders := false;
729      FBufferOffset := pNextLine-FBuffer;
730      FBufferPos := FReadPos;
731      FReadPos := 0;
732      lServerSocket.StartResponse(Self, true);
733      exit(false);
734    end;
735    iEnd := IndexByte(FParsePos^, iEnd, ord(':'));
736    if (iEnd = -1) or (FParsePos[iEnd+1] <> ' ') then
737      break;
738    FParsePos[iEnd] := #0;
739    pValue := FParsePos+iEnd+2;
740    if StrIComp(FParsePos, 'Content-type') = 0 then
741    begin
742      lServerSocket.FResponseInfo.ContentType := pValue;
743    end else
744    if StrIComp(FParsePos, 'Location') = 0 then
745    begin
746      if StrLIComp(pValue, 'http://', 7) = 0 then
747      begin
748        lServerSocket.FResponseInfo.Status := hsMovedPermanently;
749        { add location header as-is to response }
750        AddExtraHeader;
751      end else
752        InternalWrite('WARNING: unimplemented ''Location'' response received from CGI script');
753    end else
754    if StrIComp(FParsePos, 'Status') = 0 then
755    begin
756      { sometimes we get '<status code> space <reason>' }
757      iEnd := IndexByte(pValue^, pLineEnd-pValue, ord(' '));
758      if iEnd <> -1 then
759        pValue[iEnd] := #0;
760      Val(pValue, lStatus, lCode);
761      if lCode <> 0 then
762        break;
763      for lHttpStatus := Low(TLHTTPStatus) to High(TLHTTPStatus) do
764        if HTTPStatusCodes[lHttpStatus] = lStatus then
765          lServerSocket.FResponseInfo.Status := lHttpStatus;
766    end else
767    if StrIComp(FParsePos, 'Content-Length') = 0 then
768    begin
769      Val(pValue, lLength, lCode);
770      if lCode <> 0 then
771        break;
772      lServerSocket.FHeaderOut.ContentLength := lLength;
773    end else
774    if StrIComp(FParsePos, 'Last-Modified') = 0 then
775    begin
776      if not TryHTTPDateStrToDateTime(pValue,
777          lServerSocket.FResponseInfo.LastModified) then
778        InternalWrite('WARNING: unable to parse last-modified string from CGI script: ' + pValue);
779    end else
780      AddExtraHeader;
781    FParsePos := pNextLine;
782  until false;
783
784  { error happened }
785  lServerSocket.FResponseInfo.Status := hsInternalError;
786  exit(true);
787end;
788
789function TCGIOutput.FillBuffer: TWriteBlockStatus;
790begin
791  if not FParsingHeaders then
792    FReadPos := FBufferPos;
793  Result := WriteCGIData;
794  if FParsingHeaders then
795  begin
796    if ParseHeaders then
797    begin
798      { error while parsing }
799      FEof := true;
800      exit(wsDone);
801    end;
802  end else
803    FBufferPos := FReadPos;
804end;
805
806procedure TCGIOutput.WriteCGIBlock;
807begin
808  { CGI process has output pending, we can write a block to socket }
809  if FParsingHeaders then
810  begin
811    if (FillBuffer = wsDone) and FParsingHeaders then
812    begin
813      { still parsing headers ? something's wrong }
814      FParsingHeaders := false;
815      CGIOutputError;
816      TLHTTPServerSocket(FSocket).StartResponse(Self);
817    end;
818  end;
819  if not FParsingHeaders then
820    FSocket.WriteBlock;
821end;
822
823{ TSimpleCGIOutput }
824
825constructor TSimpleCGIOutput.Create(ASocket: TLHTTPSocket);
826begin
827  inherited;
828  FProcess := TLProcess.Create(nil);
829  FProcess.Options := FProcess.Options + [poUsePipes];
830  FProcess.OnNeedInput := @CGIProcNeedInput;
831  FProcess.OnHasOutput := @CGIProcHasOutput;
832  FProcess.OnHasStderr := @CGIProcHasStderr;
833end;
834
835destructor TSimpleCGIOutput.Destroy;
836begin
837  inherited;
838  FProcess.Free;
839end;
840
841function TSimpleCGIOutput.WriteCGIData: TWriteBlockStatus;
842var
843  lRead: integer;
844begin
845  lRead := FProcess.Output.Read(FBuffer[FReadPos], FBufferSize-FReadPos);
846  if lRead = 0 then exit(wsDone);
847  Inc(FReadPos, lRead);
848  Result := InputBufferEmptyToWriteStatus[lRead = 0];
849end;
850
851procedure TSimpleCGIOutput.AddEnvironment(const AName, AValue: string);
852begin
853  FProcess.Environment.Add(AName+'='+AValue);
854end;
855
856procedure TSimpleCGIOutput.DoneInput;
857begin
858  FProcess.CloseInput;
859end;
860
861function TSimpleCGIOutput.HandleInput(ABuffer: pchar; ASize: integer): integer;
862begin
863  if ASize > 0 then
864    Result := FProcess.Input.Write(ABuffer^, ASize)
865  else
866    Result := 0;
867  FProcess.InputEvent.IgnoreWrite := ASize = 0;
868end;
869
870procedure TSimpleCGIOutput.StartRequest;
871begin
872  inherited;
873
874  FProcess.Eventer := FSocket.Eventer;
875  FProcess.Execute;
876end;
877
878procedure TSimpleCGIOutput.CGIOutputError;
879var
880  ServerSocket: TLHTTPServerSocket;
881begin
882  ServerSocket := TLHTTPServerSocket(FSocket);
883  if FProcess.ExitStatus = 127 then
884    ServerSocket.FResponseInfo.Status := hsNotFound
885  else
886    ServerSocket.FResponseInfo.Status := hsInternalError;
887end;
888
889procedure TSimpleCGIOutput.CGIProcNeedInput(AHandle: TLHandle);
890begin
891  FProcess.InputEvent.IgnoreWrite := true;
892  FSocket.ParseBuffer;
893end;
894
895procedure TSimpleCGIOutput.CGIProcHasOutput(AHandle: TLHandle);
896begin
897  WriteCGIBlock;
898end;
899
900procedure TSimpleCGIOutput.CGIProcHasStderr(AHandle: TLHandle);
901var
902  lBuf: array[0..1023] of char;
903  lRead: integer;
904begin
905  lRead := FProcess.Stderr.Read(lBuf, sizeof(lBuf)-1);
906  lBuf[lRead] := #0;
907  write(pchar(@lBuf[0]));
908end;
909
910{ TFastCGIOutput }
911
912constructor TFastCGIOutput.Create(ASocket: TLHTTPSocket);
913begin
914  inherited;
915end;
916
917destructor TFastCGIOutput.Destroy;
918begin
919  if FRequest <> nil then
920  begin
921    FRequest.OnInput := nil;
922    FRequest.OnOutput := nil;
923    FRequest.OnStderr := nil;
924    FRequest.OnEndRequest := nil;
925    FRequest.AbortRequest;
926  end;
927  inherited;
928end;
929
930procedure TFastCGIOutput.AddEnvironment(const AName, AValue: string);
931begin
932  FRequest.SendParam(AName, AValue);
933end;
934
935procedure TFastCGIOutput.CGIOutputError;
936begin
937  TLHTTPServerSocket(FSocket).FResponseInfo.Status := hsInternalError;
938end;
939
940procedure TFastCGIOutput.DoneInput;
941begin
942  if FRequest <> nil then
943    FRequest.DoneInput;
944end;
945
946procedure TFastCGIOutput.RequestEnd(ARequest: TLFastCGIRequest);
947begin
948  FRequest.OnEndRequest := nil;
949  FRequest.OnInput := nil;
950  FRequest.OnOutput := nil;
951  FRequest := nil;
952  { trigger final write, to flush output to socket }
953  WriteCGIBlock;
954end;
955
956procedure TFastCGIOutput.RequestNeedInput(ARequest: TLFastCGIRequest);
957begin
958  FSocket.ParseBuffer;
959end;
960
961procedure TFastCGIOutput.RequestHasOutput(ARequest: TLFastCGIRequest);
962begin
963  WriteCGIBlock;
964end;
965
966procedure TFastCGIOutput.RequestHasStderr(ARequest: TLFastCGIRequest);
967var
968  lBuf: array[0..1023] of char;
969  lRead: integer;
970begin
971  lRead := ARequest.Get(lBuf, sizeof(lBuf)-1);
972  lBuf[lRead] := #0;
973  write(pchar(@lBuf[0]));
974end;
975
976function  TFastCGIOutput.HandleInput(ABuffer: pchar; ASize: integer): integer;
977begin
978  Result := FRequest.SendInput(ABuffer, ASize);
979end;
980
981function  TFastCGIOutput.WriteCGIData: TWriteBlockStatus;
982var
983  lRead: integer;
984begin
985  if FRequest = nil then exit(wsDone);
986  if FRequest.OutputDone then exit(wsDone);
987  lRead := FRequest.Get(@FBuffer[FReadPos], FBufferSize-FReadPos);
988  Inc(FReadPos, lRead);
989  Result := InputBufferEmptyToWriteStatus[lRead = 0];
990end;
991
992function  TFastCGIOutput.WriteBlock: TWriteBlockStatus;
993begin
994  if (FRequest <> nil) and FRequest.OutputPending then
995  begin
996    FRequest.ParseClientBuffer;
997    Result := wsWaitingData;
998  end else
999    Result := inherited;
1000end;
1001
1002procedure TFastCGIOutput.StartRequest;
1003begin
1004  FRequest.OnEndRequest := @RequestEnd;
1005  FRequest.OnInput := @RequestNeedInput;
1006  FRequest.OnOutput := @RequestHasOutput;
1007  FRequest.OnStderr := @RequestHasStderr;
1008  inherited;
1009  FRequest.DoneParams;
1010end;
1011
1012{ TFormOutput }
1013
1014constructor TFormOutput.Create(ASocket: TLHTTPSocket);
1015begin
1016  inherited;
1017  FRequestVars := TStringList.Create;
1018end;
1019
1020destructor TFormOutput.Destroy;
1021var
1022  I: integer;
1023  tmpObj: TObject;
1024begin
1025  for I := 0 to FRequestVars.Count - 1 do
1026  begin
1027    tmpObj := FRequestVars.Objects[I];
1028    Finalize(string(tmpObj));
1029    FRequestVars.Objects[I] := nil;
1030  end;
1031  FRequestVars.Free;
1032  inherited;
1033end;
1034
1035function TFormOutput.AddVariables(Variables: pchar; ASize: integer; SepChar: char): integer;
1036var
1037  varname, sep, next: pchar;
1038  strName, strValue: string;
1039  tmpObj: TObject;
1040  i: integer;
1041begin
1042  if Variables = nil then
1043    exit(0);
1044  if ASize = -1 then
1045    ASize := StrLen(Variables);
1046  varname := Variables;
1047  repeat
1048    sep := varname + IndexChar(varname^, ASize, '=');
1049    if sep < varname then
1050      break;
1051    dec(ASize, sep-varname);
1052    next := sep + IndexChar(sep^, ASize, SepChar);
1053    if next < sep then
1054    begin
1055      next := sep + ASize;
1056      ASize := 0;
1057    end else
1058      dec(ASize, next+1-sep);
1059    if sep > varname then
1060    begin
1061      setlength(strName, sep-varname);
1062      move(varname[0], strName[1], sep-varname);
1063      setlength(strValue, next-sep-1);
1064      move(sep[1], strValue[1], next-sep-1);
1065      i := FRequestVars.Add(strName);
1066      tmpObj := nil;
1067      string(tmpObj) := strValue;
1068      FRequestVars.Objects[i] := tmpObj;
1069    end;
1070    varname := next+1;
1071  until false;
1072  Result := ASize;
1073end;
1074
1075procedure TFormOutput.DoneInput;
1076begin
1077  if Assigned(FOnExtraHeaders) then
1078    FOnExtraHeaders(Self);
1079  TLHTTPServerSocket(FSocket).StartResponse(Self);
1080end;
1081
1082function TFormOutput.HandleInputFormURL(ABuffer: pchar; ASize: integer): integer;
1083begin
1084  Result := ASize-AddVariables(ABuffer, ASize, URIParamSepChar)
1085end;
1086
1087procedure TFormOutput.ParseMultipartHeader(ABuffer, ALineEnd: pchar);
1088var
1089  I: TLMultipartParameter;
1090  len: integer;
1091begin
1092  for I := Low(TLMultipartParameter) to High(TLMultipartParameter) do
1093  begin
1094    len := Length(MPParameterStrings[I]);
1095    if ABuffer+len >= ALineEnd then
1096      continue;
1097    if (ABuffer[len] = ':')
1098      and (StrLIComp(ABuffer, PChar(MPParameterStrings[I]), len) = 0) then
1099    begin
1100      Inc(ABuffer, len+2);
1101      repeat
1102        if ABuffer = ALineEnd then exit;
1103        if ABuffer^ <> ' ' then break;
1104        inc(ABuffer);
1105      until false;
1106      FMPParameters[I] := ABuffer;
1107      if I = mpContentType then
1108      begin
1109        repeat
1110          if ABuffer = ALineEnd then exit;
1111          if ABuffer = ';' then break;
1112          inc(ABuffer);
1113        until false;
1114
1115      end;
1116      break;
1117    end;
1118  end;
1119end;
1120
1121function TFormOutput.FindBoundary(ABuffer: pchar): pchar;
1122begin
1123  {$warning TODO}
1124  Result := nil;
1125end;
1126
1127function TFormOutput.HandleInputMultipart(ABuffer: pchar; ASize: integer): integer;
1128var
1129  pos, next, endline: pchar;
1130begin
1131  pos := ABuffer;
1132  repeat
1133    case FMPState of
1134      msStart:
1135      begin
1136        { discard until first boundary }
1137        next := FindBoundary(pos);
1138        if next = nil then
1139          exit(ASize);
1140        FMPState := msBodypartHeader;
1141      end;
1142      msBodypartHeader:
1143      begin
1144        endline := pos + IndexChar(pos, ASize, #10);
1145        if endline < pos then
1146          exit(pos-ABuffer);
1147        next := endline+1;
1148        if (endline > pos) and ((endline-1)^ = #13) then
1149          dec(endline);
1150        endline^ := #0;
1151        if endline > pos then
1152          ParseMultipartHeader(pos, endline)
1153        else
1154          FMPState := msBodypartData;
1155      end;
1156      msBodypartData:
1157      begin
1158        { decode based on content-transfer-encoding ? }
1159        { CRLF before boundary, belongs to boundary, not data! }
1160        next := FindBoundary(ABuffer);
1161      end;
1162    else
1163      exit(ASize);
1164    end;
1165    dec(ASize, next-pos);
1166    pos := next;
1167  until false;
1168end;
1169
1170function TFormOutput.HandleInputDiscard(ABuffer: pchar; ASize: integer): integer;
1171begin
1172  Result := ASize;
1173end;
1174
1175function TFormOutput.HandleInput(ABuffer: pchar; ASize: integer): integer;
1176begin
1177  Result := FHandleInput(ABuffer, ASize);
1178end;
1179
1180function TFormOutput.FillBuffer: TWriteBlockStatus;
1181begin
1182  Result := wsDone;
1183  if Assigned(FOnFillBuffer) then
1184    FOnFillBuffer(Self, Result);
1185end;
1186
1187procedure TFormOutput.DeleteCookie(const AName: string; const APath: string = '/';
1188  const ADomain: string = '');
1189begin
1190  { cookies expire when expires is in the past, duh }
1191  SetCookie(AName, '', Now - 7.0, APath, ADomain);
1192end;
1193
1194procedure TFormOutput.SetCookie(const AName, AValue: string; const AExpires: TDateTime;
1195  const APath: string = '/'; const ADomain: string = '');
1196var
1197  headers: PStringBuffer;
1198begin
1199  headers := @TLHTTPServerSocket(FSocket).FHeaderOut.ExtraHeaders;
1200  AppendString(headers^, 'Set-Cookie: ' + HTTPEncode(AName) + '=' + HTTPEncode(AValue));
1201  AppendString(headers^, ';path=' + APath + ';expires=' + FormatDateTime(HTTPDateFormat, AExpires));
1202  if Length(ADomain) > 0 then
1203  begin
1204    AppendString(headers^, ';domain=');
1205    AppendString(headers^, ADomain);
1206  end;
1207  AppendString(headers^, #13#10);
1208end;
1209
1210{ TFormHandler }
1211
1212procedure TFormHandler.SelectMultipart(AFormOutput: TFormOutput; AContentType: pchar);
1213var
1214  boundary, endquote: pchar;
1215begin
1216  boundary := StrScan(AContentType, '=');
1217  if boundary <> nil then
1218  begin
1219    Inc(boundary);
1220    if boundary^ = '"' then
1221    begin
1222      Inc(boundary);
1223      endquote := StrScan(boundary, '"');
1224      if endquote <> nil then
1225        endquote^ := #0;
1226    end;
1227  end;
1228
1229  AFormOutput.FBoundary := boundary;
1230  AFormOutput.FHandleInput := @AFormOutput.HandleInputMultipart;
1231end;
1232
1233function TFormHandler.HandleURI(ASocket: TLHTTPServerSocket): TOutputItem;
1234var
1235  newFormOutput: TFormOutput;
1236  contentType: pchar;
1237begin
1238  if not Assigned(FOnHandleURI) then
1239    exit(nil);
1240
1241  newFormOutput := FOnHandleURI(ASocket);
1242  if newFormOutput = nil then
1243    exit(nil);
1244
1245  newFormOutput.AddVariables(ASocket.FRequestInfo.QueryParams, -1, URIParamSepChar);
1246  newFormOutput.AddVariables(ASocket.Parameters[hpCookie], -1, CookieSepChar);
1247  contentType := TLHTTPServerSocket(ASocket).Parameters[hpContentType];
1248  if StrIComp(contentType, FormURLContentType) = 0 then
1249    newFormOutput.FHandleInput := @newFormOutput.HandleInputFormURL
1250  else if StrIComp(contentType, MultipartContentType) = 0 then
1251    SelectMultipart(newFormOutput, contentType)
1252  else
1253    newFormOutput.FHandleInput := @newFormOutput.HandleInputDiscard;
1254
1255  Result := newFormOutput;
1256end;
1257
1258end.
1259