From 5ed483825a50cadb1d3d2dd55f9e4ebc52716660 Mon Sep 17 00:00:00 2001 From: Martin Ashby Date: Mon, 4 Mar 2024 15:55:07 +0000 Subject: Move web server to it's own subdir --- src/main.zig | 431 ----------------------------------------------------------- 1 file changed, 431 deletions(-) delete mode 100644 src/main.zig (limited to 'src') 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 -- cgit v1.2.3-ZIG