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:
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
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:
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
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:
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
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:
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
<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:
function escapeHTML pText
replace "&" with "&" in pText
replace "<" with "<" in pText
replace ">" with ">" in pText
replace quote with """ 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
<p>Hello, Zoë</p> <p>Hello, <script></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
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
1 2 1 3
Words in this lesson
httpdStarthttpdResponsehttpdStopmimeTheMIMETypeOfExtensionJSONExportJSONImportlibURLLastRHHeaderslaunch urlswitchEach one finds every lesson that uses it. The Dictionary has the whole story of each, with its syntax and more examples.