aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMartin Ashby <martin@ashbysoft.com>2024-02-17 21:10:59 +0000
committerMartin Ashby <martin@ashbysoft.com>2024-02-17 21:10:59 +0000
commit3395a562c419c60d046cdc295a88b759d4bc87fd (patch)
tree7c544717d4a336be11fa38da7e2c83879b6dd09c /src
parentfe102460acc09df065f7e1141a04c55306f8975e (diff)
downloadmfashby.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')
-rw-r--r--src/main.zig232
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