From 0284553f75fd927091dc35c502e37df856af05b6 Mon Sep 17 00:00:00 2001 From: Martin Ashby Date: Sun, 18 Feb 2024 20:28:55 +0000 Subject: Add CGI handling to my basic server Fix some missing bits from the comments CGI program, specifically - respect content_length from the web server - write the http status response as a header in the result --- .gitignore | 2 +- comments/src/main.zig | 31 ++++++--- src/main.zig | 188 +++++++++++++++++++++++++++++++++++++++----------- zig-out/bin/server | Bin 3154008 -> 0 bytes 4 files changed, 172 insertions(+), 49 deletions(-) delete mode 100755 zig-out/bin/server diff --git a/.gitignore b/.gitignore index 4f1f83e..e4fec02 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ public/ .env zig-cache/ -zig-build/ +zig-out/ diff --git a/comments/src/main.zig b/comments/src/main.zig index ac8193e..4044ae7 100644 --- a/comments/src/main.zig +++ b/comments/src/main.zig @@ -35,6 +35,8 @@ const Err = error{ SystemResources, Unexpected, WouldBlock, + Overflow, + EndOfStream, }; const Ctx = struct { db: pq.Db, @@ -80,13 +82,19 @@ const Response = struct { return std.io.getStdIn().reader(); } fn do(self: @This()) !void { + const wtr = std.io.getStdOut().writer(); + if (self.status.phrase()) |phrase| { + try std.fmt.format(wtr, "status: {} {s}\r\n", .{@intFromEnum(self.status), phrase}); + } else { + try std.fmt.format(wtr, "status: {}\r\n", .{@intFromEnum(self.status)}); + } for (self.headers._internal.items) |tup| { - try std.io.getStdOut().writeAll(tup.key); - try std.io.getStdOut().writeAll(": "); - try std.io.getStdOut().writeAll(tup.val); - try std.io.getStdOut().writeAll("\r\n"); + try wtr.writeAll(tup.key); + try wtr.writeAll(": "); + try wtr.writeAll(tup.val); + try wtr.writeAll("\r\n"); } - try std.io.getStdOut().writeAll("\r\n"); + try wtr.writeAll("\r\n"); } fn writer(_: @This()) std.fs.File.Writer { return std.io.getStdOut().writer(); @@ -136,7 +144,7 @@ pub fn main() !void { var res = Response{ .allocator = allocator, .request = req, - .status = .bad_request, + .status = .ok, .transfer_encoding = .none, .headers = Headers.init(allocator), }; @@ -203,9 +211,12 @@ fn get_comment(res: *Response, ctx: Ctx, _: Params) Err!void { } fn post_comment(res: *Response, ctx: Ctx, _: Params) Err!void { - var body_aa = std.ArrayList(u8).init(res.allocator); - try res.reader().readAllArrayList(&body_aa, 1_000_000); - const body = try body_aa.toOwnedSlice(); + const cl = if (std.os.getenv("CONTENT_LENGTH")) |clh| try std.fmt.parseInt(usize, clh, 10) else { + return error.InvalidLength; + }; + const body = try res.allocator.alloc(u8, cl); + defer res.allocator.free(body); + try res.reader().readNoEof(body); var form = try zws.Form.parse(res.allocator, body); const Form = struct { url: []const u8, @@ -215,6 +226,7 @@ fn post_comment(res: *Response, ctx: Ctx, _: Params) Err!void { capcha_answer: []const u8, }; const form_val = form.form_to_struct(Form) catch { + std.log.err("couldn't parse Form", .{}); try badrequest(res, ctx); return; }; @@ -231,6 +243,7 @@ fn post_comment(res: *Response, ctx: Ctx, _: Params) Err!void { } const ans = try stmt.read_column(0, []const u8); if (!std.mem.eql(u8, ans, form_val.capcha_answer)) { + std.log.err("bad capcha answer {s} expected {s}", .{form_val.capcha_answer, ans}); try constresponse(res, @embedFile("templates/capchainvalid.html"), std.http.Status.unauthorized); return; } diff --git a/src/main.zig b/src/main.zig index 86809aa..96dafc9 100644 --- a/src/main.zig +++ b/src/main.zig @@ -6,9 +6,9 @@ const log = std.log.scoped(.server); // Initially it replaces caddy, and it needs to be capable of these things // to actually be usable // - http(s) (otherwise it's not a web server...) ✅ https isn't supported because zig http server (in fact there's no TLS server implementation). For now I'll have to use haproxy -// - routing, including virtual host ❌ +// - routing, including virtual host ✅ // - serving static content from selected folders ✅ -// - executing CGI programs ❌ +// - executing CGI programs ✅ // - reverse proxy ❌ // And I should probably test it thoroughly before exposing is to the 'net @@ -48,7 +48,7 @@ pub fn main() !void { fn handle(res: *std.http.Server.Response) void { defer res.allocator.destroy(res); defer res.deinit(); - handleErr(res) catch |e| { + handleRoute(res) catch |e| { log.info("error {}", .{e}); if (@errorReturnTrace()) |trace| { std.debug.dumpStackTrace(trace.*); @@ -62,29 +62,7 @@ fn handle(res: *std.http.Server.Response) void { }; } -fn sendError(res: *std.http.Server.Response, e: anyerror) !void { - switch (res.state) { - .first, .start, .waited => { - if (res.state != .waited) { - try res.wait(); - } - const errmsg = try std.fmt.allocPrint(res.allocator, "Error: {}", .{e}); - defer res.allocator.free(errmsg); - // Now send an error - res.status = .internal_server_error; - res.transfer_encoding = .{ .content_length = errmsg.len }; - try res.send(); - try res.writeAll(errmsg); - try res.finish(); - }, - .responded, .finished => { - // Too late! - log.err("can't send an error, response already sent, state {}", .{res.state}); - }, - } -} - -fn handleErr(res: *std.http.Server.Response) !void { +fn handleRoute(res: *std.http.Server.Response) !void { try res.wait(); // Route, virtual host first @@ -94,20 +72,117 @@ fn handleErr(res: *std.http.Server.Response) !void { host = spl.first(); } - // var target = res.request.target; + const uri = std.Uri.parseWithoutScheme(res.request.target) catch { + res.status = .bad_request; + const msg = "bad request target"; + res.transfer_encoding = .{ .content_length = msg.len }; + try res.send(); + try res.writeAll(msg); + try res.finish(); + return; + }; - // if (std.mem.eql(u8, host, "mfashby.net")) { - // if () + if (std.mem.eql(u8, host, "localhost")) { + if (std.mem.startsWith(u8, uri.path, "/api")) { + try serveCgi(res, "/home/martin/dev/mfashby.net/comments/zig-out/bin/comments", + &.{"DATABASE_URL","SMTP_USERNAME","SMTP_PASSWORD","NOTIFICATION_ADDRESS","SMTP_SERVER"}); + } else { try serveStatic(res, "public"); - // } else { - // // Fallback site - // const ans = "You have reached mfashby.net ... but did you mean to?"; - // res.status = .ok; - // res.transfer_encoding = .{ .content_length = ans.len }; - // try res.send(); - // try res.writeAll(ans); - // try res.finish(); - // } + } + } else { + const ans = "You have reached mfashby.net ... but did you mean to?"; + res.status = .ok; + res.transfer_encoding = .{ .content_length = ans.len }; + try res.send(); + try res.writeAll(ans); + try res.finish(); + } +} + +fn serveCgi(res: *std.http.Server.Response, executable: []const u8, + comptime env_copy: []const []const u8) !void { + var child = std.ChildProcess.init(&.{executable}, res.allocator); + child.stdin_behavior = .Pipe; + child.stdout_behavior = .Pipe; + child.stderr_behavior = .Pipe; + + var env = std.process.EnvMap.init(res.allocator); + try env.put("REQUEST_METHOD", @tagName(res.request.method)); + try env.put("REQUEST_URI", res.request.target); + inline for (env_copy) |key| { + try env.put(key, std.os.getenv(key) orelse ""); + } + var clb = [_]u8{0}**30; + if (res.request.method == .POST) { + if (res.request.content_length) |cl| { + try env.put("CONTENT_LENGTH", try std.fmt.bufPrint(&clb, "{}", .{cl})); + } + } + child.env_map = &env; + try child.spawn(); + + if (res.request.method == .POST) { + if (res.request.content_length) |cl| { + log.info("sending {} data as CGI body", .{cl}); + try pump(res.reader(), child.stdin.?.writer(), cl); + } else { + _ = try pumpUnknown(res.reader(), child.stdin.?.writer()); + } + } + var stdout = std.ArrayList(u8).init(res.allocator); + var stderr = std.ArrayList(u8).init(res.allocator); + defer stdout.deinit(); + defer stderr.deinit(); + defer { + if (stderr.items.len>0) { + log.err("CGI error: {s}", .{stderr.items}); + } + } + try child.collectOutput(&stdout, &stderr, 1_000_000); + const term = try child.wait(); + if (term.Exited != 0) { + return error.ProcessError; + } + + var fbs = std.io.fixedBufferStream(stdout.items); + var reader = fbs.reader(); + var headerLine = std.ArrayList(u8).init(res.allocator); + defer headerLine.deinit(); + while (true) { + headerLine.clearRetainingCapacity(); + try reader.streamUntilDelimiter(headerLine.writer(), '\r', 8192); + _ = try reader.skipBytes(1, .{}); // \n + if (headerLine.items.len == 0) { + break; + } + var spl = std.mem.splitScalar(u8, headerLine.items, ':'); + const key = try std.ascii.allocLowerString(res.allocator, spl.first()); + defer res.allocator.free(key); + if (std.mem.eql(u8, key, "status")) { + const value = spl.rest(); + var spl2 = std.mem.splitScalar(u8, std.mem.trim(u8, value, " "), ' '); + res.status = @enumFromInt(try std.fmt.parseInt(u16, spl2.first(), 10)); + log.info("status from CGI {}", .{res.status}); + } else if (std.mem.eql(u8, key, "content-length")) { + const value = spl.rest(); + res.transfer_encoding = .{.content_length = try std.fmt.parseInt(usize, value, 10)}; + log.info("transfer_encoding from CGI {}", .{res.transfer_encoding}); + } else { + const value = spl.rest(); + try res.headers.append(key, std.mem.trim(u8, value, " ")); + log.info("header from CGI {s}: {s}", .{key, value}); + } + } + + if (res.transfer_encoding == .content_length) { + try res.send(); + try pump(reader, res.writer(), res.transfer_encoding.content_length); + } else { + res.transfer_encoding = .chunked; + try res.send(); + _ = try pumpUnknown(reader, res.writer()); + } + try res.finish(); } fn serveStatic(res: *std.http.Server.Response, dirname: []const u8) !void { @@ -216,6 +291,18 @@ fn serveStatic(res: *std.http.Server.Response, dirname: []const u8) !void { } +fn pumpUnknown(reader: anytype, writer: anytype) !usize { + var read: usize = 0; + var buf: [1024]u8 = undefined; + while (true) { + const sz = try reader.read(&buf); + if (sz == 0) break; + read += sz; + try writer.writeAll(buf[0..sz]); + } + return read; +} + fn pump(reader: anytype, writer: anytype, expected: usize) !void { var read: usize = 0; var buf: [1024]u8 = undefined; @@ -229,4 +316,27 @@ fn pump(reader: anytype, writer: anytype, expected: usize) !void { if (read != expected) { return error.NotEnoughData; } -} \ No newline at end of file +} + + +fn sendError(res: *std.http.Server.Response, e: anyerror) !void { + switch (res.state) { + .first, .start, .waited => { + if (res.state != .waited) { + try res.wait(); + } + const errmsg = try std.fmt.allocPrint(res.allocator, "Error: {}", .{e}); + defer res.allocator.free(errmsg); + // Now send an error + res.status = .internal_server_error; + res.transfer_encoding = .{ .content_length = errmsg.len }; + try res.send(); + try res.writeAll(errmsg); + try res.finish(); + }, + .responded, .finished => { + // Too late! + log.err("can't send an error, response already sent, state {}", .{res.state}); + }, + } +} diff --git a/zig-out/bin/server b/zig-out/bin/server deleted file mode 100755 index 019371d..0000000 Binary files a/zig-out/bin/server and /dev/null differ -- cgit v1.2.3-ZIG