aboutsummaryrefslogtreecommitdiff
path: root/examples/http-server/http-server.tm
diff options
context:
space:
mode:
authorBruce Hill <bruce@bruce-hill.com>2025-04-01 21:15:17 -0400
committerBruce Hill <bruce@bruce-hill.com>2025-04-01 21:15:17 -0400
commit428f035d9ea21d3e23dc9b96e72d3c7cd6a8c493 (patch)
treed5c141d52e60d8d7d88056ab1c98a27df13f23b5 /examples/http-server/http-server.tm
parentf32d2a25c10977f52c83ddef77008afd22e7f0ce (diff)
Add http-server example
Diffstat (limited to 'examples/http-server/http-server.tm')
-rw-r--r--examples/http-server/http-server.tm159
1 files changed, 159 insertions, 0 deletions
diff --git a/examples/http-server/http-server.tm b/examples/http-server/http-server.tm
new file mode 100644
index 00000000..ad489594
--- /dev/null
+++ b/examples/http-server/http-server.tm
@@ -0,0 +1,159 @@
+#!/bin/env tomo
+
+# This file provides an HTTP server module and standalone executable
+
+use <stdio.h>
+use <stdlib.h>
+use <string.h>
+use <unistd.h>
+use <arpa/inet.h>
+use <err.h>
+
+use commands
+use pthreads
+use patterns
+
+use ./connection-queue.tm
+
+func serve(port:Int32, handler:func(request:HTTPRequest -> HTTPResponse), num_threads=16):
+ connections := ConnectionQueue()
+ workers := &[:@pthread_t]
+ for i in num_threads:
+ workers:insert(pthread_t.new(func():
+ repeat:
+ connection := connections:dequeue()
+ request_text := inline C : Text {
+ Text_t request = EMPTY_TEXT;
+ char buf[1024] = {};
+ for (ssize_t n; (n = read(_$connection, buf, sizeof(buf) - 1)) > 0; ) {
+ buf[n] = 0;
+ request = Text$concat(request, Text$from_strn(buf, n));
+ if (request.length > 1000000 || strstr(buf, "\r\n\r\n"))
+ break;
+ }
+ request
+ }
+
+ request := HTTPRequest.from_text(request_text) or skip
+ response := handler(request):bytes()
+ inline C {
+ if (_$response.stride != 1)
+ Array$compact(&_$response, 1);
+ write(_$connection, _$response.data, _$response.length);
+ close(_$connection);
+ }
+ ))
+
+
+ sock := inline C : Int32 {
+ int s = socket(AF_INET, SOCK_STREAM, 0);
+ if (s < 0) err(1, "Couldn't connect to socket!");
+
+ int opt = 1;
+ if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0)
+ err(1, "Couldn't set socket option");
+
+ struct sockaddr_in addr = {AF_INET, htons(_$port), INADDR_ANY};
+ if (bind(s, (struct sockaddr*)&addr, sizeof(addr)) < 0)
+ err(1, "Couldn't bind to socket");
+ if (listen(s, 8) < 0)
+ err(1, "Couldn't listen on socket");
+
+ s
+ }
+
+ repeat:
+ conn := inline C : Int32 { accept(_$sock, NULL, NULL) }
+ stop if conn < 0
+ connections:enqueue(conn)
+
+ say("Shutting down...")
+ for w in workers:
+ w:cancel()
+
+struct HTTPRequest(method:Text, path:Text, version:Text, headers:[Text], body:Text):
+ func from_text(text:Text -> HTTPRequest?):
+ m := text:pattern_captures($Pat'{word} {..} HTTP/{..}{crlf}{..}') or return none
+ method := m[1]
+ path := m[2]:replace_pattern($Pat'{2+ /}', '/')
+ version := m[3]
+ rest := m[-1]:pattern_captures($Pat/{..}{2 crlf}{0+ ..}/) or return none
+ headers := rest[1]:split_pattern($Pat/{crlf}/)
+ body := rest[-1]
+ return HTTPRequest(method, path, version, headers, body)
+
+struct HTTPResponse(body:Text, status=200, content_type="text/plain", headers={:Text,Text}):
+ func bytes(r:HTTPResponse -> [Byte]):
+ body_bytes := r.body:bytes()
+ extra_headers := (++: "$k: $v$(\r\n)" for k,v in r.headers) or ""
+ return "
+ HTTP/1.1 $(r.status) OK$\r
+ Content-Length: $(body_bytes.length + 2)$\r
+ Content-Type: $(r.content_type)$\r
+ Connection: close$\r
+ $extra_headers
+ $\r$\n
+ ":bytes() ++ body_bytes
+
+func _content_type(file:Path -> Text):
+ when file:extension() is "html": return "text/html"
+ is "tm": return "text/html"
+ is "js": return "text/javascript"
+ is "css": return "text/css"
+ else: return "text/plain"
+
+enum RouteEntry(ServeFile(file:Path), Redirect(destination:Text)):
+ func respond(entry:RouteEntry, request:HTTPRequest -> HTTPResponse):
+ when entry is ServeFile(file):
+ body := if file:can_execute():
+ output := Command(Text(file)):get_output()!
+ "
+ <!DOCTYPE HTML>
+ <html>
+ <head><title>$file</title></head>
+ <body>
+ <h1>$file program output</h1>
+ <pre>$output</pre>
+ </body>
+ </html>
+ "
+ else:
+ file:read()!
+ return HTTPResponse(body, content_type=_content_type(file))
+ is Redirect(destination):
+ return HTTPResponse("Found", 302, headers={"Location"=destination})
+
+func load_routes(directory:Path -> {Text,RouteEntry}):
+ routes := &{:Text,RouteEntry}
+ for file in (directory ++ (./*)):glob():
+ skip unless file:is_file()
+ contents := file:read() or skip
+ server_path := "/" ++ "/":join(file:relative_to(directory).components)
+ if file:base_name() == "index.html":
+ canonical := server_path:without_suffix("index.html")
+ routes[server_path] = Redirect(canonical)
+ routes[canonical] = ServeFile(file)
+ else if file:extension() == "html":
+ canonical := server_path:without_suffix(".html")
+ routes[server_path] = Redirect(canonical)
+ routes[canonical] = ServeFile(file)
+ else if file:extension() == "tm":
+ canonical := server_path:without_suffix(".tm")
+ routes[server_path] = Redirect(canonical)
+ routes[canonical] = ServeFile(file)
+ else:
+ routes[server_path] = ServeFile(file)
+ return routes[]
+
+func main(directory:Path, port=Int32(8080)):
+ say("Serving on port $port")
+ routes := load_routes(directory)
+ !! Hosting: $routes
+
+ serve(port, func(request:HTTPRequest):
+ if handler := routes[request.path]:
+ return handler:respond(request)
+ else:
+ return HTTPResponse("Not found!", 404)
+ )
+