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; } }