diff options
author | Martin Ashby <martin@ashbysoft.com> | 2024-02-17 21:10:59 +0000 |
---|---|---|
committer | Martin Ashby <martin@ashbysoft.com> | 2024-02-17 21:10:59 +0000 |
commit | 3395a562c419c60d046cdc295a88b759d4bc87fd (patch) | |
tree | 7c544717d4a336be11fa38da7e2c83879b6dd09c | |
parent | fe102460acc09df065f7e1141a04c55306f8975e (diff) | |
download | mfashby.net-3395a562c419c60d046cdc295a88b759d4bc87fd.tar.gz mfashby.net-3395a562c419c60d046cdc295a88b759d4bc87fd.tar.bz2 mfashby.net-3395a562c419c60d046cdc295a88b759d4bc87fd.tar.xz mfashby.net-3395a562c419c60d046cdc295a88b759d4bc87fd.zip |
Initial work on zig web server
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | README.md | 10 | ||||
-rw-r--r-- | build.zig | 36 | ||||
-rw-r--r-- | build.zig.zon | 9 | ||||
-rw-r--r-- | src/main.zig | 232 | ||||
-rwxr-xr-x | zig-out/bin/server | bin | 0 -> 3154008 bytes |
6 files changed, 281 insertions, 8 deletions
@@ -1,2 +1,4 @@ public/ .env +zig-cache/ +zig-build/ @@ -1,10 +1,4 @@ -mfashby.net -=========== +# server -Personal blog at [mfashby.net](https://mfashby.net) +Web server to act as frontend for mfashby.net -TODO: -In the vein of doing things myself, I want to try replacing bits that I'm using for this website (and other stuff that I run) - -[ ] Replace web server -[ ] Replace hugo static site generator diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..1f481c8 --- /dev/null +++ b/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/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..d68b536 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,9 @@ +.{ + .name = "server", + .version = "0.0.0", + .dependencies = .{ + }, + .paths = .{ + "", + }, +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..86809aa --- /dev/null +++ b/src/main.zig @@ -0,0 +1,232 @@ +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 { + log.info("deinit gpa", .{}); + _ = 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 }); + const addr = try std.net.Address.parseIp("0.0.0.0", 8081); + try svr.listen(addr); + log.info("listening on {}", .{addr}); + while (true) { + // 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(); + try pool.spawn(handle, .{res}); + } +} + +fn handle(res: *std.http.Server.Response) void { + defer res.allocator.destroy(res); + defer res.deinit(); + handleErr(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 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 { + 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(); + } + + // var target = res.request.target; + + // if (std.mem.eql(u8, host, "mfashby.net")) { + // if () + 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(); + // } +} + +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 }); + + + 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 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; + } +}
\ No newline at end of file diff --git a/zig-out/bin/server b/zig-out/bin/server Binary files differnew file mode 100755 index 0000000..019371d --- /dev/null +++ b/zig-out/bin/server |