LessonsExtensions: Built inLesson 75

A web server of your own

Serve web pages, files and a JSON API from your own program with the HTTP server library: routes for each address, the right status and type, a folder of files served safely, and data sent in.

  • Extensions
  • Lesson 75 of 83
  • About 12 minutes
  • 6 examples

One handler, many addresses

The web and the internet started a tiny server to have something to fetch from. A server of your own can do much more: show a page to a phone on the same Wi-Fi, give data to another program, or be a whole app that people use from a browser. httpdStart sends every request to one handler, with an array that says what was asked: its method (GET to read, POST to send), its resource (the address's path, such as /about), its parameters, headers and content. A switch on the path is a list of routes:

ExampleRun it in the playground
on serve pSocket, pRequest
   switch pRequest["resource"]
      case "/"
         httpdResponse pSocket, 200, "Welcome"
         break
      case "/about"
         httpdResponse pSocket, 200, "Made with OXT-Beyond"
         break
      default
         httpdResponse pSocket, 404, "Nothing at" && pRequest["resource"]
   end switch
end serve

httpdStart "serve", 0
put "http://127.0.0.1:" & it into tServer
put URL (tServer & "/")
put URL (tServer & "/about")
get URL (tServer & "/missing")
put the result into tProblem
put it
put tProblem
httpdStop
Output
Welcome
Made with OXT-Beyond
Nothing at /missing
error 404 Not Found

The page for a missing path still arrives, and the result has its status; it is read straight after the get, because the next command, even a put, clears it. The number with each answer is its status: 200 means "here it is", 404 "no such thing". Others worth knowing are 400 (the request makes no sense), 405 (not with that method), 303 (look over there, with a Location header) and 500 (the server went wrong; the library sends this itself if your handler stops with an error). httpdStop with no port stops every server this program started.

Saying what you send

A browser decides how to show what it gets from its Content-Type header: text/html is a page to lay out, text/plain is shown as it is, application/json is data. Give it as the fourth parameter of httpdResponse, a header a line, and give text as UTF-8 and say so, or accented letters arrive broken:

ExampleRun it in the playground
on serve pSocket, pRequest
   put "<!DOCTYPE html><title>Café</title><h1>Bienvenue au café</h1>" into tPage
   httpdResponse pSocket, 200, textEncode(tPage, "UTF-8"), "Content-Type: text/html; charset=utf-8"
end serve

httpdStart "serve", 0
put "http://127.0.0.1:" & it into tServer
put URL tServer into tBytes
put textDecode(tBytes, "UTF-8") contains "café"
put libURLLastRHHeaders() into tHeaders
replace numToChar(13) with empty in tHeaders
filter tHeaders with "Content-Type:*"
put tHeaders
httpdStop
Output
true
Content-Type: text/html; charset=utf-8

To see it in a real browser, start the server with a port of your choosing, such as httpdStart "serve", 8080, and launch url "http://localhost:8080/". A phone on the same network reaches it at your computer's own address on that network, such as http://192.168.1.20:8080/.

A JSON API

An API is a server for programs rather than people: it answers with data, usually JSON (JSON). The same path can do different things for each method: here GET /notes lists the notes, and POST /notes adds the one sent in its content:

ExampleRun it in the playground
global gNotes

on serve pSocket, pRequest
   global gNotes
   if pRequest["resource"] is not "/notes" then
      httpdResponse pSocket, 404, "{}", "Content-Type: application/json"
   else if pRequest["method"] is "GET" then
      httpdResponse pSocket, 200, JSONExport(gNotes), "Content-Type: application/json"
   else if pRequest["method"] is "POST" then
      put the number of elements in gNotes + 1 into tNext
      put JSONImport(pRequest["content"]) into gNotes[tNext]
      httpdResponse pSocket, 201, JSONExport(gNotes[tNext]), "Content-Type: application/json"
   else
      httpdResponse pSocket, 405, "{}", "Content-Type: application/json"
   end if
end serve

put empty into gNotes
httpdStart "serve", 0
put "http://127.0.0.1:" & it & "/notes" into tNotes
set the httpHeaders to "Content-Type: application/json"
post "{" & quote & "text" & quote & ":" & quote & "Buy tea" & quote & "}" to URL tNotes
post "{" & quote & "text" & quote & ":" & quote & "Call Ada" & quote & "}" to URL tNotes
set the httpHeaders to empty
put JSONImport(URL tNotes) into tList
put the number of elements in tList
put tList[2]["text"]
httpdStop
Output
2
Call Ada

201 means "made". The notes live in a global, so they last while the program runs and are gone when it quits; a real app keeps them in a file or a database (Databases).

Serving a folder of files

A web site is mostly files: pages, pictures, style sheets. A route that finds the file named by the path and sends it, with the type the MIME library gives for its ending, serves a whole folder. One rule matters more than any other: never send a file outside the folder. A request for /../../secret.txt climbs out of it, so refuse any path with .. in it:

ExampleRun it in the playground
global gSite

on serve pSocket, pRequest
   global gSite
   put pRequest["resource"] into tPath
   if tPath is "/" then put "/index.html" into tPath
   if ".." is in tPath or "\" is in tPath then
      httpdResponse pSocket, 400, "Bad path"
      exit serve
   end if
   if there is not a file (gSite & tPath) then
      httpdResponse pSocket, 404, "No such file"
      exit serve
   end if
   set the itemDelimiter to "."
   put mimeTheMIMETypeOfExtension(the last item of tPath) into tType
   httpdResponse pSocket, 200, URL ("binfile:" & gSite & tPath), "Content-Type:" && tType
end serve

put specialFolderPath("temporary") & "/oxt-lesson-site" into gSite
create folder gSite
put "<h1>Home</h1>" into URL ("binfile:" & gSite & "/index.html")
put "h1 { color: teal }" into URL ("binfile:" & gSite & "/style.css")

httpdStart "serve", 0
put "http://127.0.0.1:" & it into tServer
put URL tServer
get URL (tServer & "/style.css")
put libURLLastRHHeaders() into tHeaders
replace numToChar(13) with empty in tHeaders
filter tHeaders with "Content-Type:*"
put tHeaders
get URL (tServer & "/../../etc/passwd")
put the result
httpdStop

delete file (gSite & "/index.html")
delete file (gSite & "/style.css")
delete folder gSite
Output
<h1>Home</h1>
Content-Type: text/css
error 400 Bad Request

The browser shows a page from /, so it is common to send index.html for it, as here. Check the path before anything else: a server that forgets is one of the oldest ways of being broken into.

Data from a form

An HTML form sends what was typed as parameters, which the library decodes into an array, for a GET in the address and for a POST from its content. The values arrive as UTF-8, so decode them, and when you put one back into a page, escape it, so that someone who types <script> cannot put code into your page:

ExampleRun it in the playground
function escapeHTML pText
   replace "&" with "&amp;" in pText
   replace "<" with "&lt;" in pText
   replace ">" with "&gt;" in pText
   replace quote with "&quot;" in pText
   return pText
end escapeHTML

on serve pSocket, pRequest
   put textDecode(pRequest["parameters"]["name"], "UTF-8") into tName
   put "<p>Hello, " & escapeHTML(tName) & "</p>" into tPage
   httpdResponse pSocket, 200, textEncode(tPage, "UTF-8"), "Content-Type: text/html; charset=utf-8"
end serve

httpdStart "serve", 0
put "http://127.0.0.1:" & it & "/hello" into tHello
post "name=" & urlEncode(textEncode("Zoë", "UTF-8")) to URL tHello
put textDecode(it, "UTF-8")
post "name=" & urlEncode("<script>") to URL tHello
put it
httpdStop
Output
<p>Hello, Zoë</p>
<p>Hello, &lt;script&gt;</p>

The page that has the form needs only <form method="post" action="/hello"><input name="name"><button>Send</button></form>, served from another route.

Try it yourself

Write a server that counts visits to each path, and answers every request with how many times that path has been asked for, counting this one: the first visit to /a gets 1, the next 2, and /b counts on its own.

Show a solution
ExampleRun it in the playground
global gVisits

on serve pSocket, pRequest
   global gVisits
   add 1 to gVisits[pRequest["resource"]]
   httpdResponse pSocket, 200, gVisits[pRequest["resource"]]
end serve

put empty into gVisits
httpdStart "serve", 0
put "http://127.0.0.1:" & it into tServer
put URL (tServer & "/a")
put URL (tServer & "/a")
put URL (tServer & "/b")
put URL (tServer & "/a")
httpdStop
Output
1
2
1
3

Words in this lesson

Each one finds every lesson that uses it. The Dictionary has the whole story of each, with its syntax and more examples.