├── .dockerignore ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── main.yml ├── .gitignore ├── .gitmodules ├── Caddyfile ├── LICENSE ├── README.md ├── build.zig ├── build.zig.zon ├── src ├── callback.zig ├── curl.zig ├── io.zig ├── lib.zig ├── main.zig └── test.zig └── static └── foobar.txt /.dockerignore: -------------------------------------------------------------------------------- 1 | .zig-cache 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.c linguist-vendored 2 | *.h linguist-vendored 3 | *.zig text=auto eol=lf 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | create: 5 | push: 6 | branches: master 7 | pull_request: 8 | schedule: 9 | - cron: "0 18 * * *" 10 | workflow_dispatch: 11 | 12 | concurrency: 13 | group: ci-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: mlugg/setup-zig@v2 22 | with: 23 | version: master 24 | - run: zig fmt --check . 25 | - run: sudo apt-get update && sudo apt-get install -y libcurl4-openssl-dev 26 | - run: zig build 27 | - run: zig build test 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /zig-out 2 | /.zig-cache 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vrischmann/zig-io_uring-http-server/ce5f9fd0ba70127a4ec9e5676a39e847fa0a7203/.gitmodules -------------------------------------------------------------------------------- /Caddyfile: -------------------------------------------------------------------------------- 1 | { 2 | auto_https off 3 | } 4 | 5 | http://localhost:2015 { 6 | file_server { 7 | root static 8 | browse 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Vincent Rischmann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zig-io\_uring-http-server 2 | 3 | Experiment writing a sort of working HTTP server using: 4 | * [io\_uring](https://unixism.net/loti/what_is_io_uring.html) 5 | * [Zig](https://ziglang.org) 6 | * [picohttpparser](https://github.com/h2o/picohttpparser) 7 | 8 | # Requirements 9 | 10 | * Linux 5.11 minimum 11 | * [Zig master](https://ziglang.org/download/) 12 | * libcurl and its development files (`libcurl-devel` on Fedora, `libcurl4-openssl-dev` on Debian) 13 | 14 | # Building 15 | 16 | Just run this: 17 | ``` 18 | zig build 19 | ``` 20 | 21 | The binary will be at `zig-out/bin/httpserver`. 22 | 23 | # Testing 24 | 25 | Just run this: 26 | ``` 27 | zig build test 28 | ``` 29 | 30 | The test harness need libcurl installed to perform request on the HTTP server. 31 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.Build) void { 4 | const target = b.standardTargetOptions(.{}); 5 | const optimize = b.standardOptimizeOption(.{}); 6 | 7 | const debug_callback_internals = b.option(bool, "debug-callback-internals", "Enable callback debugging") orelse false; 8 | const debug_accepts = b.option(bool, "debug-accepts", "Enable debugging for accepts") orelse false; 9 | 10 | const build_options = b.addOptions(); 11 | build_options.addOption(bool, "debug_callback_internals", debug_callback_internals); 12 | build_options.addOption(bool, "debug_accepts", debug_accepts); 13 | 14 | if (target.result.os.tag != .linux) { 15 | b.default_step.dependOn(&b.addFail("io_uring is only available on linux").step); 16 | return; 17 | } 18 | 19 | // 20 | 21 | const picohttpparser_dep = b.dependency("picohttpparser", .{}); 22 | const picohttpparser_mod = picohttpparser_dep.module("picohttpparser"); 23 | 24 | const args_dep = b.dependency("args", .{}); 25 | const args_mod = args_dep.module("args"); 26 | 27 | // 28 | 29 | const exe = b.addExecutable(.{ 30 | .name = "httpserver", 31 | .root_source_file = b.path("src/main.zig"), 32 | .target = target, 33 | .optimize = optimize, 34 | }); 35 | exe.linkLibC(); 36 | exe.root_module.addImport("args", args_mod); 37 | exe.root_module.addImport("picohttpparser", picohttpparser_mod); 38 | exe.root_module.addImport("build_options", build_options.createModule()); 39 | b.installArtifact(exe); 40 | 41 | const tests = b.addTest(.{ 42 | .root_source_file = b.path("src/test.zig"), 43 | .target = target, 44 | .optimize = optimize, 45 | }); 46 | tests.linkSystemLibrary("curl"); 47 | tests.linkLibC(); 48 | tests.root_module.addImport("picohttpparser", picohttpparser_mod); 49 | tests.root_module.addImport("build_options", build_options.createModule()); 50 | const run_tests = b.addRunArtifact(tests); 51 | 52 | const test_step = b.step("test", "Run library tests"); 53 | test_step.dependOn(&run_tests.step); 54 | } 55 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .io_uring_http_server, 3 | .fingerprint = 0x3e0e9b80905d08e1, 4 | .minimum_zig_version = "0.14.0", 5 | .version = "0.1.0", 6 | .paths = .{"."}, 7 | .dependencies = .{ 8 | .picohttpparser = .{ 9 | .url = "git+https://github.com/vrischmann/zig-picohttpparser?ref=master#04c0127dd0e5ebd690c7f436e38e64e67bf0b549", 10 | .hash = "picohttpparser-0.0.0-qPswf6chAACVrJz22IbsMUTltO-7dxLjocn_2BNm8nJN", 11 | }, 12 | .args = .{ 13 | .url = "git+https://github.com/vrischmann/zig-args?ref=fix-latest-zig#2f948fa0292a4bc2a51f1db2a4660802bec4ab32", 14 | .hash = "args-0.0.0-CiLiqv_NAAC97fGpk9hS2K681jkiqPsWP6w3ucb_ctGH", 15 | }, 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /src/callback.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const fmt = std.fmt; 3 | const mem = std.mem; 4 | 5 | const assert = std.debug.assert; 6 | 7 | const io_uring_cqe = std.os.linux.io_uring_cqe; 8 | 9 | /// Callback encapsulates a context and a function pointer that will be called when 10 | /// the server loop will process the CQEs. 11 | /// Pointers to this structure is what get passed as user data in a SQE and what we later get back in a CQE. 12 | /// 13 | /// There are two kinds of callbacks currently: 14 | /// * operations associated with a client 15 | /// * operations not associated with a client 16 | pub fn Callback(comptime ServerType: type, comptime ClientContext: type) type { 17 | return struct { 18 | const Self = @This(); 19 | 20 | server: ServerType, 21 | client_context: ?ClientContext = null, 22 | call: *const fn (ServerType, ?ClientContext, io_uring_cqe) anyerror!void, 23 | 24 | next: ?*Self = null, 25 | 26 | /// Pool is a pool of callback objects that facilitates lifecycle management of a callback. 27 | /// The implementation is a free list of pre-allocated objects. 28 | /// 29 | /// For each SQEs a callback must be obtained via get(). 30 | /// When the server loop is processing CQEs it will use the callback and then release it with put(). 31 | pub const Pool = struct { 32 | allocator: mem.Allocator, 33 | nb: usize, 34 | free_list: ?*Self, 35 | 36 | pub fn init(allocator: mem.Allocator, server: ServerType, nb: usize) !Pool { 37 | var res = Pool{ 38 | .allocator = allocator, 39 | .nb = nb, 40 | .free_list = null, 41 | }; 42 | 43 | // Preallocate as many callbacks as ring entries. 44 | 45 | var i: usize = 0; 46 | while (i < nb) : (i += 1) { 47 | const callback = try allocator.create(Self); 48 | callback.* = .{ 49 | .server = server, 50 | .client_context = undefined, 51 | .call = undefined, 52 | .next = res.free_list, 53 | }; 54 | res.free_list = callback; 55 | } 56 | 57 | return res; 58 | } 59 | 60 | pub fn deinit(self: *Pool) void { 61 | // All callbacks must be put back in the pool before deinit is called 62 | assert(self.count() == self.nb); 63 | 64 | var ret = self.free_list; 65 | while (ret) |item| { 66 | ret = item.next; 67 | self.allocator.destroy(item); 68 | } 69 | } 70 | 71 | /// Returns the number of callback in the pool. 72 | pub fn count(self: *Pool) usize { 73 | var n: usize = 0; 74 | var ret = self.free_list; 75 | while (ret) |item| { 76 | n += 1; 77 | ret = item.next; 78 | } 79 | return n; 80 | } 81 | 82 | /// Returns a ready to use callback or an error if none are available. 83 | /// `cb` must be a function with either one of the following signatures: 84 | /// * fn(ServerType, io_uring_cqe) 85 | /// * fn(ServerType, ClientContext, io_uring_cqe) 86 | /// 87 | /// If `cb` takes a ClientContext `args` must be a tuple with at least the first element being a ClientContext. 88 | pub fn get(self: *Pool, comptime cb: anytype, args: anytype) !*Self { 89 | const ret = self.free_list orelse return error.OutOfCallback; 90 | self.free_list = ret.next; 91 | 92 | // Provide a wrapper based on the callback function. 93 | 94 | const func_args = std.meta.fields(std.meta.ArgsTuple(@TypeOf(cb))); 95 | 96 | switch (func_args.len) { 97 | 3 => { 98 | comptime { 99 | expectFuncArgType(func_args, 0, ServerType); 100 | expectFuncArgType(func_args, 1, ClientContext); 101 | expectFuncArgType(func_args, 2, io_uring_cqe); 102 | } 103 | 104 | ret.client_context = args[0]; 105 | ret.call = struct { 106 | fn wrapper(server: ServerType, client_context: ?ClientContext, cqe: io_uring_cqe) anyerror!void { 107 | return cb(server, client_context.?, cqe); 108 | } 109 | }.wrapper; 110 | }, 111 | 2 => { 112 | comptime { 113 | expectFuncArgType(func_args, 0, ServerType); 114 | expectFuncArgType(func_args, 1, io_uring_cqe); 115 | } 116 | 117 | ret.client_context = null; 118 | ret.call = struct { 119 | fn wrapper(server: ServerType, client_context: ?ClientContext, cqe: io_uring_cqe) anyerror!void { 120 | _ = client_context; 121 | return cb(server, cqe); 122 | } 123 | }.wrapper; 124 | }, 125 | else => @compileError("invalid callback function " ++ @typeName(@TypeOf(cb))), 126 | } 127 | 128 | ret.next = null; 129 | 130 | return ret; 131 | } 132 | 133 | /// Reset the callback and puts it back into the pool. 134 | pub fn put(self: *Pool, callback: *Self) void { 135 | callback.client_context = null; 136 | callback.next = self.free_list; 137 | self.free_list = callback; 138 | } 139 | }; 140 | }; 141 | } 142 | 143 | /// Checks that the argument at `idx` has the type `exp`. 144 | fn expectFuncArgType(comptime args: []const std.builtin.Type.StructField, comptime idx: usize, comptime exp: type) void { 145 | if (args[idx].type != exp) { 146 | const msg = fmt.comptimePrint("expected func arg {d} to be of type {s}, got {s}", .{ 147 | idx, 148 | @typeName(exp), 149 | @typeName(args[idx].field_type), 150 | }); 151 | @compileError(msg); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/curl.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const heap = std.heap; 3 | const io = std.io; 4 | const mem = std.mem; 5 | 6 | const c = @cImport({ 7 | @cInclude("curl/curl.h"); 8 | }); 9 | 10 | pub const Response = struct { 11 | allocator: mem.Allocator, 12 | 13 | response_code: usize, 14 | data: []const u8, 15 | 16 | pub fn deinit(self: *Response) void { 17 | self.allocator.free(self.data); 18 | } 19 | }; 20 | 21 | pub fn do(allocator: mem.Allocator, method: []const u8, url: [:0]const u8, body_opt: ?[]const u8) !Response { 22 | _ = method; 23 | 24 | if (c.curl_global_init(c.CURL_GLOBAL_ALL) != c.CURLE_OK) { 25 | return error.CURLGlobalInitFailed; 26 | } 27 | defer c.curl_global_cleanup(); 28 | 29 | const handle = c.curl_easy_init() orelse return error.CURLHandleInitFailed; 30 | defer c.curl_easy_cleanup(handle); 31 | 32 | var response = std.ArrayList(u8).init(allocator); 33 | 34 | // setup curl options 35 | _ = c.curl_easy_setopt(handle, c.CURLOPT_URL, url.ptr); 36 | 37 | // set write function callbacks 38 | _ = c.curl_easy_setopt(handle, c.CURLOPT_WRITEFUNCTION, &writeToArrayListCallback); 39 | _ = c.curl_easy_setopt(handle, c.CURLOPT_WRITEDATA, &response); 40 | 41 | // set read function callbacks 42 | 43 | var headers: [*c]c.curl_slist = null; 44 | defer c.curl_slist_free_all(headers); 45 | 46 | if (body_opt) |data| { 47 | headers = c.curl_slist_append(headers, "Content-Type: application/json"); 48 | 49 | _ = c.curl_easy_setopt(handle, c.CURLOPT_HTTPHEADER, headers); 50 | _ = c.curl_easy_setopt(handle, c.CURLOPT_POSTFIELDSIZE, data.len); 51 | _ = c.curl_easy_setopt(handle, c.CURLOPT_COPYPOSTFIELDS, data.ptr); 52 | } 53 | 54 | // perform 55 | if (c.curl_easy_perform(handle) != c.CURLE_OK) { 56 | return error.FailedToPerformRequest; 57 | } 58 | 59 | // get information 60 | var res = Response{ 61 | .allocator = allocator, 62 | .response_code = 0, 63 | .data = try response.toOwnedSlice(), 64 | }; 65 | 66 | _ = c.curl_easy_getinfo(handle, c.CURLINFO_RESPONSE_CODE, &res.response_code); 67 | 68 | return res; 69 | } 70 | 71 | fn writeToArrayListCallback(data: *anyopaque, size: c_uint, nmemb: c_uint, user_data: *anyopaque) callconv(.C) c_uint { 72 | var buffer = @as(*std.ArrayList(u8), @ptrFromInt(@intFromPtr(user_data))); 73 | var typed_data = @as([*]u8, @ptrFromInt(@intFromPtr(data))); 74 | 75 | buffer.appendSlice(typed_data[0 .. nmemb * size]) catch return 0; 76 | 77 | return nmemb * size; 78 | } 79 | -------------------------------------------------------------------------------- /src/io.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const debug = std.debug; 3 | const mem = std.mem; 4 | const net = std.net; 5 | const os = std.os; 6 | const posix = std.posix; 7 | 8 | const IoUring = std.os.linux.IoUring; 9 | 10 | const logger = std.log.scoped(.io_helpers); 11 | 12 | // TODO(vincent): make this dynamic 13 | const max_connections = 128; 14 | 15 | pub const RegisteredFile = struct { 16 | fd: posix.fd_t, 17 | size: u64, 18 | }; 19 | 20 | /// Manages a set of registered file descriptors. 21 | /// The set size is fixed at compile time. 22 | /// 23 | /// A client must acquire a file descriptor to use it, and release it when it disconnects. 24 | pub const RegisteredFileDescriptors = struct { 25 | const Self = @This(); 26 | 27 | const State = enum { 28 | used, 29 | free, 30 | }; 31 | 32 | fds: [max_connections]posix.fd_t = [_]posix.fd_t{-1} ** max_connections, 33 | states: [max_connections]State = [_]State{.free} ** max_connections, 34 | 35 | pub fn register(self: *Self, ring: *IoUring) !void { 36 | logger.debug("REGISTERED FILE DESCRIPTORS, fds={d}", .{ 37 | self.fds, 38 | }); 39 | 40 | try ring.register_files(self.fds[0..]); 41 | } 42 | 43 | pub fn update(self: *Self, ring: *IoUring) !void { 44 | logger.debug("UPDATE FILE DESCRIPTORS, fds={d}", .{ 45 | self.fds, 46 | }); 47 | 48 | try ring.register_files_update(0, self.fds[0..]); 49 | } 50 | 51 | pub fn acquire(self: *Self, fd: posix.fd_t) ?i32 { 52 | // Find a free slot in the states array 53 | for (&self.states, 0..) |*state, i| { 54 | if (state.* == .free) { 55 | // Slot is free, change its state and set the file descriptor. 56 | 57 | state.* = .used; 58 | self.fds[i] = fd; 59 | 60 | return @as(i32, @intCast(i)); 61 | } 62 | } else { 63 | return null; 64 | } 65 | } 66 | 67 | pub fn release(self: *Self, index: i32) void { 68 | const idx = @as(usize, @intCast(index)); 69 | 70 | debug.assert(self.states[idx] == .used); 71 | debug.assert(self.fds[idx] != -1); 72 | 73 | self.states[idx] = .free; 74 | self.fds[idx] = -1; 75 | } 76 | }; 77 | 78 | /// Creates a server socket, bind it and listen on it. 79 | /// 80 | /// This enables SO_REUSEADDR so that we can have multiple listeners 81 | /// on the same port, that way the kernel load balances connections to our workers. 82 | pub fn createSocket(port: u16) !posix.socket_t { 83 | const sockfd = try posix.socket(posix.AF.INET6, posix.SOCK.STREAM, 0); 84 | errdefer posix.close(sockfd); 85 | 86 | // Enable reuseaddr if possible 87 | posix.setsockopt( 88 | sockfd, 89 | posix.SOL.SOCKET, 90 | posix.SO.REUSEPORT, 91 | &mem.toBytes(@as(c_int, 1)), 92 | ) catch {}; 93 | 94 | // Disable IPv6 only 95 | try posix.setsockopt( 96 | sockfd, 97 | posix.IPPROTO.IPV6, 98 | os.linux.IPV6.V6ONLY, 99 | &mem.toBytes(@as(c_int, 0)), 100 | ); 101 | 102 | const addr = try net.Address.parseIp6("::0", port); 103 | 104 | try posix.bind(sockfd, &addr.any, @sizeOf(posix.sockaddr.in6)); 105 | try posix.listen(sockfd, std.math.maxInt(u31)); 106 | 107 | return sockfd; 108 | } 109 | -------------------------------------------------------------------------------- /src/lib.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const build_options = @import("build_options"); 3 | const ascii = std.ascii; 4 | const debug = std.debug; 5 | const fmt = std.fmt; 6 | const heap = std.heap; 7 | const io = std.io; 8 | const mem = std.mem; 9 | const net = std.net; 10 | const os = std.os; 11 | const time = std.time; 12 | const posix = std.posix; 13 | 14 | const picohttpparser = @import("picohttpparser"); 15 | 16 | const Atomic = std.atomic.Value; 17 | const assert = std.debug.assert; 18 | 19 | const IoUring = std.os.linux.IoUring; 20 | const io_uring_cqe = std.os.linux.io_uring_cqe; 21 | const io_uring_sqe = std.os.linux.io_uring_sqe; 22 | 23 | pub const createSocket = @import("io.zig").createSocket; 24 | const RegisteredFile = @import("io.zig").RegisteredFile; 25 | const RegisteredFileDescriptors = @import("io.zig").RegisteredFileDescriptors; 26 | const Callback = @import("callback.zig").Callback; 27 | 28 | const logger = std.log.scoped(.main); 29 | 30 | /// HTTP types and stuff 31 | pub const Method = enum(u4) { 32 | get, 33 | head, 34 | post, 35 | put, 36 | delete, 37 | connect, 38 | options, 39 | trace, 40 | patch, 41 | 42 | pub fn toString(self: Method) []const u8 { 43 | switch (self) { 44 | .get => return "GET", 45 | .head => return "HEAD", 46 | .post => return "POST", 47 | .put => return "PUT", 48 | .delete => return "DELETE", 49 | .connect => return "CONNECT", 50 | .options => return "OPTIONS", 51 | .trace => return "TRACE", 52 | .patch => return "PATCH", 53 | } 54 | } 55 | 56 | fn fromString(s: []const u8) !Method { 57 | if (ascii.eqlIgnoreCase(s, "GET")) { 58 | return .get; 59 | } else if (ascii.eqlIgnoreCase(s, "HEAD")) { 60 | return .head; 61 | } else if (ascii.eqlIgnoreCase(s, "POST")) { 62 | return .post; 63 | } else if (ascii.eqlIgnoreCase(s, "PUT")) { 64 | return .put; 65 | } else if (ascii.eqlIgnoreCase(s, "DELETE")) { 66 | return .delete; 67 | } else if (ascii.eqlIgnoreCase(s, "CONNECT")) { 68 | return .connect; 69 | } else if (ascii.eqlIgnoreCase(s, "OPTIONS")) { 70 | return .options; 71 | } else if (ascii.eqlIgnoreCase(s, "TRACE")) { 72 | return .trace; 73 | } else if (ascii.eqlIgnoreCase(s, "PATCH")) { 74 | return .patch; 75 | } else { 76 | return error.InvalidMethod; 77 | } 78 | } 79 | }; 80 | 81 | pub const StatusCode = enum(u10) { 82 | // informational 83 | continue_ = 100, 84 | switching_protocols = 101, 85 | 86 | // success 87 | ok = 200, 88 | created = 201, 89 | accepted = 202, 90 | no_content = 204, 91 | partial_content = 206, 92 | 93 | // redirection 94 | moved_permanently = 301, 95 | found = 302, 96 | not_modified = 304, 97 | temporary_redirect = 307, 98 | permanent_redirect = 308, 99 | 100 | // client error 101 | bad_request = 400, 102 | unauthorized = 401, 103 | forbidden = 403, 104 | not_found = 404, 105 | method_not_allowed = 405, 106 | not_acceptable = 406, 107 | gone = 410, 108 | too_many_requests = 429, 109 | 110 | // server error 111 | internal_server_error = 500, 112 | bad_gateway = 502, 113 | service_unavailable = 503, 114 | gateway_timeout = 504, 115 | 116 | pub fn toString(self: StatusCode) []const u8 { 117 | switch (self) { 118 | // informational 119 | .continue_ => return "Continue", 120 | .switching_protocols => return "Switching Protocols", 121 | 122 | .ok => return "OK", 123 | .created => return "Created", 124 | .accepted => return "Accepted", 125 | .no_content => return "No Content", 126 | .partial_content => return "Partial Content", 127 | 128 | // redirection 129 | .moved_permanently => return "Moved Permanently", 130 | .found => return "Found", 131 | .not_modified => return "Not Modified", 132 | .temporary_redirect => return "Temporary Redirected", 133 | .permanent_redirect => return "Permanent Redirect", 134 | 135 | // client error 136 | .bad_request => return "Bad Request", 137 | .unauthorized => return "Unauthorized", 138 | .forbidden => return "Forbidden", 139 | .not_found => return "Not Found", 140 | .method_not_allowed => return "Method Not Allowed", 141 | .not_acceptable => return "Not Acceptable", 142 | .gone => return "Gone", 143 | .too_many_requests => return "Too Many Requests", 144 | 145 | // server error 146 | .internal_server_error => return "Internal Server Error", 147 | .bad_gateway => return "Bad Gateway", 148 | .service_unavailable => return "Service Unavailable", 149 | .gateway_timeout => return "Gateway Timeout", 150 | } 151 | } 152 | }; 153 | 154 | pub const Headers = struct { 155 | storage: [picohttpparser.RawRequest.max_headers]picohttpparser.RawHeader, 156 | view: []picohttpparser.RawHeader, 157 | 158 | fn create(req: picohttpparser.RawRequest) !Headers { 159 | assert(req.num_headers < picohttpparser.RawRequest.max_headers); 160 | 161 | var res = Headers{ 162 | .storage = undefined, 163 | .view = undefined, 164 | }; 165 | 166 | const num_headers = try req.copyHeaders(&res.storage); 167 | res.view = res.storage[0..num_headers]; 168 | 169 | return res; 170 | } 171 | 172 | pub fn get(self: Headers, name: []const u8) ?picohttpparser.RawHeader { 173 | for (self.view) |item| { 174 | if (ascii.eqlIgnoreCase(name, item.name)) { 175 | return item; 176 | } 177 | } 178 | return null; 179 | } 180 | }; 181 | 182 | /// Contains peer information for a request. 183 | pub const Peer = struct { 184 | addr: net.Address, 185 | }; 186 | 187 | /// Contains request data. 188 | /// This is what the handler will receive. 189 | pub const Request = struct { 190 | method: Method, 191 | path: []const u8, 192 | minor_version: usize, 193 | headers: Headers, 194 | body: ?[]const u8, 195 | 196 | fn create(req: picohttpparser.RawRequest, body: ?[]const u8) !Request { 197 | return Request{ 198 | .method = try Method.fromString(req.getMethod()), 199 | .path = req.getPath(), 200 | .minor_version = req.getMinorVersion(), 201 | .headers = try Headers.create(req), 202 | .body = body, 203 | }; 204 | } 205 | }; 206 | 207 | /// The response returned by the handler. 208 | pub const Response = union(enum) { 209 | /// The response is a simple buffer. 210 | response: struct { 211 | status_code: StatusCode, 212 | headers: []picohttpparser.RawHeader, 213 | data: []const u8, 214 | }, 215 | /// The response is a static file that will be read from the filesystem. 216 | send_file: struct { 217 | status_code: StatusCode, 218 | headers: []picohttpparser.RawHeader, 219 | path: []const u8, 220 | }, 221 | }; 222 | 223 | pub fn RequestHandler(comptime Context: type) type { 224 | return *const fn (Context, mem.Allocator, Peer, Request) anyerror!Response; 225 | } 226 | 227 | const ResponseStateFileDescriptor = union(enum) { 228 | direct: posix.fd_t, 229 | registered: posix.fd_t, 230 | 231 | pub fn format(self: ResponseStateFileDescriptor, comptime fmt_string: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { 232 | _ = options; 233 | 234 | if (comptime !mem.eql(u8, "s", fmt_string)) @compileError("format string must be s"); 235 | switch (self) { 236 | .direct => |fd| try writer.print("(direct fd={d})", .{fd}), 237 | .registered => |fd| try writer.print("(registered fd={d})", .{fd}), 238 | } 239 | } 240 | }; 241 | 242 | const ClientState = struct { 243 | const RequestState = struct { 244 | parse_result: picohttpparser.ParseRequestResult = .{ 245 | .raw_request = .{}, 246 | .consumed = 0, 247 | }, 248 | content_length: ?usize = null, 249 | /// this is a view into the client buffer 250 | body: ?[]const u8 = null, 251 | }; 252 | 253 | /// Holds state used to send a response to the client. 254 | const ResponseState = struct { 255 | /// status code and header are overwritable in the handler 256 | status_code: StatusCode = .ok, 257 | headers: []picohttpparser.RawHeader = &[_]picohttpparser.RawHeader{}, 258 | 259 | /// state used when we need to send a static file from the filesystem. 260 | file: struct { 261 | path: [:0]u8 = undefined, 262 | fd: ResponseStateFileDescriptor = undefined, 263 | statx_buf: os.linux.Statx = undefined, 264 | 265 | offset: usize = 0, 266 | } = .{}, 267 | }; 268 | 269 | gpa: mem.Allocator, 270 | 271 | /// peer information associated with this client 272 | peer: Peer, 273 | fd: posix.socket_t, 274 | 275 | // Buffer and allocator used for small allocations (nul-terminated path, integer to int conversions etc). 276 | temp_buffer: [128]u8 = undefined, 277 | temp_buffer_fba: heap.FixedBufferAllocator = undefined, 278 | 279 | // TODO(vincent): prevent going over the max_buffer_size somehow ("limiting" allocator ?) 280 | // TODO(vincent): right now we always use clearRetainingCapacity() which may keep a lot of memory 281 | // allocated for no reason. 282 | // Implement some sort of statistics to determine if we should release memory, for example: 283 | // * max size used by the last 100 requests for reads or writes 284 | // * duration without any request before releasing everything 285 | buffer: std.ArrayList(u8), 286 | 287 | request_state: RequestState = .{}, 288 | response_state: ResponseState = .{}, 289 | 290 | pub fn init(self: *ClientState, allocator: mem.Allocator, peer_addr: net.Address, client_fd: posix.socket_t, max_buffer_size: usize) !void { 291 | self.* = .{ 292 | .gpa = allocator, 293 | .peer = .{ 294 | .addr = peer_addr, 295 | }, 296 | .fd = client_fd, 297 | .buffer = undefined, 298 | }; 299 | self.temp_buffer_fba = heap.FixedBufferAllocator.init(&self.temp_buffer); 300 | 301 | self.buffer = try std.ArrayList(u8).initCapacity(self.gpa, max_buffer_size); 302 | } 303 | 304 | pub fn deinit(self: *ClientState) void { 305 | self.buffer.deinit(); 306 | } 307 | 308 | fn refreshBody(self: *ClientState) void { 309 | const consumed = self.request_state.parse_result.consumed; 310 | if (consumed > 0) { 311 | self.request_state.body = self.buffer.items[consumed..]; 312 | } 313 | } 314 | 315 | pub fn reset(self: *ClientState) void { 316 | self.request_state = .{}; 317 | self.response_state = .{}; 318 | self.buffer.clearRetainingCapacity(); 319 | } 320 | 321 | fn startWritingResponse(self: *ClientState, content_length: ?usize) !void { 322 | var writer = self.buffer.writer(); 323 | 324 | try writer.print("HTTP/1.1 {d} {s}\n", .{ 325 | @intFromEnum(self.response_state.status_code), 326 | self.response_state.status_code.toString(), 327 | }); 328 | for (self.response_state.headers) |header| { 329 | try writer.print("{s}: {s}\n", .{ header.name, header.value }); 330 | } 331 | if (content_length) |n| { 332 | try writer.print("Content-Length: {d}\n", .{n}); 333 | } 334 | try writer.print("\n", .{}); 335 | } 336 | }; 337 | 338 | pub const ServerOptions = struct { 339 | max_ring_entries: u13 = 512, 340 | max_buffer_size: usize = 4096, 341 | max_connections: usize = 128, 342 | }; 343 | 344 | /// The HTTP server. 345 | /// 346 | /// This struct does nothing by itself, the caller must drive it to achieve anything. 347 | /// After initialization the caller must, in a loop: 348 | /// * call maybeAccept 349 | /// * call submit 350 | /// * call processCompletions 351 | /// 352 | /// Then the server will accept connections and process requests. 353 | /// 354 | /// NOTE: this is _not_ thread safe ! You must create on Server object per thread. 355 | pub fn Server(comptime Context: type) type { 356 | return struct { 357 | const Self = @This(); 358 | const CallbackType = Callback(*Self, *ClientState); 359 | 360 | /// allocator used to allocate each client state 361 | root_allocator: mem.Allocator, 362 | 363 | /// uring dedicated to this server object. 364 | ring: IoUring, 365 | 366 | /// options controlling the behaviour of the server. 367 | options: ServerOptions, 368 | 369 | /// indicates if the server should continue running. 370 | /// This is _not_ owned by the server but by the caller. 371 | running: *Atomic(bool), 372 | 373 | /// This field lets us keep track of the number of pending operations which is necessary to implement drain() properly. 374 | /// 375 | /// Note that this is different than the number of SQEs pending in the submission queue or CQEs pending in the completion queue. 376 | /// For example an accept operation which has been consumed by the kernel but hasn't accepted any connection yet must be considered 377 | /// pending for us but it's not pending in either the submission or completion queue. 378 | /// Another example is a timeout: once accepted and until expired it won't be available in the completion queue. 379 | pending: usize = 0, 380 | 381 | /// Listener state 382 | listener: struct { 383 | /// server file descriptor used for accept(2) operation. 384 | /// Must have had bind(2) and listen(2) called on it before being passed to `init()`. 385 | server_fd: posix.socket_t, 386 | 387 | /// indicates if an accept operation is pending. 388 | accept_waiting: bool = false, 389 | 390 | /// the timeout data for the link_timeout operation linked to the previous accept. 391 | /// 392 | /// Each accept operation has a following timeout linked to it; this works in such a way 393 | /// that if the timeout has expired the accept operation is cancelled and if the accept has finished 394 | /// before the timeout then the timeout operation is cancelled. 395 | /// 396 | /// This is useful to run the main loop for a bounded duration. 397 | timeout: os.linux.kernel_timespec = .{ 398 | .sec = 0, 399 | .nsec = 0, 400 | }, 401 | 402 | // Next peer we're accepting. 403 | // Will be valid after a successful CQE for an accept operation. 404 | peer_addr: net.Address = net.Address{ 405 | .any = undefined, 406 | }, 407 | peer_addr_size: u32 = @sizeOf(posix.sockaddr), 408 | }, 409 | 410 | /// CQEs storage 411 | cqes: []io_uring_cqe = undefined, 412 | 413 | /// List of client states. 414 | /// A new state is created for each socket accepted and destroyed when the socket is closed for any reason. 415 | clients: std.ArrayList(*ClientState), 416 | 417 | /// Free list of callback objects necessary for working with the uring. 418 | /// See the documentation of Callback.Pool. 419 | callbacks: CallbackType.Pool, 420 | 421 | /// Set of registered file descriptors for use with the uring. 422 | /// 423 | /// TODO(vincent): make use of this somehow ? right now it crashes the kernel. 424 | registered_fds: RegisteredFileDescriptors, 425 | registered_files: std.StringHashMap(RegisteredFile), 426 | 427 | user_context: Context, 428 | handler: RequestHandler(Context), 429 | 430 | /// initializes a Server object. 431 | pub fn init( 432 | self: *Self, 433 | /// General purpose allocator which will: 434 | /// * allocate all client states (including request/response bodies). 435 | /// * allocate the callback pool 436 | /// Depending on the workload the allocator can be hit quite often (for example if all clients close their connection). 437 | allocator: mem.Allocator, 438 | /// controls the behaviour of the server (max number of connections, max buffer size, etc). 439 | options: ServerOptions, 440 | /// owned by the caller and indicates if the server should shutdown properly. 441 | running: *Atomic(bool), 442 | /// must be a socket properly initialized with listen(2) and bind(2) which will be used for accept(2) operations. 443 | server_fd: posix.socket_t, 444 | /// user provided context that will be passed to the request handlers. 445 | user_context: Context, 446 | /// user provied request handler. 447 | comptime handler: RequestHandler(Context), 448 | ) !void { 449 | // TODO(vincent): probe for available features for io_uring ? 450 | 451 | self.* = .{ 452 | .root_allocator = allocator, 453 | .ring = try std.os.linux.IoUring.init(options.max_ring_entries, 0), 454 | .options = options, 455 | .running = running, 456 | .listener = .{ 457 | .server_fd = server_fd, 458 | }, 459 | .cqes = try allocator.alloc(io_uring_cqe, options.max_ring_entries), 460 | .clients = try std.ArrayList(*ClientState).initCapacity(allocator, options.max_connections), 461 | .callbacks = undefined, 462 | .registered_fds = .{}, 463 | .registered_files = std.StringHashMap(RegisteredFile).init(allocator), 464 | .user_context = user_context, 465 | .handler = handler, 466 | }; 467 | 468 | self.callbacks = try CallbackType.Pool.init(allocator, self, options.max_ring_entries); 469 | 470 | try self.registered_fds.register(&self.ring); 471 | } 472 | 473 | pub fn deinit(self: *Self) void { 474 | var registered_files_iterator = self.registered_files.iterator(); 475 | while (registered_files_iterator.next()) |entry| { 476 | self.root_allocator.free(entry.key_ptr.*); 477 | } 478 | self.registered_files.deinit(); 479 | 480 | for (self.clients.items) |client| { 481 | client.deinit(); 482 | self.root_allocator.destroy(client); 483 | } 484 | self.clients.deinit(); 485 | 486 | self.callbacks.deinit(); 487 | self.root_allocator.free(self.cqes); 488 | self.ring.deinit(); 489 | } 490 | 491 | /// Runs the main loop until the `running` boolean is false. 492 | /// 493 | /// `accept_timeout` controls how much time the loop can wait for an accept operation to finish. 494 | /// This duration is the lower bound duration before the main loop can stop when `running` is false; 495 | pub fn run(self: *Self, accept_timeout: u63) !void { 496 | // TODO(vincent): we don't properly shutdown the peer sockets; we should do that. 497 | // This can be done using standard close(2) calls I think. 498 | 499 | while (self.running.load(.seq_cst)) { 500 | // first step: (maybe) submit and accept with a link_timeout linked to it. 501 | // 502 | // Nothing is submitted if: 503 | // * a previous accept operation is already waiting. 504 | // * the number of connected clients reached the predefined limit. 505 | try self.maybeAccept(accept_timeout); 506 | 507 | // second step: submit to the kernel all previous queued SQE. 508 | // 509 | // SQEs might be queued by the maybeAccept call above or by the processCompletions call below, but 510 | // obviously in that case its SQEs queued from the _previous iteration_ that are submitted to the kernel. 511 | // 512 | // Additionally we wait for at least 1 CQE to be available, if none is available the thread will be put to sleep by the kernel. 513 | // Note that this doesn't work if the uring is setup with busy-waiting. 514 | const submitted = try self.submit(1); 515 | 516 | // third step: process all available CQEs. 517 | // 518 | // This asks the kernel to wait for at least `submitted` CQE to be available. 519 | // Since we successfully submitted that many SQEs it is guaranteed we will _at some point_ 520 | // get that many CQEs but there's no guarantee they will be available instantly; if the 521 | // kernel lags in processing the SQEs we can have a delay in getting the CQEs. 522 | // This is further accentuated by the number of pending SQEs we can have. 523 | // 524 | // One example would be submitting a lot of fdatasync operations on slow devices. 525 | _ = try self.processCompletions(submitted); 526 | } 527 | try self.drain(); 528 | } 529 | 530 | fn maybeAccept(self: *Self, timeout: u63) !void { 531 | if (!self.running.load(.seq_cst)) { 532 | // we must stop: stop accepting connections. 533 | return; 534 | } 535 | if (self.listener.accept_waiting or self.clients.items.len >= self.options.max_connections) { 536 | return; 537 | } 538 | 539 | // Queue an accept and link it to a timeout. 540 | 541 | var sqe = try self.submitAccept(); 542 | sqe.flags |= os.linux.IOSQE_IO_LINK; 543 | 544 | self.listener.timeout.sec = 0; 545 | self.listener.timeout.nsec = timeout; 546 | 547 | _ = try self.submitAcceptLinkTimeout(); 548 | 549 | self.listener.accept_waiting = true; 550 | } 551 | 552 | /// Continuously submit SQEs and process completions until there are 553 | /// no more pending operations. 554 | /// 555 | /// This must be called when shutting down. 556 | fn drain(self: *Self) !void { 557 | // This call is only useful if pending > 0. 558 | // 559 | // It is currently impossible to have pending == 0 after an iteration of the main loop because: 560 | // * if no accept waiting maybeAccept `pending` will increase by 2. 561 | // * if an accept is waiting but we didn't get a connection, `pending` must still be >= 1. 562 | // * if an accept is waiting and we got a connection, the previous processCompletions call 563 | // increased `pending` while doing request processing. 564 | // * if no accept waiting and too many connections, the previous processCompletions call 565 | // increased `pending` while doing request processing. 566 | // 567 | // But to be extra sure we do this submit call outside the drain loop to ensure we have flushed all queued SQEs 568 | // submitted in the last processCompletions call in the main loop. 569 | 570 | _ = try self.submit(0); 571 | 572 | while (self.pending > 0) { 573 | _ = try self.submit(0); 574 | _ = try self.processCompletions(self.pending); 575 | } 576 | } 577 | 578 | /// Submits all pending SQE to the kernel, if any. 579 | /// Waits for `nr` events to be completed before returning (0 means don't wait). 580 | /// 581 | /// This also increments `pending` by the number of events submitted. 582 | /// 583 | /// Returns the number of events submitted. 584 | fn submit(self: *Self, nr: u32) !usize { 585 | const n = try self.ring.submit_and_wait(nr); 586 | self.pending += n; 587 | return n; 588 | } 589 | 590 | /// Process all ready CQEs, if any. 591 | /// Waits for `nr` events to be completed before processing begins (0 means don't wait). 592 | /// 593 | /// This also decrements `pending` by the number of events processed. 594 | /// 595 | /// Returnsd the number of events processed. 596 | fn processCompletions(self: *Self, nr: usize) !usize { 597 | // TODO(vincent): how should we handle EAGAIN and EINTR ? right now they will shutdown the server. 598 | const cqe_count = try self.ring.copy_cqes(self.cqes, @as(u32, @intCast(nr))); 599 | 600 | for (self.cqes[0..cqe_count]) |cqe| { 601 | debug.assert(cqe.user_data != 0); 602 | 603 | // We know that a SQE/CQE is _always_ associated with a pointer of type Callback. 604 | 605 | var cb = @as(*CallbackType, @ptrFromInt(cqe.user_data)); 606 | defer self.callbacks.put(cb); 607 | 608 | // Call the provided function with the proper context. 609 | // 610 | // Note that while the callback function signature can return an error we don't bubble them up 611 | // simply because we can't shutdown the server due to a processing error. 612 | 613 | cb.call(cb.server, cb.client_context, cqe) catch |err| { 614 | self.handleCallbackError(cb.client_context, err); 615 | }; 616 | } 617 | 618 | self.pending -= cqe_count; 619 | 620 | return cqe_count; 621 | } 622 | 623 | fn handleCallbackError(self: *Self, client_opt: ?*ClientState, err: anyerror) void { 624 | if (err == error.Canceled) return; 625 | 626 | if (client_opt) |client| { 627 | switch (err) { 628 | error.ConnectionResetByPeer => { 629 | logger.info("ctx#{s:<4} client fd={d} disconnected", .{ self.user_context, client.fd }); 630 | }, 631 | error.UnexpectedEOF => { 632 | logger.debug("ctx#{s:<4} unexpected eof", .{self.user_context}); 633 | }, 634 | else => { 635 | logger.err("ctx#{s:<4} unexpected error {!}", .{ self.user_context, err }); 636 | }, 637 | } 638 | 639 | _ = self.submitClose(client, client.fd, onCloseClient) catch {}; 640 | } else { 641 | logger.err("ctx#{s:<4} unexpected error {!}", .{ self.user_context, err }); 642 | } 643 | } 644 | 645 | fn submitAccept(self: *Self) !*io_uring_sqe { 646 | if (build_options.debug_accepts) { 647 | logger.debug("ctx#{s:<4} submitting accept on {d}", .{ 648 | self.user_context, 649 | self.listener.server_fd, 650 | }); 651 | } 652 | 653 | const tmp = try self.callbacks.get(onAccept, .{}); 654 | 655 | return try self.ring.accept( 656 | @intFromPtr(tmp), 657 | self.listener.server_fd, 658 | &self.listener.peer_addr.any, 659 | &self.listener.peer_addr_size, 660 | 0, 661 | ); 662 | } 663 | 664 | fn submitAcceptLinkTimeout(self: *Self) !*io_uring_sqe { 665 | if (build_options.debug_accepts) { 666 | logger.debug("ctx#{s:<4} submitting link timeout", .{self.user_context}); 667 | } 668 | 669 | const tmp = try self.callbacks.get(onAcceptLinkTimeout, .{}); 670 | 671 | return self.ring.link_timeout( 672 | @intFromPtr(tmp), 673 | &self.listener.timeout, 674 | 0, 675 | ); 676 | } 677 | 678 | fn submitStandaloneClose(self: *Self, fd: posix.fd_t, comptime cb: anytype) !*io_uring_sqe { 679 | logger.debug("ctx#{s:<4} submitting close of {d}", .{ 680 | self.user_context, 681 | fd, 682 | }); 683 | 684 | const tmp = try self.callbacks.get(cb, .{}); 685 | 686 | return self.ring.close( 687 | @intFromPtr(tmp), 688 | fd, 689 | ); 690 | } 691 | 692 | fn submitClose(self: *Self, client: *ClientState, fd: posix.fd_t, comptime cb: anytype) !*io_uring_sqe { 693 | logger.debug("ctx#{s:<4} addr={} submitting close of {d}", .{ 694 | self.user_context, 695 | client.peer.addr, 696 | fd, 697 | }); 698 | 699 | const tmp = try self.callbacks.get(cb, .{client}); 700 | 701 | return self.ring.close( 702 | @intFromPtr(tmp), 703 | fd, 704 | ); 705 | } 706 | 707 | fn onAccept(self: *Self, cqe: os.linux.io_uring_cqe) !void { 708 | defer self.listener.accept_waiting = false; 709 | 710 | switch (cqe.err()) { 711 | .SUCCESS => {}, 712 | .INTR => { 713 | logger.debug("ctx#{s:<4} ON ACCEPT interrupted", .{self.user_context}); 714 | return error.Canceled; 715 | }, 716 | .CANCELED => { 717 | if (build_options.debug_accepts) { 718 | logger.debug("ctx#{s:<4} ON ACCEPT timed out", .{self.user_context}); 719 | } 720 | return error.Canceled; 721 | }, 722 | else => |err| { 723 | logger.err("ctx#{s:<4} ON ACCEPT unexpected errno={}", .{ self.user_context, err }); 724 | return error.Unexpected; 725 | }, 726 | } 727 | 728 | logger.debug("ctx#{s:<4} ON ACCEPT accepting connection from {}", .{ self.user_context, self.listener.peer_addr }); 729 | 730 | const client_fd = @as(posix.socket_t, @intCast(cqe.res)); 731 | 732 | var client = try self.root_allocator.create(ClientState); 733 | errdefer self.root_allocator.destroy(client); 734 | 735 | try client.init( 736 | self.root_allocator, 737 | self.listener.peer_addr, 738 | client_fd, 739 | self.options.max_buffer_size, 740 | ); 741 | errdefer client.deinit(); 742 | 743 | try self.clients.append(client); 744 | 745 | _ = try self.submitRead(client, client_fd, 0, onReadRequest); 746 | } 747 | 748 | fn onAcceptLinkTimeout(self: *Self, cqe: os.linux.io_uring_cqe) !void { 749 | switch (cqe.err()) { 750 | .CANCELED => { 751 | if (build_options.debug_accepts) { 752 | logger.debug("ctx#{s:<4} ON LINK TIMEOUT operation finished, timeout canceled", .{self.user_context}); 753 | } 754 | }, 755 | .ALREADY => { 756 | if (build_options.debug_accepts) { 757 | logger.debug("ctx#{s:<4} ON LINK TIMEOUT operation already finished before timeout expired", .{self.user_context}); 758 | } 759 | }, 760 | .TIME => { 761 | if (build_options.debug_accepts) { 762 | logger.debug("ctx#{s:<4} ON LINK TIMEOUT timeout finished before accept", .{self.user_context}); 763 | } 764 | }, 765 | else => |err| { 766 | logger.err("ctx#{s:<4} ON LINK TIMEOUT unexpected errno={}", .{ self.user_context, err }); 767 | return error.Unexpected; 768 | }, 769 | } 770 | } 771 | 772 | fn onCloseClient(self: *Self, client: *ClientState, cqe: os.linux.io_uring_cqe) !void { 773 | logger.debug("ctx#{s:<4} addr={} ON CLOSE CLIENT fd={}", .{ 774 | self.user_context, 775 | client.peer.addr, 776 | client.fd, 777 | }); 778 | 779 | // Cleanup resources 780 | client.deinit(); 781 | self.root_allocator.destroy(client); 782 | 783 | // Remove client from list 784 | const maybe_pos: ?usize = for (self.clients.items, 0..) |item, i| { 785 | if (item == client) { 786 | break i; 787 | } 788 | } else blk: { 789 | break :blk null; 790 | }; 791 | if (maybe_pos) |pos| _ = self.clients.orderedRemove(pos); 792 | 793 | switch (cqe.err()) { 794 | .SUCCESS => {}, 795 | else => |err| { 796 | logger.err("ctx#{s:<4} unexpected errno={}", .{ self.user_context, err }); 797 | return error.Unexpected; 798 | }, 799 | } 800 | } 801 | 802 | fn onClose(self: *Self, cqe: os.linux.io_uring_cqe) !void { 803 | logger.debug("ctx#{s:<4} ON CLOSE", .{self.user_context}); 804 | 805 | switch (cqe.err()) { 806 | .SUCCESS => {}, 807 | else => |err| { 808 | logger.err("ctx#{s:<4} unexpected errno={}", .{ self.user_context, err }); 809 | return error.Unexpected; 810 | }, 811 | } 812 | } 813 | 814 | fn onReadRequest(self: *Self, client: *ClientState, cqe: io_uring_cqe) !void { 815 | switch (cqe.err()) { 816 | .SUCCESS => {}, 817 | .PIPE => { 818 | logger.err("ctx#{s:<4} addr={} broken pipe", .{ self.user_context, client.peer.addr }); 819 | return error.BrokenPipe; 820 | }, 821 | .CONNRESET => { 822 | logger.debug("ctx#{s:<4} addr={} connection reset by peer", .{ self.user_context, client.peer.addr }); 823 | return error.ConnectionResetByPeer; 824 | }, 825 | else => |err| { 826 | logger.err("ctx#{s:<4} addr={} unexpected errno={}", .{ self.user_context, client.peer.addr, err }); 827 | return error.Unexpected; 828 | }, 829 | } 830 | if (cqe.res <= 0) { 831 | return error.UnexpectedEOF; 832 | } 833 | 834 | const read = @as(usize, @intCast(cqe.res)); 835 | 836 | logger.debug("ctx#{s:<4} addr={} ON READ REQUEST read of {d} bytes succeeded", .{ self.user_context, client.peer.addr, read }); 837 | 838 | const previous_len = client.buffer.items.len; 839 | try client.buffer.appendSlice(client.temp_buffer[0..read]); 840 | 841 | if (try picohttpparser.parseRequest(client.buffer.items, previous_len)) |result| { 842 | client.request_state.parse_result = result; 843 | try processRequest(self, client); 844 | } else { 845 | // Not enough data, read more. 846 | 847 | logger.debug("ctx#{s:<4} addr={} HTTP request incomplete, submitting read", .{ self.user_context, client.peer.addr }); 848 | 849 | _ = try self.submitRead( 850 | client, 851 | client.fd, 852 | 0, 853 | onReadRequest, 854 | ); 855 | } 856 | } 857 | 858 | fn onWriteResponseBuffer(self: *Self, client: *ClientState, cqe: io_uring_cqe) !void { 859 | switch (cqe.err()) { 860 | .SUCCESS => {}, 861 | .PIPE => { 862 | logger.err("ctx#{s:<4} addr={} broken pipe", .{ self.user_context, client.peer.addr }); 863 | return error.BrokenPipe; 864 | }, 865 | .CONNRESET => { 866 | logger.err("ctx#{s:<4} addr={} connection reset by peer", .{ self.user_context, client.peer.addr }); 867 | return error.ConnectionResetByPeer; 868 | }, 869 | else => |err| { 870 | logger.err("ctx#{s:<4} addr={} unexpected errno={}", .{ self.user_context, client.peer.addr, err }); 871 | return error.Unexpected; 872 | }, 873 | } 874 | 875 | const written = @as(usize, @intCast(cqe.res)); 876 | 877 | if (written < client.buffer.items.len) { 878 | // Short write, write the remaining data 879 | 880 | // Remove the already written data 881 | try client.buffer.replaceRange(0, written, &[0]u8{}); 882 | 883 | _ = try self.submitWrite(client, client.fd, 0, onWriteResponseBuffer); 884 | return; 885 | } 886 | 887 | logger.debug("ctx#{s:<4} addr={} ON WRITE RESPONSE done", .{ 888 | self.user_context, 889 | client.peer.addr, 890 | }); 891 | 892 | // Response written, read the next request 893 | client.request_state = .{}; 894 | client.buffer.clearRetainingCapacity(); 895 | 896 | _ = try self.submitRead(client, client.fd, 0, onReadRequest); 897 | } 898 | 899 | fn onCloseResponseFile(self: *Self, client: *ClientState, cqe: os.linux.io_uring_cqe) !void { 900 | logger.debug("ctx#{s:<4} addr={} ON CLOSE RESPONSE FILE fd={s}", .{ 901 | self.user_context, 902 | client.peer.addr, 903 | client.response_state.file.fd, 904 | }); 905 | 906 | switch (cqe.err()) { 907 | .SUCCESS => {}, 908 | else => |err| { 909 | logger.err("ctx#{s:<4} unexpected errno={}", .{ self.user_context, err }); 910 | return error.Unexpected; 911 | }, 912 | } 913 | } 914 | 915 | fn onWriteResponseFile(self: *Self, client: *ClientState, cqe: io_uring_cqe) !void { 916 | debug.assert(client.buffer.items.len > 0); 917 | 918 | switch (cqe.err()) { 919 | .SUCCESS => {}, 920 | .PIPE => { 921 | logger.err("ctx#{s:<4} addr={} broken pipe", .{ self.user_context, client.peer.addr }); 922 | return error.BrokenPipe; 923 | }, 924 | .CONNRESET => { 925 | logger.err("ctx#{s:<4} addr={} connection reset by peer", .{ self.user_context, client.peer.addr }); 926 | return error.ConnectionResetByPeer; 927 | }, 928 | else => |err| { 929 | logger.err("ctx#{s:<4} addr={} ON WRITE RESPONSE FILE unexpected errno={}", .{ self.user_context, client.peer.addr, err }); 930 | return error.Unexpected; 931 | }, 932 | } 933 | if (cqe.res <= 0) { 934 | return error.UnexpectedEOF; 935 | } 936 | 937 | const written = @as(usize, @intCast(cqe.res)); 938 | 939 | logger.debug("ctx#{s:<4} addr={} ON WRITE RESPONSE FILE write of {d} bytes to {d} succeeded", .{ 940 | self.user_context, 941 | client.peer.addr, 942 | written, 943 | client.fd, 944 | }); 945 | 946 | if (written < client.buffer.items.len) { 947 | // Short write, write the remaining data 948 | 949 | // Remove the already written data 950 | try client.buffer.replaceRange(0, written, &[0]u8{}); 951 | 952 | _ = try self.submitWrite(client, client.fd, 0, onWriteResponseFile); 953 | return; 954 | } 955 | 956 | if (client.response_state.file.offset < client.response_state.file.statx_buf.size) { 957 | // More data to read from the file, submit another read 958 | 959 | client.buffer.clearRetainingCapacity(); 960 | 961 | const offset = client.response_state.file.offset; 962 | 963 | switch (client.response_state.file.fd) { 964 | .direct => |fd| { 965 | _ = try self.submitRead(client, fd, offset, onReadResponseFile); 966 | }, 967 | .registered => |fd| { 968 | var sqe = try self.submitRead(client, fd, offset, onReadResponseFile); 969 | sqe.flags |= os.linux.IOSQE_FIXED_FILE; 970 | }, 971 | } 972 | return; 973 | } 974 | 975 | logger.debug("ctx#{s:<4} addr={} ON WRITE RESPONSE FILE done", .{ 976 | self.user_context, 977 | client.peer.addr, 978 | }); 979 | 980 | // Response file written, read the next request 981 | 982 | // Close the response file descriptor 983 | switch (client.response_state.file.fd) { 984 | .direct => |fd| { 985 | _ = try self.submitClose(client, fd, onCloseResponseFile); 986 | client.response_state.file.fd = .{ .direct = -1 }; 987 | }, 988 | .registered => {}, 989 | } 990 | 991 | // Reset the client state 992 | client.reset(); 993 | 994 | _ = try self.submitRead(client, client.fd, 0, onReadRequest); 995 | } 996 | 997 | fn onReadResponseFile(self: *Self, client: *ClientState, cqe: io_uring_cqe) !void { 998 | switch (cqe.err()) { 999 | .SUCCESS => {}, 1000 | else => |err| { 1001 | logger.err("ctx#{s:<4} addr={} ON READ RESPONSE FILE unexpected errno={}", .{ self.user_context, client.peer.addr, err }); 1002 | return error.Unexpected; 1003 | }, 1004 | } 1005 | if (cqe.res <= 0) { 1006 | return error.UnexpectedEOF; 1007 | } 1008 | 1009 | const read = @as(usize, @intCast(cqe.res)); 1010 | 1011 | client.response_state.file.offset += read; 1012 | 1013 | logger.debug("ctx#{s:<4} addr={} ON READ RESPONSE FILE read of {d} bytes from {s} succeeded, data=\"{s}\"", .{ 1014 | self.user_context, 1015 | client.peer.addr, 1016 | read, 1017 | client.response_state.file.fd, 1018 | fmt.fmtSliceEscapeLower(client.temp_buffer[0..read]), 1019 | }); 1020 | 1021 | try client.buffer.appendSlice(client.temp_buffer[0..read]); 1022 | 1023 | _ = try self.submitWrite(client, client.fd, 0, onWriteResponseFile); 1024 | } 1025 | 1026 | fn onStatxResponseFile(self: *Self, client: *ClientState, cqe: io_uring_cqe) !void { 1027 | switch (cqe.err()) { 1028 | .SUCCESS => { 1029 | debug.assert(client.buffer.items.len == 0); 1030 | }, 1031 | .CANCELED => { 1032 | return error.Canceled; 1033 | }, 1034 | else => |err| { 1035 | logger.err("ctx#{s:<4} addr={} ON STATX RESPONSE FILE unexpected errno={}", .{ self.user_context, client.peer.addr, err }); 1036 | return error.Unexpected; 1037 | }, 1038 | } 1039 | 1040 | logger.debug("ctx#{s:<4} addr={} ON STATX RESPONSE FILE path=\"{s}\" fd={s}, size={s}", .{ 1041 | self.user_context, 1042 | client.peer.addr, 1043 | client.response_state.file.path, 1044 | client.response_state.file.fd, 1045 | fmt.fmtIntSizeBin(client.response_state.file.statx_buf.size), 1046 | }); 1047 | 1048 | // Prepare the preambule + headers. 1049 | // This will be written to the socket on the next write operation following 1050 | // the first read operation for this file. 1051 | client.response_state.status_code = .ok; 1052 | try client.startWritingResponse(client.response_state.file.statx_buf.size); 1053 | 1054 | // If the file has already been registered, use its registered file descriptor. 1055 | if (self.registered_files.get(client.response_state.file.path)) |entry| { 1056 | logger.debug("ctx#{s:<4} addr={} ON STATX RESPONSE FILE file descriptor already registered, path=\"{s}\" registered fd={d}", .{ 1057 | self.user_context, 1058 | client.peer.addr, 1059 | client.response_state.file.path, 1060 | entry.fd, 1061 | }); 1062 | 1063 | var sqe = try self.submitRead(client, entry.fd, 0, onReadResponseFile); 1064 | sqe.flags |= os.linux.IOSQE_FIXED_FILE; 1065 | 1066 | return; 1067 | } 1068 | 1069 | // The file has not yet been registered, try to do it 1070 | 1071 | // Assert the file descriptor is of type .direct, if it isn't it's a bug. 1072 | debug.assert(client.response_state.file.fd == .direct); 1073 | const fd = client.response_state.file.fd.direct; 1074 | 1075 | if (self.registered_fds.acquire(fd)) |registered_fd| { 1076 | // We were able to acquire a registered file descriptor, make use of it. 1077 | 1078 | logger.debug("ctx#{s:<4} addr={} ON STATX RESPONSE FILE registered file descriptor, path=\"{s}\" registered fd={d}", .{ 1079 | self.user_context, 1080 | client.peer.addr, 1081 | client.response_state.file.path, 1082 | registered_fd, 1083 | }); 1084 | 1085 | client.response_state.file.fd = .{ .registered = registered_fd }; 1086 | 1087 | try self.registered_fds.update(&self.ring); 1088 | 1089 | const entry = try self.registered_files.getOrPut(client.response_state.file.path); 1090 | if (!entry.found_existing) { 1091 | entry.key_ptr.* = try self.root_allocator.dupe(u8, client.response_state.file.path); 1092 | entry.value_ptr.* = RegisteredFile{ 1093 | .fd = registered_fd, 1094 | .size = client.response_state.file.statx_buf.size, 1095 | }; 1096 | } 1097 | 1098 | var sqe = try self.submitRead(client, registered_fd, 0, onReadResponseFile); 1099 | sqe.flags |= os.linux.IOSQE_FIXED_FILE; 1100 | return; 1101 | } 1102 | 1103 | // The file isn't registered and we weren't able to register it, do a standard read. 1104 | _ = try self.submitRead(client, fd, 0, onReadResponseFile); 1105 | } 1106 | 1107 | fn onReadBody(self: *Self, client: *ClientState, cqe: io_uring_cqe) !void { 1108 | assert(client.request_state.content_length != null); 1109 | assert(client.request_state.body != null); 1110 | 1111 | switch (cqe.err()) { 1112 | .SUCCESS => {}, 1113 | .PIPE => { 1114 | logger.err("ctx#{s:<4} addr={} broken pipe", .{ self.user_context, client.peer.addr }); 1115 | return error.BrokenPipe; 1116 | }, 1117 | .CONNRESET => { 1118 | logger.err("ctx#{s:<4} addr={} connection reset by peer", .{ self.user_context, client.peer.addr }); 1119 | return error.ConnectionResetByPeer; 1120 | }, 1121 | else => |err| { 1122 | logger.err("ctx#{s:<4} addr={} unexpected errno={}", .{ self.user_context, client.peer.addr, err }); 1123 | return error.Unexpected; 1124 | }, 1125 | } 1126 | if (cqe.res <= 0) { 1127 | return error.UnexpectedEOF; 1128 | } 1129 | 1130 | const read = @as(usize, @intCast(cqe.res)); 1131 | 1132 | logger.debug("ctx#{s:<4} addr={} ON READ BODY read of {d} bytes succeeded", .{ self.user_context, client.peer.addr, read }); 1133 | 1134 | try client.buffer.appendSlice(client.temp_buffer[0..read]); 1135 | client.refreshBody(); 1136 | 1137 | const content_length = client.request_state.content_length.?; 1138 | const body = client.request_state.body.?; 1139 | 1140 | if (body.len < content_length) { 1141 | logger.debug("ctx#{s:<4} addr={} buffer len={d} bytes, content length={d} bytes", .{ 1142 | self.user_context, 1143 | client.peer.addr, 1144 | body.len, 1145 | content_length, 1146 | }); 1147 | 1148 | // Not enough data, read more. 1149 | _ = try self.submitRead(client, client.fd, 0, onReadBody); 1150 | return; 1151 | } 1152 | 1153 | // Request is complete: call handler 1154 | try self.callHandler(client); 1155 | } 1156 | 1157 | fn onOpenResponseFile(self: *Self, client: *ClientState, cqe: io_uring_cqe) !void { 1158 | debug.assert(client.buffer.items.len == 0); 1159 | 1160 | switch (cqe.err()) { 1161 | .SUCCESS => {}, 1162 | .NOENT => { 1163 | client.temp_buffer_fba.reset(); 1164 | 1165 | logger.warn("ctx#{s:<4} addr={} no such file or directory, path=\"{s}\"", .{ 1166 | self.user_context, 1167 | client.peer.addr, 1168 | fmt.fmtSliceEscapeLower(client.response_state.file.path), 1169 | }); 1170 | 1171 | try self.submitWriteNotFound(client); 1172 | return; 1173 | }, 1174 | else => |err| { 1175 | logger.err("ctx#{s:<4} addr={} unexpected errno={}", .{ self.user_context, client.peer.addr, err }); 1176 | return error.Unexpected; 1177 | }, 1178 | } 1179 | 1180 | client.response_state.file.fd = .{ .direct = @as(posix.fd_t, @intCast(cqe.res)) }; 1181 | 1182 | logger.debug("ctx#{s:<4} addr={} ON OPEN RESPONSE FILE fd={s}", .{ self.user_context, client.peer.addr, client.response_state.file.fd }); 1183 | 1184 | client.temp_buffer_fba.reset(); 1185 | } 1186 | 1187 | fn callHandler(self: *Self, client: *ClientState) !void { 1188 | // Create a request for the handler. 1189 | // This doesn't own any data and it only lives for the duration of this function call. 1190 | const req = try Request.create( 1191 | client.request_state.parse_result.raw_request, 1192 | client.request_state.body, 1193 | ); 1194 | 1195 | // Call the user provided handler to get a response. 1196 | const response = try self.handler( 1197 | self.user_context, 1198 | client.gpa, 1199 | client.peer, 1200 | req, 1201 | ); 1202 | // TODO(vincent): cleanup in case of errors ? 1203 | // errdefer client.reset(); 1204 | 1205 | // At this point the request data is no longer needed so we can clear the buffer. 1206 | client.buffer.clearRetainingCapacity(); 1207 | 1208 | // Process the response: 1209 | // * `response` contains a simple buffer that we can write to the socket straight away. 1210 | // * `send_file` contains a file path that we need to open and statx before we can read/write it to the socket. 1211 | 1212 | switch (response) { 1213 | .response => |res| { 1214 | client.response_state.status_code = res.status_code; 1215 | client.response_state.headers = res.headers; 1216 | 1217 | try client.startWritingResponse(res.data.len); 1218 | try client.buffer.appendSlice(res.data); 1219 | 1220 | _ = try self.submitWrite(client, client.fd, 0, onWriteResponseBuffer); 1221 | }, 1222 | .send_file => |res| { 1223 | client.response_state.status_code = res.status_code; 1224 | client.response_state.headers = res.headers; 1225 | client.response_state.file.path = try client.temp_buffer_fba.allocator().dupeZ(u8, res.path); 1226 | 1227 | if (self.registered_files.get(client.response_state.file.path)) |registered_file| { 1228 | logger.debug("ctx#{s:<4} addr={} FILE path=\"{s}\" is already registered, fd={d}", .{ 1229 | self.user_context, 1230 | client.peer.addr, 1231 | client.response_state.file.path, 1232 | registered_file.fd, 1233 | }); 1234 | 1235 | client.response_state.file.fd = .{ .registered = registered_file.fd }; 1236 | client.temp_buffer_fba.reset(); 1237 | 1238 | // Prepare the preambule + headers. 1239 | // This will be written to the socket on the next write operation following 1240 | // the first read operation for this file. 1241 | client.response_state.status_code = .ok; 1242 | try client.startWritingResponse(registered_file.size); 1243 | 1244 | // Now read the response file 1245 | var sqe = try self.submitRead(client, registered_file.fd, 0, onReadResponseFile); 1246 | sqe.flags |= os.linux.IOSQE_FIXED_FILE; 1247 | } else { 1248 | var sqe = try self.submitOpenFile( 1249 | client, 1250 | client.response_state.file.path, 1251 | .{ .ACCMODE = .RDONLY, .NOFOLLOW = true }, 1252 | 0o644, 1253 | onOpenResponseFile, 1254 | ); 1255 | sqe.flags |= os.linux.IOSQE_IO_LINK; 1256 | 1257 | _ = try self.submitStatxFile( 1258 | client, 1259 | client.response_state.file.path, 1260 | os.linux.AT.SYMLINK_NOFOLLOW, 1261 | os.linux.STATX_SIZE, 1262 | &client.response_state.file.statx_buf, 1263 | onStatxResponseFile, 1264 | ); 1265 | } 1266 | }, 1267 | } 1268 | } 1269 | 1270 | fn submitWriteNotFound(self: *Self, client: *ClientState) !void { 1271 | logger.debug("ctx#{s:<4} addr={} returning 404 Not Found", .{ 1272 | self.user_context, 1273 | client.peer.addr, 1274 | }); 1275 | 1276 | const static_response = "Not Found"; 1277 | 1278 | client.response_state.status_code = .not_found; 1279 | try client.startWritingResponse(static_response.len); 1280 | try client.buffer.appendSlice(static_response); 1281 | 1282 | _ = try self.submitWrite(client, client.fd, 0, onWriteResponseBuffer); 1283 | } 1284 | 1285 | fn processRequest(self: *Self, client: *ClientState) !void { 1286 | // Try to find the content length. If there's one we switch to reading the body. 1287 | const content_length = try client.request_state.parse_result.raw_request.getContentLength(); 1288 | if (content_length) |n| { 1289 | logger.debug("ctx#{s:<4} addr={} content length: {d}", .{ self.user_context, client.peer.addr, n }); 1290 | 1291 | client.request_state.content_length = n; 1292 | client.refreshBody(); 1293 | 1294 | if (client.request_state.body) |body| { 1295 | logger.debug("ctx#{s:<4} addr={} body incomplete, usable={d} bytes, content length: {d} bytes", .{ 1296 | self.user_context, 1297 | client.peer.addr, 1298 | body.len, 1299 | n, 1300 | }); 1301 | 1302 | _ = try self.submitRead(client, client.fd, 0, onReadBody); 1303 | return; 1304 | } 1305 | 1306 | // Request is complete: call handler 1307 | try self.callHandler(client); 1308 | return; 1309 | } 1310 | 1311 | // Otherwise it's a simple call to the handler. 1312 | try self.callHandler(client); 1313 | } 1314 | 1315 | fn submitRead(self: *Self, client: *ClientState, fd: posix.socket_t, offset: u64, comptime cb: anytype) !*io_uring_sqe { 1316 | logger.debug("ctx#{s:<4} addr={} submitting read from {d}, offset {d}", .{ 1317 | self.user_context, 1318 | client.peer.addr, 1319 | fd, 1320 | offset, 1321 | }); 1322 | 1323 | const tmp = try self.callbacks.get(cb, .{client}); 1324 | 1325 | return self.ring.read( 1326 | @intFromPtr(tmp), 1327 | fd, 1328 | .{ .buffer = &client.temp_buffer }, 1329 | offset, 1330 | ); 1331 | } 1332 | 1333 | fn submitWrite(self: *Self, client: *ClientState, fd: posix.fd_t, offset: u64, comptime cb: anytype) !*io_uring_sqe { 1334 | logger.debug("ctx#{s:<4} addr={} submitting write of {s} to {d}, offset {d}, data=\"{s}\"", .{ 1335 | self.user_context, 1336 | client.peer.addr, 1337 | fmt.fmtIntSizeBin(client.buffer.items.len), 1338 | fd, 1339 | offset, 1340 | fmt.fmtSliceEscapeLower(client.buffer.items), 1341 | }); 1342 | 1343 | const tmp = try self.callbacks.get(cb, .{client}); 1344 | 1345 | return self.ring.write( 1346 | @intFromPtr(tmp), 1347 | fd, 1348 | client.buffer.items, 1349 | offset, 1350 | ); 1351 | } 1352 | 1353 | fn submitOpenFile(self: *Self, client: *ClientState, path: [:0]const u8, flags: os.linux.O, mode: posix.mode_t, comptime cb: anytype) !*io_uring_sqe { 1354 | logger.debug("ctx#{s:<4} addr={} submitting open, path=\"{s}\"", .{ 1355 | self.user_context, 1356 | client.peer.addr, 1357 | fmt.fmtSliceEscapeLower(path), 1358 | }); 1359 | 1360 | const tmp = try self.callbacks.get(cb, .{client}); 1361 | 1362 | return try self.ring.openat( 1363 | @intFromPtr(tmp), 1364 | os.linux.AT.FDCWD, 1365 | path, 1366 | flags, 1367 | mode, 1368 | ); 1369 | } 1370 | 1371 | fn submitStatxFile(self: *Self, client: *ClientState, path: [:0]const u8, flags: u32, mask: u32, buf: *os.linux.Statx, comptime cb: anytype) !*io_uring_sqe { 1372 | logger.debug("ctx#{s:<4} addr={} submitting statx, path=\"{s}\"", .{ 1373 | self.user_context, 1374 | client.peer.addr, 1375 | fmt.fmtSliceEscapeLower(path), 1376 | }); 1377 | 1378 | const tmp = try self.callbacks.get(cb, .{client}); 1379 | 1380 | return self.ring.statx( 1381 | @intFromPtr(tmp), 1382 | os.linux.AT.FDCWD, 1383 | path, 1384 | flags, 1385 | mask, 1386 | buf, 1387 | ); 1388 | } 1389 | }; 1390 | } 1391 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const debug = std.debug; 3 | const fmt = std.fmt; 4 | const heap = std.heap; 5 | const io = std.io; 6 | const mem = std.mem; 7 | const net = std.net; 8 | const os = std.os; 9 | const posix = std.posix; 10 | const time = std.time; 11 | 12 | const Atomic = std.atomic.Value; 13 | const assert = std.debug.assert; 14 | 15 | const IO_Uring = std.os.linux.IO_Uring; 16 | const io_uring_cqe = std.os.linux.io_uring_cqe; 17 | const io_uring_sqe = std.os.linux.io_uring_sqe; 18 | 19 | const httpserver = @import("lib.zig"); 20 | 21 | const argsParser = @import("args"); 22 | const picohttp = @import("picohttpparser"); 23 | 24 | const logger = std.log.scoped(.main); 25 | 26 | var global_running: Atomic(bool) = Atomic(bool).init(true); 27 | 28 | fn addSignalHandlers() !void { 29 | // Ignore broken pipes 30 | { 31 | const act = posix.Sigaction{ 32 | .handler = .{ 33 | .handler = posix.SIG.IGN, 34 | }, 35 | .mask = posix.sigemptyset(), 36 | .flags = 0, 37 | }; 38 | posix.sigaction(posix.SIG.PIPE, &act, null); 39 | } 40 | 41 | // Catch SIGINT/SIGTERM for proper shutdown 42 | { 43 | var act = posix.Sigaction{ 44 | .handler = .{ 45 | .handler = struct { 46 | fn wrapper(sig: c_int) callconv(.C) void { 47 | logger.info("caught signal {d}", .{sig}); 48 | 49 | global_running.store(false, .seq_cst); 50 | } 51 | }.wrapper, 52 | }, 53 | .mask = posix.sigemptyset(), 54 | .flags = 0, 55 | }; 56 | posix.sigaction(posix.SIG.TERM, &act, null); 57 | posix.sigaction(posix.SIG.INT, &act, null); 58 | } 59 | } 60 | 61 | const ServerContext = struct { 62 | const Self = @This(); 63 | 64 | id: usize, 65 | server: httpserver.Server(*Self), 66 | thread: std.Thread, 67 | 68 | pub fn format(self: *const Self, comptime fmt_string: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { 69 | _ = options; 70 | 71 | if (comptime !mem.eql(u8, "s", fmt_string)) @compileError("format string must be s"); 72 | try writer.print("{d}", .{self.id}); 73 | } 74 | 75 | fn handleRequest(self: *Self, per_request_allocator: mem.Allocator, peer: httpserver.Peer, req: httpserver.Request) anyerror!httpserver.Response { 76 | _ = per_request_allocator; 77 | 78 | logger.debug("ctx#{d:<4} IN HANDLER addr={} method: {s}, path: {s}, minor version: {d}, body: \"{?s}\"", .{ 79 | self.id, 80 | peer.addr, 81 | req.method.toString(), 82 | req.path, 83 | req.minor_version, 84 | req.body, 85 | }); 86 | 87 | if (mem.startsWith(u8, req.path, "/static")) { 88 | return httpserver.Response{ 89 | .send_file = .{ 90 | .status_code = .ok, 91 | .headers = &[_]picohttp.RawHeader{}, 92 | .path = req.path[1..], 93 | }, 94 | }; 95 | } else { 96 | return httpserver.Response{ 97 | .response = .{ 98 | .status_code = .ok, 99 | .headers = &[_]picohttp.RawHeader{}, 100 | .data = "Hello, World in handler!", 101 | }, 102 | }; 103 | } 104 | } 105 | }; 106 | 107 | pub fn main() anyerror!void { 108 | var gpa = heap.GeneralPurposeAllocator(.{}){}; 109 | defer if (gpa.deinit() == .leak) { 110 | debug.panic("leaks detected", .{}); 111 | }; 112 | var allocator = gpa.allocator(); 113 | 114 | // 115 | 116 | const options = try argsParser.parseForCurrentProcess(struct { 117 | @"listen-port": u16 = 3405, 118 | 119 | @"max-server-threads": usize = 1, 120 | @"max-ring-entries": u13 = 512, 121 | @"max-buffer-size": usize = 4096, 122 | @"max-connections": usize = 128, 123 | }, allocator, .print); 124 | defer options.deinit(); 125 | 126 | const listen_port = options.options.@"listen-port"; 127 | const max_server_threads = options.options.@"max-server-threads"; 128 | const max_ring_entries = options.options.@"max-ring-entries"; 129 | const max_buffer_size = options.options.@"max-buffer-size"; 130 | const max_connections = options.options.@"max-connections"; 131 | 132 | // NOTE(vincent): for debugging 133 | // var logging_allocator = heap.loggingAllocator(gpa.allocator()); 134 | // var allocator = logging_allocator.allocator(); 135 | 136 | try addSignalHandlers(); 137 | 138 | // Create the server socket 139 | const server_fd = try httpserver.createSocket(listen_port); 140 | 141 | logger.info("listening on :{d}", .{listen_port}); 142 | logger.info("max server threads: {d}, max ring entries: {d}, max buffer size: {d}, max connections: {d}", .{ 143 | max_server_threads, 144 | max_ring_entries, 145 | max_buffer_size, 146 | max_connections, 147 | }); 148 | 149 | // Create the servers 150 | 151 | const servers = try allocator.alloc(ServerContext, max_server_threads); 152 | errdefer allocator.free(servers); 153 | 154 | for (servers, 0..) |*item, i| { 155 | item.id = i; 156 | try item.server.init( 157 | allocator, 158 | .{ 159 | .max_ring_entries = max_ring_entries, 160 | .max_buffer_size = max_buffer_size, 161 | .max_connections = max_connections, 162 | }, 163 | &global_running, 164 | server_fd, 165 | item, 166 | ServerContext.handleRequest, 167 | ); 168 | } 169 | defer { 170 | for (servers) |*item| item.server.deinit(); 171 | allocator.free(servers); 172 | } 173 | 174 | for (servers) |*item| { 175 | item.thread = try std.Thread.spawn( 176 | .{}, 177 | struct { 178 | fn worker(server: *httpserver.Server(*ServerContext)) !void { 179 | return server.run(1 * time.ns_per_s); 180 | } 181 | }.worker, 182 | .{&item.server}, 183 | ); 184 | } 185 | 186 | for (servers) |*item| item.thread.join(); 187 | } 188 | -------------------------------------------------------------------------------- /src/test.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const fmt = std.fmt; 3 | const heap = std.heap; 4 | const mem = std.mem; 5 | const net = std.net; 6 | const os = std.os; 7 | const testing = std.testing; 8 | const time = std.time; 9 | const posix = std.posix; 10 | 11 | const Atomic = std.atomic.Value; 12 | const assert = std.debug.assert; 13 | 14 | const picohttp = @import("picohttpparser"); 15 | const httpserver = @import("lib.zig"); 16 | 17 | const curl = @import("curl.zig"); 18 | 19 | const port = 34450; 20 | 21 | const TestHarness = struct { 22 | const Self = @This(); 23 | 24 | root_allocator: mem.Allocator, 25 | arena: heap.ArenaAllocator, 26 | socket: posix.socket_t, 27 | running: Atomic(bool) = Atomic(bool).init(true), 28 | server: httpserver.Server(*Self), 29 | thread: std.Thread, 30 | 31 | pub fn format(self: *const Self, comptime fmt_string: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { 32 | _ = self; 33 | _ = fmt_string; 34 | _ = options; 35 | try writer.writeAll("0"); 36 | } 37 | 38 | fn create(allocator: mem.Allocator, comptime handler: httpserver.RequestHandler(*Self)) !*TestHarness { 39 | const socket = blk: { 40 | const sockfd = try posix.socket(posix.AF.INET6, posix.SOCK.STREAM, 0); 41 | errdefer posix.close(sockfd); 42 | 43 | posix.setsockopt( 44 | sockfd, 45 | posix.SOL.SOCKET, 46 | posix.SO.REUSEADDR, 47 | &mem.toBytes(@as(c_int, 1)), 48 | ) catch {}; 49 | 50 | const addr = try net.Address.parseIp6("::0", port); 51 | 52 | try posix.bind(sockfd, &addr.any, @sizeOf(posix.sockaddr.in6)); 53 | try posix.listen(sockfd, std.math.maxInt(u31)); 54 | 55 | break :blk sockfd; 56 | }; 57 | 58 | var res = try allocator.create(TestHarness); 59 | errdefer allocator.destroy(res); 60 | 61 | res.* = .{ 62 | .root_allocator = allocator, 63 | .arena = heap.ArenaAllocator.init(allocator), 64 | .socket = socket, 65 | .server = undefined, 66 | .thread = undefined, 67 | }; 68 | try res.server.init( 69 | allocator, 70 | .{}, 71 | &res.running, 72 | socket, 73 | res, 74 | handler, 75 | ); 76 | 77 | // Start thread 78 | 79 | res.thread = try std.Thread.spawn( 80 | .{}, 81 | struct { 82 | fn worker(server: *httpserver.Server(*Self)) !void { 83 | return server.run(10 * time.ns_per_ms); 84 | } 85 | }.worker, 86 | .{&res.server}, 87 | ); 88 | 89 | return res; 90 | } 91 | 92 | fn deinit(self: *TestHarness) void { 93 | // Wait for the server to finish 94 | self.running.store(false, .seq_cst); 95 | self.thread.join(); 96 | 97 | // Clean up the server 98 | self.server.deinit(); 99 | posix.close(self.socket); 100 | 101 | // Clean up our own data 102 | self.arena.deinit(); 103 | self.root_allocator.destroy(self); 104 | } 105 | 106 | fn do(self: *TestHarness, method: []const u8, path: []const u8, body_opt: ?[]const u8) !curl.Response { 107 | var buf: [1024]u8 = undefined; 108 | const url = try fmt.bufPrintZ(&buf, "http://localhost:{d}{s}", .{ 109 | port, 110 | path, 111 | }); 112 | 113 | return curl.do(self.root_allocator, method, url, body_opt); 114 | } 115 | }; 116 | 117 | test "GET 200 OK" { 118 | // Try to test multiple end conditions for the serving loop 119 | 120 | var i: usize = 1; 121 | while (i < 20) : (i += 1) { 122 | var th = try TestHarness.create( 123 | testing.allocator, 124 | struct { 125 | fn handle(ctx: *TestHarness, per_request_allocator: mem.Allocator, peer: httpserver.Peer, req: httpserver.Request) anyerror!httpserver.Response { 126 | _ = ctx; 127 | _ = per_request_allocator; 128 | _ = peer; 129 | 130 | try testing.expectEqualStrings("/plaintext", req.path); 131 | try testing.expectEqual(httpserver.Method.get, req.method); 132 | try testing.expect(req.headers.get("Host") != null); 133 | try testing.expectEqualStrings("*/*", req.headers.get("Accept").?.value); 134 | try testing.expect(req.headers.get("Content-Length") == null); 135 | try testing.expect(req.headers.get("Content-Type") == null); 136 | try testing.expect(req.body == null); 137 | 138 | return httpserver.Response{ 139 | .response = .{ 140 | .status_code = .ok, 141 | .headers = &[_]picohttp.RawHeader{}, 142 | .data = "Hello, World!", 143 | }, 144 | }; 145 | } 146 | }.handle, 147 | ); 148 | defer th.deinit(); 149 | 150 | var j: usize = 0; 151 | while (j < i) : (j += 1) { 152 | var resp = try th.do("GET", "/plaintext", null); 153 | defer resp.deinit(); 154 | 155 | try testing.expectEqual(@as(usize, 200), resp.response_code); 156 | try testing.expectEqualStrings("Hello, World!", resp.data); 157 | } 158 | } 159 | } 160 | 161 | test "POST 200 OK" { 162 | const body = 163 | \\Perspiciatis eligendi aspernatur iste delectus et et quo repudiandae. Iusto repellat tempora nisi alias. Autem inventore rerum magnam sunt voluptatem aspernatur. 164 | \\Consequuntur quae non fugit dignissimos at quis. Mollitia nisi minus voluptatem voluptatem sed sunt dolore. Expedita ullam ut ex voluptatem delectus. Fuga quos asperiores consequatur similique voluptatem provident vel. Repudiandae rerum quia dolorem totam. 165 | ; 166 | 167 | var th = try TestHarness.create( 168 | testing.allocator, 169 | struct { 170 | fn handle(ctx: *TestHarness, per_request_allocator: mem.Allocator, peer: httpserver.Peer, req: httpserver.Request) anyerror!httpserver.Response { 171 | _ = ctx; 172 | _ = per_request_allocator; 173 | _ = peer; 174 | 175 | try testing.expectEqualStrings("/foobar", req.path); 176 | try testing.expectEqual(httpserver.Method.post, req.method); 177 | try testing.expect(req.headers.get("Host") != null); 178 | try testing.expectEqualStrings("*/*", req.headers.get("Accept").?.value); 179 | try testing.expectEqualStrings("application/json", req.headers.get("Content-Type").?.value); 180 | try testing.expectEqual(body.len, try fmt.parseInt(usize, req.headers.get("Content-Length").?.value, 10)); 181 | try testing.expectEqualStrings(body, req.body.?); 182 | 183 | return httpserver.Response{ 184 | .response = .{ 185 | .status_code = .ok, 186 | .headers = &[_]picohttp.RawHeader{}, 187 | .data = "Hello, World!", 188 | }, 189 | }; 190 | } 191 | }.handle, 192 | ); 193 | defer th.deinit(); 194 | 195 | var i: usize = 1; 196 | while (i < 20) : (i += 1) { 197 | var j: usize = 0; 198 | while (j < i) : (j += 1) { 199 | var resp = try th.do("POST", "/foobar", body); 200 | defer resp.deinit(); 201 | 202 | try testing.expectEqual(@as(usize, 200), resp.response_code); 203 | try testing.expectEqualStrings("Hello, World!", resp.data); 204 | } 205 | } 206 | } 207 | 208 | test "GET files" { 209 | var th = try TestHarness.create( 210 | testing.allocator, 211 | struct { 212 | fn handle(ctx: *TestHarness, per_request_allocator: mem.Allocator, peer: httpserver.Peer, req: httpserver.Request) anyerror!httpserver.Response { 213 | _ = ctx; 214 | _ = per_request_allocator; 215 | _ = peer; 216 | 217 | try testing.expect(mem.startsWith(u8, req.path, "/static")); 218 | try testing.expect(req.headers.get("Host") != null); 219 | try testing.expectEqualStrings("*/*", req.headers.get("Accept").?.value); 220 | try testing.expect(req.headers.get("Content-Length") == null); 221 | try testing.expect(req.headers.get("Content-Type") == null); 222 | try testing.expect(req.body == null); 223 | 224 | const path = req.path[1..]; 225 | 226 | return httpserver.Response{ 227 | .send_file = .{ 228 | .status_code = .ok, 229 | .headers = &[_]picohttp.RawHeader{}, 230 | .path = path, 231 | }, 232 | }; 233 | } 234 | }.handle, 235 | ); 236 | defer th.deinit(); 237 | 238 | const test_cases = &[_]struct { 239 | path: []const u8, 240 | exp_data: []const u8, 241 | exp_response_code: usize, 242 | }{ 243 | .{ .path = "/static/foobar.txt", .exp_data = "foobar content\n", .exp_response_code = 200 }, 244 | .{ .path = "/static/notfound.txt", .exp_data = "Not Found", .exp_response_code = 404 }, 245 | }; 246 | 247 | inline for (test_cases) |tc| { 248 | var i: usize = 0; 249 | while (i < 20) : (i += 1) { 250 | var resp = try th.do("GET", tc.path, null); 251 | defer resp.deinit(); 252 | 253 | try testing.expectEqual(tc.exp_response_code, resp.response_code); 254 | try testing.expectEqualStrings(tc.exp_data, resp.data); 255 | } 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /static/foobar.txt: -------------------------------------------------------------------------------- 1 | foobar content 2 | --------------------------------------------------------------------------------