summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMartin Ashby <martin@ashbysoft.com>2023-08-06 06:59:44 +0100
committerMartin Ashby <martin@ashbysoft.com>2023-08-06 06:59:44 +0100
commit95699a6ed77e1480e7b9256035225981cb33bfbb (patch)
tree4ac270c06a6e675761731ff65943a39072ff8d52 /src
parent292e1bf1692091fa218ced45a8b49fc799a2759a (diff)
downloadzigwebserver-95699a6ed77e1480e7b9256035225981cb33bfbb.tar.gz
zigwebserver-95699a6ed77e1480e7b9256035225981cb33bfbb.tar.bz2
zigwebserver-95699a6ed77e1480e7b9256035225981cb33bfbb.tar.xz
zigwebserver-95699a6ed77e1480e7b9256035225981cb33bfbb.zip
Add path parsing, use it
Diffstat (limited to 'src')
-rw-r--r--src/main.zig89
-rw-r--r--src/zigwebserver.zig103
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;
}