Atomic Web Server 2 ( works on windows, linux & mac )
Posted: Sat Nov 15, 2025 9:50 pm
Hi everyone!
While going through my old backups, I found a small project I started some time ago and decided to finish it: a simple, multiplatform local webserver, written in PureBasic, designed to run only locally and serve content for applications that use a WEBVIEW APP (SPIDERBASIC) .
Back when I first worked on it, I was experimenting with some ideas and even got help from Idle , which made it much easier to refine some parts of the code.
Main features:
Multiplatform (tested on windows(x64) , Linux ( Parallels Desktop ARM) , macOs (M2) )
Lightweight and straightforward
Local-only (no external port exposure)
Designed for WebView integration in native apps with SpiderBasic
Easy to embed and adapt
Just a reminder that the code is not fully finished, it still has many issues, such as using MessageRequester inside a thread, among several other mistakes that are beyond my current experience to fix.
If you can help improve the code, you are absolutely welcome to jump in!
code :
screnshot : 
Thanks!
While going through my old backups, I found a small project I started some time ago and decided to finish it: a simple, multiplatform local webserver, written in PureBasic, designed to run only locally and serve content for applications that use a WEBVIEW APP (SPIDERBASIC) .
Back when I first worked on it, I was experimenting with some ideas and even got help from Idle , which made it much easier to refine some parts of the code.
Main features:
Just a reminder that the code is not fully finished, it still has many issues, such as using MessageRequester inside a thread, among several other mistakes that are beyond my current experience to fix.
If you can help improve the code, you are absolutely welcome to jump in!
code :
Code: Select all
EnableExplicit
;Atomic Webserver threaded
;Version 2.0.1
;Supports /Get with parameters
;Added a URIhandler
CompilerIf Not #PB_Compiler_Thread
MessageRequester("Atomic Web Server v2","Compile with thread safe!")
End
CompilerEndIf
Structure Client
ID.i
type.s
Request.s
RequestedFile.s
Map parameters.s(64)
EndStructure
;-Global settings
Global Title.s = "Atomic WebServer v2 threaded"
Global Port = 80
Global WWWDirectory.s = "www/"
Global WWWIndex.s = "index.html"
Global WWWError.s = "error.html"
Global BufferSize = 1024
Global maxlen.i = 60000
;-URI Handler prototype
Prototype pURIHandler(*client.Client,*contentType.string)
Structure handlers
*pt.pURIHandler
EndStructure
Global NewMap URIHandlers.handlers(64)
;-Mime Types
Global NewMap MimeType.s(128)
Procedure Init_MimeTypes()
;Ref : https://fr.wikipedia.org/wiki/Type_MIME
MimeType("doc") = "application/msword"
MimeType("eps") = "application/postscript"
MimeType("exe") = "application/octet-stream"
MimeType("json") = "application/json"
MimeType("pdf") = "application/pdf"
MimeType("ps") = "application/postscript"
MimeType("rtf") = "application/rtf"
MimeType("xhtml") = "application/xhtml+xml"
MimeType("xsl") = "application/xml"
MimeType("xslt") = "application/xml"
MimeType("ttf") = "application/font-sfnt"
MimeType("cff") = "application/font-sfnt"
MimeType("otf") = "application/font-sfnt"
MimeType("aat") = "application/font-sfnt"
MimeType("sil") = "application/font-sfnt"
MimeType("pfr") = "application/font-tdpfr"
MimeType("woff") = "application/font-woff"
MimeType("woff2") = "application/font-woff2"
MimeType("eot") = "application/vnd.ms-fontobject"
MimeType("mp3") = "audio/mpeg"
MimeType("oga") = "audio/ogg"
MimeType("ogg") = "audio/ogg"
MimeType("wav") = "audio/wav"
MimeType("gif") = "image/gif"
MimeType("ief") = "image/ief"
MimeType("jpeg") = "image/jpeg"
MimeType("jpg") = "image/jpeg"
MimeType("jpm") = "image/jpm"
MimeType("jpx") = "image/jpx"
MimeType("png") = "image/png"
MimeType("svg") = "image/svg+xml"
MimeType("tif") = "image/tiff"
MimeType("tiff") = "image/tiff"
MimeType("wrl") = "model/vrml"
MimeType("pbh") = "text/pbh"
MimeType("js") = "text/javascript"
MimeType("css") = "text/css"
MimeType("csv") = "text/csv"
MimeType("htm") = "text/html"
MimeType("html") = "text/html"
MimeType("sgm") = "text/sgml"
MimeType("shtm") = "text/html"
MimeType("shtml") = "text/html"
MimeType("txt") = "text/plain"
MimeType("xml") = "text/xml"
MimeType("sass") = "text/x-sass"
MimeType("scss") = "text/x-scss"
MimeType("pb") = "text/plain"
MimeType("pbi") = "text/plain"
MimeType("mov") = "video/quicktime"
MimeType("mp4") = "video/mp4"
MimeType("mpeg") = "video/mpeg"
MimeType("mpg") = "video/mpeg"
MimeType("ogv") = "video/ogg"
MimeType("qt") = "video/quicktime"
MimeType("arj") = "application/x-arj-compressed"
MimeType("gz") = "application/x-gunzip"
MimeType("rar") = "application/x-arj-compressed"
MimeType("swf") = "application/x-shockwave-flash"
MimeType("tar") = "application/x-tar"
MimeType("tgz") = "application/x-tar-gz"
MimeType("torrent") = "application/x-bittorrent"
MimeType("ppt") = "application/x-mspowerpoint"
MimeType("xls") = "application/x-msexcel"
MimeType("zip") = "application/x-zip-compressed"
MimeType("aac") = "audio/aac"
MimeType("aif") = "audio/x-aif"
MimeType("m3u") = "audio/x-mpegurl"
MimeType("mid") = "audio/x-midi"
MimeType("ra") = "audio/x-pn-realaudio"
MimeType("ram") = "audio/x-pn-realaudio"
MimeType("wav") = "audio/x-wav"
MimeType("bmp") = "image/bmp"
MimeType("ico") = "image/x-icon"
MimeType("pct") = "image/x-pct"
MimeType("pict") = "image/pict"
MimeType("rgb") = "image/x-rgb"
MimeType("webm") = "video/webm"
MimeType("asf") = "video/x-ms-asf"
MimeType("avi") = "video/x-msvideo"
MimeType("m4v") = "video/x-m4v"
EndProcedure
Init_MimeTypes()
;-Declares
Declare Start()
Declare ProcessRequest(ClientID)
Declare BuildRequestHeader(*Buffer, DataLength, ContentType.s)
Declare Resize()
Declare Exit()
Global gquit
Enumeration #PB_EventType_FirstCustomValue
#ATOMIC_EVENT_ADD
EndEnumeration
;-Load tests
#Loadtest=0
CompilerIf #Loadtest <> 0
Global semtest = CreateSemaphore()
Procedure _loadLest(amount)
Protected get.s, rec.s,con,a,try=1
rec = "Host: 127.0.0.1:" + Str(port) + #CRLF$
rec + "Connection: keep-alive" + #CRLF$
rec + "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0" + #CRLF$
rec + "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7" + #CRLF$
rec + "Sec-Fetch-Site: none" + #CRLF$
rec + "Sec-Fetch-Mode: navigate" + #CRLF$
rec + "Sec-Fetch-User: ?1" + #CRLF$
rec + "Sec-Fetch-Dest: document" + #CRLF$
rec + "Accept-Encoding: gzip, deflate, br" + #CRLF$
rec + "Accept-Language: en-US,en;q=0.9" + #CRLF$ + #CRLF$
WaitSemaphore(semtest)
Repeat
con = OpenNetworkConnection("127.0.0.1",port,#PB_Network_TCP | #PB_Network_IPv4,1000)
If con
get = "GET / HTTP/1.1" + #CRLF$
SendNetworkString(Con,get+rec)
Delay(100)
get = "GET /favicon.ico HTTP/1.1" + #CRLF$
SendNetworkString(Con,get+rec)
Debug "Sent on try " + Str(try)
Break
Else
Debug "can't connect retry " + Str(try)
Delay(10)
try+1
EndIf
Until try > 5
If try > 5
Debug "couldn't connect"
EndIf
EndProcedure
Procedure loadTest(amount)
Protected a
Dim threads(amount)
For a = 1 To amount
threads(a) = CreateThread(@_loadLest(),0)
Next
Delay(1000)
Debug "signal"
For a = 1 To amount
SignalSemaphore(semtest)
Delay(1)
Next
For a = 1 To amount
If IsThread(threads(a))
WaitThread(threads(a),1000)
EndIf
Next
Debug "done test"
EndProcedure
CompilerEndIf
;-Server
Procedure ServerThread(void)
Protected ServerEvent, Result, ClientID
Protected *buffer = AllocateMemory(BufferSize)
Protected *client.Client
If CreateNetworkServer(0, Port)
Repeat
ServerEvent = NetworkServerEvent()
If ServerEvent
ClientID = EventClient()
Select ServerEvent
Case #PB_NetworkEvent_Data
*client.Client = AllocateStructure(client)
*client\ID = clientID
Repeat
FillMemory(*buffer,BufferSize,0)
Result = ReceiveNetworkData(ClientID, *Buffer, BufferSize)
*client\Request + PeekS(*Buffer, -1, #PB_UTF8)
Until Result <> BufferSize
CreateThread(@ProcessRequest(),*client)
EndSelect
Else
Delay(1) ; Ne pas saturer le CPU / Don't stole the whole CPU !
EndIf
Until gquit
CloseNetworkServer(0)
FreeMemory(*buffer)
Else
MessageRequester(Title, "Error: can't create the server (port " + port + " in use ?)")
FreeMemory(*buffer)
End
EndIf
EndProcedure
Procedure Start()
Protected event
If FileSize("./"+Trim(WWWDirectory,"/")) = -2
CreateThread(@ServerThread(),0)
CompilerIf #Loadtest
Delay(1000)
CreateThread(@loadtest(),5000)
CompilerEndIf
If OpenWindow(0, 0, 0, 800, 600, Title, #PB_Window_SystemMenu | #PB_Window_SizeGadget | #PB_Window_MaximizeGadget | #PB_Window_MinimizeGadget)
If CreateMenu(0, WindowID(0)) ; Create a regular menu with title and one item
MenuTitle("File")
MenuItem(1, "Reload" + #TAB$ + "Ctrl+O")
EndIf
; EditorGadget(0, 0, 0, 800, 560, #PB_Editor_ReadOnly)
; AddGadgetItem(0, -1, "Server Running on port " + Port)
;
; CheckBoxGadget(1, 10, 570, 200, 22, "Show Log")
; SetGadgetState(1, #PB_Checkbox_Checked)
WebViewGadget(0, 0, 0, 800, 600)
SetGadgetText(0, "http://localhost")
; Show the window once the webview has been fully loaded
HideWindow(0, #False)
;Déclencheur / Trigger
BindEvent(#PB_Event_SizeWindow, @Resize())
BindEvent(#PB_Event_CloseWindow, @Exit())
SetWindowState(0,#PB_Window_Maximize)
Repeat
event = WaitWindowEvent()
Select event
Case #ATOMIC_EVENT_ADD
AddGadgetItem(0, -1, PeekS(EventData(),-1,#PB_UTF8))
FreeMemory(EventData())
Case #PB_Event_Menu
Select EventMenu()
Case 1
SetGadgetText(0, "http://localhost")
EndSelect
Case #PB_Event_CloseWindow
Break
EndSelect
ForEver
EndIf
Else
MessageRequester("Atomic Web Server","please set a www directory")
End
EndIf
EndProcedure
Procedure ProcessRequest(*req.client)
Protected RequestedFile.s, FileLength, MaxPosition, Position, ContentType.s=""
Protected *FileBuffer,fn,*msg
Protected BufferOffset.s, *BufferOffset
Protected outpos,fulllen,trylen,sendlen
Protected request.s,count,*p.Unicode,FileLeft,*data
request.s = URLDecoder(*req\Request)
If Left(Request, 3) = "GET"
;Extract page html from GET /yourpage.html HTTP/1.1
MaxPosition = FindString(Request, Chr(13), 5)
Position = FindString(Request, ".", 5)
Position = FindString(Request, " ", Position)
If Position < MaxPosition
RequestedFile = Mid(Request, 6, Position-5) ; Automatically remove the leading '/'
RequestedFile = RTrim(RequestedFile)
Else
RequestedFile = Mid(Request, 6, MaxPosition-5) ; When a command like 'GET /' is sent..
EndIf
Position = FindString(RequestedFile,"?") ;Find the parameters
If Position > 0
Fileleft = Position-1
Position+1
count=1
*p.Unicode = @RequestedFile+((Position)*2)
While *p\u
Select *p\u
Case '&',' '
*req\parameters() = Mid(RequestedFile,Position,count) ;add value to map
Position+count+1
count = 0
If *p\u = ' '
Break
EndIf
*p+2
Case '='
AddMapElement(*req\parameters(),Mid(RequestedFile,Position,count)) ;add key to map
Position+count+1
count = 0
*p+2
EndSelect
*p+2
count+1
Wend
If count > 1
*req\parameters() = Mid(RequestedFile,Position,count) ;add remaning value to map
EndIf
EndIf
If Fileleft
RequestedFile = Left(RequestedFile,Fileleft) ;trim the request file
EndIf
If FindString(RequestedFile,"HTTP/1.1") ; if there was no page requested
RequestedFile = WWWIndex
EndIf
*req\RequestedFile = RequestedFile
;Envoyer la page HTML au client / Send the HTML page to the client
fn = ReadFile(-1, WWWDirectory + RequestedFile)
If fn
;Préparation de la page HTML à envoyer
FileLength = Lof(fn)
;Definition du content-type / Setup content-type
If FindMapElement(MimeType(),GetExtensionPart(RequestedFile))
ContentType = MimeType()
Else
ContentType = "text/html"
EndIf
Else
If FindMapElement(URIHandlers(),StringField(RequestedFile,1,"."))
*data = URIHandlers()\pt(*req,@ContentType)
FileLength = MemorySize(*data)
*FileBuffer = AllocateMemory(FileLength + 200)
*BufferOffset = BuildRequestHeader(*FileBuffer, FileLength, ContentType)
CopyMemory(*data,*BufferOffset,FileLength)
outpos = 0
fulllen = *BufferOffset - *FileBuffer + FileLength
Repeat
trylen = fulllen - outpos
If trylen > maxlen
trylen = maxlen
EndIf
sendlen = SendNetworkData(*req\id, *FileBuffer+outpos, trylen)
If sendlen > 0
outpos + sendlen
Delay(1)
Else
Break
EndIf
Until outpos >= fulllen
FreeMemory(*FileBuffer)
FreeMemory(*data)
Else
;Affichage de la page d'erreur si url inexistant / Display error page if url nonexistent
fn = ReadFile(-1, WWWDirectory + WWWError, #PB_UTF8)
If fn
FileLength = Lof(fn)
ContentType = "text/html"
EndIf
EndIf
EndIf
If fn
;Envoie des données au client / Sends data to the client
*FileBuffer = AllocateMemory(FileLength + 200)
*BufferOffset = BuildRequestHeader(*FileBuffer, FileLength, ContentType)
ReadData(fn, *BufferOffset, FileLength)
CloseFile(fn)
outpos = 0
fulllen = *BufferOffset - *FileBuffer + FileLength
Repeat
trylen = fulllen - outpos
If trylen > maxlen
trylen = maxlen
EndIf
sendlen = SendNetworkData(*req\id, *FileBuffer+outpos, trylen)
If sendlen > 0
outpos + sendlen
Delay(1)
Else
Break
EndIf
Until outpos >= fulllen
FreeMemory(*FileBuffer)
EndIf
EndIf
FreeStructure(*req)
EndProcedure
;Creation entete HTTP / Create HTTP header
Procedure BuildRequestHeader(*FileBuffer, FileLength, ContentType.s)
Protected Length
Protected Week.s = "Sun, Mon,Tue,Wed,Thu,Fri,Sat"
Protected MonthsOfYear.s = "Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec"
Protected DayOfWeek.s = StringField("Sun, Mon,Tue,Wed,Thu,Fri,Sat", DayOfWeek(Date()) + 1, ",")
Protected Day = Day(Date())
Protected Month.s = StringField("Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec", Month(Date()), ",")
Protected Year.s = Str(Year(Date()))
Protected Time.s = FormatDate("%hh:%ii:%ss GMT", Date())
Length = PokeS(*FileBuffer, "HTTP/1.1 200 OK" + #CRLF$, -1, #PB_UTF8) : *FileBuffer + Length
Length = PokeS(*FileBuffer, "Date: " + DayOfWeek + ", " + Day + " " + Month + " " + Year + " " + Time + #CRLF$, -1, #PB_UTF8) : *FileBuffer + Length
Length = PokeS(*FileBuffer, "Server: "+ Title + #CRLF$, -1, #PB_UTF8) : *FileBuffer + Length
Length = PokeS(*FileBuffer, "Content-Length: " + Str(FileLength) + #CRLF$, -1, #PB_UTF8) : *FileBuffer + Length
Length = PokeS(*FileBuffer, "Content-Type: " + ContentType + #CRLF$, -1, #PB_UTF8) : *FileBuffer + Length
Length = PokeS(*FileBuffer, #CRLF$, -1, #PB_UTF8) : *FileBuffer + Length
ProcedureReturn *FileBuffer
EndProcedure
;-Applications
Procedure Resize()
Protected Width = WindowWidth(0)
Protected Height = WindowHeight(0)
ResizeGadget(0, #PB_Ignore, #PB_Ignore, Width, Height)
;ResizeGadget(1, #PB_Ignore, Height, #PB_Ignore, #PB_Ignore)
EndProcedure
;Sortie / Exit
Procedure Exit()
gquit = 1
End
EndProcedure
;-Example URIHandler
Procedure URIfoo(*request.client,*contentType)
Protected *data
Debug *request\Request
Protected sout.s = "<!DOCTYPE html><html><head><title>Atomic Web Server</title></head><title>Atomic Web Server</title></head>"
sout + "<body><h1 style='text-align:center';>Atomic Web Server</h1>"
ForEach *request\parameters()
sout + "<p style='text-align:center'><font color='red'>" + MapKey(*request\parameters()) + "=" + *request\parameters() +"</p>"
Next
sout + "<body></html>"
*data = UTF8(sout)
PokeS(*contentType,"text/html") ;Set the contentType
ProcedureReturn *data
EndProcedure
;URIHandlers("foo")\pt = @URIfoo()
;navigate to http://127.0.0.1/foo?foo=1234&bar=5678
start()

Thanks!