From 80c06b9c4e8b4cbd2df9dafa42cafab4657430fd Mon Sep 17 00:00:00 2001 From: Szymon Nowakowski Date: Thu, 10 Jul 2025 21:28:41 +0200 Subject: [PATCH 1/7] Initial commit From af8bfe1518cbec4a03c1b7240cce0ec6c3e698b2 Mon Sep 17 00:00:00 2001 From: Szymon Nowakowski Date: Thu, 10 Jul 2025 21:31:52 +0200 Subject: [PATCH 2/7] Proof of concept --- packages/myid/.gitignore | 6 + packages/myid/build.zig | 63 +++++++++ packages/myid/build.zig.zon | 17 +++ packages/myid/src/default.zig | 2 + packages/myid/src/http.zig | 66 ++++++++++ packages/myid/src/main.zig | 136 ++++++++++++++++++++ packages/myid/src/root.zig | 235 ++++++++++++++++++++++++++++++++++ packages/myid/src/uuid.zig | 41 ++++++ 8 files changed, 566 insertions(+) create mode 100644 packages/myid/.gitignore create mode 100644 packages/myid/build.zig create mode 100644 packages/myid/build.zig.zon create mode 100644 packages/myid/src/default.zig create mode 100644 packages/myid/src/http.zig create mode 100644 packages/myid/src/main.zig create mode 100644 packages/myid/src/root.zig create mode 100644 packages/myid/src/uuid.zig diff --git a/packages/myid/.gitignore b/packages/myid/.gitignore new file mode 100644 index 0000000..2e1e385 --- /dev/null +++ b/packages/myid/.gitignore @@ -0,0 +1,6 @@ +.zig-cache +*.sock +*.sqlite3 +*.sqlite3-shm +*.sqlite3-wal +zig-out diff --git a/packages/myid/build.zig b/packages/myid/build.zig new file mode 100644 index 0000000..46b505f --- /dev/null +++ b/packages/myid/build.zig @@ -0,0 +1,63 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const sqlite_dep = b.dependency("sqlite", .{ + .target = target, + .optimize = optimize, + }); + + const sqlite_mod = sqlite_dep.module("sqlite"); + + const myid_mod = b.addModule("myid", .{ + .root_source_file = b.path("src/root.zig"), + .target = target, + .imports = &.{ + .{ .name = "sqlite", .module = sqlite_mod }, + }, + }); + + const myid_exe = b.addExecutable(.{ + .name = "myid", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "myid", .module = myid_mod }, + .{ .name = "sqlite", .module = sqlite_mod }, + }, + }), + }); + + b.installArtifact(myid_exe); + + const run_step = b.step("run", "Run the app"); + + const run_cmd = b.addRunArtifact(myid_exe); + run_step.dependOn(&run_cmd.step); + + run_cmd.step.dependOn(b.getInstallStep()); + + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const mod_tests = b.addTest(.{ + .root_module = myid_mod, + }); + + const run_mod_tests = b.addRunArtifact(mod_tests); + + const exe_tests = b.addTest(.{ + .root_module = myid_exe.root_module, + }); + + const run_exe_tests = b.addRunArtifact(exe_tests); + + const test_step = b.step("test", "Run tests"); + test_step.dependOn(&run_mod_tests.step); + test_step.dependOn(&run_exe_tests.step); +} diff --git a/packages/myid/build.zig.zon b/packages/myid/build.zig.zon new file mode 100644 index 0000000..19337a0 --- /dev/null +++ b/packages/myid/build.zig.zon @@ -0,0 +1,17 @@ +.{ + .name = .myid, + .version = "0.0.0", + .dependencies = .{ + .sqlite = .{ + .url = "git+https://github.com/vrischmann/zig-sqlite#be8b4965b46fc1a7a819bf3cba09f370c0e9c64c", + .hash = "sqlite-3.48.0-F2R_a9GLDgAXT-c49TfkFMt6yPOMQAYfp4ig8bRNdZs4", + }, + }, + .fingerprint = 0x4dc8d6b2af89dd8f, // Changing this has security and trust implications. + .minimum_zig_version = "0.15.0-dev.919+044ccf413", + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/packages/myid/src/default.zig b/packages/myid/src/default.zig new file mode 100644 index 0000000..56a6e70 --- /dev/null +++ b/packages/myid/src/default.zig @@ -0,0 +1,2 @@ +pub const database_path = "db.sqlite3"; +pub const socket_path = "myid.sock"; diff --git a/packages/myid/src/http.zig b/packages/myid/src/http.zig new file mode 100644 index 0000000..01772d2 --- /dev/null +++ b/packages/myid/src/http.zig @@ -0,0 +1,66 @@ +const std = @import("std"); +const main = @import("main.zig"); + +threadlocal var read_buffer: [2 * 1024 * 1024]u8 = undefined; +threadlocal var write_buffer: [2 * 1024 * 1024]u8 = undefined; + +const http_400 = "HTTP/1.1 400 Bad Request\r\n\r\n"; +const http_404 = "HTTP/1.1 404 Not Found\r\n\r\n"; +const http_413 = "HTTP/1.1 413 Content Too Large\r\n\r\n"; +const http_431 = "HTTP/1.1 431 Request Header Fields Too Large\r\n\r\n"; +const http_500 = "HTTP/1.1 500 Internal Server Error\r\n\r\n"; + +const log = std.log.scoped(.http); + +fn http_200(content_type: []const u8, response_body: []const u8) ![]const u8 { + var fbs = std.io.fixedBufferStream(&write_buffer); + const writer = fbs.writer(); + + try writer.print("HTTP/1.1 200 OK\r\n", .{}); + try writer.print("Content-Type: {s}\r\n", .{content_type}); + try writer.print("Content-Length: {d}\r\n", .{response_body.len}); + try writer.print("\r\n", .{}); + try writer.print("{s}", .{response_body}); + + return fbs.getWritten(); +} + +pub fn process(conn: std.net.Server.Connection) !void { + defer conn.stream.close(); + + var arena = std.heap.ArenaAllocator.init(main.allocator); + defer arena.deinit(); + + const allocator = arena.allocator(); + _ = allocator; + + var running = true; + while (running) { + var header_end: ?usize = null; + var request_len: usize = 0; + + const request_head = blk: while (conn.stream.read(read_buffer[request_len..])) |len| { + if (len == 0) { + running = false; + return; + } + + header_end = std.mem.indexOfPos(u8, &read_buffer, request_len, "\r\n\r\n"); + request_len += len; + + if (header_end) |end| { + break :blk read_buffer[0..end]; + } + } else |err| { + return err; + }; + + const response = try http_200("text/plain; charset=utf-8", "PONG\n"); + + try conn.stream.writeAll(response); + + if (request_len > request_head.len + 4) { + @memmove(&read_buffer, read_buffer[request_head.len + 4 .. request_len]); + } + } +} diff --git a/packages/myid/src/main.zig b/packages/myid/src/main.zig new file mode 100644 index 0000000..1271281 --- /dev/null +++ b/packages/myid/src/main.zig @@ -0,0 +1,136 @@ +const std = @import("std"); +const default = @import("default.zig"); +const http = @import("http.zig"); + +pub var allocator: std.mem.Allocator = undefined; +pub var args: [][:0]u8 = undefined; +pub var env: std.process.EnvMap = undefined; +pub var config: struct { + database_path: []const u8 = default.database_path, + socket_path: []const u8 = default.socket_path, +} = .{}; + +pub fn main() !void { + var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; + defer _ = gpa.deinit(); + + allocator = gpa.allocator(); + + // --- READ CONFIG --------------------------------------------------------- + + args = try std.process.argsAlloc(allocator); + defer std.process.argsFree(allocator, args); + + env = try std.process.getEnvMap(allocator); + defer env.deinit(); + + // override config by environment variables + + if (env.get("DATABASE_PATH")) |database_path| { + config.database_path = database_path; + } + + if (env.get("SOCKET_PATH")) |socket_path| { + config.socket_path = socket_path; + } + + // override config by args + + var show_help = false; + + var arg_i: usize = 1; + while (arg_i < args.len) : (arg_i += 1) { + const arg = args[arg_i]; + + if (std.mem.eql(u8, arg, "--database-path")) { + if (arg_i < args.len - 1) { + std.log.err("Missing argument for --database-path", .{}); + return; + } + + arg_i += 1; + config.database_path = args[arg_i]; + } else if (std.mem.eql(u8, arg, "--socket-path")) { + if (arg_i < args.len - 1) { + std.log.err("Missing argument for --socket-path", .{}); + return; + } + + arg_i += 1; + config.socket_path = args[arg_i]; + } else if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { + show_help = true; + } else { + std.log.err("Unrecognized option: {s}", .{arg}); + return; + } + } + + if (show_help) { + const stderr = std.io.getStdErr().writer(); + try stderr.print( + \\Usage: myid [flags] + \\ + \\Flags: + \\ --database-path + \\ The path to an SQLite 3 database file. You can specify ":memory:" to use + \\ in-memory ephemeral database. + \\ Default: "{[default_database_path]s}" + \\ --socket-path + \\ The path to a unix socket on which the server will listen. + \\ Default: "{[default_socket_path]s}" + \\ + \\Environment variables: + \\ DATABASE_PATH + \\ Equivalent to --database-path flag. When both are present, the flag + \\ takes precedence. + \\ SOCKET_PATH + \\ Equivalent to --socket-path flag. When both are present, the flag takes + \\ precedence. + \\ + , .{ + .default_database_path = default.database_path, + .default_socket_path = default.socket_path, + }); + return; + } + + // --- INITIALIZE SERVER --------------------------------------------------- + + std.posix.unlink(config.socket_path) catch |err| switch (err) { + error.FileNotFound => {}, + else => return err, + }; + + const address = try std.net.Address.initUnix(config.socket_path); + + var server = try address.listen(.{}); + defer { + server.deinit(); + std.log.info("Server stopped", .{}); + } + + std.log.info("Listening on unix:{s}", .{config.socket_path}); + + // --- LOOP ---------------------------------------------------------------- + + while (server.accept()) |conn| { + const thread = std.Thread.spawn(.{ .allocator = allocator }, process, .{conn}) catch |err| { + std.log.err("Error while spawning thread: {}", .{err}); + continue; + }; + thread.detach(); + } else |err| { + std.log.err("Error while trying to accept connection: {}", .{err}); + } +} + +fn process(conn: std.net.Server.Connection) void { + http.process(conn) catch |err| { + // TODO Bandaid spam prevention until we manage closing connections properly + if (err == error.ConnectionResetByPeer) { + return; + } + std.log.err("Error while processing connection: {}", .{err}); + }; +} diff --git a/packages/myid/src/root.zig b/packages/myid/src/root.zig new file mode 100644 index 0000000..1f91fce --- /dev/null +++ b/packages/myid/src/root.zig @@ -0,0 +1,235 @@ +const std = @import("std"); +const sqlite = @import("sqlite"); +const uuid = @import("uuid.zig"); + +fn Id(comptime _tag: @Type(.enum_literal)) type { + return struct { + pub const tag = _tag; + + bytes: [16]u8, + + pub fn new() @This() { + return .{ .bytes = uuid.uuid_v7() }; + } + + pub fn eql(a: @This(), b: @This()) bool { + return std.mem.eql(u8, &a.bytes, &b.bytes); + } + + pub fn decode(encoded: *const [22]u8) !@This() { + var bytes: [16]u8 = undefined; + try std.base64.url_safe_no_pad.Decoder.decode(&bytes, encoded); + return .{ .bytes = bytes }; + } + + pub fn encode(self: @This()) [22]u8 { + var text: [22]u8 = undefined; + std.base64.url_safe_no_pad.Encoder.encode(&text, self.bytes); + return text; + } + }; +} + +pub const AppId = Id(.app_id); +pub const UserId = Id(.user_id); + +pub const CreateAppResult = struct { + aid: AppId, + plain_secret: [32]u8, +}; + +pub const Database = struct { + db: sqlite.Db, + allocator: std.mem.Allocator, + + pub fn init(path: [:0]const u8, allocator: std.mem.Allocator) !Database { + var db = try sqlite.Db.init(.{ + .mode = .{ .File = path }, + .open_flags = .{ + .create = true, + .write = true, + }, + .threading_mode = .MultiThread, + }); + errdefer db.deinit(); + + _ = try db.one(void, "PRAGMA journal_mode = WAL", .{}, .{}); + _ = try db.one(void, "PRAGMA foreign_keys = ON", .{}, .{}); + + return .{ + .db = db, + .allocator = allocator, + }; + } + + pub fn deinit(self: *Database) void { + self.db.deinit(); + self.* = undefined; + } + + // --- MIGRATION ----------------------------------------------------------- + + fn getUserVersion(self: *Database) !i32 { + const version = try self.db.one(i32, "PRAGMA user_version", .{}, .{}); + return version.?; + } + + fn setUserVersion(self: *Database, version: i32) !void { + var buf: [100]u8 = undefined; + const query = std.fmt.bufPrint(&buf, "PRAGMA user_version = {d}", .{version}) catch unreachable; + _ = try self.db.oneDynamic(void, query, .{}, .{}); + } + + pub fn migrate(self: *Database) !void { + var user_version = try self.getUserVersion(); + + if (user_version == 0) { + _ = try self.db.exec( + \\CREATE TABLE users ( + \\ uid BLOB NOT NULL, + \\ name TEXT NOT NULL, + \\ email TEXT NOT NULL UNIQUE, + \\ password TEXT NOT NULL, + \\ PRIMARY KEY (uid) + \\) + , .{}, .{}); + _ = try self.db.exec( + \\CREATE TABLE apps ( + \\ aid BLOB NOT NULL, + \\ name TEXT NOT NULL, + \\ secret TEXT NOT NULL, + \\ PRIMARY KEY (aid) + \\) + , .{}, .{}); + _ = try self.db.exec( + \\CREATE TABLE app_callbacks ( + \\ aid BLOB NOT NULL, + \\ callback TEXT NOT NULL, + \\ PRIMARY KEY (aid, callback), + \\ FOREIGN KEY (aid) REFERENCES apps (aid) + \\ ON UPDATE CASCADE + \\ ON DELETE CASCADE + \\) + , .{}, .{}); + user_version += 1; + try self.setUserVersion(user_version); + } + } + + // --- USERS --------------------------------------------------------------- + + pub fn createUser(self: *Database, name: []const u8, email: []const u8, plain_password: []const u8) !UserId { + const uid = UserId.new(); + var password_buf: [1000]u8 = undefined; + const password = try std.crypto.pwhash.argon2.strHash(plain_password, .{ + .allocator = self.allocator, + .mode = .argon2id, + .params = .owasp_2id, + }, &password_buf); + + try self.db.exec("INSERT INTO users (uid, name, email, password) VALUES (?, ?, ?, ?)", .{}, .{ + .uid = uid.bytes, + .name = name, + .email = email, + .password = password, + }); + + return uid; + } + + // --- APPS ---------------------------------------------------------------- + + pub fn createApp(self: *Database, name: []const u8, callbacks: []const []const u8) !CreateAppResult { + _ = try self.db.exec("BEGIN", .{}, .{}); + errdefer self.db.exec("ROLLBACK", .{}, .{}) catch unreachable; + + const secret_bytes = blk: { + var bytes: [24]u8 = undefined; + std.crypto.random.bytes(&bytes); + break :blk bytes; + }; + + var plain_secret_buf: [32]u8 = undefined; + const plain_secret = std.base64.url_safe_no_pad.Encoder.encode(&plain_secret_buf, &secret_bytes); + std.debug.assert(plain_secret_buf.len == plain_secret.len); + + const aid = AppId.new(); + var secret_buf: [1000]u8 = undefined; + const secret = try std.crypto.pwhash.argon2.strHash(plain_secret, .{ + .allocator = self.allocator, + .mode = .argon2id, + .params = .owasp_2id, + }, &secret_buf); + + try self.db.exec("INSERT INTO apps (aid, name, secret) VALUES (?, ?, ?)", .{}, .{ + .aid = aid.bytes, + .name = name, + .secret = secret, + }); + + var insert_callback = try self.db.prepare("INSERT INTO app_callbacks (aid, callback) VALUES (?, ?)"); + defer insert_callback.deinit(); + for (callbacks) |callback| { + try insert_callback.exec(.{}, .{ .aid = aid.bytes, .callback = callback }); + insert_callback.reset(); + } + + try self.db.exec("COMMIT", .{}, .{}); + return .{ + .aid = aid, + .plain_secret = plain_secret_buf, + }; + } +}; + +test "user version" { + var db = try Database.init(":memory:", std.testing.allocator); + defer db.deinit(); + + try std.testing.expectEqual(0, try db.getUserVersion()); + try db.setUserVersion(1); + try std.testing.expectEqual(1, try db.getUserVersion()); +} + +test "migrate" { + var db = try Database.init(":memory:", std.testing.allocator); + defer db.deinit(); + + try db.migrate(); +} + +test "create user" { + var db = try Database.init(":memory:", std.testing.allocator); + defer db.deinit(); + + try db.migrate(); + + const uid = try db.createUser("admin", "admin@test.invalid", "admin"); + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + const maybe_user = try db.db.oneAlloc(struct { + name: []const u8, + email: []const u8, + password: []const u8, + }, arena.allocator(), "SELECT name, email, password FROM users WHERE uid = ?", .{}, .{ + .uid = uid.bytes, + }); + defer arena.deinit(); + + try std.testing.expect(maybe_user != null); + if (maybe_user) |user| { + try std.testing.expectEqualSlices(u8, "admin", user.name); + } +} + +test "create app" { + var db = try Database.init(":memory:", std.testing.allocator); + defer db.deinit(); + + try db.migrate(); + + _ = try db.createApp("app", &.{ + "http://localhost:3000/callback", + "https://example.com/callback", + }); +} diff --git a/packages/myid/src/uuid.zig b/packages/myid/src/uuid.zig new file mode 100644 index 0000000..13a3dc1 --- /dev/null +++ b/packages/myid/src/uuid.zig @@ -0,0 +1,41 @@ +const std = @import("std"); + +var lock: std.Thread.Mutex = .{}; +var last_timestamp: std.atomic.Value(u64) = .{ .raw = 0 }; +var counter: std.atomic.Value(u32) = .{ .raw = 0 }; + +fn getCount(timestamp: u64) u32 { + lock.lock(); + defer lock.unlock(); + + if (last_timestamp.swap(timestamp, .monotonic) != timestamp) { + counter.store(0, .monotonic); + } + + return counter.fetchAdd(1, .monotonic) % 4096; +} + +pub fn uuid_v7() [16]u8 { + const timestamp: u64 = @intCast(@max(0, std.time.milliTimestamp())); + const count = getCount(timestamp); + const random = blk: { + var bytes: [8]u8 = undefined; + std.crypto.random.bytes(&bytes); + break :blk bytes; + }; + + var res: [16]u8 = undefined; + + res[0] = @truncate(timestamp >> 40); + res[1] = @truncate(timestamp >> 32); + res[2] = @truncate(timestamp >> 24); + res[3] = @truncate(timestamp >> 16); + res[4] = @truncate(timestamp >> 8); + res[5] = @truncate(timestamp); + res[6] = (@as(u8, 7) << 4) | @as(u8, @truncate((count >> 8) & 0x0F)); + res[7] = @truncate(count); + res[8] = 0x80 | (random[0] & 0x3F); + @memcpy(res[9..16], random[1..8]); + + return res; +} From 9998847c47be323f9998bf6f709fd9f9ac44c918 Mon Sep 17 00:00:00 2001 From: Szymon Nowakowski Date: Thu, 10 Jul 2025 22:56:12 +0200 Subject: [PATCH 3/7] Status text constant and response builders --- packages/myid/src/http.zig | 101 ++++++++++++++++++++++++++++++++----- 1 file changed, 89 insertions(+), 12 deletions(-) diff --git a/packages/myid/src/http.zig b/packages/myid/src/http.zig index 01772d2..482f3ab 100644 --- a/packages/myid/src/http.zig +++ b/packages/myid/src/http.zig @@ -4,23 +4,98 @@ const main = @import("main.zig"); threadlocal var read_buffer: [2 * 1024 * 1024]u8 = undefined; threadlocal var write_buffer: [2 * 1024 * 1024]u8 = undefined; -const http_400 = "HTTP/1.1 400 Bad Request\r\n\r\n"; -const http_404 = "HTTP/1.1 404 Not Found\r\n\r\n"; -const http_413 = "HTTP/1.1 413 Content Too Large\r\n\r\n"; -const http_431 = "HTTP/1.1 431 Request Header Fields Too Large\r\n\r\n"; -const http_500 = "HTTP/1.1 500 Internal Server Error\r\n\r\n"; - const log = std.log.scoped(.http); -fn http_200(content_type: []const u8, response_body: []const u8) ![]const u8 { +const status = struct { + pub const ok = "HTTP/1.1 200 OK\r\n"; + pub const created = "HTTP/1.1 201 Created\r\n"; + pub const accepted = "HTTP/1.1 202 Accepted\r\n"; + pub const non_authoritative_information = "HTTP/1.1 203 Non-Authoritative Information\r\n"; + pub const no_content = "HTTP/1.1 204 No Content\r\n"; + pub const reset_content = "HTTP/1.1 205 Reset Content\r\n"; + pub const partial_content = "HTTP/1.1 206 Partial Content\r\n"; + pub const multi_status = "HTTP/1.1 207 Multi-Status\r\n"; + pub const already_reported = "HTTP/1.1 208 Already Reported\r\n"; + + pub const multiple_choices = "HTTP/1.1 300 Multiple Choices\r\n"; + pub const moved_permanently = "HTTP/1.1 301 Moved Permanently\r\n"; + pub const found = "HTTP/1.1 302 Found\r\n"; + pub const see_other = "HTTP/1.1 303 See Other\r\n"; + pub const not_modified = "HTTP/1.1 304 Not Modified\r\n"; + pub const temporary_redirect = "HTTP/1.1 307 Temporary Redirect\r\n"; + pub const permanent_redirect = "HTTP/1.1 308 Permanent Redirect\r\n"; + + pub const bad_request = "HTTP/1.1 400 Bad Request\r\n"; + pub const unauthorized = "HTTP/1.1 401 Unauthorized\r\n"; + pub const payment_required = "HTTP/1.1 402 Payment Required\r\n"; + pub const forbidden = "HTTP/1.1 403 Forbidden\r\n"; + pub const not_found = "HTTP/1.1 404 Not Found\r\n"; + pub const method_not_allowed = "HTTP/1.1 405 Method Not Allowed\r\n"; + pub const not_acceptable = "HTTP/1.1 406 Not Acceptable\r\n"; + pub const proxy_authentication_required = "HTTP/1.1 407 Proxy Authentication Required\r\n"; + pub const request_timeout = "HTTP/1.1 408 Request Timeout\r\n"; + pub const conflict = "HTTP/1.1 409 Conflict\r\n"; + pub const gone = "HTTP/1.1 410 Gone\r\n"; + pub const length_required = "HTTP/1.1 411 Length Required\r\n"; + pub const precondition_failed = "HTTP/1.1 412 Precondition Failed\r\n"; + pub const content_too_large = "HTTP/1.1 413 Content Too Large\r\n"; + pub const uri_too_long = "HTTP/1.1 414 URI Too Long\r\n"; + pub const unsupported_media_type = "HTTP/1.1 415 Unsupported Media Type\r\n"; + pub const range_not_satisfiable = "HTTP/1.1 416 Range Not Satisfiable\r\n"; + pub const expectation_failed = "HTTP/1.1 417 Expectation Failed\r\n"; + pub const im_a_teapot = "HTTP/1.1 418 I'm a teapot\r\n"; + pub const misdirected_request = "HTTP/1.1 421 Misdirected Request\r\n"; + pub const unprocessable_content = "HTTP/1.1 422 Unprocessable Content\r\n"; + pub const locked = "HTTP/1.1 423 Locked\r\n"; + pub const failed_dependency = "HTTP/1.1 424 Failed Dependency\r\n"; + pub const upgrade_required = "HTTP/1.1 426 Upgrade Required\r\n"; + pub const precondition_required = "HTTP/1.1 428 Precondition Required\r\n"; + pub const too_many_requests = "HTTP/1.1 429 Too Many Requests\r\n"; + pub const request_header_fields_too_large = "HTTP/1.1 431 Request Header Fields Too Large\r\n"; + pub const unavailable_for_legal_reasons = "HTTP/1.1 451 Unavailable For Legal Reasons\r\n"; + + pub const internal_server_error = "HTTP/1.1 500 Internal Server Error\r\n"; + pub const not_implemented = "HTTP/1.1 501 Not Implemented\r\n"; + pub const bad_gateway = "HTTP/1.1 502 Bad Gateway\r\n"; + pub const service_unavailable = "HTTP/1.1 503 Service Unavailable\r\n"; + pub const gateway_timeout = "HTTP/1.1 504 Gateway Timeout\r\n"; + pub const http_version_not_supported = "HTTP/1.1 505 HTTP Version Not Supported\r\n"; + pub const variant_also_negotiates = "HTTP/1.1 506 Variant Also Negotiates\r\n"; + pub const insufficient_storage = "HTTP/1.1 507 Insufficient Storage\r\n"; + pub const loop_detected = "HTTP/1.1 508 Loop Detected\r\n"; + pub const not_extended = "HTTP/1.1 510 Not Extended\r\n"; + pub const network_authentication_required = "HTTP/1.1 511 Network Authentication Required\r\n"; +}; + +const ResponseEmptyOptions = struct { + status_text: []const u8 = status.ok, +}; + +const ResponseOptions = struct { + status_text: []const u8 = status.ok, + media_type: []const u8 = "text/plain; charset=utf-8", + response_body: []const u8, +}; + +fn makeResponseEmpty(options: ResponseEmptyOptions) ![]const u8 { var fbs = std.io.fixedBufferStream(&write_buffer); const writer = fbs.writer(); - try writer.print("HTTP/1.1 200 OK\r\n", .{}); - try writer.print("Content-Type: {s}\r\n", .{content_type}); - try writer.print("Content-Length: {d}\r\n", .{response_body.len}); + try writer.print("{s}", .{options.status_text}); try writer.print("\r\n", .{}); - try writer.print("{s}", .{response_body}); + + return fbs.getWritten(); +} + +fn makeResponse(options: ResponseOptions) ![]const u8 { + var fbs = std.io.fixedBufferStream(&write_buffer); + const writer = fbs.writer(); + + try writer.print("{s}", .{options.status_text}); + try writer.print("Content-Type: {s}\r\n", .{options.media_type}); + try writer.print("Content-Length: {d}\r\n", .{options.response_body.len}); + try writer.print("\r\n", .{}); + try writer.print("{s}", .{options.response_body}); return fbs.getWritten(); } @@ -55,7 +130,9 @@ pub fn process(conn: std.net.Server.Connection) !void { return err; }; - const response = try http_200("text/plain; charset=utf-8", "PONG\n"); + const response = try makeResponse(.{ + .response_body = "PONG\n", + }); try conn.stream.writeAll(response); From fcdbf2487f314ecc776ca1c1207c6d7b6f2e04d9 Mon Sep 17 00:00:00 2001 From: Szymon Nowakowski Date: Sun, 20 Jul 2025 18:47:32 +0200 Subject: [PATCH 4/7] HTTP request parser --- packages/myid/src/http.zig | 93 +++++++--- packages/myid/src/http/Parser.zig | 297 ++++++++++++++++++++++++++++++ 2 files changed, 366 insertions(+), 24 deletions(-) create mode 100644 packages/myid/src/http/Parser.zig diff --git a/packages/myid/src/http.zig b/packages/myid/src/http.zig index 482f3ab..6feb10a 100644 --- a/packages/myid/src/http.zig +++ b/packages/myid/src/http.zig @@ -1,6 +1,8 @@ const std = @import("std"); const main = @import("main.zig"); +const Parser = @import("http/Parser.zig"); + threadlocal var read_buffer: [2 * 1024 * 1024]u8 = undefined; threadlocal var write_buffer: [2 * 1024 * 1024]u8 = undefined; @@ -87,6 +89,17 @@ fn makeResponseEmpty(options: ResponseEmptyOptions) ![]const u8 { return fbs.getWritten(); } +fn makeResponseClose(options: ResponseEmptyOptions) ![]const u8 { + var fbs = std.io.fixedBufferStream(&write_buffer); + const writer = fbs.writer(); + + try writer.print("{s}", .{options.status_text}); + try writer.print("Connection: close\r\n", .{}); + try writer.print("\r\n", .{}); + + return fbs.getWritten(); +} + fn makeResponse(options: ResponseOptions) ![]const u8 { var fbs = std.io.fixedBufferStream(&write_buffer); const writer = fbs.writer(); @@ -103,41 +116,73 @@ fn makeResponse(options: ResponseOptions) ![]const u8 { pub fn process(conn: std.net.Server.Connection) !void { defer conn.stream.close(); - var arena = std.heap.ArenaAllocator.init(main.allocator); - defer arena.deinit(); + var leftover_bytes: usize = 0; - const allocator = arena.allocator(); - _ = allocator; + while (true) { + var parser = Parser.init(); + var total_bytes_read: usize = 0; - var running = true; - while (running) { - var header_end: ?usize = null; - var request_len: usize = 0; + while (true) { + var bytes_read: usize = undefined; + var chars: []const u8 = undefined; - const request_head = blk: while (conn.stream.read(read_buffer[request_len..])) |len| { - if (len == 0) { - running = false; - return; + if (leftover_bytes > 0) { + bytes_read = leftover_bytes; + chars = read_buffer[0..leftover_bytes]; + } else { + bytes_read = try conn.stream.read(read_buffer[total_bytes_read..]); + chars = read_buffer[total_bytes_read .. total_bytes_read + bytes_read]; } - header_end = std.mem.indexOfPos(u8, &read_buffer, request_len, "\r\n\r\n"); - request_len += len; + total_bytes_read += bytes_read; - if (header_end) |end| { - break :blk read_buffer[0..end]; + const res = parser.consume(chars) catch |err| switch (err) { + error.MethodNotSupported => { + const response = try makeResponseClose(.{ .status_text = status.method_not_allowed }); + try conn.stream.writeAll(response); + return; + }, + error.HttpVersionNotSupported => { + const response = try makeResponseClose(.{ .status_text = status.http_version_not_supported }); + try conn.stream.writeAll(response); + return; + }, + error.MissingLineFeed => { + const response = try makeResponseClose(.{ .status_text = status.bad_request }); + try conn.stream.writeAll(response); + return; + }, + error.InvalidContentLength => { + const response = try makeResponseClose(.{ .status_text = status.bad_request }); + try conn.stream.writeAll(response); + return; + }, + }; + + if (total_bytes_read >= read_buffer.len and !res.done) { + if (parser.state == .body) { + const response = try makeResponseClose(.{ .status_text = status.content_too_large }); + try conn.stream.writeAll(response); + return; + } else { + const response = try makeResponseClose(.{ .status_text = status.request_header_fields_too_large }); + try conn.stream.writeAll(response); + return; + } } - } else |err| { - return err; - }; - const response = try makeResponse(.{ - .response_body = "PONG\n", - }); + if (res.done) { + leftover_bytes = bytes_read - res.consumed; + break; + } + } + + const response = try makeResponse(.{ .response_body = "PONG\n" }); try conn.stream.writeAll(response); - if (request_len > request_head.len + 4) { - @memmove(&read_buffer, read_buffer[request_head.len + 4 .. request_len]); + if (leftover_bytes > 0) { + @memmove(&read_buffer, read_buffer[total_bytes_read - leftover_bytes .. total_bytes_read]); } } } diff --git a/packages/myid/src/http/Parser.zig b/packages/myid/src/http/Parser.zig new file mode 100644 index 0000000..dc26d56 --- /dev/null +++ b/packages/myid/src/http/Parser.zig @@ -0,0 +1,297 @@ +const std = @import("std"); + +const Parser = @This(); + +const log = std.log.scoped(.http_parser); + +const Error = error{ + MethodNotSupported, + HttpVersionNotSupported, + MissingLineFeed, + InvalidContentLength, +}; + +const State = union(enum) { + init: void, + method_d: void, + method_g: void, + method_h: void, + method_p: void, + method_de: void, + method_ge: void, + method_he: void, + method_pa: void, + method_po: void, + method_pu: void, + method_del: void, + method_hea: void, + method_pat: void, + method_pos: void, + method_dele: void, + method_patc: void, + method_delet: void, + method_complete: void, + pathname: []const u8, + pathname_complete: void, + version_h: void, + version_ht: void, + version_htt: void, + version_http: void, + @"version_http/@": void, + @"version_http/1@": void, + @"version_http/1.@": void, + version_complete: void, + start_line_end: void, + header_name_start: void, + header_name: []const u8, + header_value: []const u8, + header_line_end: void, + headers_end: void, + body: []const u8, +}; + +pub const ConsumeResult = struct { + consumed: usize, + done: bool, +}; + +pub const ConsumeCharResult = enum { + not_done, + done, +}; + +const Method = enum { + DELETE, + GET, + HEAD, + PATCH, + POST, + PUT, +}; + +state: State = .init, +current_header_is_content_length: bool = false, +content_length: usize = 0, + +pub fn init() Parser { + return .{}; +} + +pub fn consume(self: *Parser, chars: []const u8) Error!ConsumeResult { + for (chars, 1..) |*c_ptr, len| { + const res = try self.consumeChar(c_ptr); + if (res == .done) return .{ + .consumed = len, + .done = true, + }; + } + + return .{ + .consumed = chars.len, + .done = false, + }; +} + +pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult { + const c = c_ptr.*; + const c_slice = @as([*]const u8, @ptrCast(c_ptr))[0..1]; + switch (self.state) { + .init => switch (c) { + 'D' => self.state = .method_d, + 'G' => self.state = .method_g, + 'H' => self.state = .method_h, + 'P' => self.state = .method_p, + else => return error.MethodNotSupported, + }, + .method_d => switch (c) { + 'E' => self.state = .method_de, + else => return error.MethodNotSupported, + }, + .method_g => switch (c) { + 'E' => self.state = .method_ge, + else => return error.MethodNotSupported, + }, + .method_h => switch (c) { + 'E' => self.state = .method_he, + else => return error.MethodNotSupported, + }, + .method_p => switch (c) { + 'A' => self.state = .method_pa, + 'O' => self.state = .method_po, + 'U' => self.state = .method_pu, + else => return error.MethodNotSupported, + }, + .method_de => switch (c) { + 'L' => self.state = .method_del, + else => return error.MethodNotSupported, + }, + .method_ge => switch (c) { + 'T' => { + self.state = .method_complete; + log.debug("Method: GET", .{}); + }, + else => return error.MethodNotSupported, + }, + .method_he => switch (c) { + 'A' => self.state = .method_hea, + else => return error.MethodNotSupported, + }, + .method_pa => switch (c) { + 'T' => self.state = .method_pat, + else => return error.MethodNotSupported, + }, + .method_po => switch (c) { + 'S' => self.state = .method_pos, + else => return error.MethodNotSupported, + }, + .method_pu => switch (c) { + 'T' => { + self.state = .method_complete; + log.debug("Method: PUT", .{}); + }, + else => return error.MethodNotSupported, + }, + .method_del => switch (c) { + 'E' => self.state = .method_dele, + else => return error.MethodNotSupported, + }, + .method_hea => switch (c) { + 'D' => { + self.state = .method_complete; + log.debug("Method: HEAD", .{}); + }, + else => return error.MethodNotSupported, + }, + .method_pat => switch (c) { + 'C' => self.state = .method_patc, + else => return error.MethodNotSupported, + }, + .method_pos => switch (c) { + 'T' => { + self.state = .method_complete; + log.debug("Method: POST", .{}); + }, + else => return error.MethodNotSupported, + }, + .method_dele => switch (c) { + 'T' => self.state = .method_delet, + else => return error.MethodNotSupported, + }, + .method_patc => switch (c) { + 'H' => { + self.state = .method_complete; + log.debug("Method: PATCH", .{}); + }, + else => return error.MethodNotSupported, + }, + .method_delet => switch (c) { + 'E' => { + self.state = .method_complete; + log.debug("Method: DELETE", .{}); + }, + else => return error.MethodNotSupported, + }, + .method_complete => switch (c) { + ' ' => self.state = .{ .pathname = @as([*]const u8, @ptrCast(c_ptr))[1..1] }, + else => return error.MethodNotSupported, + }, + .pathname => |pathname| switch (c) { + ' ' => { + self.state = .pathname_complete; + log.debug("Pathname [{}]: {s}", .{ pathname.len, pathname }); + }, + else => self.state = .{ .pathname = pathname.ptr[0 .. pathname.len + 1] }, + }, + .pathname_complete => switch (c) { + 'H' => self.state = .version_h, + else => return error.HttpVersionNotSupported, + }, + .version_h => switch (c) { + 'T' => self.state = .version_ht, + else => return error.HttpVersionNotSupported, + }, + .version_ht => switch (c) { + 'T' => self.state = .version_htt, + else => return error.HttpVersionNotSupported, + }, + .version_htt => switch (c) { + 'P' => self.state = .version_http, + else => return error.HttpVersionNotSupported, + }, + .version_http => switch (c) { + '/' => self.state = .@"version_http/@", + else => return error.HttpVersionNotSupported, + }, + .@"version_http/@" => switch (c) { + '1' => self.state = .@"version_http/1@", + else => return error.HttpVersionNotSupported, + }, + .@"version_http/1@" => switch (c) { + '.' => self.state = .@"version_http/1.@", + else => return error.HttpVersionNotSupported, + }, + .@"version_http/1.@" => switch (c) { + '1' => { + self.state = .version_complete; + log.debug("Version: HTTP/1.1", .{}); + }, + else => return error.HttpVersionNotSupported, + }, + .version_complete => switch (c) { + '\r' => self.state = .start_line_end, + else => return error.HttpVersionNotSupported, + }, + .start_line_end => switch (c) { + '\n' => self.state = .header_name_start, + else => return error.MissingLineFeed, + }, + .header_name_start => switch (c) { + '\r' => self.state = .headers_end, + else => self.state = .{ .header_name = c_slice }, + }, + .header_name => |name| switch (c) { + ':' => { + self.state = .{ .header_value = @as([*]const u8, @ptrCast(c_ptr))[1..1] }; + self.current_header_is_content_length = std.ascii.eqlIgnoreCase(name, "Content-Length"); + log.debug("Header name [{}]: {s}", .{ name.len, name }); + }, + else => self.state = .{ .header_name = name.ptr[0 .. name.len + 1] }, + }, + .header_value => |value| switch (c) { + '\r' => { + self.state = .header_line_end; + const value_trimmed = std.mem.trim(u8, value, " \t"); + log.debug("Header value [{}]: {s}", .{ value_trimmed.len, value_trimmed }); + if (self.current_header_is_content_length) { + self.content_length = std.fmt.parseInt(usize, value_trimmed, 10) catch return error.InvalidContentLength; + self.current_header_is_content_length = false; + } + }, + else => self.state = .{ .header_value = value.ptr[0 .. value.len + 1] }, + }, + .header_line_end => switch (c) { + '\n' => self.state = .header_name_start, + else => return error.MissingLineFeed, + }, + .headers_end => switch (c) { + '\n' => { + if (self.content_length == 0) { + log.debug("End of request (no body)", .{}); + return .done; + } + self.state = .{ .body = @as([*]const u8, @ptrCast(c_ptr))[1..1] }; + }, + else => return error.MissingLineFeed, + }, + .body => |body| { + const new_body = body.ptr[0 .. body.len + 1]; + self.state = .{ .body = new_body }; + if (new_body.len >= self.content_length) { + log.debug("End of request ({} body bytes)", .{new_body.len}); + return .done; + } + }, + } + + return .not_done; +} From ad634562305c5b0f08594014b48b6fd14f8635e3 Mon Sep 17 00:00:00 2001 From: Szymon Nowakowski Date: Tue, 22 Jul 2025 17:58:28 +0200 Subject: [PATCH 5/7] Fix a bug before it appears --- packages/myid/src/http.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/myid/src/http.zig b/packages/myid/src/http.zig index 6feb10a..76b7539 100644 --- a/packages/myid/src/http.zig +++ b/packages/myid/src/http.zig @@ -129,6 +129,7 @@ pub fn process(conn: std.net.Server.Connection) !void { if (leftover_bytes > 0) { bytes_read = leftover_bytes; chars = read_buffer[0..leftover_bytes]; + leftover_bytes = 0; } else { bytes_read = try conn.stream.read(read_buffer[total_bytes_read..]); chars = read_buffer[total_bytes_read .. total_bytes_read + bytes_read]; From 0b4129a8cf7e0220df42faca8cea22dc8a72c98a Mon Sep 17 00:00:00 2001 From: Szymon Nowakowski Date: Tue, 22 Jul 2025 20:22:52 +0200 Subject: [PATCH 6/7] Comsume whole body at once --- packages/myid/src/http/Parser.zig | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/packages/myid/src/http/Parser.zig b/packages/myid/src/http/Parser.zig index dc26d56..06f3bb1 100644 --- a/packages/myid/src/http/Parser.zig +++ b/packages/myid/src/http/Parser.zig @@ -78,12 +78,29 @@ pub fn init() Parser { } pub fn consume(self: *Parser, chars: []const u8) Error!ConsumeResult { - for (chars, 1..) |*c_ptr, len| { - const res = try self.consumeChar(c_ptr); - if (res == .done) return .{ - .consumed = len, - .done = true, - }; + var i: usize = 0; + while (i < chars.len) { + switch (self.state) { + .body => |body| { + const to_consume = @min(chars.len - i, self.content_length - body.len); + const new_body = body.ptr[0 .. body.len + to_consume]; + self.state = .{ .body = new_body }; + i += to_consume; + + return .{ + .consumed = i, + .done = new_body.len >= self.content_length, + }; + }, + else => { + const res = try self.consumeChar(&chars[i]); + i += 1; + if (res == .done) return .{ + .consumed = i, + .done = true, + }; + }, + } } return .{ @@ -287,7 +304,6 @@ pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult { const new_body = body.ptr[0 .. body.len + 1]; self.state = .{ .body = new_body }; if (new_body.len >= self.content_length) { - log.debug("End of request ({} body bytes)", .{new_body.len}); return .done; } }, From bc2a0434c7764854b0047ad7bc4d78f6b8f01d89 Mon Sep 17 00:00:00 2001 From: Szymon Nowakowski Date: Tue, 22 Jul 2025 21:29:49 +0200 Subject: [PATCH 7/7] Parser callbacks, measure and log request time --- packages/myid/src/http.zig | 19 +++- packages/myid/src/http/Parser.zig | 149 +++++++++++++++++++----------- 2 files changed, 114 insertions(+), 54 deletions(-) diff --git a/packages/myid/src/http.zig b/packages/myid/src/http.zig index 76b7539..6623a23 100644 --- a/packages/myid/src/http.zig +++ b/packages/myid/src/http.zig @@ -119,7 +119,14 @@ pub fn process(conn: std.net.Server.Connection) !void { var leftover_bytes: usize = 0; while (true) { - var parser = Parser.init(); + const start = try std.time.Instant.now(); + + var route: Parser.Route = undefined; + + var parser = Parser.init(.{ + .self = &route, + .route = routeCallback, + }); var total_bytes_read: usize = 0; while (true) { @@ -185,5 +192,15 @@ pub fn process(conn: std.net.Server.Connection) !void { if (leftover_bytes > 0) { @memmove(&read_buffer, read_buffer[total_bytes_read - leftover_bytes .. total_bytes_read]); } + + const end = try std.time.Instant.now(); + const time_ns = end.since(start); + const time_us = @divFloor(time_ns, std.time.ns_per_us); + + log.info("{s} {s} ({} μs)", .{ @tagName(route.method), route.pathname, time_us }); } } + +fn routeCallback(self: ?*anyopaque, route: Parser.Route) void { + @as(*Parser.Route, @alignCast(@ptrCast(self))).* = route; +} diff --git a/packages/myid/src/http/Parser.zig b/packages/myid/src/http/Parser.zig index 06f3bb1..cdb36c6 100644 --- a/packages/myid/src/http/Parser.zig +++ b/packages/myid/src/http/Parser.zig @@ -2,7 +2,15 @@ const std = @import("std"); const Parser = @This(); -const log = std.log.scoped(.http_parser); +const Callbacks = struct { + self: ?*anyopaque = null, + + route: ?*const fn (self: ?*anyopaque, route: Route) void = null, + header: ?*const fn (self: ?*anyopaque, name: []const u8, value: []const u8) void = null, + body: ?*const fn (self: ?*anyopaque, body: []const u8) void = null, + + pub const init: Callbacks = .{}; +}; const Error = error{ MethodNotSupported, @@ -12,6 +20,32 @@ const Error = error{ }; const State = union(enum) { + pub fn methodComplete(method: Method) State { + return .{ + .method_complete = .{ + .method = method, + }, + }; + } + + pub fn pathname(method: Method, p: []const u8) State { + return .{ + .pathname_state = .{ + .method = method, + .pathname = p, + }, + }; + } + + pub fn headerValue(name: []const u8, value: []const u8) State { + return .{ + .header_value = .{ + .name = name, + .value = value, + }, + }; + } + init: void, method_d: void, method_g: void, @@ -30,8 +64,8 @@ const State = union(enum) { method_dele: void, method_patc: void, method_delet: void, - method_complete: void, - pathname: []const u8, + method_complete: struct { method: Method }, + pathname_state: struct { method: Method, pathname: []const u8 }, pathname_complete: void, version_h: void, version_ht: void, @@ -44,23 +78,23 @@ const State = union(enum) { start_line_end: void, header_name_start: void, header_name: []const u8, - header_value: []const u8, + header_value: struct { name: []const u8, value: []const u8 }, header_line_end: void, headers_end: void, body: []const u8, }; -pub const ConsumeResult = struct { +const ConsumeResult = struct { consumed: usize, done: bool, }; -pub const ConsumeCharResult = enum { +const ConsumeCharResult = enum { not_done, done, }; -const Method = enum { +pub const Method = enum { DELETE, GET, HEAD, @@ -69,12 +103,23 @@ const Method = enum { PUT, }; -state: State = .init, -current_header_is_content_length: bool = false, -content_length: usize = 0, +pub const Route = struct { + method: Method, + pathname: []const u8, +}; -pub fn init() Parser { - return .{}; +callbacks: Callbacks, +state: State, +current_header_is_content_length: bool, +content_length: usize, + +pub fn init(callbacks: Callbacks) Parser { + return .{ + .callbacks = callbacks, + .state = .init, + .current_header_is_content_length = false, + .content_length = 0, + }; } pub fn consume(self: *Parser, chars: []const u8) Error!ConsumeResult { @@ -87,9 +132,17 @@ pub fn consume(self: *Parser, chars: []const u8) Error!ConsumeResult { self.state = .{ .body = new_body }; i += to_consume; + const done = new_body.len >= self.content_length; + + if (done) { + if (self.callbacks.body) |bodyCallback| { + bodyCallback(self.callbacks.self, new_body); + } + } + return .{ .consumed = i, - .done = new_body.len >= self.content_length, + .done = done, }; }, else => { @@ -143,10 +196,7 @@ pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult { else => return error.MethodNotSupported, }, .method_ge => switch (c) { - 'T' => { - self.state = .method_complete; - log.debug("Method: GET", .{}); - }, + 'T' => self.state = .methodComplete(.GET), else => return error.MethodNotSupported, }, .method_he => switch (c) { @@ -162,10 +212,7 @@ pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult { else => return error.MethodNotSupported, }, .method_pu => switch (c) { - 'T' => { - self.state = .method_complete; - log.debug("Method: PUT", .{}); - }, + 'T' => self.state = .methodComplete(.PUT), else => return error.MethodNotSupported, }, .method_del => switch (c) { @@ -173,10 +220,7 @@ pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult { else => return error.MethodNotSupported, }, .method_hea => switch (c) { - 'D' => { - self.state = .method_complete; - log.debug("Method: HEAD", .{}); - }, + 'D' => self.state = .methodComplete(.HEAD), else => return error.MethodNotSupported, }, .method_pat => switch (c) { @@ -184,10 +228,7 @@ pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult { else => return error.MethodNotSupported, }, .method_pos => switch (c) { - 'T' => { - self.state = .method_complete; - log.debug("Method: POST", .{}); - }, + 'T' => self.state = .methodComplete(.POST), else => return error.MethodNotSupported, }, .method_dele => switch (c) { @@ -195,29 +236,28 @@ pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult { else => return error.MethodNotSupported, }, .method_patc => switch (c) { - 'H' => { - self.state = .method_complete; - log.debug("Method: PATCH", .{}); - }, + 'H' => self.state = .methodComplete(.PATCH), else => return error.MethodNotSupported, }, .method_delet => switch (c) { - 'E' => { - self.state = .method_complete; - log.debug("Method: DELETE", .{}); - }, + 'E' => self.state = .methodComplete(.DELETE), else => return error.MethodNotSupported, }, - .method_complete => switch (c) { - ' ' => self.state = .{ .pathname = @as([*]const u8, @ptrCast(c_ptr))[1..1] }, + .method_complete => |s| switch (c) { + ' ' => self.state = .pathname(s.method, @as([*]const u8, @ptrCast(c_ptr))[1..1]), else => return error.MethodNotSupported, }, - .pathname => |pathname| switch (c) { + .pathname_state => |s| switch (c) { ' ' => { self.state = .pathname_complete; - log.debug("Pathname [{}]: {s}", .{ pathname.len, pathname }); + if (self.callbacks.route) |routeCallback| { + routeCallback(self.callbacks.self, .{ + .method = s.method, + .pathname = s.pathname, + }); + } }, - else => self.state = .{ .pathname = pathname.ptr[0 .. pathname.len + 1] }, + else => self.state = .pathname(s.method, s.pathname.ptr[0 .. s.pathname.len + 1]), }, .pathname_complete => switch (c) { 'H' => self.state = .version_h, @@ -248,10 +288,7 @@ pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult { else => return error.HttpVersionNotSupported, }, .@"version_http/1.@" => switch (c) { - '1' => { - self.state = .version_complete; - log.debug("Version: HTTP/1.1", .{}); - }, + '1' => self.state = .version_complete, else => return error.HttpVersionNotSupported, }, .version_complete => switch (c) { @@ -268,23 +305,24 @@ pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult { }, .header_name => |name| switch (c) { ':' => { - self.state = .{ .header_value = @as([*]const u8, @ptrCast(c_ptr))[1..1] }; + self.state = .headerValue(name, @as([*]const u8, @ptrCast(c_ptr))[1..1]); self.current_header_is_content_length = std.ascii.eqlIgnoreCase(name, "Content-Length"); - log.debug("Header name [{}]: {s}", .{ name.len, name }); }, else => self.state = .{ .header_name = name.ptr[0 .. name.len + 1] }, }, - .header_value => |value| switch (c) { + .header_value => |s| switch (c) { '\r' => { self.state = .header_line_end; - const value_trimmed = std.mem.trim(u8, value, " \t"); - log.debug("Header value [{}]: {s}", .{ value_trimmed.len, value_trimmed }); + const value_trimmed = std.mem.trim(u8, s.value, " \t"); if (self.current_header_is_content_length) { self.content_length = std.fmt.parseInt(usize, value_trimmed, 10) catch return error.InvalidContentLength; self.current_header_is_content_length = false; } + if (self.callbacks.header) |headerCallback| { + headerCallback(self.callbacks.self, s.name, value_trimmed); + } }, - else => self.state = .{ .header_value = value.ptr[0 .. value.len + 1] }, + else => self.state = .headerValue(s.name, s.value.ptr[0 .. s.value.len + 1]), }, .header_line_end => switch (c) { '\n' => self.state = .header_name_start, @@ -293,7 +331,9 @@ pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult { .headers_end => switch (c) { '\n' => { if (self.content_length == 0) { - log.debug("End of request (no body)", .{}); + if (self.callbacks.body) |bodyCallback| { + bodyCallback(self.callbacks.self, &.{}); + } return .done; } self.state = .{ .body = @as([*]const u8, @ptrCast(c_ptr))[1..1] }; @@ -304,6 +344,9 @@ pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult { const new_body = body.ptr[0 .. body.len + 1]; self.state = .{ .body = new_body }; if (new_body.len >= self.content_length) { + if (self.callbacks.body) |bodyCallback| { + bodyCallback(self.callbacks.self, new_body); + } return .done; } },