aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNathan Reiner <nathan@nathanreiner.xyz>2025-11-11 10:34:59 +0100
committerNathan Reiner <nathan@nathanreiner.xyz>2025-11-11 10:34:59 +0100
commit402888423de9764c22175df4cc41d79157895f3d (patch)
treeb954cac6454bb00ed39cc87ffc3d23c8da948ef4
parentce06aafe385f217bb0756089a88255f31f093018 (diff)
add basic server
-rw-r--r--.gitmodules3
-rw-r--r--flake.nix5
-rw-r--r--src/ansi.zig94
-rw-r--r--src/http.zig101
-rw-r--r--src/log.zig56
-rw-r--r--src/main.zig57
-rw-r--r--src/root.zig22
-rw-r--r--src/routes/fallback.zig32
-rw-r--r--src/routes/root.zig15
-rw-r--r--static/index.html9
-rw-r--r--static/index.js1
m---------static/sfw0
12 files changed, 355 insertions, 40 deletions
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..96f580f
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "static/sfw"]
+ path = static/sfw
+ url = https://git.nathanreiner.xyz/sfw
diff --git a/flake.nix b/flake.nix
index 8809297..bcf3861 100644
--- a/flake.nix
+++ b/flake.nix
@@ -11,7 +11,10 @@
in
{
devShells.x86_64-linux.default = pkgs.mkShell {
- packages = [ pkgs.zig ];
+ packages = [
+ pkgs.zig
+ pkgs.psmisc
+ ];
};
};
}
diff --git a/src/ansi.zig b/src/ansi.zig
new file mode 100644
index 0000000..44f5de6
--- /dev/null
+++ b/src/ansi.zig
@@ -0,0 +1,94 @@
+const std = @import("std");
+
+fn write_csi(writer: anytype, args: anytype, command: u8) !void {
+ try writer.writeAll("\x1b[");
+
+ inline for (args, 0..) |code, index| {
+ try writer.print("{}", .{code});
+
+ if (index < args.len - 1) {
+ try writer.writeByte(';');
+ }
+ }
+
+ try writer.writeByte(command);
+}
+
+pub const Color = enum(u8) {
+ const Self = @This();
+
+ black = 0,
+ red = 1,
+ green = 2,
+ yellow = 3,
+ blue = 4,
+ magenta = 5,
+ cyan = 6,
+ white = 7,
+ gray = 60,
+ bright_red = 61,
+ bright_green = 62,
+ bright_yellow = 63,
+ bright_blue = 64,
+ bright_magenta = 65,
+ bright_cyan = 66,
+ bright_white = 67,
+
+ const foreground_offset = 30;
+ const background_offset = 40;
+
+ pub fn to_fg(self: Self) u8 {
+ return @intFromEnum(self) + foreground_offset;
+ }
+
+ pub fn to_bg(self: Self) u8 {
+ return @intFromEnum(self) + background_offset;
+ }
+};
+
+pub const Styled = struct {
+ const Self = @This();
+
+ text: []const u8,
+ fg: Color = .white,
+ bg: Color = .black,
+ bold: bool = false,
+ dim: bool = false,
+ italic: bool = false,
+ underline: bool = false,
+
+ const Codes = struct {
+ const reset = 0;
+ const bold = 1;
+ const dim = 2;
+ const italic = 3;
+ const underline = 4;
+ };
+
+ pub fn format(
+ self: *const Self,
+ writer: *std.Io.Writer,
+ ) !void {
+ try write_csi(writer, .{self.fg.to_fg()}, 'm');
+ try write_csi(writer, .{self.bg.to_bg()}, 'm');
+
+ if (self.bold) try write_csi(writer, .{Codes.bold}, 'm');
+ if (self.dim) try write_csi(writer, .{Codes.dim}, 'm');
+ if (self.italic) try write_csi(writer, .{Codes.italic}, 'm');
+ if (self.underline) try write_csi(writer, .{Codes.underline}, 'm');
+
+ try writer.writeAll(self.text);
+
+ try write_csi(writer, .{Codes.reset}, 'm');
+ }
+};
+
+pub const ClearLine = struct {
+ pub fn format(
+ self: *const @This(),
+ writer: *std.Io.Writer,
+ ) !void {
+ _ = self;
+ try writer.writeAll("\x1b[2K\r");
+ }
+};
diff --git a/src/http.zig b/src/http.zig
new file mode 100644
index 0000000..e8ecfcf
--- /dev/null
+++ b/src/http.zig
@@ -0,0 +1,101 @@
+const std = @import("std");
+
+pub fn respond_file(
+ request: *std.http.Server.Request,
+ file: std.fs.File,
+ file_name: []const u8,
+ allocator: std.mem.Allocator,
+) !void {
+ const buffer = try file.readToEndAlloc(allocator, std.math.maxInt(usize));
+ defer allocator.free(buffer);
+
+ const suffix_pos = std.mem.lastIndexOfScalar(u8, file_name, '.') orelse file_name.len;
+ const suffix = file_name[suffix_pos..];
+ std.debug.print("{s}, {}, suffix: {s}\n", .{file_name, suffix_pos, suffix});
+
+ try request.respond(buffer, .{
+ .extra_headers = &.{
+ .{ .name = "Content-Type", .value = content_types.get(suffix) orelse "text/plain" },
+ }
+ });
+}
+
+pub const content_types = std.StaticStringMap([]const u8).initComptime(.{
+ .{ ".aac", "audio/aac" },
+ .{ ".abw", "application/x-abiword" },
+ .{ ".apng", "image/apng" },
+ .{ ".arc", "application/x-freearc" },
+ .{ ".avif", "image/avif" },
+ .{ ".avi", "video/x-msvideo" },
+ .{ ".azw", "application/vnd.amazon.ebook" },
+ .{ ".bin", "application/octet-stream" },
+ .{ ".bmp", "image/bmp" },
+ .{ ".bz", "application/x-bzip" },
+ .{ ".bz2", "application/x-bzip2" },
+ .{ ".cda", "application/x-cdf" },
+ .{ ".csh", "application/x-csh" },
+ .{ ".css", "text/css" },
+ .{ ".csv", "text/csv" },
+ .{ ".doc", "application/msword" },
+ .{ ".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" },
+ .{ ".eot", "application/vnd.ms-fontobject" },
+ .{ ".epub", "application/epub+zip" },
+ .{ ".gz", "application/gzip." },
+ .{ ".gif", "image/gif" },
+ .{ ".htm", "text/html" },
+ .{ ".html", "text/html" },
+ .{ ".ico", "image/vnd.microsoft.icon" },
+ .{ ".ics", "text/calendar" },
+ .{ ".jar", "application/java-archive" },
+ .{ ".jpeg, .jpg", "image/jpeg" },
+ .{ ".js", "text/javascript" },
+ .{ ".json", "application/json" },
+ .{ ".jsonld", "application/ld+json" },
+ .{ ".md", "text/markdown" },
+ .{ ".mid, .midi", "audio/midi," },
+ .{ ".mjs", "text/javascript" },
+ .{ ".mp3", "audio/mpeg" },
+ .{ ".mp4", "video/mp4" },
+ .{ ".mpeg", "video/mpeg" },
+ .{ ".mpkg", "application/vnd.apple.installer+xml" },
+ .{ ".odp", "application/vnd.oasis.opendocument.presentation" },
+ .{ ".ods", "application/vnd.oasis.opendocument.spreadsheet" },
+ .{ ".odt", "application/vnd.oasis.opendocument.text" },
+ .{ ".oga", "audio/ogg" },
+ .{ ".ogv", "video/ogg" },
+ .{ ".ogx", "application/ogg" },
+ .{ ".opus", "audio/ogg" },
+ .{ ".otf", "font/otf" },
+ .{ ".png", "image/png" },
+ .{ ".pdf", "application/pdf" },
+ .{ ".php", "application/x-httpd-php" },
+ .{ ".ppt", "application/vnd.ms-powerpoint" },
+ .{ ".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation" },
+ .{ ".rar", "application/vnd.rar" },
+ .{ ".rtf", "application/rtf" },
+ .{ ".sh", "application/x-sh" },
+ .{ ".svg", "image/svg+xml" },
+ .{ ".tar", "application/x-tar" },
+ .{ ".tif, .tiff", "image/tiff" },
+ .{ ".ts", "video/mp2t" },
+ .{ ".ttf", "font/ttf" },
+ .{ ".txt", "text/plain" },
+ .{ ".vsd", "application/vnd.visio" },
+ .{ ".wav", "audio/wav" },
+ .{ ".weba", "audio/webm" },
+ .{ ".webm", "video/webm" },
+ .{ ".webmanifest", "application/manifest+json" },
+ .{ ".webp", "image/webp" },
+ .{ ".woff", "font/woff" },
+ .{ ".woff2", "font/woff2" },
+ .{ ".xhtml", "application/xhtml+xml" },
+ .{ ".xls", "application/vnd.ms-excel" },
+ .{ ".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" },
+ .{ ".xml", "application/xml" },
+ .{ ".xul", "application/vnd.mozilla.xul+xml" },
+ .{ ".zip", "application/zip." },
+ .{ ".3gp", "video/3gpp;" },
+ .{ ".3g2", "video/3gpp2;" },
+ .{ ".7z", "application/x-7z-compressed" },
+});
+
diff --git a/src/log.zig b/src/log.zig
new file mode 100644
index 0000000..9d2b444
--- /dev/null
+++ b/src/log.zig
@@ -0,0 +1,56 @@
+const std = @import("std");
+
+const ansi = @import("ansi.zig");
+
+var log_buffer: [1024]u8 = undefined;
+var writer_stderr = std.fs.File.stderr().writer(&log_buffer);
+const stderr = &writer_stderr.interface;
+var log_lock = std.Thread.Mutex{};
+
+pub fn handler(
+ comptime message_level: std.log.Level,
+ comptime scope: @TypeOf(.enum_literal),
+ comptime format: []const u8,
+ args: anytype,
+) void {
+ log_lock.lock();
+ defer log_lock.unlock();
+ defer stderr.flush() catch unreachable;
+
+ const color = switch (message_level) {
+ .err => .red,
+ .warn => .yellow,
+ .info => .blue,
+ .debug => .magenta,
+ };
+
+ stderr.print(
+ "{f}{f}",
+ .{
+ ansi.ClearLine {},
+ ansi.Styled {
+ .text = " " ++ (comptime message_level.asText()) ++ " ",
+ .bg = color,
+ .bold = true,
+ },
+ },
+ ) catch unreachable;
+
+ if (scope != .default) {
+ stderr.print(
+ "{f}",
+ .{
+ ansi.Styled {
+ .text = " " ++ @tagName(scope) ++ " ",
+ .bg = .gray,
+ .fg = .black,
+ .italic = true,
+ },
+ },
+ ) catch unreachable;
+ }
+
+ stderr.writeByte(' ') catch unreachable;
+ stderr.print(format, args) catch unreachable;
+ stderr.writeAll("\n") catch unreachable;
+}
diff --git a/src/main.zig b/src/main.zig
index b5260bc..b98b112 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -1,27 +1,46 @@
const std = @import("std");
const storyboard = @import("storyboard");
+pub const std_options = std.Options {
+ .logFn = storyboard.log.handler,
+};
+
+const log = std.log.scoped(.main);
+
pub fn main() !void {
- // Prints to stderr, ignoring potential errors.
- std.debug.print("All your {s} are belong to us.\n", .{"codebase"});
- try storyboard.bufferedPrint();
-}
+ var gpa = std.heap.GeneralPurposeAllocator(.{}){};
+ defer std.debug.assert(gpa.deinit() == .ok);
-test "simple test" {
- const gpa = std.testing.allocator;
- var list: std.ArrayList(i32) = .empty;
- defer list.deinit(gpa); // Try commenting this out and see if zig detects the memory leak!
- try list.append(gpa, 42);
- try std.testing.expectEqual(@as(i32, 42), list.pop());
-}
+ const allocator = gpa.allocator();
+
+ const address = try std.net.Address.parseIpAndPort("127.0.0.1:8080");
+ var net_server = try address.listen(.{});
+ defer net_server.deinit();
+
+ log.info("listening on {f}", .{address});
+
+ while (true) {
+ const connection = net_server.accept() catch |err| {
+ log.err("error: {}", .{err});
+ continue;
+ };
+ defer connection.stream.close();
+
+ var read_buf: [1024 * 8]u8 = undefined;
+ var write_buf: [1024 * 8]u8 = undefined;
+ var reader = connection.stream.reader(&read_buf);
+ var writer = connection.stream.writer(&write_buf);
+ var http_server = std.http.Server.init(reader.interface(), &writer.interface);
+
+ while (true) {
+ var request = http_server.receiveHead() catch break;
+ log.info("{} {s}", .{request.head.method, request.head.target});
+
+ const handler = storyboard.routes.get(request.head.target);
-test "fuzz example" {
- const Context = struct {
- fn testOne(context: @This(), input: []const u8) anyerror!void {
- _ = context;
- // Try passing `--fuzz` to `zig build test` and see if it manages to fail this test case!
- try std.testing.expect(!std.mem.eql(u8, "canyoufindme", input));
+ handler(&request, allocator) catch |err| {
+ log.err("handler({s}): {}", .{request.head.target, err});
+ };
}
- };
- try std.testing.fuzz(Context{}, Context.testOne, .{});
+ }
}
diff --git a/src/root.zig b/src/root.zig
index 94c7cd0..87f1892 100644
--- a/src/root.zig
+++ b/src/root.zig
@@ -1,23 +1,5 @@
//! By convention, root.zig is the root source file when making a library.
const std = @import("std");
+pub const log = @import("log.zig");
+pub const routes = @import("routes/root.zig");
-pub fn bufferedPrint() !void {
- // Stdout is for the actual output of your application, for example if you
- // are implementing gzip, then only the compressed bytes should be sent to
- // stdout, not any debugging messages.
- var stdout_buffer: [1024]u8 = undefined;
- var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
- const stdout = &stdout_writer.interface;
-
- try stdout.print("Run `zig build test` to run the tests.\n", .{});
-
- try stdout.flush(); // Don't forget to flush!
-}
-
-pub fn add(a: i32, b: i32) i32 {
- return a + b;
-}
-
-test "basic add functionality" {
- try std.testing.expect(add(3, 7) == 10);
-}
diff --git a/src/routes/fallback.zig b/src/routes/fallback.zig
new file mode 100644
index 0000000..78d778b
--- /dev/null
+++ b/src/routes/fallback.zig
@@ -0,0 +1,32 @@
+const std = @import("std");
+const http = @import("../http.zig");
+
+const log = std.log.scoped(.fallback);
+
+pub fn handler(
+ request: *std.http.Server.Request,
+ allocator: std.mem.Allocator,
+) anyerror!void {
+ var static = try std.fs.cwd().openDir("static", .{});
+ defer static.close();
+
+ if (static.openFile(request.head.target[1..], .{})) |file| {
+ defer file.close();
+ try http.respond_file(request, file, request.head.target[1..], allocator);
+ } else |_| {
+ var subdir = if (request.head.target.len == 1) static
+ else (static.openDir(request.head.target[1..], .{}) catch {
+ try request.respond("Not Found", .{ .status = .not_found });
+ return;
+ });
+ defer if (request.head.target.len > 1) subdir.close();
+
+ if (subdir.openFile("index.html", .{})) |file| {
+ defer file.close();
+ try http.respond_file(request, file, "index.html", allocator);
+ } else |_| {
+ log.warn("File '{s}' Not Found", .{ request.head.target });
+ try request.respond("Not Found", .{ .status = .not_found });
+ }
+ }
+}
diff --git a/src/routes/root.zig b/src/routes/root.zig
new file mode 100644
index 0000000..0330404
--- /dev/null
+++ b/src/routes/root.zig
@@ -0,0 +1,15 @@
+const std = @import("std");
+
+pub const fallback = @import("fallback.zig");
+
+pub const Handler = *const fn (
+ request: *std.http.Server.Request,
+ allocator: std.mem.Allocator,
+) anyerror!void;
+
+pub const handlers = std.StaticStringMap(Handler).initComptime(.{
+});
+
+pub fn get(path: []const u8) Handler {
+ return handlers.get(path) orelse fallback.handler;
+}
diff --git a/static/index.html b/static/index.html
new file mode 100644
index 0000000..cd0d6de
--- /dev/null
+++ b/static/index.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Storyboard</title>
+ <script type="module" src="index.js"></script>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/static/index.js b/static/index.js
new file mode 100644
index 0000000..8764baf
--- /dev/null
+++ b/static/index.js
@@ -0,0 +1 @@
+console.log('hello, world!');
diff --git a/static/sfw b/static/sfw
new file mode 160000
+Subproject bf1fef8933e090ec92dbb04c66f9c868044c242