pax_global_header00006660000000000000000000000064151223540550014514gustar00rootroot0000000000000052 comment=c6645131aa825023eecb40b5324298a65dbaca65 tomo-http-server/000077500000000000000000000000001512235405500143175ustar00rootroot00000000000000tomo-http-server/.gitignore000066400000000000000000000000171512235405500163050ustar00rootroot00000000000000.* !.gitignore tomo-http-server/CHANGES.md000066400000000000000000000002621512235405500157110ustar00rootroot00000000000000# Version History ## v2025-12-22 Updated to latest Path implementation details and cleaned some dead code. ## v1.1 Updated syntax and dependencies. ## v1.0 Initial version tomo-http-server/README.md000066400000000000000000000002651512235405500156010ustar00rootroot00000000000000# HTTP Server This is a simple multithreaded [Tomo](https://tomo.bruce-hill.com) HTTP server that can be run like this: ``` tomo -e http-server.tm ./http-server ./sample-site ``` tomo-http-server/connection-queue.tm000066400000000000000000000012761512235405500201500ustar00rootroot00000000000000use pthreads func _assert_success(name:Text, val:Int32; inline) fail("$name() failed!") if val < 0 struct ConnectionQueue(_connections:@[Int32]=@[], _mutex=Mutex.new(), _cond=Condition.new()) func enqueue(queue:ConnectionQueue, connection:Int32) queue._mutex.lock() queue._connections.insert(connection) queue._mutex.unlock() queue._cond.signal() func dequeue(queue:ConnectionQueue -> Int32) conn : Int32? queue._mutex.lock() while queue._connections.length == 0 queue._cond.wait(queue._mutex) conn = queue._connections.pop(1) queue._mutex.unlock() queue._cond.signal() return conn! tomo-http-server/http-server.tm000066400000000000000000000122001512235405500171370ustar00rootroot00000000000000#!/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] for i in num_threads workers.insert(PThread.new(func() repeat connection := connections.dequeue() request_text := "" C_code` char buf[1024] = {}; for (ssize_t n; (n = read(@connection, buf, sizeof(buf) - 1)) > 0; ) { buf[n] = 0; @request_text = Text$concat(@request_text, Text$from_strn(buf, n)); if (@request_text.length > 1000000 || strstr(buf, "\\r\\n\\r\\n")) break; } ` request := HTTPRequest.from_text(request_text) or skip response := handler(request).bytes() C_code ` if (@response.stride != 1) List$compact(&@response, 1); write(@connection, @response.data, @response.length); close(@connection); ` )) sock := C_code: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 := C_code: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 := $Pat'{word} {..} HTTP/{..}{crlf}{..}'.match(text) or return none method := m.captures[1]! path := $Pat'{2+ /}'.replace(m.captures[2]!, '/') version := m.captures[3]! rest := $Pat'{..}{2 crlf}{0+ ..}'.match(m.captures[-1]!) or return none headers := $Pat'{crlf}'.split(rest.captures[1]!) body := rest.captures[-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.utf8() 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 ".utf8() ++ 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(f) body := if f.can_execute() Command(Text(f)).get_output()! else f.read()! return HTTPResponse(body, content_type=_content_type(f)) 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() 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) say("Hosting: $routes") serve(port, func(request:HTTPRequest) if handler := routes[request.path] return handler.respond(request) else return HTTPResponse("Not found!", 404) ) tomo-http-server/modules.ini000066400000000000000000000003411512235405500164660ustar00rootroot00000000000000[pthreads] version=v2.0 git=https://github.com/bruce-hill/tomo-pthreads [patterns] version=v2025-11-29 git=https://github.com/bruce-hill/tomo-patterns [commands] version=v2.1 git=https://github.com/bruce-hill/tomo-commands tomo-http-server/sample-site/000077500000000000000000000000001512235405500165425ustar00rootroot00000000000000tomo-http-server/sample-site/foo.html000066400000000000000000000001231512235405500202070ustar00rootroot00000000000000 This is the foo page. tomo-http-server/sample-site/hello.txt000066400000000000000000000000061512235405500204020ustar00rootroot00000000000000Hello tomo-http-server/sample-site/index.html000066400000000000000000000004771512235405500205470ustar00rootroot00000000000000 HTTP Example

Hello world!

Try going to /random or /foo or /hello.txt

tomo-http-server/sample-site/random.tm000077500000000000000000000005301512235405500203650ustar00rootroot00000000000000#!/bin/env tomo use random func main() say(" Random Number

Random Number

Your random number is: $(random.int(1,100)) ") tomo-http-server/sample-site/styles.css000066400000000000000000000002221512235405500205730ustar00rootroot00000000000000body{ margin:40px auto; max-width:650px; line-height:1.6; font-size:18px; color:#444; padding:0 10px; } h1,h2,h3{ line-height:1.2 }