734 lines
23 KiB
Plaintext
734 lines
23 KiB
Plaintext
; ============================================================
|
|
; WebServer.pbi: Simple HTTP / FastCGI gateway for services
|
|
; development.
|
|
;
|
|
; Routing:
|
|
; - Path matching a registered FCGI prefix -> forward to FCGI
|
|
; - Otherwise: try local file, fall back to FCGI on miss
|
|
; ============================================================
|
|
|
|
DeclareModule WebServer
|
|
Declare Open(Port, WebRoot.s, FcgiHost.s = "127.0.0.1", FcgiPort = 5600)
|
|
Declare Close(*Server)
|
|
Declare AddFcgiPrefix(*Server, Prefix.s)
|
|
EndDeclareModule
|
|
|
|
Module WebServer
|
|
EnableExplicit
|
|
|
|
;{ Structures
|
|
Structure Server
|
|
ServerID.i
|
|
Thread.i
|
|
Stop.i
|
|
Port.i
|
|
WebRoot.s
|
|
FcgiHost.s
|
|
FcgiPort.l
|
|
List Prefixes.s()
|
|
EndStructure
|
|
|
|
Structure HttpConn
|
|
*Buffer
|
|
AllocSize.i
|
|
Received.i
|
|
HeadersEnd.i ; byte AFTER \r\n\r\n; 0 means not yet found
|
|
ContentLength.i ; -1 = not yet parsed
|
|
EndStructure
|
|
|
|
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
|
|
;}
|
|
|
|
;{ Constants
|
|
#FCGI_VERSION = 1
|
|
#FCGI_BEGIN_REQUEST = 1
|
|
#FCGI_END_REQUEST = 3
|
|
#FCGI_PARAMS = 4
|
|
#FCGI_STDIN = 5
|
|
#FCGI_STDOUT = 6
|
|
#FCGI_RESPONDER = 1
|
|
#FCGI_HEADER_SIZE = 8
|
|
#FCGI_MAX_RECORD = 65528
|
|
;}
|
|
|
|
;{ MIME Types
|
|
Global NewMap MIMETypes.s()
|
|
|
|
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
|
|
;}
|
|
|
|
;- Private declarations
|
|
Declare ServerThread(*Server.Server)
|
|
Declare HandleRequest(*Server.Server, ClientID, *Data, DataLen)
|
|
Declare ForwardToFcgi(*Server.Server, ClientID, Method.s, FullURI.s, Path.s, QueryString.s, Map ReqHdrs.s(), *Body, BodyLen)
|
|
Declare FcgiBuildAndSendParams(FcgiConn, *Server.Server, ReqID.u, Method.s, FullURI.s, Path.s, QueryString.s, Map ReqHdrs.s(), BodyLen)
|
|
Declare FcgiSendBody(FcgiConn, ReqID.u, *Body, BodyLen)
|
|
Declare FcgiReadResponse(FcgiConn, *OutLen)
|
|
Declare FcgiSendHttpResponse(ClientID, *RespBuf, RespLen)
|
|
Declare ServeStaticFile(ClientID, FilePath.s)
|
|
Declare SendErrorResponse(ClientID, Code, Reason.s)
|
|
Declare SendRawHttpResponse(ClientID, StatusLine.s, Headers.s, *Body, BodyLen)
|
|
Declare ReceiveAll(ConnID, *Buffer, Length)
|
|
Declare SendAll(ConnID, *Buffer, Length)
|
|
Declare SendFcgiRecord(ConnID, Type.a, RequestID.u, *Content, ContentLen)
|
|
Declare AppendNVP(*Buffer, Offset, Name.s, Value.s)
|
|
Declare.s GetMime(FilePath.s)
|
|
Declare.s GetHeaderCI(Map H.s(), Name.s)
|
|
|
|
;- Public API
|
|
Procedure Open(Port, WebRoot.s, FcgiHost.s = "127.0.0.1", FcgiPort = 5600)
|
|
Protected ServerID = CreateNetworkServer(#PB_Any, Port, #PB_Network_TCP, "")
|
|
Protected *Server.Server
|
|
If Not ServerID : ProcedureReturn #Null : EndIf
|
|
|
|
*Server = AllocateMemory(SizeOf(Server))
|
|
If *Server
|
|
InitializeStructure(*Server, Server)
|
|
*Server\ServerID = ServerID
|
|
*Server\Port = Port
|
|
*Server\WebRoot = WebRoot
|
|
*Server\FcgiHost = FcgiHost
|
|
*Server\FcgiPort = FcgiPort
|
|
*Server\Thread = CreateThread(@ServerThread(), *Server)
|
|
If Not *Server\Thread
|
|
CloseNetworkServer(ServerID)
|
|
FreeStructure(*Server)
|
|
*Server = #Null
|
|
EndIf
|
|
Else
|
|
CloseNetworkServer(ServerID)
|
|
EndIf
|
|
ProcedureReturn *Server
|
|
EndProcedure
|
|
|
|
Procedure Close(*Server.Server)
|
|
If *Server
|
|
*Server\Stop = #True
|
|
If IsThread(*Server\Thread)
|
|
WaitThread(*Server\Thread, 5000)
|
|
If IsThread(*Server\Thread) : KillThread(*Server\Thread) : EndIf
|
|
EndIf
|
|
FreeMemory(*Server)
|
|
EndIf
|
|
EndProcedure
|
|
|
|
Procedure AddFcgiPrefix(*Server.Server, Prefix.s)
|
|
If *Server
|
|
AddElement(*Server\Prefixes())
|
|
*Server\Prefixes() = Prefix
|
|
EndIf
|
|
EndProcedure
|
|
|
|
;- Private procedures
|
|
; Helpers
|
|
Procedure.s GetHeaderCI(Map H.s(), Name.s)
|
|
; Case-insensitive header lookup so we don't depend on which capitalisation
|
|
; the browser used (Chrome and Firefox usually send "Content-Type:" but
|
|
; HTTP/2 frontends and some proxies will lowercase headers).
|
|
Protected Lower.s = LCase(Name)
|
|
ForEach H()
|
|
If LCase(MapKey(H())) = Lower
|
|
ProcedureReturn H()
|
|
EndIf
|
|
Next
|
|
ProcedureReturn ""
|
|
EndProcedure
|
|
|
|
Procedure.s GetMime(FilePath.s)
|
|
If FindMapElement(MimeTypes(), LCase(GetExtensionPart(FilePath)))
|
|
ProcedureReturn MimeTypes()
|
|
EndIf
|
|
ProcedureReturn "application/octet-stream"
|
|
EndProcedure
|
|
|
|
; Network helpers
|
|
Procedure ReceiveAll(ConnID, *Buffer, Length)
|
|
Protected Received = 0, Got
|
|
While Received < Length
|
|
Got = ReceiveNetworkData(ConnID, *Buffer + Received, Length - Received)
|
|
If Got <= 0
|
|
Received = -1
|
|
Break
|
|
EndIf
|
|
Received + Got
|
|
Wend
|
|
ProcedureReturn Received
|
|
EndProcedure
|
|
|
|
Procedure SendAll(ConnID, *Buffer, Length)
|
|
Protected Sent = 0, Chunk
|
|
While Sent < Length
|
|
Chunk = SendNetworkData(ConnID, *Buffer + Sent, Length - Sent)
|
|
If Chunk <= 0
|
|
Sent = -1
|
|
Break
|
|
EndIf
|
|
Sent + Chunk
|
|
Wend
|
|
ProcedureReturn Sent
|
|
EndProcedure
|
|
|
|
; FCGI helpers
|
|
Procedure SendFcgiRecord(ConnID, Type.a, RequestID.u, *Content, ContentLen)
|
|
Protected Result = #False
|
|
Protected Padding = (8 - (ContentLen % 8)) % 8
|
|
Protected PktSize = #FCGI_HEADER_SIZE + ContentLen + Padding
|
|
Protected *Pkt = AllocateMemory(PktSize, #PB_Memory_NoClear)
|
|
|
|
If *Pkt
|
|
PokeA(*Pkt, #FCGI_VERSION)
|
|
PokeA(*Pkt + 1, Type)
|
|
PokeA(*Pkt + 2, (RequestID >> 8) & $FF)
|
|
PokeA(*Pkt + 3, RequestID & $FF)
|
|
PokeA(*Pkt + 4, (ContentLen >> 8) & $FF)
|
|
PokeA(*Pkt + 5, ContentLen & $FF)
|
|
PokeA(*Pkt + 6, Padding)
|
|
PokeA(*Pkt + 7, 0)
|
|
|
|
If *Content And ContentLen > 0
|
|
CopyMemory(*Content, *Pkt + #FCGI_HEADER_SIZE, ContentLen)
|
|
EndIf
|
|
If Padding > 0
|
|
FillMemory(*Pkt + #FCGI_HEADER_SIZE + ContentLen, Padding, 0)
|
|
EndIf
|
|
|
|
Result = Bool(SendAll(ConnID, *Pkt, PktSize) > 0)
|
|
FreeMemory(*Pkt)
|
|
EndIf
|
|
|
|
ProcedureReturn Result
|
|
EndProcedure
|
|
|
|
Procedure AppendNVP(*Buffer, Offset, Name.s, Value.s)
|
|
Protected NameLen = StringByteLength(Name, #PB_Ascii)
|
|
Protected ValueLen = StringByteLength(Value, #PB_UTF8)
|
|
|
|
If NameLen < 128
|
|
PokeA(*Buffer + Offset, NameLen) : Offset + 1
|
|
Else
|
|
PokeA(*Buffer + Offset, ((NameLen >> 24) & $7F) | $80)
|
|
PokeA(*Buffer + Offset + 1, (NameLen >> 16) & $FF)
|
|
PokeA(*Buffer + Offset + 2, (NameLen >> 8) & $FF)
|
|
PokeA(*Buffer + Offset + 3, NameLen & $FF)
|
|
Offset + 4
|
|
EndIf
|
|
|
|
If ValueLen < 128
|
|
PokeA(*Buffer + Offset, ValueLen) : Offset + 1
|
|
Else
|
|
PokeA(*Buffer + Offset, ((ValueLen >> 24) & $7F) | $80)
|
|
PokeA(*Buffer + Offset + 1, (ValueLen >> 16) & $FF)
|
|
PokeA(*Buffer + Offset + 2, (ValueLen >> 8) & $FF)
|
|
PokeA(*Buffer + Offset + 3, ValueLen & $FF)
|
|
Offset + 4
|
|
EndIf
|
|
|
|
If NameLen > 0
|
|
PokeS(*Buffer + Offset, Name, -1, #PB_Ascii | #PB_String_NoZero)
|
|
Offset + NameLen
|
|
EndIf
|
|
If ValueLen > 0
|
|
PokeS(*Buffer + Offset, Value, -1, #PB_UTF8 | #PB_String_NoZero)
|
|
Offset + ValueLen
|
|
EndIf
|
|
|
|
ProcedureReturn Offset
|
|
EndProcedure
|
|
|
|
; HTTP response helpers
|
|
Procedure SendRawHttpResponse(ClientID, StatusLine.s, Headers.s, *Body, BodyLen)
|
|
; FIX: ensure Headers ends with CRLF so the appended CRLF below produces
|
|
; the CRLFCRLF that terminates the header block. Without this, FCGI-forwarded
|
|
; responses (whose CgiHdrs has no trailing CRLF after PeekS) produced output
|
|
; with only one CRLF between headers and body - some browsers (notably Chrome
|
|
; on POST) refused to display the result.
|
|
If Headers <> "" And Right(Headers, 2) <> #CRLF$
|
|
Headers + #CRLF$
|
|
EndIf
|
|
|
|
Protected Hdr.s = StatusLine + #CRLF$ + "Connection: close" + #CRLF$ + Headers + #CRLF$
|
|
Protected HdrLen = StringByteLength(Hdr, #PB_Ascii)
|
|
Protected *HdrBuf = AllocateMemory(HdrLen, #PB_Memory_NoClear)
|
|
If *HdrBuf
|
|
PokeS(*HdrBuf, Hdr, -1, #PB_Ascii | #PB_String_NoZero)
|
|
SendAll(ClientID, *HdrBuf, HdrLen)
|
|
FreeMemory(*HdrBuf)
|
|
EndIf
|
|
|
|
If *Body And BodyLen > 0
|
|
SendAll(ClientID, *Body, BodyLen)
|
|
EndIf
|
|
CloseNetworkConnection(ClientID)
|
|
EndProcedure
|
|
|
|
Procedure SendErrorResponse(ClientID, Code, Reason.s)
|
|
Protected Body.s = "<h1>" + Str(Code) + " " + Reason + "</h1>"
|
|
Protected BodyLen = StringByteLength(Body, #PB_Ascii)
|
|
Protected *B = AllocateMemory(BodyLen, #PB_Memory_NoClear)
|
|
If *B
|
|
PokeS(*B, Body, -1, #PB_Ascii | #PB_String_NoZero)
|
|
SendRawHttpResponse(ClientID, "HTTP/1.1 " + Str(Code) + " " + Reason,
|
|
"Content-Type: text/html" + #CRLF$ + "Content-Length: " + Str(BodyLen) + #CRLF$,
|
|
*B, BodyLen)
|
|
FreeMemory(*B)
|
|
EndIf
|
|
EndProcedure
|
|
|
|
Procedure ServeStaticFile(ClientID, FilePath.s)
|
|
Protected Result, FileSize, FileID = ReadFile(#PB_Any, FilePath, #PB_File_SharedRead)
|
|
Protected *FileData
|
|
|
|
If FileID
|
|
FileSize = Lof(FileID)
|
|
*FileData = AllocateMemory(FileSize, #PB_Memory_NoClear)
|
|
If *FileData
|
|
ReadData(FileID, *FileData, FileSize)
|
|
SendRawHttpResponse(ClientID, "HTTP/1.1 200 OK",
|
|
"Content-Type: " + GetMime(FilePath) + #CRLF$ + "Content-Length: " + Str(FileSize) + #CRLF$,
|
|
*FileData, FileSize)
|
|
FreeMemory(*FileData)
|
|
EndIf
|
|
Result = #True
|
|
CloseFile(FileID)
|
|
EndIf
|
|
|
|
ProcedureReturn Result
|
|
EndProcedure
|
|
|
|
; FCGI gateway
|
|
Procedure FcgiBuildAndSendParams(FcgiConn, *Server.Server, ReqID.u, Method.s, FullURI.s, Path.s, QueryString.s, Map ReqHdrs.s(), BodyLen)
|
|
Protected *Params, PO, PSent, PChunk, Result
|
|
Protected HName.s, HLower.s, CType.s
|
|
|
|
*Params = AllocateMemory(65536)
|
|
If Not *Params : ProcedureReturn #False : EndIf
|
|
|
|
PO = AppendNVP(*Params, PO, "GATEWAY_INTERFACE", "CGI/1.1")
|
|
PO = AppendNVP(*Params, PO, "SERVER_PROTOCOL", "HTTP/1.1")
|
|
PO = AppendNVP(*Params, PO, "SERVER_SOFTWARE", "KUMO.S/1.0")
|
|
PO = AppendNVP(*Params, PO, "SERVER_NAME", "localhost")
|
|
PO = AppendNVP(*Params, PO, "SERVER_PORT", Str(*Server\Port))
|
|
PO = AppendNVP(*Params, PO, "REQUEST_METHOD", Method)
|
|
PO = AppendNVP(*Params, PO, "REQUEST_URI", FullURI)
|
|
PO = AppendNVP(*Params, PO, "SCRIPT_NAME", Path)
|
|
PO = AppendNVP(*Params, PO, "PATH_INFO", Path)
|
|
PO = AppendNVP(*Params, PO, "QUERY_STRING", QueryString)
|
|
PO = AppendNVP(*Params, PO, "DOCUMENT_ROOT", *Server\WebRoot)
|
|
PO = AppendNVP(*Params, PO, "SCRIPT_FILENAME", *Server\WebRoot + Path)
|
|
PO = AppendNVP(*Params, PO, "CONTENT_LENGTH", Str(BodyLen))
|
|
PO = AppendNVP(*Params, PO, "REMOTE_ADDR", "127.0.0.1")
|
|
PO = AppendNVP(*Params, PO, "REMOTE_PORT", "0")
|
|
|
|
; FIX: case-insensitive lookup
|
|
CType = GetHeaderCI(ReqHdrs(), "Content-Type")
|
|
If CType <> ""
|
|
PO = AppendNVP(*Params, PO, "CONTENT_TYPE", CType)
|
|
EndIf
|
|
|
|
ForEach ReqHdrs()
|
|
HName = MapKey(ReqHdrs())
|
|
HLower = LCase(HName)
|
|
If HLower <> "content-type" And HLower <> "content-length"
|
|
PO = AppendNVP(*Params, PO, "HTTP_" + UCase(ReplaceString(HName, "-", "_")), ReqHdrs())
|
|
EndIf
|
|
Next
|
|
|
|
While PSent < PO
|
|
PChunk = PO - PSent
|
|
If PChunk > #FCGI_MAX_RECORD : PChunk = #FCGI_MAX_RECORD : EndIf
|
|
If Not SendFcgiRecord(FcgiConn, #FCGI_PARAMS, ReqID, *Params + PSent, PChunk) : Break : EndIf
|
|
PSent + PChunk
|
|
Wend
|
|
FreeMemory(*Params)
|
|
|
|
Result = Bool(Bool(PSent >= PO) And SendFcgiRecord(FcgiConn, #FCGI_PARAMS, ReqID, #Null, 0))
|
|
ProcedureReturn Result
|
|
EndProcedure
|
|
|
|
Procedure FcgiSendBody(FcgiConn, ReqID.u, *Body, BodyLen)
|
|
Protected SSent, SChunk, Result = #True
|
|
|
|
If *Body And BodyLen > 0
|
|
While SSent < BodyLen And Result
|
|
SChunk = BodyLen - SSent
|
|
If SChunk > #FCGI_MAX_RECORD : SChunk = #FCGI_MAX_RECORD : EndIf
|
|
Result = SendFcgiRecord(FcgiConn, #FCGI_STDIN, ReqID, *Body + SSent, SChunk)
|
|
SSent + SChunk
|
|
Wend
|
|
EndIf
|
|
If Result
|
|
Result = SendFcgiRecord(FcgiConn, #FCGI_STDIN, ReqID, #Null, 0)
|
|
EndIf
|
|
|
|
ProcedureReturn Result
|
|
EndProcedure
|
|
|
|
Procedure FcgiReadResponse(FcgiConn, *OutLen)
|
|
Protected FcgiHdr.FCGI_Header, PadBuf.q
|
|
Protected *RecBuf, *RespBuf, *Grown
|
|
Protected RecLen, RecPad, RespLen, RespAlloced = 65536
|
|
|
|
*RespBuf = AllocateMemory(RespAlloced)
|
|
*RecBuf = AllocateMemory(65536)
|
|
If Not *RespBuf Or Not *RecBuf
|
|
If *RespBuf : FreeMemory(*RespBuf) : EndIf
|
|
If *RecBuf : FreeMemory(*RecBuf) : EndIf
|
|
ProcedureReturn 0
|
|
EndIf
|
|
|
|
Repeat
|
|
If ReceiveAll(FcgiConn, @FcgiHdr, #FCGI_HEADER_SIZE) <> #FCGI_HEADER_SIZE : Break : EndIf
|
|
RecLen = (FcgiHdr\contentLengthB1 << 8) | FcgiHdr\contentLengthB0
|
|
RecPad = FcgiHdr\paddingLength
|
|
|
|
If RecLen > 0 And ReceiveAll(FcgiConn, *RecBuf, RecLen) <> RecLen : Break : EndIf
|
|
If RecPad > 0 : ReceiveAll(FcgiConn, @PadBuf, RecPad) : EndIf
|
|
|
|
Select FcgiHdr\type
|
|
Case #FCGI_STDOUT
|
|
If RecLen > 0
|
|
While RespLen + RecLen > RespAlloced
|
|
RespAlloced * 2
|
|
*Grown = ReAllocateMemory(*RespBuf, RespAlloced)
|
|
If Not *Grown : FreeMemory(*RespBuf) : FreeMemory(*RecBuf) : ProcedureReturn 0 : EndIf
|
|
*RespBuf = *Grown
|
|
Wend
|
|
CopyMemory(*RecBuf, *RespBuf + RespLen, RecLen)
|
|
RespLen + RecLen
|
|
EndIf
|
|
|
|
Case #FCGI_END_REQUEST
|
|
FreeMemory(*RecBuf)
|
|
PokeI(*OutLen, RespLen)
|
|
ProcedureReturn *RespBuf
|
|
EndSelect
|
|
ForEver
|
|
|
|
FreeMemory(*RecBuf)
|
|
FreeMemory(*RespBuf)
|
|
ProcedureReturn 0
|
|
EndProcedure
|
|
|
|
Procedure FcgiSendHttpResponse(ClientID, *RespBuf, RespLen)
|
|
Protected k, SepPos = -1, BodyOff, CgiBodyLen, StatusCode = 200, StatusPos, SEnd, SpPos
|
|
Protected.s CgiHdrs, ExtraHdrs, StatusText = "OK", SVal
|
|
|
|
For k = 0 To RespLen - 4
|
|
If PeekA(*RespBuf + k) = $0D And PeekA(*RespBuf + k + 1) = $0A And
|
|
PeekA(*RespBuf + k + 2) = $0D And PeekA(*RespBuf + k + 3) = $0A
|
|
SepPos = k : Break
|
|
EndIf
|
|
Next
|
|
|
|
If SepPos >= 0
|
|
CgiHdrs = PeekS(*RespBuf, SepPos, #PB_Ascii)
|
|
BodyOff = SepPos + 4
|
|
CgiBodyLen = RespLen - BodyOff
|
|
Else
|
|
BodyOff = 0 : CgiBodyLen = RespLen
|
|
EndIf
|
|
|
|
StatusPos = FindString(CgiHdrs, "Status:", 1, #PB_String_NoCase)
|
|
If StatusPos
|
|
SEnd = FindString(CgiHdrs, #CRLF$, StatusPos)
|
|
If SEnd = 0 : SEnd = Len(CgiHdrs) + 1 : EndIf
|
|
SVal = Trim(Mid(CgiHdrs, StatusPos + 7, SEnd - StatusPos - 7))
|
|
StatusCode = Val(SVal)
|
|
SpPos = FindString(SVal, " ")
|
|
If SpPos : StatusText = Trim(Mid(SVal, SpPos + 1)) : EndIf
|
|
CgiHdrs = Left(CgiHdrs, StatusPos - 1) + Mid(CgiHdrs, SEnd + 2)
|
|
EndIf
|
|
|
|
If FindString(CgiHdrs, "Content-Length:", 1, #PB_String_NoCase) = 0
|
|
ExtraHdrs = "Content-Length: " + Str(CgiBodyLen) + #CRLF$
|
|
EndIf
|
|
|
|
; CgiHdrs from PeekS does NOT end with CRLF; SendRawHttpResponse normalises that.
|
|
SendRawHttpResponse(ClientID, "HTTP/1.1 " + Str(StatusCode) + " " + StatusText,
|
|
ExtraHdrs + CgiHdrs, *RespBuf + BodyOff, CgiBodyLen)
|
|
EndProcedure
|
|
|
|
Procedure ForwardToFcgi(*Server.Server, ClientID, Method.s, FullURI.s, Path.s, QueryString.s, Map ReqHdrs.s(), *Body, BodyLen)
|
|
Protected FcgiConn, ReqID.u = 1, RespLen, *RespBuf
|
|
Protected BeginBody.FCGI_BeginRequestBody
|
|
|
|
FcgiConn = OpenNetworkConnection(*Server\FcgiHost, *Server\FcgiPort)
|
|
If Not FcgiConn
|
|
SendErrorResponse(ClientID, 502, "Bad Gateway") : ProcedureReturn
|
|
EndIf
|
|
|
|
FillMemory(@BeginBody, SizeOf(FCGI_BeginRequestBody), 0)
|
|
BeginBody\roleB0 = #FCGI_RESPONDER
|
|
|
|
; FIX: actually check that BEGIN_REQUEST went through
|
|
If Not SendFcgiRecord(FcgiConn, #FCGI_BEGIN_REQUEST, ReqID, @BeginBody, SizeOf(FCGI_BeginRequestBody))
|
|
CloseNetworkConnection(FcgiConn)
|
|
SendErrorResponse(ClientID, 502, "Bad Gateway") : ProcedureReturn
|
|
EndIf
|
|
|
|
If Not FcgiBuildAndSendParams(FcgiConn, *Server, ReqID, Method, FullURI, Path, QueryString, ReqHdrs(), BodyLen) Or
|
|
Not FcgiSendBody(FcgiConn, ReqID, *Body, BodyLen)
|
|
CloseNetworkConnection(FcgiConn)
|
|
SendErrorResponse(ClientID, 502, "Bad Gateway") : ProcedureReturn
|
|
EndIf
|
|
|
|
*RespBuf = FcgiReadResponse(FcgiConn, @RespLen)
|
|
CloseNetworkConnection(FcgiConn)
|
|
|
|
If *RespBuf
|
|
FcgiSendHttpResponse(ClientID, *RespBuf, RespLen)
|
|
FreeMemory(*RespBuf)
|
|
Else
|
|
SendErrorResponse(ClientID, 502, "Bad Gateway")
|
|
EndIf
|
|
EndProcedure
|
|
|
|
Procedure HandleRequest(*Server.Server, ClientID, *Data, DataLen)
|
|
Protected LineEnd = -1, i, Sp1, Sp2, QPos, HdrStart, HdrEnd, NLines, j, ColPos, BodyStart, BodyLen, GoFcgi
|
|
Protected *Body
|
|
Protected.s ReqLine, Method, FullURI, Path, QueryStr, HdrStr, Line, FilePath
|
|
Protected NewMap ReqHdrs.s()
|
|
|
|
For i = 0 To DataLen - 2
|
|
If PeekA(*Data + i) = $0D And PeekA(*Data + i + 1) = $0A
|
|
LineEnd = i : Break
|
|
EndIf
|
|
Next
|
|
If LineEnd < 0 : SendErrorResponse(ClientID, 400, "Bad Request") : ProcedureReturn : EndIf
|
|
|
|
ReqLine = PeekS(*Data, LineEnd, #PB_Ascii)
|
|
Sp1 = FindString(ReqLine, " ")
|
|
Sp2 = FindString(ReqLine, " ", Sp1 + 1)
|
|
If Sp1 = 0 Or Sp2 = 0 : SendErrorResponse(ClientID, 400, "Bad Request") : ProcedureReturn : EndIf
|
|
|
|
Method = Left(ReqLine, Sp1 - 1)
|
|
FullURI = Mid(ReqLine, Sp1 + 1, Sp2 - Sp1 - 1)
|
|
|
|
If Method <> "GET" And Method <> "POST"
|
|
SendErrorResponse(ClientID, 405, "Method Not Allowed") : ProcedureReturn
|
|
EndIf
|
|
|
|
QPos = FindString(FullURI, "?")
|
|
If QPos
|
|
Path = URLDecoder(Left(FullURI, QPos - 1))
|
|
QueryStr = Mid(FullURI, QPos + 1)
|
|
Else
|
|
Path = URLDecoder(FullURI)
|
|
QueryStr = ""
|
|
EndIf
|
|
|
|
If FindString(Path, "..") : Path = "/" : EndIf
|
|
|
|
HdrStart = LineEnd + 2
|
|
For i = HdrStart To DataLen - 4
|
|
If PeekA(*Data + i) = $0D And PeekA(*Data + i + 1) = $0A And
|
|
PeekA(*Data + i + 2) = $0D And PeekA(*Data + i + 3) = $0A
|
|
HdrEnd = i : Break
|
|
EndIf
|
|
Next
|
|
If HdrEnd = 0 : HdrEnd = DataLen : EndIf
|
|
|
|
HdrStr = PeekS(*Data + HdrStart, HdrEnd - HdrStart, #PB_Ascii)
|
|
NLines = CountString(HdrStr, #CRLF$) + 1
|
|
For j = 1 To NLines
|
|
Line.s = StringField(HdrStr, j, #CRLF$)
|
|
ColPos = FindString(Line, ":")
|
|
If ColPos > 1
|
|
ReqHdrs(Trim(Left(Line, ColPos - 1))) = Trim(Mid(Line, ColPos + 1))
|
|
EndIf
|
|
Next
|
|
|
|
BodyStart = HdrEnd + 4
|
|
If BodyStart < DataLen
|
|
*Body = *Data + BodyStart
|
|
BodyLen = DataLen - BodyStart
|
|
EndIf
|
|
|
|
ForEach *Server\Prefixes()
|
|
If Left(Path, Len(*Server\Prefixes())) = *Server\Prefixes()
|
|
GoFcgi = #True : Break
|
|
EndIf
|
|
Next
|
|
|
|
If GoFcgi
|
|
ForwardToFcgi(*Server, ClientID, Method, FullURI, Path, QueryStr, ReqHdrs(), *Body, BodyLen)
|
|
ProcedureReturn
|
|
EndIf
|
|
|
|
FilePath.s = *Server\WebRoot + Path
|
|
If Right(FilePath, 1) = "/" : FilePath + "index.html" : EndIf
|
|
|
|
If FileSize(FilePath) >= 0
|
|
If Not ServeStaticFile(ClientID, FilePath)
|
|
SendErrorResponse(ClientID, 500, "Internal Server Error")
|
|
EndIf
|
|
Else
|
|
ForwardToFcgi(*Server, ClientID, Method, FullURI, Path, QueryStr, ReqHdrs(), *Body, BodyLen)
|
|
EndIf
|
|
EndProcedure
|
|
|
|
Procedure ServerThread(*Server.Server)
|
|
Protected NewMap Conns.HttpConn()
|
|
Protected Got, si, CLPos, CLEnd, ReallocFailed, Event, SearchFrom, ClientID, Key.s, HBlock.s, *Grown, *Conn.HttpConn, *ChunkBuf
|
|
|
|
*ChunkBuf = AllocateMemory(8192)
|
|
If Not *ChunkBuf : ProcedureReturn : EndIf
|
|
|
|
Repeat
|
|
Event = NetworkServerEvent(*Server\ServerID)
|
|
If Event = #PB_NetworkEvent_None
|
|
Delay(1)
|
|
Else
|
|
ClientID = EventClient()
|
|
Key = Str(ClientID)
|
|
Select Event
|
|
Case #PB_NetworkEvent_Connect
|
|
*Conn = AddMapElement(Conns(), Key)
|
|
*Conn\AllocSize = 8192
|
|
*Conn\Buffer = AllocateMemory(*Conn\AllocSize)
|
|
*Conn\ContentLength = -1
|
|
If Not *Conn\Buffer
|
|
DeleteMapElement(Conns(), Key)
|
|
CloseNetworkConnection(ClientID)
|
|
EndIf
|
|
|
|
Case #PB_NetworkEvent_Data
|
|
*Conn = FindMapElement(Conns(), Key)
|
|
If Not *Conn : Continue : EndIf
|
|
|
|
Got = ReceiveNetworkData(ClientID, *ChunkBuf, 8192)
|
|
If Got <= 0 : Continue : EndIf
|
|
|
|
ReallocFailed = #False
|
|
While *Conn\Received + Got > *Conn\AllocSize
|
|
*Conn\AllocSize * 2
|
|
*Grown = ReAllocateMemory(*Conn\Buffer, *Conn\AllocSize)
|
|
If *Grown
|
|
*Conn\Buffer = *Grown
|
|
Else
|
|
FreeMemory(*Conn\Buffer)
|
|
DeleteMapElement(Conns(), Key)
|
|
CloseNetworkConnection(ClientID)
|
|
ReallocFailed = #True : Break
|
|
EndIf
|
|
Wend
|
|
If ReallocFailed : Continue : EndIf
|
|
|
|
CopyMemory(*ChunkBuf, *Conn\Buffer + *Conn\Received, Got)
|
|
*Conn\Received + Got
|
|
|
|
If *Conn\HeadersEnd = 0 And *Conn\Received >= 4
|
|
SearchFrom = *Conn\Received - Got - 3
|
|
If SearchFrom < 0 : SearchFrom = 0 : EndIf
|
|
For si = SearchFrom To *Conn\Received - 4
|
|
If PeekA(*Conn\Buffer + si) = $0D And PeekA(*Conn\Buffer + si + 1) = $0A And
|
|
PeekA(*Conn\Buffer + si + 2) = $0D And PeekA(*Conn\Buffer + si + 3) = $0A
|
|
*Conn\HeadersEnd = si + 4 : Break
|
|
EndIf
|
|
Next
|
|
EndIf
|
|
|
|
If *Conn\HeadersEnd > 0 And *Conn\ContentLength < 0
|
|
; FIX: removed duplicate PeekS call from your version
|
|
HBlock = PeekS(*Conn\Buffer, *Conn\HeadersEnd - 4, #PB_Ascii)
|
|
CLPos = FindString(HBlock, "Content-Length:", 1, #PB_String_NoCase)
|
|
If CLPos
|
|
CLEnd = FindString(HBlock, #CRLF$, CLPos)
|
|
If CLEnd = 0 : CLEnd = Len(HBlock) + 1 : EndIf
|
|
*Conn\ContentLength = Val(Trim(Mid(HBlock, CLPos + 15, CLEnd - CLPos - 15)))
|
|
Else
|
|
*Conn\ContentLength = 0
|
|
EndIf
|
|
EndIf
|
|
|
|
If *Conn\HeadersEnd > 0 And *Conn\ContentLength >= 0
|
|
If *Conn\Received >= *Conn\HeadersEnd + *Conn\ContentLength
|
|
HandleRequest(*Server, ClientID, *Conn\Buffer, *Conn\Received)
|
|
FreeMemory(*Conn\Buffer)
|
|
DeleteMapElement(Conns(), Key)
|
|
EndIf
|
|
EndIf
|
|
|
|
Case #PB_NetworkEvent_Disconnect
|
|
*Conn = FindMapElement(Conns(), Key)
|
|
If *Conn
|
|
If *Conn\Buffer : FreeMemory(*Conn\Buffer) : EndIf
|
|
DeleteMapElement(Conns(), Key)
|
|
EndIf
|
|
EndSelect
|
|
EndIf
|
|
Until *Server\Stop
|
|
|
|
FreeMemory(*ChunkBuf)
|
|
ForEach Conns()
|
|
If Conns()\Buffer : FreeMemory(Conns()\Buffer) : EndIf
|
|
Next
|
|
CloseNetworkServer(*Server\ServerID)
|
|
EndProcedure
|
|
|
|
EndModule
|
|
|
|
CompilerIf #PB_Compiler_IsMainFile
|
|
OpenConsole("KUMO.S WebServer")
|
|
|
|
Define Port = 8080
|
|
Define WebRoot.s = "C:\KUMOS\www"
|
|
|
|
Define *Srv = WebServer::Open(Port, WebRoot)
|
|
If *Srv
|
|
WebServer::AddFcgiPrefix(*Srv, "/api/")
|
|
PrintN("WebServer running on :" + Str(Port))
|
|
PrintN(" Static root : " + WebRoot)
|
|
PrintN(" FCGI backend: 127.0.0.1:5600 (prefix: /api/)")
|
|
PrintN("Press Enter to stop.")
|
|
Input()
|
|
WebServer::Close(*Srv)
|
|
PrintN("Server stopped.")
|
|
Else
|
|
PrintN("ERROR: could not bind to port " + Str(Port))
|
|
EndIf
|
|
Input()
|
|
CompilerEndIf
|
|
|
|
; IDE Options = PureBasic 6.30 (Windows - x64)
|
|
; CursorPosition = 17
|
|
; Folding = jAAA9
|
|
; EnableXP
|
|
; DPIAware |