Page 1 of 1

Atomic Web Server 2 ( works on windows, linux & mac )

Posted: Sat Nov 15, 2025 9:50 pm
by skinkairewalker
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! :mrgreen: :mrgreen: :mrgreen:

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()

screnshot : Image

Thanks!