diff options
-rw-r--r-- | src/main.zig | 89 | ||||
-rw-r--r-- | src/zigwebserver.zig | 103 |
2 files changed, 145 insertions, 47 deletions
diff --git a/src/main.zig b/src/main.zig index 54e6b4b..1f4d8f9 100644 --- a/src/main.zig +++ b/src/main.zig @@ -2,65 +2,72 @@ const std = @import("std"); const zws = @import("zigwebserver.zig"); // extremely basic http file server -pub fn main() !void { - var allocator = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = allocator.deinit(); - const alloc = allocator.allocator(); - var svr = std.http.Server.init(alloc, .{ .reuse_address = true }); - defer svr.deinit(); - const addr = try std.net.Address.resolveIp("127.0.0.1", 8080); - - try svr.listen(addr); - while (true) { - var res = try svr.accept(.{ .allocator = alloc }); - defer res.deinit(); - try res.wait(); - const target = res.request.target; - const path = try std.fs.path.join(alloc, &[_][]const u8{ ".", target }); - defer alloc.free(path); +const Context = struct { + pub fn clone(_: Context) Context { + return .{}; + } + pub fn deinit(_: Context) void {} +}; +const Handler = struct { + pub fn handle(_: Handler, res: *std.http.Server.Response, _: Context) !void { + const p = try zws.Path.parse(res.request.target); + const path = try std.fs.path.join(res.allocator, &[_][]const u8{ ".", p.path }); + defer res.allocator.free(path); if (std.fs.cwd().openFile(path, .{})) |file| { const md = try file.metadata(); if (md.kind() == .directory) { - const index_path = try std.fs.path.join(alloc, &[_][]const u8{ path, "index.html" }); - defer alloc.free(index_path); + const index_path = try std.fs.path.join(res.allocator, &[_][]const u8{ path, "index.html" }); + defer res.allocator.free(index_path); if (std.fs.cwd().openFile(index_path, .{})) |index_file| { const index_md = try index_file.metadata(); - try serve_file(&res, index_file, index_md); + try serve_file(res, index_file, index_md); } else |_| { - try serve_error(&res, .not_found); + try serve_error(res, .not_found); } } else { - try serve_file(&res, file, md); + try serve_file(res, file, md); } } else |err| { switch (err) { - error.FileNotFound => try serve_error(&res, .not_found), - else => try serve_error(&res, .bad_request), + error.FileNotFound => try serve_error(res, .not_found), + else => try serve_error(res, .bad_request), } } try res.finish(); } -} -fn serve_file(res: *std.http.Server.Response, file: std.fs.File, md: std.fs.File.Metadata) !void { - res.transfer_encoding = .{ .content_length = md.size() }; - try res.do(); - var buf = [_]u8{0} ** 1024; - while (true) { - const read = try file.read(&buf); - if (read == 0) break; - _ = try res.write(buf[0..read]); + fn serve_file(res: *std.http.Server.Response, file: std.fs.File, md: std.fs.File.Metadata) !void { + res.transfer_encoding = .{ .content_length = md.size() }; + try res.do(); + var buf = [_]u8{0} ** 1024; + while (true) { + const read = try file.read(&buf); + if (read == 0) break; + _ = try res.write(buf[0..read]); + } } -} -fn serve_error(res: *std.http.Server.Response, status: std.http.Status) !void { - res.status = status; - res.transfer_encoding = .chunked; - try res.do(); - const phrase = status.phrase() orelse "error!"; - try std.fmt.format(res.writer(), - \\ <!doctype html><html><body>{s}</body></html> - , .{phrase}); + fn serve_error(res: *std.http.Server.Response, status: std.http.Status) !void { + res.status = status; + res.transfer_encoding = .chunked; + try res.do(); + const phrase = status.phrase() orelse "error!"; + try std.fmt.format(res.writer(), + \\ <!doctype html><html><body>{s}</body></html> + , .{phrase}); + } +}; +const Server = zws.Server(Context, Handler); +var allocator = std.heap.GeneralPurposeAllocator(.{}){}; +var svr = Server{ + .allocator = allocator.allocator(), + .address = std.net.Address{ .in = std.net.Ip4Address.init(.{ 127, 0, 0, 1 }, 8080) }, + .context = Context{}, + .handler = Handler{}, +}; + +pub fn main() !void { + try svr.serve(); } test { diff --git a/src/zigwebserver.zig b/src/zigwebserver.zig index 6ce4439..37a43a4 100644 --- a/src/zigwebserver.zig +++ b/src/zigwebserver.zig @@ -37,9 +37,9 @@ pub fn Server(comptime Context: type, comptime Handler: type) type { defer res.deinit(); if (res.wait()) { if (self.handler.handle(res, ctx)) { - std.log.info("Success handling request for {}", .{res.address}); + 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.err("Error handling request for {} : {}", .{ res.address, 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, "<html><body>Server error!</body></html>", .internal_server_error)) {} else |err2| { std.log.err("Error sending error page for {} : {}", .{ res.address, err2 }); } @@ -97,6 +97,9 @@ pub fn Router(comptime Context: type, comptime ErrorType: type) type { // It is a programmer error to call this without calling .wait first. if (res.state != .waited) unreachable; + const p = try Path.parse(res.request.target); + const path = p.path; + handler_loop: for (self.handlers) |handler| { if (handler.method != res.request.method) { continue :handler_loop; @@ -106,7 +109,7 @@ pub fn Router(comptime Context: type, comptime ErrorType: type) type { defer path_params.deinit(); var handle_split = std.mem.splitScalar(u8, handler.pattern, '/'); - var req_split = std.mem.splitScalar(u8, res.request.target, '/'); + var req_split = std.mem.splitScalar(u8, path, '/'); while (true) { const maybe_handle_seg = handle_split.next(); @@ -145,9 +148,9 @@ pub fn Router(comptime Context: type, comptime ErrorType: type) type { }; } -const T = struct { +const RouterTest = struct { const TestCtx = struct {}; - const TestErr = error{ TestError, OutOfMemory }; + const TestErr = error{ TestError, OutOfMemory } || Path.ParseError; const TestRouter = Router(TestCtx, TestErr); var notfoundinvoked = false; @@ -311,6 +314,12 @@ const T = struct { .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", @@ -337,6 +346,88 @@ const T = struct { } }; +/// HTTP path parsing +/// which is a subset of URI parsing :) +pub const Path = struct { + path: []const u8, + query: []const u8, + fragment: []const u8, // technically I think the fragment is never received on the server anyway + + pub const ParseError = error{Malformatted}; + + pub fn parse(path: []const u8) ParseError!Path { + var p = Path{ + .path = path, + .query = "", + .fragment = "", + }; + const q_ix = std.mem.indexOfScalar(u8, path, '?'); + const f_ix = std.mem.indexOfScalar(u8, path, '#'); + if (q_ix) |q| { + p.path = path[0..q]; + if (f_ix) |f| { + if (f < q) { + return ParseError.Malformatted; + } + p.query = path[(q + 1)..f]; + p.fragment = path[(f + 1)..]; + } else { + p.query = path[(q + 1)..]; + } + } else if (f_ix) |f| { + p.path = path[0..f]; + p.fragment = path[(f + 1)..]; + } + return p; + } + + pub fn get_query_param(self: Path, key: []const u8) ?[]const u8 { + var it1 = std.mem.splitScalar(u8, self.query, '&'); + var t: ?[]const u8 = it1.first(); + while (t != null) : (t = it1.next()) { + var it2 = std.mem.splitScalar(u8, t.?, '='); + const k = it2.first(); + const v = it2.next(); + if (std.mem.eql(u8, key, k)) { + return v; + } + } + return null; + } +}; + +const PathTest = struct { + test "path" { + const p = try Path.parse("/"); + try std.testing.expectEqualDeep(Path{ .path = "/", .query = "", .fragment = "" }, p); + } + + test "query" { + const p = try Path.parse("/foo?bar=baz"); + try std.testing.expectEqualDeep(Path{ .path = "/foo", .query = "bar=baz", .fragment = "" }, p); + } + + test "query and fragment" { + const p = try Path.parse("/foo?bar=baz#frag"); + try std.testing.expectEqualDeep(Path{ .path = "/foo", .query = "bar=baz", .fragment = "frag" }, p); + } + + test "fragment" { + const p = try Path.parse("/foo#frag"); + try std.testing.expectEqualDeep(Path{ .path = "/foo", .query = "", .fragment = "frag" }, p); + } + + test "query param" { + const p = try Path.parse("/foo?bar=baz#frag"); + const v1 = p.get_query_param("bar"); + try std.testing.expect(v1 != null); + try std.testing.expectEqualSlices(u8, "baz", v1.?); + const v2 = p.get_query_param("bam"); + try std.testing.expect(v2 == null); + } +}; + test { - _ = T; + _ = RouterTest; + _ = PathTest; } |