2025-04-01 18:15:17 -07:00
|
|
|
#!/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
|
|
|
|
|
2025-04-06 13:07:23 -07:00
|
|
|
func serve(port:Int32, handler:func(request:HTTPRequest -> HTTPResponse), num_threads=16)
|
2025-04-01 18:15:17 -07:00
|
|
|
connections := ConnectionQueue()
|
2025-04-06 16:20:07 -07:00
|
|
|
workers : &[@pthread_t]
|
2025-04-06 13:07:23 -07:00
|
|
|
for i in num_threads
|
|
|
|
workers.insert(pthread_t.new(func()
|
|
|
|
repeat
|
2025-04-06 11:20:18 -07:00
|
|
|
connection := connections.dequeue()
|
2025-04-06 18:43:19 -07:00
|
|
|
request_text := C_code:Text(
|
2025-04-01 18:15:17 -07:00
|
|
|
Text_t request = EMPTY_TEXT;
|
|
|
|
char buf[1024] = {};
|
2025-04-06 18:43:19 -07:00
|
|
|
for (ssize_t n; (n = read(@connection, buf, sizeof(buf) - 1)) > 0; ) {
|
2025-04-01 18:15:17 -07:00
|
|
|
buf[n] = 0;
|
|
|
|
request = Text$concat(request, Text$from_strn(buf, n));
|
|
|
|
if (request.length > 1000000 || strstr(buf, "\r\n\r\n"))
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
request
|
2025-04-06 18:43:19 -07:00
|
|
|
)
|
2025-04-01 18:15:17 -07:00
|
|
|
|
|
|
|
request := HTTPRequest.from_text(request_text) or skip
|
2025-04-06 11:20:18 -07:00
|
|
|
response := handler(request).bytes()
|
2025-04-06 18:43:19 -07:00
|
|
|
C_code {
|
|
|
|
if (@response.stride != 1)
|
|
|
|
Array$compact(&@response, 1);
|
|
|
|
write(@connection, @response.data, @response.length);
|
|
|
|
close(@connection);
|
2025-04-01 18:15:17 -07:00
|
|
|
}
|
|
|
|
))
|
|
|
|
|
|
|
|
|
2025-04-06 18:43:19 -07:00
|
|
|
sock := C_code:Int32(
|
2025-04-01 18:15:17 -07:00
|
|
|
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");
|
|
|
|
|
2025-04-06 18:43:19 -07:00
|
|
|
struct sockaddr_in addr = {AF_INET, htons(@port), INADDR_ANY};
|
2025-04-01 18:15:17 -07:00
|
|
|
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
|
2025-04-06 18:43:19 -07:00
|
|
|
)
|
2025-04-01 18:15:17 -07:00
|
|
|
|
2025-04-06 13:07:23 -07:00
|
|
|
repeat
|
2025-04-06 18:43:19 -07:00
|
|
|
conn := C_code:Int32(accept(@sock, NULL, NULL))
|
2025-04-01 18:15:17 -07:00
|
|
|
stop if conn < 0
|
2025-04-06 11:20:18 -07:00
|
|
|
connections.enqueue(conn)
|
2025-04-01 18:15:17 -07:00
|
|
|
|
|
|
|
say("Shutting down...")
|
2025-04-06 13:07:23 -07:00
|
|
|
for w in workers
|
2025-04-06 11:20:18 -07:00
|
|
|
w.cancel()
|
2025-04-01 18:15:17 -07:00
|
|
|
|
2025-04-06 13:07:23 -07:00
|
|
|
struct HTTPRequest(method:Text, path:Text, version:Text, headers:[Text], body:Text)
|
|
|
|
func from_text(text:Text -> HTTPRequest?)
|
2025-04-06 11:20:18 -07:00
|
|
|
m := text.pattern_captures($Pat'{word} {..} HTTP/{..}{crlf}{..}') or return none
|
2025-04-01 18:15:17 -07:00
|
|
|
method := m[1]
|
2025-04-06 11:20:18 -07:00
|
|
|
path := m[2].replace_pattern($Pat'{2+ /}', '/')
|
2025-04-01 18:15:17 -07:00
|
|
|
version := m[3]
|
2025-04-06 11:20:18 -07:00
|
|
|
rest := m[-1].pattern_captures($Pat/{..}{2 crlf}{0+ ..}/) or return none
|
|
|
|
headers := rest[1].split_pattern($Pat/{crlf}/)
|
2025-04-01 18:15:17 -07:00
|
|
|
body := rest[-1]
|
|
|
|
return HTTPRequest(method, path, version, headers, body)
|
|
|
|
|
2025-04-06 13:07:23 -07:00
|
|
|
struct HTTPResponse(body:Text, status=200, content_type="text/plain", headers:{Text=Text}={})
|
|
|
|
func bytes(r:HTTPResponse -> [Byte])
|
2025-04-06 11:20:18 -07:00
|
|
|
body_bytes := r.body.bytes()
|
2025-04-06 19:26:12 -07:00
|
|
|
extra_headers := (++: "$k: $v\r\n" for k,v in r.headers) or ""
|
2025-04-01 18:15:17 -07:00
|
|
|
return "
|
2025-04-06 19:26:12 -07:00
|
|
|
HTTP/1.1 $(r.status) OK\r
|
|
|
|
Content-Length: $(body_bytes.length + 2)\r
|
|
|
|
Content-Type: $(r.content_type)\r
|
|
|
|
Connection: close\r
|
2025-04-01 18:15:17 -07:00
|
|
|
$extra_headers
|
2025-04-06 19:26:12 -07:00
|
|
|
\r\n
|
2025-04-06 11:20:18 -07:00
|
|
|
".bytes() ++ body_bytes
|
2025-04-01 18:15:17 -07:00
|
|
|
|
2025-04-06 13:07:23 -07:00
|
|
|
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()
|
2025-04-06 11:20:18 -07:00
|
|
|
Command(Text(file)).get_output()!
|
2025-04-06 13:07:23 -07:00
|
|
|
else
|
2025-04-06 11:20:18 -07:00
|
|
|
file.read()!
|
2025-04-01 18:15:17 -07:00
|
|
|
return HTTPResponse(body, content_type=_content_type(file))
|
2025-04-06 13:07:23 -07:00
|
|
|
is Redirect(destination)
|
2025-04-01 18:15:17 -07:00
|
|
|
return HTTPResponse("Found", 302, headers={"Location"=destination})
|
|
|
|
|
2025-04-06 13:07:23 -07:00
|
|
|
func load_routes(directory:Path -> {Text=RouteEntry})
|
2025-04-06 16:20:07 -07:00
|
|
|
routes : &{Text=RouteEntry}
|
2025-04-06 13:07:23 -07:00
|
|
|
for file in (directory ++ (./*)).glob()
|
2025-04-06 11:20:18 -07:00
|
|
|
skip unless file.is_file()
|
|
|
|
contents := file.read() or skip
|
|
|
|
server_path := "/" ++ "/".join(file.relative_to(directory).components)
|
2025-04-06 13:07:23 -07:00
|
|
|
if file.base_name() == "index.html"
|
2025-04-06 11:20:18 -07:00
|
|
|
canonical := server_path.without_suffix("index.html")
|
2025-04-01 18:15:17 -07:00
|
|
|
routes[server_path] = Redirect(canonical)
|
|
|
|
routes[canonical] = ServeFile(file)
|
2025-04-06 13:07:23 -07:00
|
|
|
else if file.extension() == "html"
|
2025-04-06 11:20:18 -07:00
|
|
|
canonical := server_path.without_suffix(".html")
|
2025-04-01 18:15:17 -07:00
|
|
|
routes[server_path] = Redirect(canonical)
|
|
|
|
routes[canonical] = ServeFile(file)
|
2025-04-06 13:07:23 -07:00
|
|
|
else if file.extension() == "tm"
|
2025-04-06 11:20:18 -07:00
|
|
|
canonical := server_path.without_suffix(".tm")
|
2025-04-01 18:15:17 -07:00
|
|
|
routes[server_path] = Redirect(canonical)
|
|
|
|
routes[canonical] = ServeFile(file)
|
2025-04-06 13:07:23 -07:00
|
|
|
else
|
2025-04-01 18:15:17 -07:00
|
|
|
routes[server_path] = ServeFile(file)
|
|
|
|
return routes[]
|
|
|
|
|
2025-04-06 13:07:23 -07:00
|
|
|
func main(directory:Path, port=Int32(8080))
|
2025-04-01 18:15:17 -07:00
|
|
|
say("Serving on port $port")
|
|
|
|
routes := load_routes(directory)
|
2025-04-06 10:40:17 -07:00
|
|
|
say(" Hosting: $routes")
|
2025-04-01 18:15:17 -07:00
|
|
|
|
2025-04-06 13:07:23 -07:00
|
|
|
serve(port, func(request:HTTPRequest)
|
|
|
|
if handler := routes[request.path]
|
2025-04-06 11:20:18 -07:00
|
|
|
return handler.respond(request)
|
2025-04-06 13:07:23 -07:00
|
|
|
else
|
2025-04-01 18:15:17 -07:00
|
|
|
return HTTPResponse("Not found!", 404)
|
|
|
|
)
|
|
|
|
|