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 FinishResponse(*Request) ; Send the response and close connection Declare.s GetCookie(*Request, Cookie.s) ; Return the value of the given cookie Declare.s GetParameter(*Request, Parameter.s) ; Return the value of the given parameter if it exists. Declare.s GetPostData(*Request) ; Return raw POST data Declare WriteResponseHeader(*Request, Header.s, Value.s) ; Write a header to the response Declare WriteResponseData(*Request, *Buffer, Length) ; Add data to the response Declare WriteResponseString(*Request, String.s, Format = #PB_UTF8) ; Add a string to the response Declare WriteResponseContentType(*Request, File.s) ; Write the MIME type based on file extension Declare RespondFile(*Request, File.s) ; Automatically send a file as a response EndDeclareModule Module FastCGI EnableExplicit Global NewMap MIMETypes.s() ;{ Constants #FCGI_VERSION = 1 ; Record types #FCGI_BEGIN_REQUEST = 1 #FCGI_ABORT_REQUEST = 2 #FCGI_END_REQUEST = 3 #FCGI_PARAMS = 4 #FCGI_STDIN = 5 #FCGI_STDOUT = 6 #FCGI_STDERR = 7 #FCGI_DATA = 8 #FCGI_GET_VALUES = 9 #FCGI_GET_VALUES_RESULT = 10 #FCGI_UNKNOWN_TYPE = 11 ; Roles #FCGI_RESPONDER = 1 #FCGI_AUTHORIZER = 2 #FCGI_FILTER = 3 ; Flags #FCGI_KEEP_CONN = 1 ; Protocol status #FCGI_REQUEST_COMPLETE = 0 #FCGI_CANT_MPX_CONN = 1 #FCGI_OVERLOADED = 2 #FCGI_UNKNOWN_ROLE = 3 #HEADER_SIZE = 8 #MAX_CONTENT_LENGTH = 65535 #MAX_RECORD_SIZE = 65528 ; 65535 - 8 (aligned to 8 bytes) ;} ;{ Structures Structure FCGI_Header version.a type.a requestIdB1.a requestIdB0.a contentLengthB1.a contentLengthB0.a paddingLength.a reserved.a EndStructure Structure FCGI_BeginRequestBody roleB1.a roleB0.a flags.a reserved.a[5] EndStructure Structure FCGI_EndRequestBody appStatusB3.a appStatusB2.a appStatusB1.a appStatusB0.a protocolStatus.a reserved.a[3] EndStructure Structure Server ServerID.i Thread.i Stop.i *Callback Mutex.i EndStructure Structure Request ClientID.i RequestID.u KeepConnection.a Role.u ParamsComplete.a StdinComplete.a Map Parameters.s() Map ResponseHeaders.s() List Cookies.s() List ResponseData.i() PostData.s *PostDataBuffer PostDataLength.i EndStructure ;} ;{ Private procedure declarations Declare ServerThread(*Server) Declare ProcessNameValuePairs(*Data, *Request.Request, Length) Declare ReceiveAllData(ClientID, *Buffer, Length) Declare SendAllData(ClientID, *Buffer, Length) Declare BuildHeader(*Header.FCGI_Header, Type.a, RequestID.u, ContentLength.u, PaddingLength.a) Declare SendRecord(ClientID, Type.a, RequestID.u, *Content, ContentLength) Declare SendEndRequest(ClientID, RequestID.u, AppStatus.l, ProtocolStatus.a) ;} ;{ Helper procedures ; Receive exactly 'Length' bytes, blocking until complete or error Procedure ReceiveAllData(ClientID, *Buffer, Length) Protected Received = 0, Result While Received < Length Result = ReceiveNetworkData(ClientID, *Buffer + Received, Length - Received) If Result <= 0 ProcedureReturn -1 ; Connection closed or error EndIf Received + Result Wend ProcedureReturn Received EndProcedure ; Send all data, blocking until complete Procedure SendAllData(ClientID, *Buffer, Length) Protected Sent = 0, Result While Sent < Length Result = SendNetworkData(ClientID, *Buffer + Sent, Length - Sent) If Result <= 0 ProcedureReturn -1 EndIf Sent + Result Wend ProcedureReturn Sent EndProcedure ; Build a FastCGI header Procedure BuildHeader(*Header.FCGI_Header, Type.a, RequestID.u, ContentLength.u, PaddingLength.a) *Header\version = #FCGI_VERSION *Header\type = Type *Header\requestIdB1 = (RequestID >> 8) & $FF *Header\requestIdB0 = RequestID & $FF *Header\contentLengthB1 = (ContentLength >> 8) & $FF *Header\contentLengthB0 = ContentLength & $FF *Header\paddingLength = PaddingLength *Header\reserved = 0 EndProcedure ; Send a complete FastCGI record Procedure SendRecord(ClientID, Type.a, RequestID.u, *Content, ContentLength) Protected *Packet, PacketSize, PaddingLength.a, Result ; Calculate padding to align to 8 bytes PaddingLength = (8 - (ContentLength % 8)) % 8 PacketSize = #HEADER_SIZE + ContentLength + PaddingLength *Packet = AllocateMemory(PacketSize, #PB_Memory_NoClear) If Not *Packet ProcedureReturn #False EndIf ; Build header BuildHeader(*Packet, Type, RequestID, ContentLength, PaddingLength) ; Copy content if any If *Content And ContentLength > 0 CopyMemory(*Content, *Packet + #HEADER_SIZE, ContentLength) EndIf ; Zero padding If PaddingLength > 0 FillMemory(*Packet + #HEADER_SIZE + ContentLength, PaddingLength, 0) EndIf ; Send Result = SendAllData(ClientID, *Packet, PacketSize) FreeMemory(*Packet) ProcedureReturn Bool(Result > 0) EndProcedure ; Send FCGI_END_REQUEST record Procedure SendEndRequest(ClientID, RequestID.u, AppStatus.l, ProtocolStatus.a) Protected Body.FCGI_EndRequestBody Body\appStatusB3 = (AppStatus >> 24) & $FF Body\appStatusB2 = (AppStatus >> 16) & $FF Body\appStatusB1 = (AppStatus >> 8) & $FF Body\appStatusB0 = AppStatus & $FF Body\protocolStatus = ProtocolStatus ProcedureReturn SendRecord(ClientID, #FCGI_END_REQUEST, RequestID, @Body, SizeOf(FCGI_EndRequestBody)) EndProcedure ; Parse name-value pairs from FCGI_PARAMS Procedure ProcessNameValuePairs(*Data, *Request.Request, Length) Protected Offset = 0 Protected NameLength.l, ValueLength.l Protected Name.s, Value.s While Offset < Length ; Read name length NameLength = PeekA(*Data + Offset) If NameLength & $80 ; High bit set = 4-byte length If Offset + 4 > Length : Break : EndIf NameLength = ((PeekA(*Data + Offset) & $7F) << 24) | (PeekA(*Data + Offset + 1) << 16) | (PeekA(*Data + Offset + 2) << 8) | PeekA(*Data + Offset + 3) Offset + 4 Else Offset + 1 EndIf ; Read value length If Offset >= Length : Break : EndIf ValueLength = PeekA(*Data + Offset) If ValueLength & $80 ; High bit set = 4-byte length If Offset + 4 > Length : Break : EndIf ValueLength = ((PeekA(*Data + Offset) & $7F) << 24) | (PeekA(*Data + Offset + 1) << 16) | (PeekA(*Data + Offset + 2) << 8) | PeekA(*Data + Offset + 3) Offset + 4 Else Offset + 1 EndIf ; Bounds check If Offset + NameLength + ValueLength > Length Break EndIf ; Read name and value If NameLength > 0 Name = PeekS(*Data + Offset, NameLength, #PB_Ascii) Offset + NameLength If ValueLength > 0 Value = PeekS(*Data + Offset, ValueLength, #PB_Ascii) Offset + ValueLength Else Value = "" EndIf *Request\Parameters(Name) = Value Else Offset + ValueLength EndIf Wend EndProcedure ;} ;{ Public procedures - Server Procedure Close(*Server.Server) If *Server *Server\Stop = #True ; Wait for thread to finish If IsThread(*Server\Thread) WaitThread(*Server\Thread, 5000) If IsThread(*Server\Thread) KillThread(*Server\Thread) EndIf EndIf If *Server\Mutex FreeMutex(*Server\Mutex) EndIf FreeMemory(*Server) EndIf EndProcedure Procedure Open(Port, *Callback, BindedIP.s = "") Protected ServerID, *Server.Server If Not *Callback ProcedureReturn #Null EndIf ServerID = CreateNetworkServer(#PB_Any, Port, #PB_Network_TCP, BindedIP) If ServerID *Server = AllocateMemory(SizeOf(Server)) If *Server *Server\ServerID = ServerID *Server\Callback = *Callback *Server\Mutex = CreateMutex() *Server\Thread = CreateThread(@ServerThread(), *Server) If Not *Server\Thread CloseNetworkServer(ServerID) If *Server\Mutex : FreeMutex(*Server\Mutex) : EndIf FreeMemory(*Server) *Server = #Null EndIf Else CloseNetworkServer(ServerID) EndIf EndIf ProcedureReturn *Server EndProcedure ;} ;{ Public procedures - Request Procedure FinishResponse(*Request.Request) Protected HeaderString.s, *HeaderData, HeaderLength Protected *ContentData, ContentLength, TotalLength Protected Offset, ChunkSize, Result If Not *Request ProcedureReturn EndIf ; Build HTTP headers string ForEach *Request\ResponseHeaders() HeaderString + MapKey(*Request\ResponseHeaders()) + ": " + *Request\ResponseHeaders() + #CRLF$ Next ForEach *Request\Cookies() HeaderString + "Set-Cookie: " + *Request\Cookies() + #CRLF$ Next HeaderString + #CRLF$ ; End of headers ; Calculate total response size HeaderLength = StringByteLength(HeaderString, #PB_Ascii) ContentLength = 0 ForEach *Request\ResponseData() ContentLength + MemorySize(*Request\ResponseData()) Next TotalLength = HeaderLength + ContentLength ; Allocate combined buffer *ContentData = AllocateMemory(TotalLength, #PB_Memory_NoClear) If Not *ContentData ProcedureReturn EndIf ; Copy headers PokeS(*ContentData, HeaderString, -1, #PB_Ascii | #PB_String_NoZero) Offset = HeaderLength ; Copy response data ForEach *Request\ResponseData() CopyMemory(*Request\ResponseData(), *ContentData + Offset, MemorySize(*Request\ResponseData())) Offset + MemorySize(*Request\ResponseData()) FreeMemory(*Request\ResponseData()) Next ClearList(*Request\ResponseData()) ; Send STDOUT records (split if necessary) Offset = 0 While Offset < TotalLength ChunkSize = TotalLength - Offset If ChunkSize > #MAX_RECORD_SIZE ChunkSize = #MAX_RECORD_SIZE EndIf Result = SendRecord(*Request\ClientID, #FCGI_STDOUT, *Request\RequestID, *ContentData + Offset, ChunkSize) If Not Result Break EndIf Offset + ChunkSize Wend FreeMemory(*ContentData) ; Send empty STDOUT to signal end of output SendRecord(*Request\ClientID, #FCGI_STDOUT, *Request\RequestID, #Null, 0) ; Send END_REQUEST SendEndRequest(*Request\ClientID, *Request\RequestID, 0, #FCGI_REQUEST_COMPLETE) ; Close connection if not keep-alive If Not *Request\KeepConnection CloseNetworkConnection(*Request\ClientID) EndIf ; Clean up POST data buffer if allocated If *Request\PostDataBuffer FreeMemory(*Request\PostDataBuffer) *Request\PostDataBuffer = #Null EndIf EndProcedure Procedure.s GetCookie(*Request.Request, Cookie.s) Protected CookieHeader.s, Result.s Protected SearchStr.s, StartPos, EndPos If Not *Request ProcedureReturn "" EndIf CookieHeader = *Request\Parameters("HTTP_COOKIE") If CookieHeader = "" ProcedureReturn "" EndIf SearchStr = Cookie + "=" StartPos = FindString(CookieHeader, SearchStr) If StartPos StartPos + Len(SearchStr) EndPos = FindString(CookieHeader, ";", StartPos) If EndPos = 0 EndPos = Len(CookieHeader) + 1 EndIf Result = Trim(Mid(CookieHeader, StartPos, EndPos - StartPos)) EndIf ProcedureReturn Result EndProcedure Procedure.s GetParameter(*Request.Request, Parameter.s) If *Request ProcedureReturn *Request\Parameters(Parameter) EndIf ProcedureReturn "" EndProcedure Procedure.s GetPostData(*Request.Request) If *Request ProcedureReturn *Request\PostData EndIf ProcedureReturn "" EndProcedure Procedure WriteResponseHeader(*Request.Request, Header.s, Value.s) If *Request If LCase(Header) = "set-cookie" Or Header = #PB_CGI_HeaderSetCookie AddElement(*Request\Cookies()) *Request\Cookies() = Value Else *Request\ResponseHeaders(Header) = Value EndIf EndIf EndProcedure Procedure WriteResponseString(*Request.Request, String.s, Format = #PB_UTF8) Protected *Buffer, Length If *Request And String <> "" Length = StringByteLength(String, Format) *Buffer = AllocateMemory(Length, #PB_Memory_NoClear) If *Buffer PokeS(*Buffer, String, -1, Format | #PB_String_NoZero) AddElement(*Request\ResponseData()) *Request\ResponseData() = *Buffer EndIf EndIf EndProcedure Procedure WriteResponseData(*Request.Request, *Buffer, Length) Protected *Copy If *Request And *Buffer And Length > 0 *Copy = AllocateMemory(Length, #PB_Memory_NoClear) If *Copy CopyMemory(*Buffer, *Copy, Length) AddElement(*Request\ResponseData()) *Request\ResponseData() = *Copy EndIf EndIf EndProcedure Procedure WriteResponseContentType(*Request.Request, File.s) Protected Extension.s, MIMEType.s If *Request Extension = LCase(GetExtensionPart(File)) If FindMapElement(MIMETypes(), Extension) MIMEType = MIMETypes() Else MIMEType = "application/octet-stream" EndIf *Request\ResponseHeaders("Content-Type") = MIMEType EndIf EndProcedure Procedure RespondFile(*Request.Request, File.s) Protected Result = #False Protected FileID, FileSize, *FileData If Not *Request ProcedureReturn #False EndIf FileID = ReadFile(#PB_Any, File, #PB_File_SharedRead) If FileID FileSize = Lof(FileID) If FileSize > 0 *FileData = AllocateMemory(FileSize, #PB_Memory_NoClear) If *FileData ReadData(FileID, *FileData, FileSize) WriteResponseContentType(*Request, File) WriteResponseData(*Request, *FileData, FileSize) FreeMemory(*FileData) Result = #True EndIf EndIf CloseFile(FileID) If Result FinishResponse(*Request) EndIf EndIf ProcedureReturn Result EndProcedure ;} ;{ Server thread Procedure ServerThread(*Server.Server) Protected Header.FCGI_Header Protected BeginBody.FCGI_BeginRequestBody Protected ContentLength, PaddingLength Protected *ContentBuffer, *PaddingBuffer Protected Event, ClientID Protected NewMap Requests.Request() Protected *Request.Request Protected RequestKey.s *ContentBuffer = AllocateMemory(#MAX_CONTENT_LENGTH) *PaddingBuffer = AllocateMemory(256) If Not *ContentBuffer Or Not *PaddingBuffer If *ContentBuffer : FreeMemory(*ContentBuffer) : EndIf If *PaddingBuffer : FreeMemory(*PaddingBuffer) : EndIf ProcedureReturn EndIf Repeat Event = NetworkServerEvent(*Server\ServerID) Select Event Case #PB_NetworkEvent_None Delay(1) Case #PB_NetworkEvent_Connect ClientID = EventClient() ; New connection - request will be created on BEGIN_REQUEST Case #PB_NetworkEvent_Data ClientID = EventClient() RequestKey = Str(ClientID) ; Read header If ReceiveAllData(ClientID, @Header, #HEADER_SIZE) = #HEADER_SIZE ContentLength = (Header\contentLengthB1 << 8) | Header\contentLengthB0 PaddingLength = Header\paddingLength ; Read content If ContentLength > 0 If ReceiveAllData(ClientID, *ContentBuffer, ContentLength) <> ContentLength Continue EndIf EndIf ; Read padding If PaddingLength > 0 ReceiveAllData(ClientID, *PaddingBuffer, PaddingLength) EndIf ; Process based on record type Select Header\type Case #FCGI_BEGIN_REQUEST ; Create new request *Request = AddMapElement(Requests(), RequestKey) If *Request *Request\ClientID = ClientID *Request\RequestID = (Header\requestIdB1 << 8) | Header\requestIdB0 If ContentLength >= SizeOf(FCGI_BeginRequestBody) CopyMemory(*ContentBuffer, @BeginBody, SizeOf(FCGI_BeginRequestBody)) *Request\Role = (BeginBody\roleB1 << 8) | BeginBody\roleB0 *Request\KeepConnection = Bool(BeginBody\flags & #FCGI_KEEP_CONN) EndIf EndIf Case #FCGI_PARAMS *Request = FindMapElement(Requests(), RequestKey) If *Request If ContentLength > 0 ProcessNameValuePairs(*ContentBuffer, *Request, ContentLength) Else ; Empty PARAMS record signals end of parameters *Request\ParamsComplete = #True EndIf EndIf Case #FCGI_STDIN *Request = FindMapElement(Requests(), RequestKey) If *Request If ContentLength > 0 ; Accumulate POST data Protected *NewBuffer, NewLength NewLength = *Request\PostDataLength + ContentLength *NewBuffer = AllocateMemory(NewLength) If *NewBuffer If *Request\PostDataBuffer CopyMemory(*Request\PostDataBuffer, *NewBuffer, *Request\PostDataLength) FreeMemory(*Request\PostDataBuffer) EndIf CopyMemory(*ContentBuffer, *NewBuffer + *Request\PostDataLength, ContentLength) *Request\PostDataBuffer = *NewBuffer *Request\PostDataLength = NewLength EndIf Else ; Empty STDIN record signals end of input *Request\StdinComplete = #True ; Convert POST data to string If *Request\PostDataBuffer And *Request\PostDataLength > 0 *Request\PostData = PeekS(*Request\PostDataBuffer, *Request\PostDataLength, #PB_UTF8 | #PB_ByteLength) EndIf ; Request is complete - call handler If *Request\ParamsComplete Protected Callback.i = *Server\Callback If Callback CallFunctionFast(Callback, *Request) EndIf ; Clean up request from map after response DeleteMapElement(Requests(), RequestKey) EndIf EndIf EndIf Case #FCGI_ABORT_REQUEST *Request = FindMapElement(Requests(), RequestKey) If *Request If *Request\PostDataBuffer FreeMemory(*Request\PostDataBuffer) EndIf ForEach *Request\ResponseData() FreeMemory(*Request\ResponseData()) Next DeleteMapElement(Requests(), RequestKey) EndIf EndSelect EndIf Case #PB_NetworkEvent_Disconnect ClientID = EventClient() RequestKey = Str(ClientID) ; Clean up any pending request *Request = FindMapElement(Requests(), RequestKey) If *Request If *Request\PostDataBuffer FreeMemory(*Request\PostDataBuffer) EndIf ForEach *Request\ResponseData() FreeMemory(*Request\ResponseData()) Next DeleteMapElement(Requests(), RequestKey) EndIf EndSelect Until *Server\Stop ; Cleanup FreeMemory(*ContentBuffer) FreeMemory(*PaddingBuffer) ; Clean up remaining requests ForEach Requests() If Requests()\PostDataBuffer FreeMemory(Requests()\PostDataBuffer) EndIf ForEach Requests()\ResponseData() FreeMemory(Requests()\ResponseData()) Next Next CloseNetworkServer(*Server\ServerID) EndProcedure ;} ;{ MIME Types Procedure LoadMimeTypes() Protected.s Ext, Type Restore MimeData Read.s Ext While Ext <> "END" Read.s Type MIMETypes(Ext) = Type Read.s Ext Wend EndProcedure LoadMimeTypes() DataSection MimeData: Data.s "aac", "audio/aac", "abw", "application/x-abiword", "apng", "image/apng", "avi", "video/x-msvideo", "bin", "application/octet-stream", "bmp", "image/bmp" Data.s "css", "text/css", "csv", "text/csv", "doc", "application/msword", "gif", "image/gif", "htm", "text/html", "html", "text/html", "ico", "image/x-icon" Data.s "jpeg", "image/jpeg", "jpg", "image/jpeg", "js", "text/javascript", "json", "application/json", "mp3", "audio/mpeg", "mp4", "video/mp4", "mpeg", "video/mpeg" Data.s "otf", "font/otf", "png", "image/png", "pdf", "application/pdf", "php", "application/x-httpd-php", "svg", "image/svg+xml", "txt", "text/plain" Data.s "wav", "audio/wav", "webm", "video/webm", "webp", "image/webp", "woff", "font/woff", "woff2", "font/woff2", "xml", "application/xml", "zip", "application/zip" Data.s "END" EndDataSection ;} EndModule ;{ Test/Demo code CompilerIf #PB_Compiler_IsMainFile Global *ImageData Global ImageSize ; Load test image If ReadFile(0, #PB_Compiler_Home + "examples/sources/Data/Map.bmp") ImageSize = Lof(0) *ImageData = AllocateMemory(ImageSize) ReadData(0, *ImageData, ImageSize) CloseFile(0) Else ; Create a simple test response if image not found *ImageData = #Null ImageSize = 0 EndIf Procedure RequestHandler(*Request) Protected URI.s URI = FastCGI::GetParameter(*Request, "REQUEST_URI") Debug "Request received: " + URI Debug "Method: " + FastCGI::GetParameter(*Request, "REQUEST_METHOD") Debug "Query: " + FastCGI::GetParameter(*Request, "QUERY_STRING") If *ImageData And ImageSize > 0 FastCGI::WriteResponseContentType(*Request, "test.bmp") FastCGI::WriteResponseData(*Request, *ImageData, ImageSize) Else FastCGI::WriteResponseHeader(*Request, "Content-Type", "text/html") FastCGI::WriteResponseString(*Request, "
Request URI: " + URI + "
") EndIf FastCGI::FinishResponse(*Request) EndProcedure OpenConsole("FastCGI Demo") PrintN("Starting FastCGI server on port 5600...") PrintN("Press Enter to stop") Global Server = FastCGI::Open(5600, @RequestHandler()) If Server PrintN("Server started successfully") Input() FastCGI::Close(Server) PrintN("Server closed") Else PrintN("Failed to start server!") EndIf If *ImageData FreeMemory(*ImageData) EndIf PrintN("Press Enter to exit") Input() CompilerEndIf ;} ; IDE Options = PureBasic 6.30 (Windows - x64) ; ExecutableFormat = Console ; CursorPosition = 55 ; Folding = GAAAA9 ; EnableXP ; DPIAware