Il se trouve que c'était assez rapide à faire : le Fast CGI date de 1996 et est donc plutôt simple, et il ne m'a fallu que quelques heures de travail pour arriver à un POC.
Si quelqu'un d'autre en a besoin, je partage mon travail ici :
Avancement :
Code :18/12/21 : Bug fixes, bug fixes everywhere!
18/12/19 : Ajouter du support des réponses plus grosses que 2^16 octets.
18/12/10 : Répondre autre chose que du texte.
Ajout de WriteResponseData().
18/12/09 : Moar cookies
Ajout de la fonction FastCGI::GetCookie(), pour ne pas avoir besoin de bidouiller avec les paramètres pour récupérer la valeur d'un cookie. /!\ Elle va déconner plein tube si il y a un espace dans le cookie, faudrait écrire une regex mais je suis pas habitué à PCRE et, en l'état, ça suffit pour mes besoins.
18/12/08 : Cookies & string format
Ajout du support des cookies, ajout d'une options d'encodage des strings (par défaut en UTF8)
18/12/07 : Padding size fix
Fix une erreur grossière avec le padding des packets : Il était calculé mais pas inclus dans les packets... Par un hasard malheureux, tous mes packets de tests avaient une taille multiple de 8, le bug était donc passé sous le radar.
18/12/04 : POC.
Seule une petite partie du protocole FCGI est implémentée et il y a pas mal de risque de memory leaks, mais les bases sont suffisantes pour qui veut étudier ou s'amuser avec le FCGI
Code : Tout sélectionner
DeclareModule FastCGI
; Server
Declare Open(Port, *Callback, BindedIP.s = "") ;Create a FCGI Application on the given port. Return a Server object if succeed or 0 otherwise. Callback format : Callback(Request)
Declare Close(*Server) ;Close the given Server.
; Request
Declare FinishReponse(*Request) ;Send the response
Declare.s GetCookie(*Request, Cookie.s) ;Return the value of the given
Declare.s GetParameter(*Request, Parameter.s) ;Return the value of the give parameter if it exists.
Declare WriteResponseHeader(*Request, Header.s, Value.s) ;Write a header to the response
Declare WriteResponseData(*Request,*Buffer, Lenght) ;Add data to the response
Declare WriteResponseString(*Request, String.s, Format = #PB_UTF8) ;Add a string to the response
EndDeclareModule
Module FastCGI
EnableExplicit
;{ private variables declaration
#__VERSION = 1
#__ROLE_RESPONDER = 1
#__ROLE_AUTHORIZER = 2
#__ROLE_FILTER = 3
#__TYPE_BEGIN = 1
#__TYPE_ABORT = 2
#__TYPE_END = 3
#__TYPE_PARAMS = 4
#__TYPE_STDIN = 5
#__TYPE_STDOUT = 6
#__TYPE_STDERR = 7
#__TYPE_DATA = 8
#__TYPE_GETVALUES = 9
#__TYPE_GETVALUES_RESULT = 10
#__TYPE_UNKOWNTYPE = 11
Structure Server
ServerID.i
Thread.i
Stop.i
*Callback
EndStructure
Structure Record
Version.a
Type.a
RequestIdB1.a
RequestIdB0.a
ContentLengthB1.a
ContentLengthB0.a
PaddingLength.a
Reserved.a
EndStructure
Structure Request
ClientID.i
RequestIdB1.a
RequestIdB0.a
Alive.a
Responded.a
Map Variable.s()
Map Response.s()
List Cookie.s()
List ResponseData.i()
EndStructure
#__HEADER_SIZE = SizeOf(Record)
#__MULTIRECORDSIZE = 65512
;}
;{ Private procedures declaration
Declare ServerThread(Server)
Declare ProcessPairs(*Data, *Request.Request ,Lenght)
;}
;{ Public procedures
;- Server
Procedure Close(*Server.Server)
; Wow, no mutex? Well... I feel like living dangerously! (Also, I need to test : I don't think it can be a problem...)
*Server\Stop = #True
EndProcedure
Procedure Open(Port, *Callback, BindedIP.s = "")
Protected Server, *ServerData.Server
Server = CreateNetworkServer(#PB_Any,Port,#PB_Network_TCP,BindedIP)
If Server
*ServerData.Server = AllocateMemory(SizeOf(Server))
*ServerData\ServerID = Server
*ServerData\Thread = CreateThread(@ServerThread(),*ServerData)
*ServerData\Callback = *Callback
EndIf
ProcedureReturn *ServerData
EndProcedure
;- Request
Procedure FinishReponse(*Request.Request)
Protected Size, PaddingSize, Position, DataPosition, RecordCount = 1,LastPackage = 0, RecordProgress , MultipleRecordSize = #__MULTIRECORDSIZE,SentData, Progress
Protected *Record.Record
Protected *Data, *Packet
; 1- Calculate the packet size
ForEach *Request\Response()
Size + StringByteLength(MapKey(*Request\Response())+*Request\Response(),#PB_Ascii) + 3 ; 3 byte as : 1 byte for LF and 2 bytes for ": "
Next
ForEach *Request\ResponseData()
Size + MemorySize(*Request\ResponseData())
Next
ForEach *Request\Cookie()
Size + StringByteLength(*Request\Cookie(),#PB_Ascii) + 13 ; 12 is for "Set-Cookie: ", 1 = LF
Next
Size +1 ; le dernier LF
If Size > #__MULTIRECORDSIZE ; Ok so, if we are trying to send more than the maximum size of a fcgi record/packet, we'll split them into several records. (Size of said records can be changed with #__MULTIRECORDSIZE. Default size is (2 ^16 - #__HEADER_SIZE - #__HEADER_SIZE - 8) bytes and it is the absolute maximum!)
RecordCount = Round(Size/#__MULTIRECORDSIZE,#PB_Round_Up)
LastPackage = Size % #__MULTIRECORDSIZE
If LastPackage = 0
LastPackage = #__MULTIRECORDSIZE
RecordCount -1
PaddingSize = 0
Else
PaddingSize = Bool(LastPackage % 8 > 0) * (8 -LastPackage % 8)
EndIf
Else
LastPackage = Size
PaddingSize = Bool(Size % 8 > 0) * (8 -Size % 8)
EndIf
; 2- Allocate memory and fill it with datathe needed records
*Data = AllocateMemory(Size,#PB_Memory_NoClear)
ForEach *Request\Response() ; TBH, I expect it to fail miserably if we have more than 64k worth of response/cookies... But it probably should not happen, so I hope I'm on the safe side.
Position + PokeS(*Data + Position,MapKey(*Request\Response())+": "+*Request\Response()+#LF$,-1,#PB_Ascii|#PB_String_NoZero)
Next
ForEach *Request\Cookie()
Position + PokeS(*Data + Position,"Set-Cookie: " + *Request\Cookie()+#LF$,-1,#PB_Ascii)
Next
Position + PokeS(*Data + Position,#LF$,-1,#PB_Ascii|#PB_String_NoZero)
ForEach *Request\ResponseData()
CopyMemory(*Request\ResponseData(),*Data + Position,MemorySize(*Request\ResponseData()))
Position + MemorySize(*Request\ResponseData())
FreeMemory(*Request\ResponseData())
DeleteElement(*Request\ResponseData())
Next
*Packet = AllocateMemory(RecordCount * #__HEADER_SIZE + Size + PaddingSize + #__HEADER_SIZE + 8)
Position = 0
; 3 - Let's create one big packet containing all the records.
For RecordProgress = 1 To RecordCount
*Record = *Packet + Position
*Record\RequestIdB0 = *Request\RequestIdB0
*Record\RequestIdB1 = *Request\RequestIdB1
*Record\Type = #__TYPE_STDOUT
*Record\Version = #__VERSION
If RecordProgress = RecordCount
*Record\ContentLengthB0 = LastPackage
*Record\ContentLengthB1 = LastPackage >> 8
*Record\PaddingLength = PaddingSize
CopyMemory(*Data+DataPosition,*Packet + Position+#__HEADER_SIZE,LastPackage)
Else
*Record\ContentLengthB0 = MultipleRecordSize
*Record\ContentLengthB1 = MultipleRecordSize >> 8
*Record\PaddingLength = 0
CopyMemory(*Data+DataPosition,*Packet + Position+#__HEADER_SIZE,#__MULTIRECORDSIZE)
DataPosition + #__MULTIRECORDSIZE
SentData = 0
Repeat
Progress = SendNetworkData(*Request\ClientID,*Packet + Position + SentData,#__MULTIRECORDSIZE + #__HEADER_SIZE - SentData)
If Progress > 0
SentData + Progress
EndIf
Until SentData = #__MULTIRECORDSIZE + #__HEADER_SIZE
Position + #__MULTIRECORDSIZE + #__HEADER_SIZE
EndIf
Next
FreeMemory(*Data)
; 4 - write the END record
*Record = *Packet + Position + #__HEADER_SIZE + LastPackage + PaddingSize
*Record\ContentLengthB0 = 8
*Record\ContentLengthB1 = 0
*Record\RequestIdB0 = *Request\RequestIdB0
*Record\RequestIdB1 = *Request\RequestIdB1
*Record\Type = #__TYPE_END
*Record\Version = #__VERSION
*Record\PaddingLength = 0
*Request\Responded = #True
SendNetworkData(*Request\ClientID,*Packet+ Position,#__HEADER_SIZE + LastPackage + PaddingSize + #__HEADER_SIZE + 8)
FreeMemory(*Packet)
EndProcedure
Procedure.s GetCookie(*Request, Cookie.s)
Protected.s Parameter = FastCGI::GetParameter(*Request,"HTTP_COOKIE"), Result
Protected Position1,Position2
Cookie + "="
Position1 = FindString(Parameter, Cookie)
If Position1
Position1 + Len(Cookie)
Position2 = FindString(Parameter, " ",Position1)
If Position2 = 0
Position2 = Len(Parameter)
EndIf
Result = Mid(Parameter,Position1,Position2-Position1+1)
EndIf
ProcedureReturn Result
EndProcedure
Procedure.s GetParameter(*Request.Request, Parameter.s)
ProcedureReturn *Request\Variable(Parameter)
EndProcedure
Procedure WriteResponseHeader(*Request.Request, Header.s, Value.s)
If Header = #PB_CGI_HeaderSetCookie
AddElement(*Request\Cookie())
*Request\Cookie() = Value
Else
*Request\Response(Header) = Value
EndIf
EndProcedure
Procedure WriteResponseString(*Request.Request, String.s, Format = #PB_UTF8)
AddElement(*Request\ResponseData())
*Request\ResponseData() = AllocateMemory(StringByteLength(String,Format),#PB_Memory_NoClear)
PokeS(*Request\ResponseData(),String,-1,Format|#PB_String_NoZero) ; // Temp
EndProcedure
Procedure WriteResponseData(*Request.Request,*Buffer, Lenght)
AddElement(*Request\ResponseData())
*Request\ResponseData() = AllocateMemory(Lenght,#PB_Memory_NoClear)
CopyMemory(*Buffer,*Request\ResponseData(),Lenght)
EndProcedure
;}
;{ Private procedures
Procedure ServerThread(*ServerData.Server)
Protected Lenght, ContentLenght
Protected NewMap ClientMap.Request(), *Request.Request, *Record.Record = AllocateMemory(#__HEADER_SIZE,#PB_Memory_NoClear), *Data = AllocateMemory(65535,#PB_Memory_NoClear)
Repeat
Select NetworkServerEvent(*ServerData\ServerID)
Case #PB_NetworkEvent_None
Delay(1)
Case #PB_NetworkEvent_Connect
*Request = AddMapElement(ClientMap(), Str(EventClient()),#PB_Map_NoElementCheck)
*Request\ClientID = EventClient()
*Request\Alive = #True
Case #PB_NetworkEvent_Data
*Request = FindMapElement(ClientMap(), Str(EventClient()))
While ReceiveNetworkData(*Request\ClientID,*Record,#__HEADER_SIZE) > 0
ContentLenght = (*Record\contentLengthB1 <<8 + *Record\contentLengthB0)
Select *Record\type
Case #__TYPE_BEGIN
*Request\RequestIdB0 = *Record\RequestIdB0
*Request\RequestIdB1 = *Record\RequestIdB1
If ContentLenght
Lenght = ReceiveNetworkData(*Request\ClientID,*Data,ContentLenght)
ProcessPairs(*Data, *Request,Lenght)
EndIf
Case #__TYPE_ABORT
Case #__TYPE_END
Case #__TYPE_PARAMS
If ContentLenght
Lenght = ReceiveNetworkData(*Request\ClientID,*Data,ContentLenght)
ProcessPairs(*Data, *Request,Lenght)
EndIf
Case #__TYPE_STDIN
If ContentLenght
Lenght = ReceiveNetworkData(*Request\ClientID,*Data,ContentLenght)
ProcessPairs(*Data, *Request,Lenght)
EndIf
CreateThread(*ServerData\Callback,*Request)
Case #__TYPE_STDOUT
Case #__TYPE_STDERR
Case #__TYPE_DATA
Case #__TYPE_GETVALUES
Case #__TYPE_GETVALUES_RESULT
Case #__TYPE_UNKOWNTYPE
EndSelect
If *Record\PaddingLength
ReceiveNetworkData(*Request\ClientID,*Data,*Record\PaddingLength)
EndIf
Wend
Case #PB_NetworkEvent_Disconnect
*Request = FindMapElement(ClientMap(), Str(EventClient()))
If *Request\Responded
DeleteMapElement(ClientMap(),Str(EventClient()))
Else
*Request\Alive = #False
EndIf
EndSelect
If *ServerData\Stop
Break
EndIf
ForEver
CloseNetworkServer(*ServerData\ServerID)
ProcedureReturn #Null
EndProcedure
Procedure ProcessPairs(*Data, *Request.Request ,Lenght)
EnableExplicit
Protected Progress, namelenght, valuelenght,Name.s, Value.s
While Progress < lenght
namelenght = PeekA(*data + Progress)
If namelenght = 128
namelenght = ((PeekA(*data + Progress) & $7f) << 24) + (PeekA(*data + Progress + 1) << 16) + (PeekA(*data + Progress + 2) << 8) + PeekA(*data + Progress + 3);
Progress + 3
EndIf
Progress + 1
valuelenght = PeekA(*data + Progress)
If valuelenght = 128
valuelenght = ((PeekA(*data + Progress) & $7f) << 24) + (PeekA(*data + Progress + 1) << 16) + (PeekA(*data + Progress + 2) << 8) + PeekA(*data + Progress + 3);
Progress + 3
EndIf
Progress + 1
Name = PeekS(*data + Progress,namelenght,#PB_Ascii)
If Len(Name)
AddMapElement(*Request\Variable(),PeekS(*data + Progress,namelenght,#PB_Ascii),#PB_Map_NoElementCheck)
Progress + namelenght
*Request\Variable() = PeekS(*data + Progress,valuelenght,#PB_Ascii)
Progress +valuelenght
;Debug MapKey(*Request\Variable()) + " : "+ *Request\Variable()
EndIf
Wend
DisableExplicit
EndProcedure
;}
EndModule
CompilerIf #PB_Compiler_IsMainFile
ReadFile(0,"C:\Users\poshu\Desktop\image.bmp") ;<- replace this path by an existing one.
Global *imagedata = AllocateMemory(Lof(0))
ReadData(0,*imagedata,Lof(0))
CloseFile(0)
; Demo
Procedure Handler_FCGIRequest(Request)
Debug FastCGI::GetCookie(Request,"acookie")
;To send an image
FastCGI::WriteResponseHeader(Request,#PB_CGI_HeaderContentType,"image/bmp")
FastCGI::WriteResponseData(Request,*imagedata,MemorySize(*imagedata))
;To send some good old html
; FastCGI::WriteResponseHeader(Request,#PB_CGI_HeaderContentType,"text/html")
; FastCGI::WriteResponseString(Request,~"<head><meta charset=\"UTF-8\"></head>" +
; "<html><title>PureBasic - FastCGI</title><body>" +
; "Hello from PureBasic Re-FCGI くも!<br>" +
; "Actual time: <b>"+FormatDate("%hh:%ii", Date()) + "</b>" +
; "</body></html>")
; FastCGI::WriteResponseHeader(Request,#PB_CGI_HeaderSetCookie,"acookie=avalue")
FastCGI::FinishReponse(Request)
EndProcedure
OpenConsole("fastCGI Demo")
InitNetwork()
Server = FastCGI::Open(5600,@Handler_FCGIRequest())
Input()
FastCGI::Close(Server)
PrintN("Server closed")
Input()
End
CompilerEndIf