Here we go, kicked everything out.
first request is served... 2nd or 3rd request crashes ... in Windows only... in the Cleanup Procedure when it is closed in CloseNetworkConnection
I am checking if client disconnected... trying to be as defensive as possible... w/o success...
Code: Select all
EnableExplicit
XIncludeFile "httpresponse.pbi"
Structure ClientData
ClientID.i
EndStructure
Declare HandleRoutes(clientID.i, method.s, path.s)
Declare CleanupClient(clientID.i, *buffer, *client.ClientData, *tempbuffer, clientConnected = #True)
Declare HandleClient(*client.ClientData)
Declare Main()
Main()
; Path Routing in HandleClient Requests
Procedure.i HandleRoutes(ClientID.i, Method.s, Path.s)
Debug "Start HandleRoutes..."
If UCase(Method) = "GET" And Path = "/"
HttpResponse::SendHttpResponse(ClientID, "Hello!")
Debug "SendHttpResponse(ClientID, Hello!)"
ProcedureReturn #True
ElseIf UCase(Method) = "GET" And LCase(Path) = "/api/ping"
HttpResponse::SendHttpResponse(ClientID, "pong")
Debug "SendHttpResponse(ClientID, pong)"
ProcedureReturn #True
ElseIf UCase(Method) = "GET" And Len(Path)
HttpResponse::SendHttpError(ClientID, 403, "Forbidden")
Debug "SendHttpError(ClientID, 403, Forbidden)"
ProcedureReturn #True
EndIf
ProcedureReturn #False
Debug "End HandleRoutes = false"
EndProcedure
Procedure HandleClient(*client.ClientData)
Protected clientID.i = *client\ClientID
Protected bufferSize = 4096
Protected clientConnected = #True
Protected received, totalReceived = 0, headerEndPos = -1
Protected Header.s, Body.s, Method.s
Protected authHeader.s = "Authorization: Basic "
Protected i = 0, contentLength = 0
Protected startTime = ElapsedMilliseconds()
Protected HeaderMaxSizeInBytes.i = 8192
Protected HeaderTimeoutInMs.i = 5000
Protected DelayInMs.i = 10
; Create Buffer
; HTTP 500 when Allocating Error (e.g. memory exhausted)
Protected *tempbuffer = #Null
Protected *buffer = #Null
*buffer = AllocateMemory(bufferSize)
If *buffer = #Null
HttpResponse::SendHttpError(clientID, 500, "Memory Allocation Failed")
CleanupClient(clientID, *buffer, *client, *tempbuffer, clientConnected)
Debug "SendHttpError(ClientID, 500, alloc failed)"
ProcedureReturn
EndIf
; Read Binary-safe complete HTTP Header
; Limit Header Size and Timeouts
;
; and answer with RFC compliant HTTP Status Codes
Repeat
; Client gone away prematurely
If NetworkClientEvent(clientID) = #PB_NetworkEvent_Disconnect
clientConnected = #False
Debug "Client disconnected"
Break
EndIf
; Timeout-Check
; HTTP 408 Status when HeaderTimeoutInMs exceeded
If ElapsedMilliseconds() - startTime > HeaderTimeoutInMs
HttpResponse::SendHttpError(clientID, 408, "Request Timeout")
clientConnected = #False
Debug "SendHttpError(clientID, 408, Request Timeout)"
Break
EndIf
received = ReceiveNetworkData(clientID, *buffer + totalReceived, bufferSize - totalReceived - 1)
If received < 0
; Unkown Error occured when negative
; answer with HTTP 400 Status Code
HttpResponse::SendHttpError(clientID, 400, "Bad Request")
Debug "SendHttpError(clientID, 400, Bad Request)"
Break
ElseIf received = 0
; Free CPU Cycles
Delay(DelayInMs)
Continue
EndIf
totalReceived + received
; Limit Headersize
; HTTP 431 Status when HeaderMaxSizeInBytes exceeded
If totalReceived > HeaderMaxSizeInBytes
HttpResponse::SendHttpError(clientID, 431, "Request Header Fields Too Large")
Debug "SendHttpError(clientID, 431, Header Fields too large)"
Break
EndIf
; Search UTF-safe End of Header (CRLF CRLF)
; Break out when Reading Header finished
For i = 0 To totalReceived - 4
If PeekA(*buffer + i) = 13 And PeekA(*buffer + i + 1) = 10 And
PeekA(*buffer + i + 2) = 13 And PeekA(*buffer + i + 3) = 10
headerEndPos = i
Break 2
EndIf
Next
; Increase Buffer
; HTTP 500 when ReAllocating Error (e.g. memory exhausted)
If totalReceived >= bufferSize - 1
bufferSize + 4096
*buffer = ReAllocateMemory(*buffer, bufferSize +1)
If *buffer = 0
HttpResponse::SendHttpError(clientID, 500, "Server Memory Error")
Debug "SendHttpError(clientID, 500, alloc error)"
Break
EndIf
EndIf
Until clientConnected = #False
; Process only if Client is still there to recieve an answer
If clientConnected
; Set Nullterminator
PokeA(*buffer + totalReceived, 0)
; Break without Header
If headerEndPos = -1
CleanupClient(clientID, *buffer, *client, *tempbuffer, clientConnected)
ProcedureReturn
EndIf
; Parse Header, Body and Method
Header = PeekS(*buffer, headerEndPos, #PB_UTF8)
Body = PeekS(*buffer + headerEndPos + 4, totalReceived - headerEndPos - 4, #PB_UTF8)
Method = StringField(Header, 1, " ")
; Handle OPTIONS
If UCase(Method) = "OPTIONS"
HttpResponse::SendHttpResponse(clientID,"")
Debug "OPTIONS sent"
CleanupClient(clientID, *buffer, *client, *tempbuffer, clientConnected)
ProcedureReturn
EndIf
; For Routing get RequestLine and uriPath
Protected RequestLine.s = StringField(Header, 1, #CRLF$)
Protected uriPath.s = StringField(RequestLine, 2, " ")
; Route-Handling
; Checks on existing routes. if routing then process and break
If HandleRoutes(clientID, Method, uriPath)
CleanupClient(clientID, *buffer, *client, *tempbuffer, clientConnected)
ProcedureReturn
EndIf
; Handle POST ---
If UCase(Method) = "POST"
Protected contentPos = FindString(UCase(Header), "CONTENT-LENGTH:")
If contentPos
contentLength = Val(Trim(StringField(Mid(Header, contentPos + 15), 1, #CRLF$)))
; Get chunked Body part until ContentLenght reached
While StringByteLength(Body, #PB_UTF8) < contentLength
; Use temp Buffer
Protected restSize = contentLength - StringByteLength(Body, #PB_UTF8)
*tempBuffer = AllocateMemory(restSize +1)
; HTTP 500 when Allocating Error (e.g. memory exhausted)
If *tempBuffer = #Null
HttpResponse::SendHttpError(clientID, 500, "Server Memory Error")
Debug "SendHttpError(clientID, 500, alloc)"
CleanupClient(clientID, *buffer, *client, *tempbuffer, clientConnected)
ProcedureReturn
EndIf
; Client gone away prematurely
If NetworkClientEvent(clientID) = #PB_NetworkEvent_Disconnect
Debug "client gone away"
clientConnected = #False
Break
EndIf
received = ReceiveNetworkData(clientID, *tempBuffer, restSize)
If received > restSize : received = restSize : EndIf
; Set Nullterminator
PokeA(*tempBuffer + received, 0)
; HTTP 400 for incomplete Body post
If received <= 0
HttpResponse::SendHttpError(clientID, 400, "Incomplete POST Body")
Debug "SendHttpError(clientID, 400, incomplete body)"
CleanupClient(clientID, *buffer, *client, *tempbuffer, clientConnected)
ProcedureReturn
EndIf
body + PeekS(*tempBuffer, received, #PB_UTF8)
; Free CPU Cycles
Delay(DelayInMs)
Wend
If clientConnected
HttpResponse::SendHttpResponse(clientID, "POST Body: " + Body)
Debug "send body back"
EndIf
Else
; Exception when missing Content-Length, which is required by RFC
HttpResponse::SendHttpError(clientID, 411, "Length Required")
Debug "SendHttpError(clientID, 411, Length Required)"
EndIf
Else
; Handle any unsupported methods or missing Auth
; only process when Client is still there
If clientConnected
HttpResponse::SendHttpError(clientID, 405, "Method Not Allowed")
Debug "SendHttpError(clientID, 405, Method not allowed)"
EndIf
EndIf
EndIf
; Everything finished
; Cleanup everything to close thread
CleanupClient(clientID, *buffer, *client, *tempbuffer, clientConnected)
ProcedureReturn
EndProcedure
; Clean Up ClientHandling Routine
Procedure CleanupClient(clientID.i, *buffer, *client.ClientData, *tempbuffer, clientConnected = #True)
Debug "cleanup started..."
; Check first before trying to close Connections and free memory
If *buffer : FreeMemory(*buffer) : EndIf
If *tempbuffer : FreeMemory(*tempbuffer) : EndIf
If *client : FreeMemory(*client) : EndIf
If clientConnected
If clientID : CloseNetworkConnection(clientID) : EndIf
EndIf
Debug "...cleanup stopped."
EndProcedure
; Main server loop
; Initialize HTTP Server on given Port
; creates new Thread on each new Connections
Procedure Main()
Debug "Main startet..."
If CreateNetworkServer(#PB_Any, 8080)
Protected DelayInMs.i = 10
Repeat
Select NetworkServerEvent()
Case #PB_NetworkEvent_Connect
Protected connectionID = EventClient()
Debug "new Client connecting"
If connectionID >= 0
Protected *client.ClientData = #Null
*client = AllocateMemory(SizeOf(ClientData))
Debug "Creating new thread..."
If *client
*client\ClientID = connectionID
If CreateThread(@HandleClient(), *client) = 0
Debug "Failed to create thread aborting..."
CloseNetworkConnection(connectionID)
FreeMemory(*client)
EndIf
Else
CloseNetworkConnection(connectionID)
Debug "Failed to create thread aborting..."
EndIf
Debug "...Thread created."
EndIf
Case #PB_NetworkEvent_None
;Free CPU Cycles
Delay(DelayInMs)
EndSelect
ForEver
EndIf
EndProcedure
Code: Select all
EnableExplicit
; Declares
DeclareModule HttpResponse
; Constants
#CONTENTTYPE = "text/plain; charset=UTF-8"
Declare.s BuildHttpResponse(body.s, contentType.s = #CONTENTTYPE)
Declare SendHttpResponse(clientID.i, body.s, contentType.s = #CONTENTTYPE)
Declare SendHttpError(clientID.i, code.i, message.s)
EndDeclareModule
Module HttpResponse
; Create a complete HTTP 200 OK response string
Procedure.s BuildHttpResponse(body.s, contentType.s = #CONTENTTYPE)
Protected length = StringByteLength(body, #PB_UTF8)
Protected header.s = "HTTP/1.1"
; OPTIONS without Body
If length > 0
header + " 200 OK" + #CRLF$
Else
header + " 204 No Content" + #CRLF$
EndIf
header + "Content-Type: " + contentType + #CRLF$
header + "Content-Length: " + Str(length) + #CRLF$
header + "Connection: close" + #CRLF$ + #CRLF$
ProcedureReturn header + body
EndProcedure
; Send full HTTP-Response to client
Procedure SendHttpResponse(clientID.i, body.s, contentType.s = #CONTENTTYPE)
Protected response.s = BuildHttpResponse(body, contentType)
Protected size = StringByteLength(response, #PB_UTF8)
Protected *mem = #Null
*mem = AllocateMemory(size + 1)
If *mem = #Null : ProcedureReturn : EndIf
If *mem
PokeS(*mem, response, -1, #PB_UTF8)
SendNetworkData(clientID, *mem, size)
FreeMemory(*mem)
EndIf
EndProcedure
; Send HTTP Errors
Procedure SendHttpError(clientID.i, code.i, message.s)
Protected body.s = Str(code) + " " + message
Protected length = StringByteLength(body, #PB_UTF8)
Protected header.s = "HTTP/1.1 " + Str(code) + " " + message + #CRLF$
header + "Content-Type: " + #CONTENTTYPE + #CRLF$
header + "Content-Length: " + Str(length) + #CRLF$
header + "Connection: close" + #CRLF$ + #CRLF$
Protected response.s = header + body
Protected size = StringByteLength(response, #PB_UTF8)
Protected *mem = #Null
*mem = AllocateMemory(size + 1)
If *mem = #Null : ProcedureReturn : EndIf
If *mem
PokeS(*mem, response, -1, #PB_UTF8)
SendNetworkData(clientID, *mem, size)
FreeMemory(*mem)
EndIf
EndProcedure
EndModule