aboutsummaryrefslogtreecommitdiff
path: root/src/main.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.zig')
-rw-r--r--src/main.zig188
1 files changed, 149 insertions, 39 deletions
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});
+ },
+ }
+}