aboutsummaryrefslogtreecommitdiff
path: root/src/main.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.zig')
-rw-r--r--src/main.zig431
1 files changed, 0 insertions, 431 deletions
diff --git a/src/main.zig b/src/main.zig
deleted file mode 100644
index f9f204b..0000000
--- a/src/main.zig
+++ /dev/null
@@ -1,431 +0,0 @@
-const std = @import("std");
-const log = std.log.scoped(.server);
-
-// Web server for mfashby.net
-// It does _not_ follow the unix philosophy (:
-// 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 ✅
-// - serving static content from selected folders ✅
-// - executing CGI programs ✅
-// - reverse proxy ❌
-
-// And I should probably test it thoroughly before exposing is to the 'net
-
-// Future possibilities:
-// - subsume some CGI programs directly into the web server e.g. my comments program
-// - and maybe even cgit (although I might scrap it in favour of stagit if I can figure out archive downloads
-// - do something about efficiency :) it's thread-per-request right now
-pub fn main() !void {
- var gpa = std.heap.GeneralPurposeAllocator(.{ .thread_safe = true }){};
- defer _ = gpa.deinit();
- const a = gpa.allocator();
- var pool: std.Thread.Pool = undefined;
- try std.Thread.Pool.init(&pool, .{ .allocator = a, .n_jobs = 32 });
- defer pool.deinit();
- var svr = std.http.Server.init(.{ .reuse_address = true, .reuse_port = true });
- defer svr.deinit();
- const port = 8081;
- const addr = std.net.Address.initIp4(.{0,0,0,0}, port);
- try svr.listen(addr);
- log.info("listening on {}", .{addr});
- try acceptLoop(a, &svr, &pool, struct{
- fn cancelled() bool {
- return false;
- }
- });
-}
-
-fn acceptLoop(a: std.mem.Allocator, svr: *std.http.Server, pool: *std.Thread.Pool, cancel: anytype) !void {
- while (!cancel.cancelled()) {
- // Create the Response into the heap so that the ownership goes to the handling thread
- const res = try a.create(std.http.Server.Response);
- errdefer a.destroy(res);
- res.* = try svr.accept(.{ .allocator = a });
- errdefer res.deinit();
- if (cancel.cancelled()) {
- res.deinit();
- a.destroy(res);
- break;
- }
- try pool.spawn(handle, .{res});
- }
-}
-
-fn handle(res: *std.http.Server.Response) void {
- defer res.allocator.destroy(res);
- defer res.deinit();
- handleRoute(res) catch |e| {
- log.info("error {}", .{e});
- if (@errorReturnTrace()) |trace| {
- std.debug.dumpStackTrace(trace.*);
- }
- sendError(res, e) catch |e2| {
- log.info("error sending error {}", .{e2});
- if (@errorReturnTrace()) |trace| {
- std.debug.dumpStackTrace(trace.*);
- }
- };
- };
-}
-
-fn handleRoute(res: *std.http.Server.Response) !void {
- try res.wait();
-
- // Route, virtual host first
- var host: []const u8 = "";
- if (res.request.headers.getFirstValue("Host")) |host_header| {
- var spl = std.mem.splitScalar(u8, host_header, ':');
- host = spl.first();
- }
-
- 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, "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 {
- 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 {
- const dirpath = try std.fs.realpathAlloc(res.allocator, dirname);
- defer res.allocator.free(dirpath);
-
- // Path massaging
- 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;
- };
- var requested_path = uri.path;
- requested_path = try std.fs.path.join(res.allocator, &.{ dirpath, requested_path });
- defer res.allocator.free(requested_path);
-
-
- const path = std.fs.realpathAlloc(res.allocator, requested_path) catch |e| {
- res.status = switch (e) {
- error.FileNotFound => .not_found,
- error.AccessDenied => .forbidden,
- error.BadPathName => .bad_request,
- else => .internal_server_error,
- };
- const msg = try std.fmt.allocPrint(res.allocator, "error: {}", .{e});
- defer res.allocator.free(msg);
- res.transfer_encoding = .{ .content_length = msg.len };
- try res.send();
- try res.writeAll(msg);
- try res.finish();
- return;
- };
-
- defer res.allocator.free(path);
- if (!std.mem.startsWith(u8, path, dirpath)) {
- res.status = .bad_request;
- const msg = try std.fmt.allocPrint(res.allocator, "Trying to escape the root directory {s}", .{path});
- defer res.allocator.free(msg);
- res.transfer_encoding = .{ .content_length = msg.len };
- try res.send();
- try res.writeAll(msg);
- try res.finish();
- return;
- }
- const f = std.fs.openFileAbsolute(path, .{}) catch |e| {
- res.status = switch (e) {
- error.FileNotFound => .not_found,
- error.AccessDenied => .forbidden,
- error.BadPathName, error.NameTooLong => .bad_request,
- else => .internal_server_error,
- };
- const msg = try std.fmt.allocPrint(res.allocator, "error: {}", .{e});
- defer res.allocator.free(msg);
- res.transfer_encoding = .{ .content_length = msg.len };
- try res.send();
- try res.writeAll(msg);
- try res.finish();
- return;
- };
- defer f.close();
- const stat = try f.stat();
- switch (stat.kind) {
- .file => {
- res.transfer_encoding = .{ .content_length = stat.size };
- try res.send();
- try pump(f.reader(), res.writer(), stat.size);
- try res.finish();
- },
- .directory => {
- const index_path = try std.fs.path.join(res.allocator, &.{path, "index.html"});
- defer res.allocator.free(index_path);
- const index_f = std.fs.openFileAbsolute(index_path, .{}) catch |e| {
- res.status = switch (e) {
- error.FileNotFound => .not_found,
- error.AccessDenied => .forbidden,
- error.BadPathName, error.NameTooLong => .bad_request,
- else => .internal_server_error,
- };
- const msg = try std.fmt.allocPrint(res.allocator, "error: {}", .{e});
- defer res.allocator.free(msg);
- res.transfer_encoding = .{ .content_length = msg.len };
- try res.send();
- try res.writeAll(msg);
- try res.finish();
- return;
- };
- defer index_f.close();
- const index_stat = try index_f.stat();
- res.transfer_encoding = .{ .content_length = index_stat.size };
- try res.send();
- try pump(index_f.reader(), res.writer(), index_stat.size);
- try res.finish();
- },
- else => {
- const msg = "unable to serve unsupported file kind";
- res.status = .unavailable_for_legal_reasons;
- res.transfer_encoding = .{.content_length = msg.len};
- try res.send();
- try res.writeAll(msg);
- try res.finish();
- }
- }
-
-}
-
-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;
- while (true) {
- const sz = try reader.read(&buf);
- if (sz == 0) break;
- read += sz;
- if (read > expected) return error.TooMuchData;
- try writer.writeAll(buf[0..sz]);
- }
- if (read != expected) {
- return error.NotEnoughData;
- }
-}
-
-
-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});
- },
- }
-}
-
-const Fixture = struct {
- a: std.mem.Allocator,
- port: u16,
- t: std.Thread,
- cancel: std.atomic.Value(bool),
- base_url: []const u8,
- thread_pool: std.Thread.Pool,
- server: std.http.Server,
- client: std.http.Client,
-
- fn init(a: std.mem.Allocator) !*Fixture {
- var res: *Fixture = try a.create(Fixture);
- errdefer a.destroy(res);
- res.a = a;
- try std.Thread.Pool.init(&res.thread_pool, .{ .allocator = a, .n_jobs = 32 });
- errdefer res.thread_pool.deinit();
- res.server = std.http.Server.init(.{ .reuse_address = true, .reuse_port = true });
- errdefer res.server.deinit();
- const addr = std.net.Address.initIp4(.{127,0,0,1}, 0);
- try res.server.listen(addr);
- res.port = res.server.socket.listen_address.in.getPort();
- res.base_url = try std.fmt.allocPrint(a, "http://localhost:{}", .{res.port});
- errdefer a.free(res.base_url);
- res.cancel = std.atomic.Value(bool).init(false);
- res.t = try std.Thread.spawn(.{}, acceptLoop, .{a, &res.server, &res.thread_pool, res});
- res.client = .{.allocator = a};
- return res;
- }
-
- fn deinit(self: *Fixture) void {
- self.client.deinit();
- self.a.free(self.base_url);
- self.cancel.store(true, .Unordered);
- // Trigger the server's accept() method to wake it up
- if (std.net.tcpConnectToAddress(std.net.Address.initIp4(.{127,0,0,1}, self.port))) |stream| {
- stream.close();
- } else |_| {}
- self.t.join();
- self.thread_pool.deinit();
- self.server.deinit();
- self.a.destroy(self);
- }
-
- fn cancelled(self: *Fixture) bool {
- return self.cancel.load(.Unordered);
- }
-};
-
-
-test "static pages" {
- const a = std.testing.allocator;
- var f = try Fixture.init(a);
- defer f.deinit();
- const TestCase = struct {
- path: []const u8,
- file: []const u8,
- };
- const test_cases = [_]TestCase{
- .{ .path = "/", .file = "public/index.html"},
- .{ .path = "/posts/2018-05-31-new-site/", .file = "public/posts/2018-05-31-new-site/index.html"},
- };
-
- for (test_cases) |test_case| {
- const url = try std.fmt.allocPrint(a, "{s}{s}", .{f.base_url, test_case.path});
- defer a.free(url);
- var fr = try f.client.fetch(a, .{
- .location = .{.url = url}
- });
- defer fr.deinit();
- try std.testing.expectEqual(std.http.Status.ok, fr.status);
- const expected = try std.fs.cwd().readFileAlloc(a, test_case.file, 1_000_000);
- defer a.free(expected);
- try std.testing.expectEqualStrings(expected, fr.body.?);
- }
-}
-
-// test "404" {
-
-// } \ No newline at end of file