xtool/contrib/mORMot/SQLite3/mORMotFastCgiServer.pas

875 lines
29 KiB
ObjectPascal

/// FastCGI HTTP/1.1 Server implementation for mORMot
// - this unit is a part of the freeware Synopse mORMot framework,
// licensed under a MPL/GPL/LGPL tri-license; version 1.18
unit mORMotFastCgiServer;
{
This file is part of Synopse mORMot framework.
Synopse mORMot framework. Copyright (C) 2022 Arnaud Bouchez
Synopse Informatique - https://synopse.info
*** BEGIN LICENSE BLOCK *****
Version: MPL 1.1/GPL 2.0/LGPL 2.1
The contents of this file are subject to the Mozilla Public License Version
1.1 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL
Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the License.
The Original Code is Synopse mORMot framework.
The Initial Developer of the Original Code is Arnaud Bouchez.
Portions created by the Initial Developer are Copyright (C) 2022
the Initial Developer. All Rights Reserved.
Contributor(s):
Alternatively, the contents of this file may be used under the terms of
either the GNU General Public License Version 2 or later (the "GPL"), or
the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
in which case the provisions of the GPL or the LGPL are applicable instead
of those above. If you wish to allow use of your version of this file only
under the terms of either the GPL or the LGPL, and not to allow others to
use your version of this file under the terms of the MPL, indicate your
decision by deleting the provisions above and replace them with the notice
and other provisions required by the GPL or the LGPL. If you do not delete
the provisions above, a recipient may use your version of this file under
the terms of any one of the MPL, the GPL or the LGPL.
***** END LICENSE BLOCK *****
fastcgi configuration example for lighttpd under linux:
- http://localhost/test will ask TSQLRestServer from the content
- we use only one process and one thread, which is OK for our framework
- the socket is created by lighttpd, and used by the executable
- the target executable (located at /var/bin/test) must call the
mORMotFastCGIMainProc() procedure
- default implementation under linux use the libfcgi.so library:
you'll need to exec e.g. apt-get install libfcgi0ldbl
fastcgi.server = ( "/test/" =>
(( "socket" => "/tmp/test-socket",
"bin-path" => "/var/bin/test",
"min-procs" => 1,
"max-procs" => 1,
# "max-load-per-proc" => 1, # never used if max-procs=1
"kill-signal" => 10 # libfcgi need to be killed with SIGUSR1(10)
))
)
------------------------------------------------------------------------
TODO: to be changed so that it uses THttpServerRequest, and would be
directly defined as THttpFastCGIServer = class(THttpServerGeneric)
------------------------------------------------------------------------
}
{.$define USELIBFCGI}
// if defined, the standard client library (libfcgi.dll or libfcgi.so) is
// used, instead of the 100% pascal FastCGI client
{$ifndef MSWINDOWS}
{$define USELIBFCGI}
// pure pascal FastCGI client only available under Windows yet
{$endif}
interface
uses
{$ifdef MSWINDOWS}
Windows,
SynWinSock,
{$else}
Types,
LibC,
{$endif}
{$ifndef USELIBFCGI}
SynCrtSock, // the pascal version use our Synopse socket
{$endif}
SysUtils, Classes,
SynCommons,
mORMot;
/// publishes a HTTP/1.1 RESTFUL JSON mORMot Server, using FASTCGI
// - will raise an exception if the executable was not used as a FastCGI
// process, but a normal CGI process
// - call this procedure in your main block of your program: it is up to
// the HTTP server to implement the request handling
procedure mORMotFastCGIMainProc(Server: TSQLRestServer);
{$ifdef USELIBFCGI}
(* FastCGI access through the libfcgi library
only needed libfcgi functions were defined
see http://www.fastcgi.com for additional documentation *)
const
/// Unsupported Version FastCGI error code
FCGX_UNSUPPORTED_VERSION = -2;
/// Protocol related FastCGI error code
FCGX_PROTOCOL_ERROR = -3;
/// Parameters related FastCGI error code
FCGX_PARAMS_ERROR = -4;
/// Calling Sequence FastCGI error code
FCGX_CALL_SEQ_ERROR = -5;
/// Useful FCGX_Request flag
// - Setting FCGI_FAIL_ACCEPT_ON_INTR prevents FCGX_Accept() from
// restarting upon being interrupted
FCGI_FAIL_ACCEPT_ON_INTR = 1;
/// name of the dll we use to import all FastCGI low-level functions from
// - 'libfcgi.so.0' is Ubuntu/Debian Linux standard library name: check
// your distribution
{$IFDEF Win32}
dllname = 'libfcgi.dll'; {$ELSE}
dllname = 'libfcgi.so.0';
{$ENDIF}
type
/// defines the state of a FastCGI stream
// - The state of a stream is private and should only be accessed
// as an 'opaque' pointer
PFCGX_Stream = pointer;
/// An environment (as defined by environ(7))
// - A nil-terminated array of strings, each string having the form name=value
FCGX_ParamArrayType = PPAnsiChar;
/// returns TRUE if this process appears to be a CGI process
// - in such case, you should not use the FastCGI implementation
// - caller should raise an exception in such case
function FCGX_IsCGI: boolean; cdecl; external dllname;
{{ accept a new request (NOT multi-thread safe)
- return 0 for successful call, -1 for error
- Finishes the request accepted by (and frees any storage allocated by) the
previous call to FCGX_Accept. Creates input, output, and error streams and
assigns them to *in, *out, and *err respectively.
Creates a parameters data structure to be accessed via getenv(3) (if
assigned to environ) or by FCGX_GetParam and assigns it to *envp.
- DO NOT retain pointers to the envp array or any strings
contained in it (e.g. to the result of calling FCGX_GetParam),
since these will be freed by the next call to FCGX_Finish or FCGX_Accept }
function FCGX_Accept(var _in, _out, _err: PFCGX_Stream; var params: FCGX_ParamArrayType): integer;
cdecl; external dllname;
{{ read a line from the server socket
- Reads up to n-1 consecutive bytes from the input stream into the character array str.
- Stops before n-1 bytes have been read if '\n' or EOF is read.
- The terminating '\n' is copied to str. After copying the last byte into str,
stores a '\0' terminator.
- returns NULL if EOF is the first thing read from the input stream }
function FCGX_GetLine(str: PAnsiChar; n: integer; stream: PFCGX_Stream): PAnsiChar;
cdecl; external dllname;
{{ read some data from the server socket
- reads up to n consecutive bytes from the input stream into the character
array str. Performs no interpretation of the input bytes.
- returns number of bytes read. If result is smaller than n, the end of input
has been reached }
function FCGX_GetStr(str: PAnsiChar; n: integer; stream: PFCGX_Stream): integer;
cdecl; external dllname;
{{ write some data to the server socket
- writes n consecutive bytes from the character array str into the output stream.
- performs no interpretation of the output bytes.
- returns the number of bytes written (n) for normal return,
EOF (-1) if an error occurred }
function FCGX_PutStr(str: PAnsiChar; n: integer; stream: PFCGX_Stream): integer;
cdecl; external dllname;
{{ obtain value of FCGI parameter in environment
- returns the value bound to name, NULL if name not present in the environment envp
- caller must not mutate the result or retain it past the end of this request
- see @http://hoohoo.ncsa.illinois.edu/cgi/env.html for a Environment Variables list }
function FCGX_GetParam(name: PUTF8Char; envp: FCGX_ParamArrayType): PUTF8Char;
cdecl; external dllname;
{$else}
(* NATIVE Pascal FastCGI client implementation
implemented under Windows only by now *)
type
/// FastCGI record types, i.e. the general function that the record performs
TFCGIRecType = (
rtBeginRequest = 1,
rtAbortRequest,
rtEndRequest,
rtParams,
rtStdIn,
rtStdOut,
rtStdErr,
rtData,
rtGetValues,
rtGetValuesResult,
rtUnknown);
/// FastCGI roles, only Responder role is supported in this unit version
TFCGIRole = (
rUnknown,
rResponder,
rAuthorizer,
rFilter);
PFCGIHeader = ^TFCGIHeader;
/// FastCGI header
TFCGIHeader = packed record
/// FastCGI protocol version, ever constant 1
Version: byte;
/// FastCGI record type
RecType: TFCGIRecType;
/// identifies the FastCGI request to which the record belongs
// - equals zero for a management request request
// - non zero for an application record
// Used also to determine if the session is being multiplexed
ID: word;
/// FastCGI record length
// - will send up to 64 KB of data per block
Len: word;
/// Pad length to complete 8 bytes alignment boundary in FastCGI protocol
PadLen: byte;
/// Pad field
Filler: byte;
end;
PFCGIBeginRequest = ^TFCGIBeginRequest;
/// rtBeginRequest record
TFCGIBeginRequest = packed record
/// FastCGI header
Header: TFCGIHeader;
/// Pad field
Filler: byte;
/// FastCGI role
Role: TFCGIRole;
/// Keep connection
KeepConn: boolean;
/// Pad field
Filler2: array[1..5] of byte;
end;
/// FastCGI level status (and error) code for END_REQUEST record
TFCGIProtocolStatus = (
psRequestComplete,
psCantMultiplexConnections,
psOverloaded,
psUnknownRole);
/// FastCGI connection modes
TFCGIListenType =
(ltUnused, {ltFileSync, ltFileASync,} ltSocketSync=2, {ltSocketASync,}
ltPipeSync{, ltPipeASync}); { ltSocketSync=2 so no RTTI }
/// handle Fast CGI
// - implements the official Fast CGI Specification available at
// @http://www.fastcgi.com/devkit/doc/fcgi-spec.html
// - this base type has virtual public methods ReadPacked and SendPacket,
// implementing the named pipe or socket defined by the single file
// descriptor sent by the web server which can be overridden by its children
// for proper socket/pipe handling
TFastCGIServer = class
protected
fServer: TSQLRestServer;
fRequestHeaders: RawUTF8;
fRequestMethod: RawUTF8;
fRequestURL: RawUTF8;
fRequestBody: RawUTF8;
// FastCGI role for the current request
fRequestRole: TFCGIRole;
// current FastCGI request ID
fRequestID: word;
fResponseHeaders: RawUTF8;
fResponseContent: RawByteString;
// global 64 KB buffer, used for chunking data to be sent
fTempResponse: RawUTF8;
fTempNamedPipe: RawUTF8;
fListenType: TFCGIListenType;
fhListen: THandle;
fConnectionOpened: boolean;
fSocket: TCrtSocket;
// send a response back to the server
// - the content is chuncked in 64 KB buffers, if necessary
// - call SendPacket() virtual method
// - if sent in rtStdOut, the fResponseHeaders are sent with the content; in
// this case, if no 'content-type:' exists in fResponseHeaders, use default
// 'text/html'
function SendResponse(Content: RawUTF8; aRecType: TFCGIRecType): boolean;
// send an end request, with the specified protocol status
function SendEndRequest(Status: TFCGIProtocolStatus): boolean;
// reset all fRequest* parameters
procedure ResetRequest;
property hListen: THandle read fhListen;
property listenType: TFCGIListenType read fListenType;
property ConnectionOpened: boolean read fConnectionOpened;
public
/// virtual method used to read a packet from the remote server
// - must return '' on error
// - by default, use the single file descriptor sent by the web server,
// and expect to read data from the corresponding named pipe or
// TCP/IP socket
function ReadPacked: RawUTF8; virtual;
/// virtual method used to send a packed to the remote server
// - must return FALSE on error
// - by default, use the single file descriptor sent by the web server,
// and expect to write data to the corresponding named pipe or
// TCP/IP socket
function SendPacket(Buffer: pointer; BufferLen: integer): boolean; virtual;
/// method triggered when the Web server wants to abort the request
// - do nothing by default - only to be implemented for Multiplex connection
// which are not enabled with this class
procedure LogOut; virtual;
/// method triggered to calculate the response
// - expect fRequestHeaders, fRequestMethod, fRequestBody and fRequestURL
// properties as input
// - update fResponseHeaders and fResponseContent properties as output
procedure ProcessRequest; virtual;
public
/// create the object instance to run with the specified RESTful Server
constructor Create(aServer: TSQLRestServer);
/// release the associated memory and handles
destructor Destroy; override;
/// the main loop of the FastCGI application
// - loop until application is terminated
// - use the associated RESTful Server to calculate the answer
// - call the virtual methods ReadPacked and SendPacket to handle the response
// - the FastCGI server must have been successfully connected before calling it
// - return true if communication was made successfully
function Run: boolean; virtual;
/// associated RESTful Server
property Server: TSQLRestServer read fServer;
end;
{$endif}
implementation
{$ifdef USELIBFCGI}
procedure mORMotFastCGIMainProc(Server: TSQLRestServer);
var _in, _out, _err: PFCGX_Stream;
envp: FCGX_ParamArrayType;
{function ReadString: RawUTF8;
var tmp: array[0..1023] of AnsiChar; // 1KB should be enough for HTTP headers
L: integer;
begin
if FCGX_GetLine(tmp,sizeof(tmp),_in)=nil then
// EOF reached?
result := '' else begin
// get line content
L := StrLen(tmp);
while (L>0) and (tmp[L-1]<=' ') do dec(L); // in-place trimright
SetString(result,tmp,L);
end;
end;
function RemoteIP: RawUTF8;
begin
result := FCGX_GetParam('REMOTE_ADDR',envp);
end;}
var call: TSQLRestURIParams;
ContentLength,len: integer;
param: PPUtf8CharArray;
name: string[200];
ID: cardinal;
P: PUTF8Char;
begin
if FCGX_IsCGI then
raise ECommunicationException.Create('Server not called as a FastCGI process');
ID := 0;
fillchar(call,sizeof(call),0);
if Server<>nil then
while FCGX_Accept(_in,_out,_err,envp)>=0 do begin
// get headers
ContentLength := 0;
call.Url := '';
AppendBuffersToRawUTF8(call.Url,
[FCGX_GetParam('REQUEST_URI',envp),'?',FCGX_GetParam('QUERY_STRING',envp)]);
call.Method := '';
inc(ID);
call.InHead := 'ConnectionID: '+CardinalToHex(ID)+#13#10;
param := pointer(envp);
while param[0]<>nil do begin
len := StrLen(param[0]);
if len>200 then
continue; // invalid request
SetString(name,PAnsiChar(param[0]),len);
if name='CONTENT_LENGTH' then
ContentLength := GetCardinal(param[1]) else
if name='REQUEST_METHOD' then
call.Method := RawUTF8(param[1]) else
if name='REMOTE_ADDR' then
AppendBuffersToRawUTF8(call.InHead,['RemoteIP: ',param[1],#13#10]);
AppendBuffersToRawUTF8(call.InHead,[param[0],': ',param[1],#13#10]);
param := @param[2];
end;
// get content
call.InBody := '';
if ContentLength>0 then begin
SetLength(call.InBody,ContentLength);
if FCGX_GetStr(pointer(call.InBody),ContentLength,_in)<>ContentLength then
continue; // invalid request
end;
call.RestAccessRights := @SUPERVISOR_ACCESS_RIGHTS;
// process the request in the internal TSQLRestServer instance
Server.URI(call);
ContentLength := length(call.OutBody);
call.OutHead := trim(call.OutHead);
if call.OutHead<>'' then
call.OutHead := call.OutHead+#13#10;
call.OutHead := FormatUTF8(
'%Status: %'#13#10'Server-InternalState: %'#13#10+
'X-Powered-By: mORMotFastCGIServer https://synopse.info'#13#10+
'Content-Length: %'#13#10,
[call.OutHead,call.OutStatus,call.OutInternalState,ContentLength]);
// send the answer back to the HTTP server
if FCGX_PutStr(pointer(call.OutHead),length(call.OutHead),_out)=length(call.OutHead) then
if call.OutBody<>'' then
FCGX_PutStr(pointer(call.OutBody),ContentLength,_out);
end;
end;
{$else}
/// retrieve one Name:Value pair length, as encoded by the FastCGI server
// - must be called twice, first for the Name length, then for the Value length
function ReadRequestLen(PB: PByteArray; out PNew: PByteArray): integer;
begin
if PB[0]>127 then begin
result := (PB[0] and $7f)shl 24 + PB[1] shl 16 + PB[2] shl 8 + PB[3];
PNew := PByteArray(PtrInt(PB)+4);
end else begin
result := PB[0];
PNew := PByteArray(PtrInt(PB)+1);
end;
end;
/// retrieve headers as encoded by the FastCGI server
// - return the headers as Name:Value pairs by line, ready to be
// searched with FindIniNameValue() function
function ReadRequestHeader(Buffer: pointer; BufferLen: integer): RawUTF8;
var Len0,Len1: integer;
PB: PByteArray absolute Buffer;
PC: PAnsiChar absolute Buffer;
PEnd: PAnsiChar;
PResult: PAnsiChar;
begin
result := '';
if (Buffer=nil) or (BufferLen<=0) then
exit;
PEnd := PC+BufferLen;
repeat
Len0 := ReadRequestLen(PB,PB);
Len1 := ReadRequestLen(PB,PB);
SetLength(result,length(result)+Len0+Len1+4); // 'Name: Value'#13#10
PResult := pointer(result);
move(PC^,PResult^,Len0);
inc(PC,Len0);
inc(PResult,Len0);
PResult^ := ':';
inc(PResult);
PResult^ := ' ';
inc(PResult);
move(PC^,PResult^,Len1);
inc(PC,Len1);
PWord(PResult+Len1)^ := ord(13)+ord(10)shl 8;
until PC>=PEnd;
end;
/// returns TRUE if UpperName is inside the provided headers, as encoded by the
// FastCGI server
function HasRequestHeader(Buffer: pointer; BufferLen: integer; UpperName: PAnsiChar): boolean;
var Len: integer;
PB: PByteArray absolute Buffer;
PC: PUTF8Char absolute Buffer;
PEnd: PUTF8Char;
begin
result := false;
if (Buffer=nil) or (BufferLen<=0) then
exit;
PEnd := PC+BufferLen;
repeat
Len := ReadRequestLen(PB,PB)+ReadRequestLen(PB,PB);
if IdemPChar(PC,UpperName) then begin
result := true;
exit;
end;
inc(PC,Len);
until PC>=PEnd;
end;
/// add a Name:Value parameter, encoded as expected by the FastCGI server
procedure AddParam(var Result: RawUTF8; const Name,Value: RawUTF8);
var Len: array[0..1] of cardinal;
i: integer;
P: PUTF8Char;
L: cardinal;
begin
Len[0] := length(Name);
Len[1] := length(Value);
L := length(Result);
if Len[0]>127 then
inc(L,4) else inc(L);
if Len[1]>127 then
inc(L,4) else inc(L);
SetLength(result,L+Len[0]+Len[1]);
P := pointer(result);
for i := 0 to 1 do
if Len[i]>127 then begin
PCardinal(P)^ := Len[i] shr 24 + 128 +
((Len[i] shr 16) and 255)shl 8+
((Len[i] shr 8) and 255)shl 16+
(Len[i] and 255)shl 24;
inc(P,4);
end else begin
P^ := AnsiChar(Len[i]);
inc(P);
end;
move(pointer(Name)^,P^,Len[0]);
inc(P,Len[0]);
move(pointer(Value)^,P^,Len[1]);
end;
procedure mORMotFastCGIMainProc(Server: TSQLRestServer);
var FastCGI: TFastCGIServer;
begin
FastCGI := TFastCGIServer.Create(Server);
try
FastCGI.Run;
finally
FastCGI.Free;
end;
end;
{ TFastCGIServer }
const
MAX_BUFFER = 65536;
constructor TFastCGIServer.Create(aServer: TSQLRestServer);
{$ifdef MSWINDOWS}
var oldStdIn: THandle;
pipeMode: cardinal;
{$endif}
begin
inherited Create;
fServer := aServer;
SetLength(fTempResponse,MAX_BUFFER+8); // enough place
SetLength(fTempNamedPipe,MAX_BUFFER);
{$ifdef MSWINDOWS}
// guess the type (named pipe or socket) of the file descriptor
// sent by the server, in order to use it in ReadPacked and SendPacket
oldStdIn := GetStdHandle(STD_INPUT_HANDLE);
if (GetStdHandle(STD_OUTPUT_HANDLE)=INVALID_HANDLE_VALUE) and
(GetStdHandle(STD_ERROR_HANDLE)=INVALID_HANDLE_VALUE) and
(oldStdIn<>INVALID_HANDLE_VALUE) and // FastCGI call: only STDIN
// Move the handle to a "low" number
DuplicateHandle(GetCurrentProcess, oldStdIn,
GetCurrentProcess, @hListen, 0, true, DUPLICATE_SAME_ACCESS) and
SetStdHandle(STD_INPUT_HANDLE, hListen) then begin
CloseHandle(oldStdIn);
// Set the pipe handle state so that it operates in wait mode
pipeMode := PIPE_READMODE_BYTE or PIPE_WAIT;
if SetNamedPipeHandleState(hListen,pipeMode,nil,nil) then
flistenType := ltPipeSync else begin
flistenType := ltSocketSync;
fSocket := TCrtSocket.Create(5000);
end;
exit;
end;
// if we reached here, exe was not called as a FastCGI process
raise ESynException.CreateUTF8('%.Create: % not called as a FastCGI process',
[Self,ExeVersion.ProgramFileName]);
{$else}
not implemented yet: please use libfcgi.so version, which seems stable & fast
under Linux
{$endif}
end;
destructor TFastCGIServer.Destroy;
begin
{$ifdef MSWINDOWS}
if listenType=ltPipeSync then
DisconnectNamedPipe(hListen);
{$endif}
inherited;
end;
procedure TFastCGIServer.LogOut;
begin
; // do nothing by default
end;
procedure TFastCGIServer.ProcessRequest;
var call: TSQLRestURIParams;
begin
if Server=nil then begin
ResetRequest;
exit;
end;
fillchar(call,sizeof(call),0);
with call do begin
LowLevelConnectionID := PtrInt(self);
Url := fRequestURL;
Method := fRequestMethod;
InHead := 'ConnectionID: '+CardinalToHex(fRequestID);
InBody := fRequestBody;
RestAccessRights := @SUPERVISOR_ACCESS_RIGHTS;
Server.URI(call);
fResponseContent := OutBody;
fResponseHeaders := FormatUTF8(
'Status: %'#13#10'Server-InternalState: %'#13#10+
'X-Powered-By: mORMot https://synopse.info'#13#10+
JSON_CONTENT_TYPE_HEADER+#13#10+
'Content-Length: %'#13#10+
'%', // a void line will be appened in SendResponse() method
[OutStatus,OutInternalState,length(OutBody),OutHead]);
end;
end;
function TFastCGIServer.ReadPacked: RawUTF8;
{$ifdef MSWINDOWS}
var L: integer;
{$endif}
begin
result := '';
if (self=nil) or not ConnectionOpened then
exit;
{$ifdef MSWINDOWS}
case listenType of
ltSocketSync:
result := fSocket.SockReceiveString;
ltPipeSync:
repeat
L := FileRead(hListen,pointer(fTempNamedPipe)^,length(fTempNamedPipe));
if L<0 then exit;
AppendBufferToRawUTF8(result,pointer(fTempNamedPipe),L);
until (L<length(fTempNamedPipe)) and (result<>'');
else exit;
end;
{$endif}
end;
procedure TFastCGIServer.ResetRequest;
begin
fRequestHeaders := '';
fRequestMethod := '';
fRequestURL := '';
fRequestBody := '';
fResponseHeaders := '';
fResponseContent := '';
end;
function TFastCGIServer.Run: boolean;
var Packet: RawUTF8;
PC, PEnd: PUTF8Char;
PHead: PFCGIHeader;
ValuesResult: RawUTF8;
Sin: TVarSin;
begin
result := false;
{$ifdef MSWINDOWS}
if not ConnectionOpened then
case listenType of
ltPipeSync:
if not ConnectNamedPipe(hListen,nil) then
// allow client connected after CreateNamedPipe but before ConnectNamedPipe
if GetLastError<>ERROR_PIPE_CONNECTED then
exit;
ltSocketSync: begin
fSocket.OpenBind('','',false, accept(hListen,Sin));
Assert(False,'if fSocket.SockCanRead(1000)=0 then exit;');
if fSocket.Sock<0 then
exit; // invalid socket
end;
else exit;
end;
{$else}
exit;
{$endif}
result := true;
fConnectionOpened := true;
fRequestID := 0;
if Server<>nil then
repeat
Packet := ReadPacked;
if Packet='' then
break;
PC := pointer(Packet);
PEnd := PC+length(Packet);
repeat
PHead := pointer(PC);
if PHead^.Version<>1 then begin
SendResponse('Version',rtUnknown);
break;
end;
inc(PC,sizeof(TFCGIHeader));
if (fRequestID<>0) and (PHead^.ID<>0) and (PHead^.ID<>fRequestID) then
SendEndRequest(psCantMultiplexConnections) else begin
case PHead^.RecType of
rtBeginRequest:
with PFCGIBeginRequest(PHead)^ do
if Role in [rResponder..rFilter] then begin
fRequestID := Header.ID;
fRequestRole := Role;
ResetRequest;
fRequestHeaders := 'FCGI_ROLE:'+PTypeInfo(TypeInfo(TFCGIRole))^.
EnumBaseType^.GetEnumNameTrimed(Role)+#13#10;
end else
SendEndRequest(psUnknownRole);
rtAbortRequest: begin
LogOut; // do nothing by default
ResetRequest;
end;
rtGetValues: begin
ValuesResult := '';
if HasRequestHeader(PC,PHead^.Len,'FCGI_MAX_CONNS:') then
AddParam(ValuesResult,'FCGI_MAX_CONNS','1');
if HasRequestHeader(PC,PHead^.Len,'FCGI_MAX_REQS:') then
AddParam(ValuesResult,'FCGI_MAX_REQS','1');
if HasRequestHeader(PC,PHead^.Len,'FCGI_MPXS_CONNS:') then
AddParam(ValuesResult,'FCGI_MPXS_CONNS','0');
SendResponse(ValuesResult,rtGetValuesResult);
end;
rtParams, rtStdIn, rtData: // stream records
if PHead^.Len=0 then begin
// end of stream is marked by an empty record
if PHead^.RecType=rtParams then begin
// read Name-Value pair headers stream
fRequestHeaders := ReadRequestHeader(
pointer(fRequestBody),length(fRequestBody));
fRequestURL :=
FindIniNameValue(pointer(fRequestHeaders),'REQUEST_URI:')+'?'+
FindIniNameValue(pointer(fRequestHeaders),'QUERY_STRING:');
fRequestMethod :=
FindIniNameValue(pointer(fRequestHeaders),'REQUEST_METHOD:'); // 'GET'
end else begin
// handle byte streams: stdin, data -> process in Server.URL
ProcessRequest;
if (fResponseContent<>'') or
SameTextU(fRequestMethod,'GET') or
SameTextU(fRequestMethod,'HEAD') then
SendResponse(fResponseContent,rtStdOut);
SendEndRequest(psRequestComplete);
end;
fRequestBody := '';
end else
// append stream records
AppendBufferToRawUTF8(fRequestBody,PC,PHead^.Len);
else begin
// UNKNOWN_TYPE record
SendResponse(RawUTF8(AnsiChar(PHead^.RecType)+#0#0#0#0#0#0#0), rtUnknown);
break;
end;
end; // end case PHead^.RecType
end;
inc(PC,PHead^.Len+PHead^.PadLen);
until PC>=PEnd;
until false;
end;
function TFastCGIServer.SendEndRequest(Status: TFCGIProtocolStatus): boolean;
var EndBlock: packed record
Head: TFCGIHeader; // inline SendResponse() logic
appStatus: integer;
protocolStatus: TFCGIProtocolStatus;
reserved: array[1..3] of byte; // padding for 8 bytes block length
end;
begin
assert(sizeof(EndBlock)-sizeof(EndBlock.Head)=8);
if Status<>psRequestComplete then
// on error, send back the English error message as text/html
SendResponse(UnCamelCase(PTypeInfo(TypeInfo(TFCGIProtocolStatus))^.
EnumBaseType^.GetEnumNameTrimed(Status)),rtStdOut);
fillchar(EndBlock,sizeof(EndBlock),0);
EndBlock.Head.Version := 1;
EndBlock.Head.RecType := rtEndRequest;
EndBlock.Head.ID := fRequestID;
EndBlock.Head.Len := sizeof(EndBlock)-sizeof(EndBlock.Head);
EndBlock.protocolStatus := Status;
result := SendPacket(@EndBlock,sizeof(EndBlock));
end;
function TFastCGIServer.SendPacket(Buffer: pointer; BufferLen: integer): boolean;
{$ifdef MSWINDOWS}
var L: integer;
{$endif}
begin
result := false;
if (self=nil) or not ConnectionOpened or (BufferLen=0) then
exit;
{$ifdef MSWINDOWS}
case listenType of
ltSocketSync:
result := fSocket.TrySndLow(Buffer,BufferLen);
ltPipeSync: begin
repeat
L := FileWrite(hListen,Buffer^,BufferLen);
if L<0 then exit;
dec(BufferLen,L);
inc(PtrInt(Buffer),L);
until BufferLen<=0;
result := true;
end;
else exit;
end;
{$endif}
end;
function TFastCGIServer.SendResponse(Content: RawUTF8; aRecType: TFCGIRecType): boolean;
var P: PUTF8Char;
PLen: integer;
begin
if (self=nil) or (fTempResponse='') then begin
result := false;
exit; // avoid GPF
end;
if aRecType=rtStdOut then begin // add headers to beginning of content
if IdemPChar(pointer(fRequestMethod),'HEAD') then
Content := ''; // force no content if only Header was asked for
if (fResponseHeaders<>'') and
(fResponseHeaders[length(fResponseHeaders)]<>#10) then
fResponseHeaders := fResponseHeaders+#13#10;
if not ExistsIniName(pointer(fResponseHeaders),HEADER_CONTENT_TYPE_UPPER) then
fResponseHeaders := fResponseHeaders+HTML_CONTENT_TYPE_HEADER+#13#10;
Content := fResponseHeaders+#13#10+Content;
fResponseHeaders := '';
end;
with PFCGIHeader(pointer(fTempResponse))^ do begin // use a temporary buffer
PInteger(@Version)^ := 1; // will force ID := 0
RecType := aRecType;
if not (aRecType in [rtGetValuesResult, rtUnknown]) then
ID := fRequestID;
P := pointer(Content);
PLen := length(Content);
repeat // send in 64 KB max chunks
Len := PLen-(P-pointer(Content));
if Len>MAX_BUFFER-sizeof(TFCGIHeader) then
Len := MAX_BUFFER-sizeof(TFCGIHeader);
PadLen := 7 - ((Len + 7) and 7);
move(P^,PByteArray(pointer(fTempResponse))^[sizeof(TFCGIHeader)],Len);
result := SendPacket(pointer(fTempResponse),sizeof(TFCGIHeader)+Len+PadLen);
inc(P,Len);
until not Result or (P-pointer(Content)>=PLen);
end;
end;
{$endif}
end.