From 428f035d9ea21d3e23dc9b96e72d3c7cd6a8c493 Mon Sep 17 00:00:00 2001 From: Bruce Hill Date: Tue, 1 Apr 2025 21:15:17 -0400 Subject: Add http-server example --- examples/http-server/http-server.tm | 159 ++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 examples/http-server/http-server.tm (limited to 'examples/http-server/http-server.tm') 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 +use +use +use +use +use + +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()! + " + + + $file + +

$file program output

+
$output
+ + + " + 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) + ) + -- cgit v1.2.3