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 /src/main.zig | |
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
Diffstat (limited to 'src/main.zig')
-rw-r--r-- | src/main.zig | 232 |
1 files changed, 232 insertions, 0 deletions
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 |