aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMartin Ashby <martin@ashbysoft.com>2024-02-18 20:28:55 +0000
committerMartin Ashby <martin@ashbysoft.com>2024-02-18 20:28:55 +0000
commit0284553f75fd927091dc35c502e37df856af05b6 (patch)
tree9a1e39957d7d07a0adc19fddfabbb983d4d00866
parent3395a562c419c60d046cdc295a88b759d4bc87fd (diff)
downloadmfashby.net-0284553f75fd927091dc35c502e37df856af05b6.tar.gz
mfashby.net-0284553f75fd927091dc35c502e37df856af05b6.tar.bz2
mfashby.net-0284553f75fd927091dc35c502e37df856af05b6.tar.xz
mfashby.net-0284553f75fd927091dc35c502e37df856af05b6.zip
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
-rw-r--r--.gitignore2
-rw-r--r--comments/src/main.zig31
-rw-r--r--src/main.zig188
-rwxr-xr-xzig-out/bin/serverbin3154008 -> 0 bytes
4 files changed, 172 insertions, 49 deletions
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
--- a/zig-out/bin/server
+++ /dev/null
Binary files differ