FCGI, en mode facile

Partagez votre expérience de PureBasic avec les autres utilisateurs.
Avatar de l’utilisateur
❤x1
Messages : 14
Inscription : jeu. 10/janv./2019 17:26
Contact :

FCGI, en mode facile

Message par ❤x1 »

Coucou les gens !

J'aime bien faire des services web avec PureBasic, mais je me suis rendu compte que c'était pas pratique de partager ces services, puisqu'ils ont besoin d'un serveur pour fonctionner. Et étrangement, tout le monde n'a pas une machine virtuelle avec un petit Nginx sous le coude.

Alors voilà un petit serveur tout bête qui s'occupera de faire la connexion entre un service FCGI et un browser:

Code : Tout sélectionner

; ============================================================
; 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 Ext.s, Type.s
		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","apng","image/apng","avi","video/x-msvideo"
		Data.s "bin","application/octet-stream","bmp","image/bmp"
		Data.s "css","text/css","csv","text/csv","gif","image/gif"
		Data.s "htm","text/html","html","text/html","ico","image/x-icon"
		Data.s "jpeg","image/jpeg","jpg","image/jpeg","js","text/javascript"
		Data.s "json","application/json","map","application/json"
		Data.s "mp3","audio/mpeg","mp4","video/mp4"
		Data.s "otf","font/otf","png","image/png","pdf","application/pdf"
		Data.s "svg","image/svg+xml","txt","text/plain","wasm","application/wasm"
		Data.s "wav","audio/wav","webm","video/webm","webp","image/webp"
		Data.s "woff","font/woff","woff2","font/woff2","xml","application/xml"
		Data.s "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
Mon blog : https://lastlife.net/
Mes trucs PB en open source : Inputify, UITK, SelfHost.
Mes trucs SB en open source : MaterialSB
Avatar de l’utilisateur
Kwai chang caine
Messages : 7075
Inscription : sam. 23/sept./2006 18:32
Localisation : Isere

Re: FCGI, en mode facile

Message par Kwai chang caine »

Tu dis ne pas être doué, faire des "trucs" pour rabaisser le niveau de prétention de tes créations.
A te lire on pourrait croire que des programmeurs PB tu es le dernier et du syndrome de l'imposteur être frappé

Alors sache que sans langue de bois, j'ai trouvé ton univers classe (Et poutant j'aime pas le rose :lol:) , tes programmes assez puissant, puisque je n'ai presque rien compris, si ce n'est quelques mots techniques, validant de ce fait, que tu as tort quand tu te dis fermer la file des développeurs PB puisque force est de constater que je suis derrière toi :mrgreen: :oops:

En tout cas merci du partage, même si ce cadeau fait valeur pour moi comme d'une fourchette à une poule :oops:

Et pour finir le cirage de pompes :mrgreen:
C'est grand dommage que tu te fasses si discret sur ce forum et sur le web, car il me semble que ta connaissance, même si tu la pense à tort ridicule, reste de mon humble avis... très intéressante 8)

Bonne "contrucnuation" :mrgreen: et au plaisir de te relire :wink:
ImageLe bonheur est une route...
Pas une destination

PureBasic Forum Officiel
Répondre