aboutsummaryrefslogtreecommitdiff
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
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
-rw-r--r--.gitignore2
-rw-r--r--README.md10
-rw-r--r--build.zig36
-rw-r--r--build.zig.zon9
-rw-r--r--src/main.zig232
-rwxr-xr-xzig-out/bin/serverbin0 -> 3154008 bytes
6 files changed, 281 insertions, 8 deletions
diff --git a/.gitignore b/.gitignore
index e292d34..4f1f83e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
public/
.env
+zig-cache/
+zig-build/
diff --git a/README.md b/README.md
index 5996959..8a8dc1e 100644
--- a/README.md
+++ b/README.md
@@ -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
new file mode 100755
index 0000000..019371d
--- /dev/null
+++ b/zig-out/bin/server
Binary files differ