793 lines
22 KiB
Plaintext
793 lines
22 KiB
Plaintext
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, "<html><body><h1>FastCGI Test</h1><p>Request URI: " + URI + "</p></body></html>")
|
|
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 |