Author Topic: HTTP 1.1 compliant web server (work in progress)  (Read 4952 times)

0 Members and 1 Guest are viewing this topic.

Offline luke

  • Administrator
  • Seasoned Forum Regular
  • Posts: 324
    • View Profile
HTTP 1.1 compliant web server (work in progress)
« on: January 01, 2020, 07:17:54 am »
There seemed to be lots of chat about web servers over on Discord, so I thought I'd throw my hat in the ring. This is an attempt at writing a web server that fully supports HTTP 1.1 (the most common version). It is very basic at the moment, and does not support most things - but it can do basic file serving.

By default it runs on port 8080 (change the CONST PORT line to change this) and will serve any files from the directory it's running in. For example, if you run it from your QB64 directory you can put http://localhost:8080/source/qb64.bas into your browser and it'll get the QB64 source file from your hard drive.

For the most recent version, see https://github.com/flukiluke/http-toy
« Last Edit: April 16, 2022, 07:16:18 am by luke »

Offline SpriggsySpriggs

  • Forum Resident
  • Posts: 1145
  • Larger than life
    • View Profile
    • GitHub
Re: HTTP 1.1 compliant web server (work in progress)
« Reply #1 on: July 29, 2020, 09:21:40 pm »
@luke Is there a new update on this or is this still the latest source? I thought about using this code you have to make my own REST API server that will use Galleon's qb_framework to build a JSON response and send it to the clients, containing the information they requested in the URL using either CURL or URLMON.
Shuwatch!

Offline luke

  • Administrator
  • Seasoned Forum Regular
  • Posts: 324
    • View Profile
Re: HTTP 1.1 compliant web server (work in progress)
« Reply #2 on: July 30, 2020, 05:49:54 am »
To be honest I completely forgot about this. What you see there is the latest.

If you just want to answer requests programmatically you could just rewrite the http_do_get function; it's as complex as it is because it's dealing with files.

Offline SpriggsySpriggs

  • Forum Resident
  • Posts: 1145
  • Larger than life
    • View Profile
    • GitHub
Re: HTTP 1.1 compliant web server (work in progress)
« Reply #3 on: July 30, 2020, 06:06:30 am »
Appreciate it! I'll look into it
Shuwatch!

Offline dbox

  • Newbie
  • Posts: 80
    • View Profile
Re: HTTP 1.1 compliant web server (work in progress)
« Reply #4 on: December 16, 2021, 09:26:36 am »
@luke, this is awesome!  I've been looking for a lightweight http server to test out game code that I'm converting to html.  It's even better that you've already created one for QB64!  The content I'm wanting to test is all just static html, javascript, images, etc.. So what you already have here is working perfectly for what I need.

I did find one issue that I thought I would mention.  It looks like the num_active_connections counter is being incremented regardless of whether we are using an existing available connection from the array or not.  After you've made the max number of requests (initially 8) to the web server it stops responding.   I added a new line (55) to decrement this counter when we are using an existing connection which seems to resolve the issue.
Code: QB64: [Select]
  1. DefLng A-Z
  2.  
  3. Const PORT = 8080
  4. Const MAX_CONNECTIONS = 8
  5.  
  6. Const FALSE = 0
  7. Const TRUE = -1
  8. CRLF = Chr$(13) + Chr$(10)
  9. Const HTTP_10 = 1
  10. Const HTTP_11 = 11
  11. Const HTTP_GET = 1
  12. Const HTTP_HEAD = 2
  13. Type connection_t
  14.     handle As Long
  15.     read_buf As String
  16.     http_version As Integer
  17.     method As Integer
  18.     request_uri As String
  19.  
  20. Type http_error_t
  21.     code As Integer
  22.     message As String
  23.     connection As Integer
  24.  
  25. Type file_error_t
  26.     failed As Integer
  27.     code As Integer
  28.  
  29. Dim i
  30. Dim num_active_connections
  31. Dim server_handle
  32. Dim Shared Connections(1 To MAX_CONNECTIONS) As connection_t
  33. Dim Shared Http_error_info As http_error_t
  34. Dim Shared File_error_info As file_error_t
  35.  
  36. On Error GoTo error_handler
  37.  
  38. server_handle = _OpenHost("TCP/IP:" + LTrim$(Str$(PORT)))
  39.     If num_active_connections < MAX_CONNECTIONS Then
  40.         Dim new_connection
  41.         new_connection = _OpenConnection(server_handle)
  42.         If new_connection Then
  43.             num_active_connections = num_active_connections + 1
  44.             For i = 1 To MAX_CONNECTIONS
  45.                 If Connections(i).handle = 0 Then
  46.                     Dim empty_connection As connection_t
  47.                     Connections(i) = empty_connection
  48.                     Connections(i).handle = new_connection
  49.                     num_active_connections = num_active_connections - 1
  50.                     Exit For
  51.                 End If
  52.             Next i
  53.         End If
  54.     End If
  55.  
  56.     For i = 1 To MAX_CONNECTIONS
  57.         If Connections(i).handle Then
  58.             Dim buf$
  59.             Get #Connections(i).handle, , buf$
  60.             If buf$ <> "" Then
  61.                 Connections(i).read_buf = Connections(i).read_buf + buf$
  62.                 process_request i
  63.                 http_error_complete:
  64.             End If
  65.         End If
  66.     Next i
  67.     _Limit 240
  68.  
  69.  
  70.  
  71. error_handler:
  72. If Err = 100 Then 'HTTP error
  73.     Print "HTTP error"; Http_error_info.code; Http_error_info.message; " for connection"; Http_error_info.connection
  74.     Resume http_error_complete
  75. Print "error"; Err; "on line"; _ErrorLine
  76.  
  77. file_error_handler:
  78. File_error_info.failed = TRUE
  79. File_error_info.code = Err
  80.  
  81. Sub http_send_status (c, code, message As String)
  82.     Dim s$
  83.     s$ = "HTTP/1.1" + Str$(code) + " " + message + CRLF
  84.     Put #Connections(c).handle, , s$
  85.  
  86. Sub http_send_header (c, header As String, value As String)
  87.     Dim s$
  88.     s$ = header + ": " + value + CRLF
  89.     Put #Connections(c).handle, , s$
  90.  
  91. Sub http_end_headers (c)
  92.     Put #Connections(c).handle, , CRLF
  93.  
  94. Sub http_send_body (c, body As String)
  95.     Put #Connections(c).handle, , body
  96.  
  97. Sub http_do_get (c)
  98.     Dim filepath As String, filedata As String
  99.     Dim fh
  100.     filepath = get_requested_filesystem_path(c)
  101.     Print filepath
  102.     If Not _FileExists(filepath) Then http_error 404, "Not Found", c
  103.  
  104.     On Error GoTo file_error_handler
  105.     fh = FreeFile
  106.     File_error_info.failed = FALSE
  107.     Open filepath For Binary As #fh
  108.     On Error GoTo error_handler
  109.     If File_error_info.failed Then http_error 403, "Permission Denied", c
  110.  
  111.     'Doing this all in one go isn't healthy for a number of reasons (memory usage, starving other clients)
  112.     'It should be done in chunks in the main loop
  113.     filedata = Space$(LOF(fh))
  114.     Get #fh, , filedata
  115.     Close #fh
  116.     http_send_status c, 200, "OK"
  117.     http_send_header c, "Content-Length", LTrim$(Str$(Len(filedata)))
  118.     http_send_header c, "Connection", "close"
  119.     http_end_headers c
  120.     http_send_body c, filedata
  121.     close_connection c
  122.  
  123. Sub http_do_head (c)
  124.     Print "http_do_head"
  125.     Dim s$
  126.     s$ = "HTTP/1.1 200 OK" + CRLF + CRLF
  127.     Put #Connections(c).handle, , s$
  128.  
  129. Sub close_connection (c)
  130.     Close #Connections(c).handle
  131.     Connections(c).handle = 0
  132.  
  133. Function get_requested_filesystem_path$ (c)
  134.     '7230 5.3 also 3986 for URI
  135.     'Origin form only for now
  136.     Dim raw_path As String
  137.     raw_path = Connections(c).request_uri
  138.     If Left$(raw_path, 1) <> "/" Then http_error 400, "Malformed URI", c
  139.  
  140.     Dim hash, questionmark, path_len
  141.     hash = InStr(raw_path, "#") 'Clients shouldn't be sending fragments, but we will gracefully ignore them
  142.     questionmark = InStr(raw_path, "?")
  143.     path_len = Len(raw_path)
  144.     If hash > 0 Then path_len = hash - 1
  145.     If questionmark > 0 And questionmark < hash Then path_len = questionmark - 1
  146.     ' Query strings are ignored for now
  147.  
  148.     'Dim cwd As String
  149.     'cwd = _CWD$
  150.     '$If WIN Then
  151.     '    'raw_path = GXSTR_Replace(raw_path, "/", "\")
  152.     '    cwd = GXSTR_Replace(cwd, "\", "/")
  153.     '$End If
  154.  
  155.     get_requested_filesystem_path = _CWD$ + cannonicalise_path(percent_decode(Left$(raw_path, path_len)))
  156.  
  157. Function percent_decode$ (raw_string As String)
  158.     Dim final_string As String, hexchars As String
  159.     Dim i, c
  160.     For i = 1 To Len(raw_string)
  161.         c = Asc(raw_string, i)
  162.         If c = 37 Then '%
  163.             hexchars = Mid$(raw_string, i + 1, 2)
  164.             If Len(hexchars) = 2 And InStr("0123456789abcdefABCDEF", Left$(hexchars, 1)) > 0 And InStr("0123456789abcdefABCDEF", Right$(hexchars, 1)) > 0 Then
  165.                 final_string = final_string + Chr$(Val("&H" + hexchars))
  166.             Else
  167.                 'String ends in something like "%1", or is invalid hex characters
  168.                 final_string = final_string + "%" + hexchars
  169.             End If
  170.             i = i + Len(hexchars)
  171.         Else
  172.             final_string = final_string + Chr$(c)
  173.         End If
  174.     Next i
  175.     percent_decode = final_string
  176.  
  177.  
  178. Function cannonicalise_path$ (raw_path As String)
  179.     Dim path As String
  180.     ReDim segments(1 To 1) As String
  181.     Dim i, uplevels
  182.     split raw_path, "/", segments()
  183.     For i = UBound(segments) To 1 Step -1
  184.         If segments(i) = "." Or segments(i) = "" Then
  185.             _Continue
  186.         ElseIf segments(i) = ".." Then
  187.             uplevels = uplevels + 1
  188.         Else
  189.             If uplevels = 0 Then
  190.                 path = "/" + segments(i) + path
  191.             Else
  192.                 uplevels = uplevels - 1
  193.             End If
  194.         End If
  195.     Next i
  196.     If path = "" Then path = "/"
  197.     'Note: if uplevels > 0 at this point, the path attempted to go above the root
  198.     'This is usually a client trying to be naughty
  199.     cannonicalise_path = path
  200.  
  201. 'https://www.qb64.org/forum/index.php?topic=1607.0
  202. Sub split (SplitMeString As String, delim As String, loadMeArray() As String)
  203.     Dim curpos As Long, arrpos As Long, LD As Long, dpos As Long 'fix use the Lbound the array already has
  204.     curpos = 1: arrpos = LBound(loadMeArray): LD = Len(delim)
  205.     dpos = InStr(curpos, SplitMeString, delim)
  206.     Do Until dpos = 0
  207.         loadMeArray(arrpos) = Mid$(SplitMeString, curpos, dpos - curpos)
  208.         arrpos = arrpos + 1
  209.         If arrpos > UBound(loadMeArray) Then ReDim _Preserve loadMeArray(LBound(loadMeArray) To UBound(loadMeArray) + 1000) As String
  210.         curpos = dpos + LD
  211.         dpos = InStr(curpos, SplitMeString, delim)
  212.     Loop
  213.     loadMeArray(arrpos) = Mid$(SplitMeString, curpos)
  214.     ReDim _Preserve loadMeArray(LBound(loadMeArray) To arrpos) As String 'get the ubound correct
  215.  
  216.  
  217. Sub process_request (c)
  218.     Dim eol
  219.     Dim l As String
  220.     Do
  221.         eol = InStr(Connections(c).read_buf, CRLF)
  222.         If eol = 0 Then Exit Sub
  223.         l = Left$(Connections(c).read_buf, eol - 1)
  224.         Connections(c).read_buf = Mid$(Connections(c).read_buf, eol + 2)
  225.         If Connections(c).http_version = 0 Then 'First line not yet read
  226.             process_start_line c, l
  227.         Else
  228.             If l = "" Then
  229.                 'headers complete; act upon request now
  230.                 Select Case Connections(c).method
  231.                     Case HTTP_GET
  232.                         http_do_get c
  233.                     Case HTTP_HEAD
  234.                         http_do_head c
  235.                 End Select
  236.                 Exit Sub
  237.             Else
  238.                 process_header c, l
  239.             End If
  240.         End If
  241.     Loop
  242.  
  243. Sub process_start_line (c, l As String)
  244.     '7230 3.1.1
  245.     'METHOD uri HTTP/x.y
  246.     Dim sp1, sp2
  247.     sp1 = InStr(l, " ")
  248.     If sp1 = 0 Then http_error 400, "Bad Request", c
  249.  
  250.     '7231 4.3
  251.     Select Case Left$(l, sp1 - 1)
  252.         Case "GET"
  253.             Connections(c).method = HTTP_GET
  254.         Case "HEAD"
  255.             Connections(c).method = HTTP_HEAD
  256.         Case Else
  257.             http_error 501, "Not Implemented", c
  258.     End Select
  259.  
  260.     sp2 = InStr(sp1 + 1, l, " ")
  261.     If sp2 = 0 Or sp2 - sp1 = 1 Then http_error 400, "Bad Request", c
  262.     Connections(c).request_uri = Mid$(l, sp1 + 1, sp2 - sp1 - 1)
  263.  
  264.     '7230 2.6
  265.     If Mid$(l, sp2 + 1, 5) <> "HTTP/" Then
  266.         http_error 400, "Bad Request", c
  267.     End If
  268.     Select Case Mid$(l, sp2 + 6)
  269.         Case "1.0"
  270.             Connections(c).http_version = HTTP_10
  271.         Case "1.1"
  272.             Connections(c).http_version = HTTP_11
  273.         Case Else
  274.             http_error 505, "HTTP Version Not Supported", c
  275.     End Select
  276.  
  277. Sub process_header (c, l As String)
  278.     'All headers ignored for now
  279.  
  280. Sub http_error (code, message As String, connection)
  281.     http_send_status connection, code, message
  282.     http_send_header connection, "Content-Length", "0"
  283.     http_send_header connection, "Connection", "close"
  284.     http_end_headers connection
  285.     close_connection connection
  286.     Http_error_info.code = code
  287.     Http_error_info.message = message
  288.     Http_error_info.connection = connection
  289.     Error 100
  290.  

Would it be alright if I included this in the GX game engine project (https://www.qb64.org/forum/index.php?topic=4481.0) as a tool that can be used to test out exported games?  I will of course add an attribution in the source with a link to this post.

Offline luke

  • Administrator
  • Seasoned Forum Regular
  • Posts: 324
    • View Profile
Re: HTTP 1.1 compliant web server (work in progress)
« Reply #5 on: December 16, 2021, 09:56:29 pm »
Good catch on the missing counter decrement.

As mentioned in the top post this is fairly incomplete as a webserver, but if it's sufficient for your purposes then feel free to distribute and make use of it. I have added some flowery pseudo-legal language to that effect.