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 --- build.zig | 36 ----- build.zig.zon | 9 -- server/build.zig | 36 +++++ server/build.zig.zon | 9 ++ server/src/main.zig | 431 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.zig | 431 --------------------------------------------------- 6 files changed, 476 insertions(+), 476 deletions(-) delete mode 100644 build.zig delete mode 100644 build.zig.zon create mode 100644 server/build.zig create mode 100644 server/build.zig.zon create mode 100644 server/src/main.zig delete mode 100644 src/main.zig diff --git a/build.zig b/build.zig deleted file mode 100644 index 1f481c8..0000000 --- a/build.zig +++ /dev/null @@ -1,36 +0,0 @@ -const std = @import("std"); - -pub fn build(b: *std.Build) void { - const target = b.standardTargetOptions(.{}); - const optimize = b.standardOptimizeOption(.{}); - - const exe = b.addExecutable(.{ - .name = "server", - .root_source_file = .{ .path = "src/main.zig" }, - .target = target, - .optimize = optimize, - }); - - b.installArtifact(exe); - const run_cmd = b.addRunArtifact(exe); - - run_cmd.step.dependOn(b.getInstallStep()); - - if (b.args) |args| { - run_cmd.addArgs(args); - } - - const run_step = b.step("run", "Run the app"); - run_step.dependOn(&run_cmd.step); - - const exe_unit_tests = b.addTest(.{ - .root_source_file = .{ .path = "src/main.zig" }, - .target = target, - .optimize = optimize, - }); - - const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); - - const test_step = b.step("test", "Run unit tests"); - test_step.dependOn(&run_exe_unit_tests.step); -} diff --git a/build.zig.zon b/build.zig.zon deleted file mode 100644 index d68b536..0000000 --- a/build.zig.zon +++ /dev/null @@ -1,9 +0,0 @@ -.{ - .name = "server", - .version = "0.0.0", - .dependencies = .{ - }, - .paths = .{ - "", - }, -} diff --git a/server/build.zig b/server/build.zig new file mode 100644 index 0000000..1f481c8 --- /dev/null +++ b/server/build.zig @@ -0,0 +1,36 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const exe = b.addExecutable(.{ + .name = "server", + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + b.installArtifact(exe); + const run_cmd = b.addRunArtifact(exe); + + run_cmd.step.dependOn(b.getInstallStep()); + + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + const exe_unit_tests = b.addTest(.{ + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); + + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_exe_unit_tests.step); +} diff --git a/server/build.zig.zon b/server/build.zig.zon new file mode 100644 index 0000000..d68b536 --- /dev/null +++ b/server/build.zig.zon @@ -0,0 +1,9 @@ +.{ + .name = "server", + .version = "0.0.0", + .dependencies = .{ + }, + .paths = .{ + "", + }, +} diff --git a/server/src/main.zig b/server/src/main.zig new file mode 100644 index 0000000..f9f204b --- /dev/null +++ b/server/src/main.zig @@ -0,0 +1,431 @@ +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 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