3 # This file provides an HTTP server module and standalone executable
16 use ./connection-queue.tm
18 func serve(port:Int32, handler:func(request:HTTPRequest -> HTTPResponse), num_threads=16)
19 connections := ConnectionQueue()
22 workers.insert(PThread.new(func()
24 connection := connections.dequeue()
28 for (ssize_t n; (n = read(@connection, buf, sizeof(buf) - 1)) > 0; ) {
30 @request_text = Text$concat(@request_text, Text$from_strn(buf, n));
31 if (@request_text.length > 1000000 || strstr(buf, "\\r\\n\\r\\n"))
35 request := HTTPRequest.from_text(request_text) or skip
36 response := handler(request).bytes()
38 if (@response.stride != 1)
39 List$compact(&@response, 1);
40 write(@connection, @response.data, @response.length);
47 int s = socket(AF_INET, SOCK_STREAM, 0);
48 if (s < 0) err(1, "Couldn't connect to socket!");
51 if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0)
52 err(1, "Couldn't set socket option");
54 struct sockaddr_in addr = {AF_INET, htons(@port), INADDR_ANY};
55 if (bind(s, (struct sockaddr*)&addr, sizeof(addr)) < 0)
56 err(1, "Couldn't bind to socket");
58 err(1, "Couldn't listen on socket");
64 conn := C_code:Int32`accept(@sock, NULL, NULL)`
66 connections.enqueue(conn)
68 say("Shutting down...")
72 struct HTTPRequest(method:Text, path:Text, version:Text, headers:[Text], body:Text)
73 func from_text(text:Text -> HTTPRequest?)
74 m := $Pat'{word} {..} HTTP/{..}{crlf}{..}'.match(text) or return none
75 method := m.captures[1]!
76 path := $Pat'{2+ /}'.replace(m.captures[2]!, '/')
77 version := m.captures[3]!
78 rest := $Pat'{..}{2 crlf}{0+ ..}'.match(m.captures[-1]!) or return none
79 headers := $Pat'{crlf}'.split(rest.captures[1]!)
80 body := rest.captures[-1]!
81 return HTTPRequest(method, path, version, headers, body)
83 struct HTTPResponse(body:Text, status=200, content_type="text/plain", headers:{Text:Text}={})
84 func bytes(r:HTTPResponse -> [Byte])
85 body_bytes := r.body.utf8()
86 extra_headers := (++: "$k: $v\r\n" for k,v in r.headers) or ""
88 HTTP/1.1 $(r.status) OK\r
89 Content-Length: $(body_bytes.length + 2)\r
90 Content-Type: $(r.content_type)\r
94 ".utf8() ++ body_bytes
96 func _content_type(file:Path -> Text)
97 when file.extension() is "html" return "text/html"
98 is "tm" return "text/html"
99 is "js" return "text/javascript"
100 is "css" return "text/css"
101 else return "text/plain"
103 enum RouteEntry(ServeFile(file:Path), Redirect(destination:Text))
104 func respond(entry:RouteEntry, request:HTTPRequest -> HTTPResponse)
105 when entry is ServeFile(f)
106 body := if f.can_execute()
107 Command(Text(f)).get_output()!
110 return HTTPResponse(body, content_type=_content_type(f))
111 is Redirect(destination)
112 return HTTPResponse("Found", 302, headers={"Location":destination})
114 func load_routes(directory:Path -> {Text:RouteEntry})
115 routes : &{Text:RouteEntry}
116 for file in (directory ++ (./*)).glob()
117 skip unless file.is_file()
118 server_path := "/" ++ "/".join(file.relative_to(directory).components())
119 if file.base_name() == "index.html"
120 canonical := server_path.without_suffix("index.html")
121 routes[server_path] = Redirect(canonical)
122 routes[canonical] = ServeFile(file)
123 else if file.extension() == "html"
124 canonical := server_path.without_suffix(".html")
125 routes[server_path] = Redirect(canonical)
126 routes[canonical] = ServeFile(file)
127 else if file.extension() == "tm"
128 canonical := server_path.without_suffix(".tm")
129 routes[server_path] = Redirect(canonical)
130 routes[canonical] = ServeFile(file)
132 routes[server_path] = ServeFile(file)
135 func main(directory:Path, port=Int32(8080))
136 say("Serving on port $port")
137 routes := load_routes(directory)
138 say("Hosting: $routes")
140 serve(port, func(request:HTTPRequest)
141 if handler := routes[request.path]
142 return handler.respond(request)
144 return HTTPResponse("Not found!", 404)