[WIP] FastCGI

Share your advanced PureBasic knowledge/code with the community.
Poshu
Enthusiast
Enthusiast
Posts: 459
Joined: Tue Jan 25, 2005 7:01 pm
Location: Canada

[WIP] FastCGI

Post by Poshu »

The implementation of FastCGI in PB is very limited (it's actually closer to SCGI than FCGI), and I need to write a real FCGI application... So I gave it a try : FastCGI being 22 years old, it's really simple and it only took me a few hours to have a working proof of concept.
If someone needs it too, I'll share my progress here :

Progress :
18/12/07 : Cookie and string format
Added : Cookie support, see the example to learn how to add a cookie to a response and how to read a cookie value from a request. /!\ The GetCookie() procedure won't work with space in you cookie value! /!\ That's good enough for my need though.
Changed : you can now set the string format, UTF-8 is used by default.

18/12/07 : Padding size fix
The padding was calculated but not included in the packet.

18/12/04 : POC.
It only implements a small fraction of the FCGI protocol and it's prone to memory leak. It's a good enough start to learn about the FCGI protocol.
Code

Code: Select all

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.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 WriteResponseString(*Request, String.s)							;Write a string to the response 
	Declare FinishReponse(*Request)															;Send 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
		*Thread
		Map Variable.s()
		Map Response.s()
		List ResponseData.i()
	EndStructure
	
	#__HEADER_SIZE = SizeOf(Record)
	
	;}
	
	;{ 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.s GetParameter(*Request.Request, Parameter.s)
		ProcedureReturn *Request\Variable(Parameter)
	EndProcedure
	
	Procedure WriteResponseHeader(*Request.Request, Header.s, Value.s)
		*Request\Response(Header) = Value
	EndProcedure
	
	Procedure WriteResponseString(*Request.Request, String.s)
		AddElement(*Request\ResponseData())
		*Request\ResponseData() = AllocateMemory(StringByteLength(String,#PB_Ascii),#PB_Memory_NoClear)
		PokeS(*Request\ResponseData(),String,-1,#PB_Ascii|#PB_String_NoZero) ; // Temp
	EndProcedure
	
	Procedure FinishReponse(*Request.Request)
		Protected Size, PaddingSize, Position
		Protected *Record.Record
		Protected *Data
		; 1- Calculate the packet size
		ForEach *Request\Response()
			Size + StringByteLength(MapKey(*Request\Response())+*Request\Response(),#PB_Ascii)	+ 3 ; Le plus 3 se décompose comme ça :  1 = LF, 2 = ": "
		Next
		
		ForEach *Request\ResponseData()
			Size + MemorySize(*Request\ResponseData())
		Next
		
		Size +1 ; le dernier LF
		PaddingSize = Bool(Size % 8 > 0) * (8 -Size % 8)
		
		*Data = AllocateMemory(#__HEADER_SIZE + Size + PaddingSize + #__HEADER_SIZE + 8)
		*Record = *Data
		*Record\ContentLengthB0 = Size
		*Record\ContentLengthB1 = Size >> 8
		*Record\RequestIdB0 = *Request\RequestIdB0
		*Record\RequestIdB1 = *Request\RequestIdB1
		*Record\Type = #__TYPE_STDOUT
		*Record\Version = #__VERSION
		*Record\PaddingLength = PaddingSize
		
		Position = #__HEADER_SIZE
		
		ForEach *Request\Response()
			Position + PokeS(*Data + Position,MapKey(*Request\Response())+": "+*Request\Response()+#LF$,-1,#PB_Ascii|#PB_String_NoZero)
		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
		
		Position + PaddingSize

		*Record = *Data + Position
		*Record\ContentLengthB0 = 8
		*Record\ContentLengthB1 = 0
		*Record\RequestIdB0 = *Request\RequestIdB0
		*Record\RequestIdB1 = *Request\RequestIdB1
		*Record\Type = #__TYPE_END
		*Record\Version = #__VERSION
		*Record\PaddingLength = 0
		
		SendNetworkData(*Request\ClientID,*Data,MemorySize(*Data))
		FreeMemory(*Data)		
	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()
				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
								
								*Request\Thread = 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
					DeleteMapElement(ClientMap(),Str(EventClient()))
			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
	; Demo
	
	Procedure Handler_FCGIRequest(Request)
		FastCGI::WriteResponseHeader(Request,#PB_CGI_HeaderContentType,"text/html")
		FastCGI::WriteResponseString(Request,"<html><title>PureBasic - FastCGI</title><body>" +
		                                     "Hello from PureBasic FastCGI !<br>" +
		                                     "Actual time: <b>"+FormatDate("%hh:%ii", Date()) + "</b>" +
		                                     "</body></html>")
		FastCGI::FinishReponse(Request)
	EndProcedure
	
	OpenConsole("fastCGI Demo")
	
	InitNetwork()
	Server = FastCGI::Open(5600,@Handler_FCGIRequest())
	
	Input()
	FastCGI::Close(Server)
	PrintN("Server closed")
	
	Input()
	End
CompilerEndIf