const std = @import("std"); /// Wrapper around a std.http.Server to run a multi-threaded HTTP server using thread-per-request and arena-per-request /// Context is .clone()'d and passed to each request, useful for passing user data to each request handler e.g. a database connection. /// HandlerType provides a single method handle() which is used to actually handle requests. pub fn Server(comptime Context: type, comptime Handler: type) type { return struct { address: std.net.Address, context: Context, handler: Handler, max_header_size: usize = 8192, n_threads: u32 = 50, allocator: std.mem.Allocator, pub fn serve(self: @This()) !void { var tp = std.Thread.Pool{ .threads = &[_]std.Thread{}, .allocator = self.allocator }; try tp.init(.{ .allocator = self.allocator, .n_jobs = self.n_threads }); defer tp.deinit(); var svr_internal = std.http.Server.init(self.allocator, .{ .reuse_address = true }); defer svr_internal.deinit(); try svr_internal.listen(self.address); std.log.info("server listening on {}", .{self.address}); while (true) { var aa = std.heap.ArenaAllocator.init(self.allocator); // will be freed by the spawned thread. var conn = try svr_internal.accept(.{ .allocator = aa.allocator(), .header_strategy = .{ .dynamic = self.max_header_size } }); const ctx: Context = self.context.clone(); try tp.spawn(handle, .{ self, &conn, ctx, aa }); } } fn handle(self: @This(), res: *std.http.Server.Response, ctx: Context, aa: std.heap.ArenaAllocator) void { defer aa.deinit(); defer ctx.deinit(); defer res.deinit(); if (res.wait()) { if (self.handler.handle(res, ctx)) { std.log.info("Success handling request [{s} {s} {s}] status {d} client {}", .{ @tagName(res.request.method), res.request.target, @tagName(res.request.version), @intFromEnum(res.status), res.address }); } else |err| { std.log.info("Error handling request [{s} {s} {s}] client {} error {}", .{ @tagName(res.request.method), res.request.target, @tagName(res.request.version), res.address, err }); if (handle_simple_response(res, "Server error!", .internal_server_error)) {} else |err2| { std.log.err("Error sending error page for {} : {}", .{ res.address, err2 }); } } } else |_| { // Do nothing } if (res.state != .finished) { std.log.err("request wasn't finished!", .{}); } } fn handle_simple_response(res: *std.http.Server.Response, content: []const u8, status: std.http.Status) !void { res.status = status; res.transfer_encoding = .{ .content_length = content.len }; try res.headers.append("content-type", "text/html"); try res.do(); try res.writer().writeAll(content); try res.finish(); } }; } const Response = std.http.Server.Response; pub const Params = std.StringHashMap([]const u8); /// Routing component for an http server with wildcard matching and parameter /// Handles matching a request to a handler. /// Handler pattern can either be matched exactly /// or it can have matcher segments, so /// "/" -> matches request for "/" only /// "/foo" matches request for "/foo" only /// "/foo/{bar}/baz" matches request for "/foo/123/baz" and "/foo/bar/baz", and Params would contain "bar":"123" and "bar":"bar" respectively. /// or it can have terminating wildcards, so /// "/foo/*" -> matches "/foo", "/foo/bar","/foo/bar/baz" /// "/*" -> matches all requests /// TODO something clever to parse path parameters into the appropriate types, maybe smth like "/foo/{bar:u32}/baz" /// TODO something to handle query parameters and request body too pub fn Router(comptime Context: type, comptime ErrorType: type) type { return struct { pub const Handler = struct { method: std.http.Method, pattern: []const u8, handle_fn: *const fn (res: *Response, ctx: Context, Params) ErrorType!void, }; handlers: []const Handler, notfound: *const fn (res: *Response, ctx: Context) ErrorType!void, pub fn handle(self: @This(), res: *Response, ctx: Context) ErrorType!void { // Routing can only happen after we have the headers // It is a programmer error to call this without calling .wait first. if (res.state != .waited) unreachable; var p = try Path.parse(res.allocator, res.request.target); defer p.deinit(); const path = p.path; handler_loop: for (self.handlers) |handler| { if (handler.method != res.request.method) { continue :handler_loop; } var path_params: Params = std.StringHashMap([]const u8).init(res.allocator); defer path_params.deinit(); var handle_split = std.mem.splitScalar(u8, handler.pattern, '/'); var req_split = std.mem.splitScalar(u8, path, '/'); while (true) { const maybe_handle_seg = handle_split.next(); const maybe_req_seg = req_split.next(); if (maybe_handle_seg == null and maybe_req_seg == null) { // End of both handler and request, they matched this far so // the handler must handle. try handler.handle_fn(res, ctx, path_params); break :handler_loop; } else if (maybe_handle_seg != null and std.mem.eql(u8, maybe_handle_seg.?, "*")) { // Wildcard, this matches try handler.handle_fn(res, ctx, path_params); break :handler_loop; } else if (maybe_handle_seg == null or maybe_req_seg == null) { // path lengths don't match, try the next handler continue :handler_loop; } else { const handle_seg = maybe_handle_seg.?; const req_seg = maybe_req_seg.?; if (handle_seg.len > 0 and handle_seg[0] == '{' and handle_seg[handle_seg.len - 1] == '}') { // Capture and keep going const key = handle_seg[1 .. handle_seg.len - 1]; try path_params.put(key, req_seg); } else if (std.mem.eql(u8, handle_seg, req_seg)) { // segments match, keep going } else { // mismatch, try the next handler continue :handler_loop; } } } } else { try self.notfound(res, ctx); } } }; } const RouterTest = struct { const TestCtx = struct {}; const TestErr = error{ TestError, OutOfMemory } || Path.ParseError; const TestRouter = Router(TestCtx, TestErr); var notfoundinvoked = false; fn notfound(_: *Response, _: TestCtx) TestErr!void { notfoundinvoked = true; } var route1invoked = false; var route1params: ?Params = null; fn route1(_: *Response, _: TestCtx, p: Params) TestErr!void { route1invoked = true; route1params = try p.clone(); } var route2invoked = false; fn route2(_: *Response, _: TestCtx, _: Params) TestErr!void { route2invoked = true; } fn reset() void { notfoundinvoked = false; if (route1params != null) route1params.?.deinit(); route1params = null; route1invoked = false; route2invoked = false; } fn runTestRouter(handlers: []TestRouter.Handler, target: []const u8) !void { const alloc = std.testing.allocator; const ctx = TestCtx{}; var buf: [128]u8 = undefined; const req = std.http.Server.Request{ .method = .GET, .target = target, .version = .@"HTTP/1.1", .headers = std.http.Headers.init(alloc), .parser = std.http.protocol.HeadersParser.initStatic(&buf), }; const sock = try std.net.tcpConnectToAddress(std.net.Address{ .in = std.net.Ip4Address.init(.{ 127, 0, 0, 1 }, 22) }); defer sock.close(); const conn = std.http.Server.Connection{ .stream = sock, .protocol = .plain, }; var res = Response{ .allocator = alloc, .address = std.net.Address{ .in = std.net.Ip4Address.init(.{ 127, 0, 0, 1 }, 8080) }, .connection = conn, .headers = std.http.Headers.init(alloc), .request = req, .state = .waited, }; const router = TestRouter{ .handlers = handlers, .notfound = notfound, }; try router.handle(&res, ctx); } // fn hmof(x: []const u8, y: []const u8) std.StringHashMap([]const u8) { // var hm = std.StringHashMap([]const u8).init(std.testing.allocator); // hm.put(x, y) catch @panic("failed to create hmof in test"); // return hm; // } const TestCase = struct { target: []const u8, route1: ?[]const u8 = null, route2: ?[]const u8 = null, notfoundexpected: bool = false, route1expected: bool = false, route2expected: bool = false, // route1paramsexpected: ?Params = null, }; fn expectEqual(maybe_pexp: ?Params, maybe_pact: ?Params) !void { if (maybe_pexp == null and maybe_pact == null) { // fine } else if (maybe_pexp == null or maybe_pact == null) { std.debug.print("isnull(pexp) = {} isnull(pact) = {}", .{ maybe_pexp == null, maybe_pact == null }); return error.TestUnexpectedResult; } else { const pexp = maybe_pexp.?; const pact = maybe_pact.?; try std.testing.expectEqual(pexp.count(), pact.count()); var it = pexp.keyIterator(); var kexp = it.next(); while (kexp != null) : (kexp = it.next()) { var vexp = pexp.get(kexp.?.*).?; var maybe_vact = pact.get(kexp.?.*); if (maybe_vact) |vact| { std.debug.print("{s} {s}", .{ vexp, vact }); try std.testing.expectEqual(vexp, vact); } else { std.debug.print("expected key {s} not found in actual", .{kexp.?.*}); return error.TestUnexpectedResult; } } } } test "router tests" { // var m0 = std.StringHashMap([]const u8).init(std.testing.allocator); // defer m0.deinit(); // var m1 = hmof("var", "bam"); // defer m1.deinit(); const cases = [_]TestCase{ .{ .target = "/", .notfoundexpected = true, }, .{ .target = "/", .route1 = "/", .route1expected = true, // .route1paramsexpected = m0, }, .{ .target = "/foo", .route1 = "/bar", .notfoundexpected = true, }, .{ .target = "/bar", .route1 = "/foo", .route2 = "/bar", .route2expected = true, }, .{ .target = "/baz", .route1 = "/", .notfoundexpected = true, }, .{ .target = "/baz", .route1 = "/*", .route2 = "/bar", .route1expected = true, // .route1paramsexpected = m0, }, .{ .target = "/baz", .route1 = "/*", .route2 = "/baz", .route1expected = true, // first matching route takes prio // .route1paramsexpected = m0, }, .{ .target = "/baz", .route1 = "/baz", .route2 = "/*", .route1expected = true, // first matching route takes prio // .route1paramsexpected = m0, }, .{ .target = "/baz/bam", .route1 = "/baz/{var}", .route1expected = true, // .route1paramsexpected = m1, }, .{ .target = "/baz/bam/boo", .route1 = "/baz/{var}/boo", .route1expected = true, // .route1paramsexpected = m1, }, .{ .target = "/baz/bam/boo?somequery=foo", .route1 = "/baz/{var}/boo", .route1expected = true, // .route1paramsexpected = m1, }, // .{ // .target = "/baz/bam/bar", // .route1 = "/baz/{var}/boo", // .notfoundexpected = true, // }, }; for (cases) |case| { defer reset(); var handlers = std.ArrayList(TestRouter.Handler).init(std.testing.allocator); defer handlers.deinit(); if (case.route1) |r1| { try handlers.append(TestRouter.Handler{ .pattern = r1, .method = .GET, .handle_fn = route1 }); } if (case.route2) |r2| { try handlers.append(TestRouter.Handler{ .pattern = r2, .method = .GET, .handle_fn = route2 }); } try runTestRouter(handlers.items, case.target); try std.testing.expectEqual(case.notfoundexpected, notfoundinvoked); try std.testing.expectEqual(case.route1expected, route1invoked); try std.testing.expectEqual(case.route2expected, route2invoked); // try expectEqual(case.route1paramsexpected, route1params); // TODO assert captures } } }; /// HTTP path parsing /// which is a subset of URI parsing :) /// RFC-3986 pub const Path = struct { allocator: std.mem.Allocator, path: []const u8, query: []const u8, fragment: []const u8, // technically I think the fragment is never received on the server anyway query_parsed: ?Form = null, pub const ParseError = error{Malformatted} || Form.ParseError; pub fn parse(allocator: std.mem.Allocator, str: []const u8) ParseError!Path { var path: []const u8 = str; var query: []const u8 = ""; var fragment: []const u8 = ""; const f_ix = std.mem.indexOfScalar(u8, str, '#'); const q_ix = std.mem.indexOfScalar(u8, str, '?'); if (q_ix) |q| { path = str[0..q]; if (f_ix) |f| { if (f < q) { return ParseError.Malformatted; } query = str[(q + 1)..f]; fragment = str[(f + 1)..]; } else { query = str[(q + 1)..]; } } else if (f_ix) |f| { path = str[0..f]; fragment = str[(f + 1)..]; } return Path{ .allocator = allocator, .path = path, .query = query, .fragment = fragment, }; } pub fn get_query_param(self: *Path, key: []const u8) !?[]const u8 { if (self.query_parsed == null) { self.query_parsed = try Form.parse(self.allocator, self.query); } return self.query_parsed.?.data.get(key); } pub fn query_to_struct(self: *Path, comptime T: type) !T { if (self.query_parsed == null) { self.query_parsed = try Form.parse(self.allocator, self.query); } return self.query_parsed.?.form_to_struct(T); } pub fn deinit(self: *Path) void { if (self.query_parsed != null) { self.query_parsed.?.deinit(); } } }; const PathTest = struct { test "path" { var p = try Path.parse(std.testing.allocator, "/"); defer p.deinit(); try assertPath("/", "", "", p); } fn assertPath(path: []const u8, query: []const u8, fragment: []const u8, actual: Path) !void { try std.testing.expectEqualSlices(u8, path, actual.path); try std.testing.expectEqualSlices(u8, query, actual.query); try std.testing.expectEqualSlices(u8, fragment, actual.fragment); } test "query" { var p = try Path.parse(std.testing.allocator, "/foo?bar=baz"); defer p.deinit(); try assertPath("/foo", "bar=baz", "", p); } test "query and fragment" { var p = try Path.parse(std.testing.allocator, "/foo?bar=baz#frag"); defer p.deinit(); try assertPath("/foo", "bar=baz", "frag", p); } test "fragment" { var p = try Path.parse(std.testing.allocator, "/foo#frag"); defer p.deinit(); try assertPath("/foo", "", "frag", p); } test "query param" { var p = try Path.parse(std.testing.allocator, "/foo?bar=baz#frag"); defer p.deinit(); const v1 = try p.get_query_param("bar"); try std.testing.expect(v1 != null); try std.testing.expectEqualSlices(u8, "baz", v1.?); const v2 = try p.get_query_param("bam"); try std.testing.expect(v2 == null); } test "query param mixed" { var p = try Path.parse(std.testing.allocator, "/foo?bar=baz&ba+m=bo+om&zigzag#frag"); defer p.deinit(); try assertPath("/foo", "bar=baz&ba+m=bo+om&zigzag", "frag", p); const v1 = try p.get_query_param("bar"); try std.testing.expect(v1 != null); try std.testing.expectEqualSlices(u8, "baz", v1.?); const v2 = try p.get_query_param("ba m"); try std.testing.expect(v2 != null); try std.testing.expectEqualSlices(u8, "bo om", v2.?); } test "query to struct" { var p = try Path.parse(std.testing.allocator, "/foo?bar=ba+z&bam=55&zigzag#frag"); defer p.deinit(); const T = struct { bar: []const u8, bam: u64, fn deinit(self: *@This()) void { std.testing.allocator.free(self.bar); } }; var t = try p.query_to_struct(T); defer t.deinit(); try std.testing.expectEqualDeep(T{ .bar = "ba z", .bam = 55 }, t); } }; pub const Form = struct { allocator: std.mem.Allocator, data: std.StringHashMap([]const u8), const ParseError = error{ Malformatted, InvalidLength, InvalidCharacter, NoSpaceLeft } || std.mem.Allocator.Error; // Tries to parse key=value&key2=value2 pairs from the form. // Note that a URL query segment doesn't _have_ to be key-value pairs // so this is quite lenient. // Form struct owns all the keys and values in the resulting map. pub fn parse(allocator: std.mem.Allocator, form: []const u8) ParseError!Form { var res = std.StringHashMap([]const u8).init(allocator); var iter1 = std.mem.splitScalar(u8, form, '&'); while (iter1.next()) |split| { var iter2 = std.mem.splitScalar(u8, split, '='); if (iter2.next()) |key| { if (iter2.next()) |value| { try res.put(try percent_decode(allocator, key), try percent_decode(allocator, value)); } else { // Do nothing, it's a well-formatted kv pair } } else { // Do nothing it's not a well-formatted kv pair } } return Form{ .allocator = allocator, .data = res }; } pub fn form_to_struct(self: *Form, comptime T: type) !T { return to_struct(self.allocator, T, self.data); } pub fn deinit(self: *Form) void { var it = self.data.iterator(); var e = it.next(); while (e != null) : (e = it.next()) { self.allocator.free(e.?.key_ptr.*); self.allocator.free(e.?.value_ptr.*); } self.data.deinit(); } }; fn percent_decode(allocator: std.mem.Allocator, str: []const u8) ![]const u8 { var fbs = std.io.fixedBufferStream(str); var rdr = fbs.reader(); var out = std.ArrayList(u8).init(allocator); var wtr = out.writer(); defer out.deinit(); while (true) { const b = rdr.readByte() catch break; if (b == '%') { var hex_code: [2]u8 = undefined; _ = try rdr.readAll(&hex_code); var b2: [1]u8 = .{0}; _ = try std.fmt.hexToBytes(&b2, &hex_code); try wtr.writeByte(b2[0]); } else if (b == '+') { try wtr.writeByte(' '); } else { try wtr.writeByte(b); } } return out.toOwnedSlice(); } const PercentEncodeTest = struct { test "decode" { const decoded = try percent_decode(std.testing.allocator, "%C3%A7%C3%AE%C4%85%C3%B5+hithere"); defer std.testing.allocator.free(decoded); try std.testing.expectEqualStrings("çîąõ hithere", decoded); } }; /// Populate a struct from a hashmap fn to_struct(allocator: std.mem.Allocator, comptime T: type, hm: std.StringHashMap([]const u8)) !T { const ti = @typeInfo(T); if (ti != .Struct) { @compileError("to_struct T was not a struct type"); } var t: T = undefined; inline for (ti.Struct.fields) |field| { if (field.is_comptime) { @compileError("can't dynamically set comptime field " ++ field.name); } const value: []const u8 = hm.get(field.name) orelse { return error.FieldNotPresent; // TODO somehow be more useful. }; switch (@typeInfo(field.type)) { // TODO handle more types, default values etc etc. .Int => { @field(t, field.name) = try std.fmt.parseInt(field.type, value, 10); }, .Pointer => |ptrinfo| { if (ptrinfo.size != .Slice) { @compileError("field pointer size " ++ @tagName(ptrinfo.size) ++ " is not supported, only []u8 is supported right now"); } if (ptrinfo.child != u8) { @compileError("field pointer type " ++ @tagName(@typeInfo(ptrinfo.child)) ++ " is not supported, only []u8 is supported right now"); } const dvalue = try allocator.dupe(u8, value); errdefer allocator.free(dvalue); @field(t, field.name) = dvalue; }, else => @compileError("field type " ++ @tagName(@typeInfo(field.type)) ++ " not supported on field " ++ field.name), } } return t; } const StructTest = struct { test "to struct" { const T = struct { foo: i64, bar: []const u8, pub fn deinit(self: *@This()) void { std.testing.allocator.free(self.bar); } }; var hm = std.StringHashMap([]const u8).init(std.testing.allocator); defer hm.deinit(); try hm.put("foo", "42"); try hm.put("bar", "oops"); var t = try to_struct(std.testing.allocator, T, hm); defer t.deinit(); try std.testing.expectEqualDeep(T{ .foo = 42, .bar = "oops" }, t); } }; test { _ = RouterTest; _ = PathTest; _ = PercentEncodeTest; _ = StructTest; }