diff --git a/packages/web/src/Connection.zig b/packages/web/src/Connection.zig index 6364887..b893aec 100644 --- a/packages/web/src/Connection.zig +++ b/packages/web/src/Connection.zig @@ -4,13 +4,17 @@ const Connection = @This(); const FileDescriptor = @import("FileDescriptor.zig").FileDescriptor; const openssl = @import("openssl.zig"); -const linux = std.os.linux; +const iovec = std.posix.iovec; +const iovec_const = std.posix.iovec_const; address: std.net.Address, fd: FileDescriptor, ssl: ?*openssl.Ssl, node: std.DoublyLinkedList.Node = .{}, +// TODO Consider proper usage of `send` syscall with `MSG_MORE` flag and setting +// the `TCP_CORK` option. + pub fn reinit( self: *Connection, address: std.net.Address, @@ -40,12 +44,42 @@ pub fn read(self: *const Connection, buf: []u8) !usize { pub fn readAll(self: *const Connection, buf: []u8) !void { if (self.ssl) |ssl| { - try ssl.readall(buf); + try ssl.readAll(buf); } else { try self.fd.readAll(buf); } } +pub fn readv(self: *const Connection, iov: []const iovec) !usize { + if (self.ssl) |ssl| { + var total_bytes_read: usize = 0; + for (iov) |io| { + const bytes_read = try ssl.read(io.base[0..io.len]); + total_bytes_read += bytes_read; + + if (bytes_read < io.len) { + return total_bytes_read; + } + } + + return total_bytes_read; + } else { + const bytes_read = try self.fd.readv(iov); + return bytes_read; + } +} + +/// Might modify `iov` when `readv` syscall reads partially. +pub fn readvAll(self: *const Connection, iov: []iovec) !void { + if (self.ssl) |ssl| { + for (iov) |io| { + try ssl.readAll(io.base[0..io.len]); + } + } else { + try self.fd.readvAll(iov); + } +} + pub fn write(self: *const Connection, buf: []const u8) !usize { if (self.ssl) |ssl| { const bytes_written = try ssl.write(buf); @@ -63,3 +97,51 @@ pub fn writeAll(self: *const Connection, buf: []const u8) !void { try self.fd.writeAll(buf); } } + +pub fn writev(self: *const Connection, iov: []const iovec_const) !usize { + if (self.ssl) |ssl| { + var total_bytes_written: usize = 0; + for (iov) |io| { + const bytes_written = try ssl.write(io.base[0..io.len]); + total_bytes_written += bytes_written; + + if (bytes_written < io.len) { + return total_bytes_written; + } + } + + return total_bytes_written; + } else { + const bytes_written = try self.fd.writev(iov); + return bytes_written; + } +} + +/// Might modify `iov` when `writev` syscall writes partially. +pub fn writevAll(self: *const Connection, iov: []iovec_const) !void { + if (self.ssl) |ssl| { + for (iov) |io| { + try ssl.writeAll(io.base[0..io.len]); + } + } else { + try self.fd.writevAll(iov); + } +} + +pub fn sendfile(self: *const Connection, fd: FileDescriptor, offset: usize, len: usize) !usize { + if (self.ssl) |ssl| { + const bytes_written = try ssl.sendfile(fd, offset, len); + return bytes_written; + } else { + const bytes_written = try self.fd.sendfile(fd, offset, len); + return bytes_written; + } +} + +pub fn sendfileAll(self: *const Connection, fd: FileDescriptor, offset: usize, len: usize) !void { + if (self.ssl) |ssl| { + try ssl.sendfileAll(fd, offset, len); + } else { + try self.fd.sendfileAll(fd, offset, len); + } +} diff --git a/packages/web/src/FileDescriptor.zig b/packages/web/src/FileDescriptor.zig index 63fc504..df985c1 100644 --- a/packages/web/src/FileDescriptor.zig +++ b/packages/web/src/FileDescriptor.zig @@ -3,6 +3,9 @@ const std = @import("std"); const linux = std.os.linux; const errno = linux.E.init; +const iovec = std.posix.iovec; +const iovec_const = std.posix.iovec_const; + pub const FileDescriptor = enum(i32) { stdin = 0, stdout = 1, @@ -83,7 +86,7 @@ pub const FileDescriptor = enum(i32) { pub fn read(self: FileDescriptor, buf: []u8) !usize { while (true) { - const rc = linux.read(@intFromEnum(self), buf.ptr, @intCast(buf.len)); + const rc = linux.read(@intFromEnum(self), buf.ptr, buf.len); switch (errno(rc)) { .SUCCESS => return rc, .INTR => continue, @@ -102,8 +105,41 @@ pub const FileDescriptor = enum(i32) { } } + pub fn readv(self: FileDescriptor, iov: []const iovec) !usize { + while (true) { + const rc = linux.readv(@intFromEnum(self), iov.ptr, iov.len); + switch (errno(rc)) { + .SUCCESS => return rc, + .INTR => continue, + else => return error.SystemError, + } + } + } + + /// Might modify `iov` when `readv` syscall reads partially. + pub fn readvAll(self: FileDescriptor, iov: []iovec) !void { + var total_bytes_read: usize = 0; + var i: usize = 0; + while (i < iov.len) { + var bytes_read = try self.readv(iov[i..]); + total_bytes_read += bytes_read; + + // skip whole buffers + while (i < iov.len and bytes_read >= iov[i].len) { + bytes_read -= iov[i].len; + i += 1; + } + + // skip part of a buffer + if (bytes_read > 0) { + iov[i].base += bytes_read; + iov[i].len -= bytes_read; + } + } + } + pub fn setsockopt(self: FileDescriptor, level: i32, optname: u32, opt: []const u8) !void { - const rc = linux.setsockopt(@intFromEnum(self), level, optname, opt.ptr, @intCast(opt.len)); + const rc = linux.setsockopt(@intFromEnum(self), level, optname, opt.ptr, opt.len); return switch (errno(rc)) { .SUCCESS => {}, else => error.SystemError, @@ -112,7 +148,7 @@ pub const FileDescriptor = enum(i32) { pub fn write(self: FileDescriptor, buf: []const u8) !usize { while (true) { - const rc = linux.write(@intFromEnum(self), buf.ptr, @intCast(buf.len)); + const rc = linux.write(@intFromEnum(self), buf.ptr, buf.len); switch (errno(rc)) { .SUCCESS => return rc, .INTR => continue, @@ -130,4 +166,68 @@ pub const FileDescriptor = enum(i32) { total_bytes_written += bytes_written; } } + + pub fn writev(self: FileDescriptor, iov: []const iovec_const) !usize { + while (true) { + const rc = linux.writev(@intFromEnum(self), iov.ptr, iov.len); + switch (errno(rc)) { + .SUCCESS => return rc, + .INTR => continue, + else => return error.SystemError, + } + } + } + + /// Might modify `iov` when `writev` syscall writes partially. + pub fn writevAll(self: FileDescriptor, iov: []iovec_const) !void { + var total_bytes_written: usize = 0; + var i: usize = 0; + while (i < iov.len) { + var bytes_written = try self.writev(iov[i..]); + total_bytes_written += bytes_written; + + // skip whole buffers + while (i < iov.len and bytes_written >= iov[i].len) { + bytes_written -= iov[i].len; + i += 1; + } + + // skip part of a buffer + if (bytes_written > 0) { + iov[i].base += bytes_written; + iov[i].len -= bytes_written; + } + } + } + + /// Calls `fstat` and returns total size in bytes. + pub fn size(self: FileDescriptor) !usize { + var stat = std.mem.zeroes(linux.Stat); + const rc = linux.fstat(@intFromEnum(self), &stat); + return switch (errno(rc)) { + .SUCCESS => @intCast(stat.size), + else => error.SystemError, + }; + } + + pub fn sendfile(self: FileDescriptor, fd: FileDescriptor, offset: usize, len: usize) !usize { + var offset_mut = offset; + while (true) { + const rc = linux.sendfile(@intFromEnum(self), @intFromEnum(fd), @ptrCast(&offset_mut), len); + switch (errno(rc)) { + .SUCCESS => return rc, + .INTR => continue, + else => return error.SystemError, + } + } + } + + pub fn sendfileAll(self: FileDescriptor, fd: FileDescriptor, offset: usize, len: usize) !void { + var total_bytes_sent: usize = 0; + + while (total_bytes_sent < len) { + const bytes_sent = try self.sendfile(fd, offset + total_bytes_sent, len - total_bytes_sent); + total_bytes_sent += bytes_sent; + } + } }; diff --git a/packages/web/src/Id.zig b/packages/web/src/Id.zig index 53fe086..a8249be 100644 --- a/packages/web/src/Id.zig +++ b/packages/web/src/Id.zig @@ -35,7 +35,7 @@ pub fn Id(comptime _tag: @Type(.enum_literal)) type { } pub fn encodeInto(self: @This(), text: *[22]u8) void { - std.base64.url_safe_no_pad.Encoder.encode(text, self.bytes); + encoder.encode(text, self.bytes); } }; } diff --git a/packages/web/src/Request.zig b/packages/web/src/Request.zig new file mode 100644 index 0000000..a8a9123 --- /dev/null +++ b/packages/web/src/Request.zig @@ -0,0 +1,66 @@ +const std = @import("std"); +const Request = @This(); + +const http = @import("http.zig"); + +method: http.Method, +pathname: []const u8, +headers: *HeaderHashMap, +body: []const u8, + +pub const HeaderHashMap = std.HashMapUnmanaged( + http.FieldName, + HeaderList, + http.FieldName.HashMapContext, + std.hash_map.default_max_load_percentage, +); + +pub const HeaderList = struct { + list: std.SinglyLinkedList, + len: usize, +}; + +pub const HeaderValue = struct { + value: []const u8, + node: std.SinglyLinkedList.Node, +}; + +/// Gets a header field value of a given `name`. When there is no such header, +/// `null` is returned. When there is more than one header with the same name, +/// the value of the field that came later will be returned. +pub fn getHeader(self: *const Request, name: http.FieldName) ?[]const u8 { + if (self.headers.get(name)) |list| { + if (list.list.first) |head| { + const header_value: *HeaderValue = @fieldParentPtr("node", head); + return header_value.value; + } + } +} + +pub fn getHeaderKnown(self: *const Request, known: http.KnownFieldName) ?[]const u8 { + return self.getHeader(.initKnonw(known)); +} + +pub fn getHeaders(self: *const Request, name: http.FieldName, values: [][]const u8) []const []const u8 { + var i: usize = 0; + var node: ?*std.SinglyLinkedList.Node = if (self.headers.get(name)) |list| list.list else null; + + while (i < values.len) { + if (node) |n| { + const header_value: *HeaderValue = @fieldParentPtr("node", n); + values[i] = header_value.value; + + i += 1; + node = n.next; + } else { + break; + } + } + + return values[0..i]; +} + +pub fn getHeaderCount(self: *const Request, name: http.FieldName) usize { + const list = self.headers.get(name) orelse return 0; + return list.len; +} diff --git a/packages/web/src/RequestHandler.zig b/packages/web/src/RequestHandler.zig index 698149c..8fea688 100644 --- a/packages/web/src/RequestHandler.zig +++ b/packages/web/src/RequestHandler.zig @@ -1,56 +1,16 @@ const std = @import("std"); const RequestHandler = @This(); -const Header = @import("http/Header.zig"); +const Request = @import("Request.zig"); const Response = @import("Response.zig"); -const Route = @import("Route.zig"); -const Worker = @import("Worker.zig"); ptr: *anyopaque, vtable: *const VTable, pub const VTable = struct { - /// Called multiple times (could be zero) for each header in the request. - header: *const fn (self: *anyopaque, response: *Response, header: Header) anyerror!void, - /// Called exactly once after the whole request is received. When there is - /// no body, then `body.len == 0`. - body: *const fn (self: *anyopaque, response: *Response, body: []const u8) anyerror!void, - /// Called when the request parsing has halted. Possible reasons are: - /// - /// 1. One of the calls to this object returned an error. - /// 2. The request was malformed and the HTTP parser returned an error. - /// 3. The whole request was received. - /// - /// When no errors occurs (the third case), this method will be call after - /// `body`. This method should only be used to clean up internal resources, - /// if necessary. - finalize: *const fn (self: *anyopaque) void, + handle: *const fn (self: *anyopaque, request: *Request, response: *Response) anyerror!void, }; -pub fn noHeader(self: *anyopaque, response: *Response, header: Header) anyerror!void { - _ = self; - _ = response; - _ = header; -} - -pub fn noBody(self: *anyopaque, response: *Response, body: []const u8) anyerror!void { - _ = self; - _ = response; - _ = body; -} - -pub fn noFinalize(self: *anyopaque) void { - _ = self; -} - -pub inline fn rawHeader(rh: RequestHandler, response: *Response, header: Header) anyerror!void { - return rh.vtable.header(rh.ptr, response, header); -} - -pub inline fn rawBody(rh: RequestHandler, response: *Response, body: []const u8) anyerror!void { - return rh.vtable.body(rh.ptr, response, body); -} - -pub inline fn rawFinalize(rh: RequestHandler) void { - rh.vtable.finalize(rh.ptr); +pub inline fn handle(self: RequestHandler, request: *Request, response: *Response) anyerror!void { + try self.vtable.handle(self.ptr, request, response); } diff --git a/packages/web/src/RequestRouter.zig b/packages/web/src/RequestRouter.zig deleted file mode 100644 index ecf466e..0000000 --- a/packages/web/src/RequestRouter.zig +++ /dev/null @@ -1,16 +0,0 @@ -const std = @import("std"); -const RequestRouter = @This(); - -const RequestHandler = @import("RequestHandler.zig"); -const Route = @import("Route.zig"); - -ptr: *anyopaque, -vtable: *const VTable, - -pub const VTable = struct { - route: *const fn (self: *anyopaque, route: Route) anyerror!RequestHandler, -}; - -pub inline fn rawRoute(self: RequestRouter, route: Route) anyerror!RequestHandler { - return self.vtable.route(self.ptr, route); -} diff --git a/packages/web/src/Response.zig b/packages/web/src/Response.zig index 104bc07..445a91a 100644 --- a/packages/web/src/Response.zig +++ b/packages/web/src/Response.zig @@ -2,69 +2,84 @@ const std = @import("std"); const Response = @This(); const Connection = @import("Connection.zig"); +const FileDescriptor = @import("FileDescriptor.zig").FileDescriptor; const http = @import("http.zig"); +const iovec = std.posix.iovec; +const iovec_const = std.posix.iovec_const; + pub const State = union(enum) { init: void, sent: void, errored: anyerror, + + pub fn initErrored(err: anyerror) State { + return .{ .errored = err }; + } }; connection: *Connection, -writer: std.Io.Writer, +header_writer: std.Io.Writer, +body_writer: std.Io.Writer, state: State, -pub fn init(connection: *Connection, write_buffer: []u8) Response { +pub fn init(connection: *Connection, header_write_buffer: []u8, body_write_buffer: []u8) Response { return .{ .connection = connection, - .writer = .fixed(write_buffer), + .header_writer = .fixed(header_write_buffer), + .writer = .fixed(body_write_buffer), .state = .init, }; } -pub const ResponseEmptyOptions = struct { - status_text: []const u8 = http.status.ok, -}; - -pub const ResponseOptions = struct { - status_text: []const u8 = http.status.ok, - media_type: []const u8 = "text/plain; charset=utf-8", - response_body: []const u8, -}; - -pub fn sendEmpty(self: *Response, options: ResponseEmptyOptions) !void { - try self.writer.print("{s}", .{options.status_text}); - try self.writer.print("\r\n", .{}); - - self.finalize(); -} - -pub fn sendClose(self: *Response, options: ResponseEmptyOptions) !void { - try self.writer.print("{s}", .{options.status_text}); - try self.writer.print("Connection: close\r\n", .{}); - try self.writer.print("\r\n", .{}); - - self.finalize(); -} - -pub fn sendResponse(self: *Response, options: ResponseOptions) !void { - try self.writer.print("{s}", .{options.status_text}); - try self.writer.print("Content-Type: {s}\r\n", .{options.media_type}); - try self.writer.print("Content-Length: {d}\r\n", .{options.response_body.len}); - try self.writer.print("\r\n", .{}); - try self.writer.print("{s}", .{options.response_body}); - - self.finalize(); -} - -/// Send the respnose immediatelly. Can be called only once. If never called, -/// the response will be sent once -pub fn finalize(self: *Response) void { +pub fn sendHeadersOnly(self: *Response) void { std.debug.assert(self.state == .init); - if (self.connection.writeAll(self.writer.buffered())) { + const headers_slice = self.header_writer.buffered(); + std.debug.assert(headers_slice.len > 0); + + if (self.connection.writeAll(headers_slice)) { self.state = .sent; } else |err| { - self.state = .{ .errored = err }; + self.state = .initErrored(err); + } +} + +pub fn sendHeadersAndBody(self: *Response) void { + std.debug.assert(self.state == .init); + + const headers_slice = self.header_writer.buffered(); + const body_slice = self.body_writer.buffered(); + std.debug.assert(headers_slice.len > 0); + std.debug.assert(body_slice.len > 0); + + var iov = [_]iovec_const{ + .{ .base = headers_slice.ptr, .len = headers_slice.len }, + .{ .base = body_slice.ptr, .len = body_slice.len }, + }; + + if (self.connection.writevAll(&iov)) { + self.state = .sent; + } else |err| { + self.state = .initErrored(err); + } +} + +pub fn sendHeadersAndFile(self: *Response, fd: FileDescriptor, offset: usize, maybe_len: ?usize) void { + std.debug.assert(self.state == .init); + + const headers_slice = self.header_writer.buffered(); + std.debug.assert(headers_slice.len > 0); + + const len = maybe_len orelse (try fd.size()) - offset; + + if (self.connection.writeAll(headers_slice)) { + if (self.connection.sendfileAll(fd, offset, len)) { + self.state = .sent; + } else |err2| { + self.state = .initErrored(err2); + } + } else |err1| { + self.state = .initErrored(err1); } } diff --git a/packages/web/src/Server.zig b/packages/web/src/Server.zig index 63917af..16b7a08 100644 --- a/packages/web/src/Server.zig +++ b/packages/web/src/Server.zig @@ -5,7 +5,8 @@ const Connection = @import("Connection.zig"); const FileDescriptor = @import("FileDescriptor.zig").FileDescriptor; const http = @import("http.zig"); const openssl = @import("openssl.zig"); -const RequestRouter = @import("RequestRouter.zig"); +const Request = @import("Request.zig"); +const RequestHandler = @import("RequestHandler.zig"); const Worker = @import("Worker.zig"); const linux = std.os.linux; @@ -16,7 +17,7 @@ address: std.net.Address, ssl_ctx: ?*openssl.SslContext, workers: []Worker, threads: []std.Thread, -request_router: RequestRouter, +request_handler: RequestHandler, connection_queue: std.DoublyLinkedList, // NOTE Connection pool has no need for being doubly-linked, but the queue has @@ -30,11 +31,13 @@ mutex: std.Thread.Mutex, cond_connection_queued: std.Thread.Condition, cond_connection_freed: std.Thread.Condition, +/// 4 kiB +const page_size = 4 * 1024; /// 2 MiB const huge_page_size = 2 * 1024 * 1024; pub const Options = struct { - request_router: RequestRouter, + request_handler: RequestHandler, address: std.net.Address = .initIp4(.{ 127, 0, 0, 1 }, 8000), /// If not `null`, the server will use TLS with the provided OpenSSL /// context. @@ -43,14 +46,25 @@ pub const Options = struct { /// The number of worker threads. If set to `0`, the number of worker /// threads will be equal to the number of logical CPU cores. worker_count: u32 = 0, + /// The maximum number of header fields the `Request` object will be able to + /// store. An HTTP request will be rejected if it has more header fields + /// than the capacity. + max_header_fields: u32 = 256, /// The number of 2 MiB pages reserved for a single read buffer. Each worker /// has its own read buffer. An HTTP request (headers and content combined) /// will be rejected if it is larger than the read buffer. - read_buffer_pages: u32 = 1, - /// The number of 2 MiB pages reserved for a single write buffer. Each - /// worker has its own write buffer. An HTTP response (headers and content - /// combined) must be larger than the write buffer. - write_buffer_pages: u32 = 1, + read_buffer_huge_pages: u32 = 1, + /// The number of 4 kiB pages reserved for a single header write buffer. + /// Each worker has its own header write buffer. The HTTP status line, all + /// header fields and the CRLF terminator must all fit in the header write + /// buffer. + header_write_buffer_pages: u32 = 1, + /// The number of 2 MiB pages reserved for a single body write buffer. Each + /// worker has its own body write buffer. The HTTP response body must fit + /// entirely within the body write buffer. This restriction only applies to + /// bodies generated with the body writer and not to bodies sent with + /// `sendfile`. + body_write_buffer_huge_pages: u32 = 1, }; pub fn init(allocator: std.mem.Allocator, options: Options) !Server { @@ -134,35 +148,62 @@ pub fn init(allocator: std.mem.Allocator, options: Options) !Server { )); } - // Allocate write buffer + // Allocate header write buffer - const single_write_buffer_size = @as(usize, options.write_buffer_pages) * huge_page_size; - const all_write_buffers_size = worker_count * single_write_buffer_size; + const single_header_write_buffer_size = @as(usize, options.header_write_buffer_pages) * page_size; + const all_header_write_buffers_size = worker_count * single_header_write_buffer_size; - const write_buffer_ptr = try errOrPtr(linux.mmap( + const header_write_buffer_ptr = try errOrPtr(linux.mmap( null, - all_write_buffers_size, + all_header_write_buffers_size, linux.PROT.READ | linux.PROT.WRITE, linux.MAP{ .TYPE = .PRIVATE, .ANONYMOUS = true }, -1, 0, )); - errdefer _ = linux.munmap(write_buffer_ptr, all_write_buffers_size); - _ = linux.madvise(write_buffer_ptr, all_write_buffers_size, linux.MADV.HUGEPAGE); + errdefer _ = linux.munmap(header_write_buffer_ptr, all_header_write_buffers_size); + + // Allocate body write buffer + + const single_body_write_buffer_size = @as(usize, options.write_buffer_pages) * huge_page_size; + const all_body_write_buffers_size = worker_count * single_body_write_buffer_size; + + const body_write_buffer_ptr = try errOrPtr(linux.mmap( + null, + all_body_write_buffers_size, + linux.PROT.READ | linux.PROT.WRITE, + linux.MAP{ .TYPE = .PRIVATE, .ANONYMOUS = true }, + -1, + 0, + )); + errdefer _ = linux.munmap(body_write_buffer_ptr, all_body_write_buffers_size); + _ = linux.madvise(body_write_buffer_ptr, all_body_write_buffers_size, linux.MADV.HUGEPAGE); // Initialize workers + var workers_initialized: usize = 0; + errdefer { + for (workers[0..workers_initialized]) |*worker| { + worker.deinit(allocator); + } + } + for (workers, 0..) |*worker, i| { const read_offset = i * double_single_read_buffer_size; - const write_offset = i * single_write_buffer_size; - worker.* = .{ + const header_write_offset = i * single_header_write_buffer_size; + const body_write_offset = i * single_body_write_buffer_size; + worker.* = try Worker.init(allocator, .{ + .worker_id = i, + + .max_header_fields = options.max_header_fields, + .read_buffer_ptr = read_buffer_ptr + read_offset, .read_buffer_size = single_read_buffer_size, - .read_head = 0, - .read_tail = 0, - .write_buffer = (write_buffer_ptr + write_offset)[0..single_write_buffer_size], - }; + .header_write_buffer = (header_write_buffer_ptr + header_write_offset)[0..single_header_write_buffer_size], + .body_write_buffer = (body_write_buffer_ptr + body_write_offset)[0..single_body_write_buffer_size], + }); + workers_initialized += 1; } // Fill connection pool @@ -178,7 +219,7 @@ pub fn init(allocator: std.mem.Allocator, options: Options) !Server { .ssl_ctx = options.ssl_ctx, .workers = workers, .threads = threads, - .request_router = options.request_router, + .request_handler = options.request_handler, .connection_queue = .{}, .connection_pool = connection_pool, @@ -191,11 +232,36 @@ pub fn init(allocator: std.mem.Allocator, options: Options) !Server { } pub fn deinit(self: *Server, allocator: std.mem.Allocator) void { - // TODO Deinitialize workers - self.fd.close(); + const worker_count = self.workers.len; + + const single_read_buffers_size = self.workers[0].read_buffer_size; + const all_read_buffers_size = worker_count * single_read_buffers_size; + const double_all_read_buffers_size = 2 * all_read_buffers_size; + + const single_header_write_buffer_size = self.workers[0].header_write_buffer.len; + const all_header_write_buffers_size = worker_count * single_header_write_buffer_size; + + const single_body_write_buffer_size = self.workers[0].body_write_buffer.len; + const all_body_write_buffers_size = worker_count * single_body_write_buffer_size; + + const read_buffer_ptr = self.workers[0].read_buffer_ptr; + const header_write_buffer_ptr = self.workers[0].header_write_buffer.ptr; + const body_write_buffer_ptr = self.workers[0].body_write_buffer.ptr; + + for (self.workers) |*worker| { + worker.deinit(allocator); + } + + _ = linux.munmap(body_write_buffer_ptr, all_body_write_buffers_size); + _ = linux.munmap(header_write_buffer_ptr, all_header_write_buffers_size); + _ = linux.munmap(read_buffer_ptr, double_all_read_buffers_size); + allocator.free(self.threads); allocator.free(self.connection_buffer); allocator.free(self.workers); + + self.fd.close(); + self.* = undefined; } diff --git a/packages/web/src/ShortString.zig b/packages/web/src/ShortString.zig deleted file mode 100644 index 868ff45..0000000 --- a/packages/web/src/ShortString.zig +++ /dev/null @@ -1,33 +0,0 @@ -const std = @import("std"); -const ShortString = @This(); - -len: u8, -data: [15]u8, - -pub fn init(string: []const u8) ShortString { - std.debug.assert(string.len <= 15); - - const len: u8 = @intCast(string.len); - var data: [15]u8 = undefined; - - @memcpy(data[0..len], string); - - return .{ - .len = len, - .data = data, - }; -} - -/// Check whether `string` can be converted into a `ShortString` and converts -/// it. Returns `null` if conversion is not possible. -pub fn isShortString(string: []const u8) ?ShortString { - return if (string.len <= 15) init(string) else null; -} - -pub fn slice(self: *const ShortString) []const u8 { - return self.data[0..self.len]; -} - -pub fn eql(a: ShortString, b: ShortString) bool { - return std.mem.eql(u8, a.slice(), b.slice()); -} diff --git a/packages/web/src/Worker.zig b/packages/web/src/Worker.zig index 16af451..fe522a0 100644 --- a/packages/web/src/Worker.zig +++ b/packages/web/src/Worker.zig @@ -4,17 +4,68 @@ const Worker = @This(); const Connection = @import("Connection.zig"); const FileDescriptor = @import("FileDescriptor.zig").FileDescriptor; const http = @import("http.zig"); +const Request = @import("Request.zig"); const RequestHandler = @import("RequestHandler.zig"); -const RequestRouter = @import("RequestRouter.zig"); const Response = @import("Response.zig"); const Server = @import("Server.zig"); +/// Integer unique for this worker. Has no functional meaning. Can be used for +/// debugging and profiling. +worker_id: usize, + read_buffer_ptr: [*]u8, read_buffer_size: usize, read_head: usize, read_tail: usize, -write_buffer: []u8, +header_write_buffer: []u8, +body_write_buffer: []u8, + +header_hash_map: Request.HeaderHashMap, +header_value_buffer: []Request.HeaderValue, + +pub const Options = struct { + worker_id: usize, + + max_header_fields: u32, + + read_buffer_ptr: [*]u8, + read_buffer_size: usize, + + header_write_buffer: []u8, + body_write_buffer: []u8, +}; + +pub fn init(allocator: std.mem.Allocator, options: Options) !Worker { + var header_hash_map: Request.HeaderHashMap = .empty; + try header_hash_map.ensureTotalCapacity(allocator, options.max_header_fields); + errdefer header_hash_map.deinit(allocator); + + const header_list_buffer = try allocator.alloc(Request.HeaderList, options.max_header_fields); + errdefer allocator.free(header_list_buffer); + + return .{ + .worker_id = options.worker_id, + + .read_buffer_ptr = options.read_buffer_ptr, + .read_buffer_size = options.read_buffer_size, + .read_head = 0, + .read_tail = 0, + + .header_write_buffer = options.header_write_buffer, + .body_write_buffer = options.body_write_buffer, + + .header_hash_map = header_hash_map, + .header_value_buffer = header_list_buffer, + }; +} + +pub fn deinit(self: *Worker, allocator: std.mem.Allocator) void { + allocator.free(self.header_value_buffer); + self.header_hash_map.deinit(allocator); + + self.* = undefined; +} pub fn worker( self: *Worker, @@ -35,7 +86,7 @@ pub fn worker( server.cond_connection_freed.signal(); } - self.handleConnection(server.request_router, connection, running) catch |err| { + self.handleConnection(server.request_handler, connection, running) catch |err| { std.log.err("Error while handling connection: {}", .{err}); }; } else { @@ -46,14 +97,14 @@ pub fn worker( fn handleConnection( self: *Worker, - request_router: RequestRouter, + request_handler: RequestHandler, connection: *Connection, running: *const std.atomic.Value(bool), ) !void { defer connection.deinit(); while (running.load(.acquire)) { - const res = self.handleRequest(request_router, connection) catch |err| { + const res = self.handleRequest(request_handler, connection) catch |err| { std.log.err("Error while handling request: {}", .{err}); return err; }; @@ -64,16 +115,22 @@ fn handleConnection( fn handleRequest( self: *Worker, - request_router: RequestRouter, + request_handler: RequestHandler, connection: *Connection, ) !bool { + self.header_hash_map.clearRetainingCapacity(); + + var request: Request = .{ + .method = undefined, + .pathname = undefined, + .headers = &self.header_hash_map, + .body = undefined, + }; var response: Response = .init(connection, self.write_buffer); - var parser: http.Parser = .init(request_router, &response); - defer { - if (parser.request_handler) |rh| { - rh.rawFinalize(); - } - } + var parser: http.Parser = .init(); + + var next_header_index: usize = 0; + var ignore: bool = false; var leftover_bytes = self.read_tail - self.read_head; const max_read_tail = self.read_head + self.read_buffer_size; @@ -84,7 +141,7 @@ fn handleRequest( if (leftover_bytes > 0) { bytes_read = leftover_bytes; - chunk = self.read_buffer_ptr[self.read_head..self.read_tail]; + chunk = self.read_buffer_ptr[self.read_tail - leftover_bytes .. self.read_tail]; leftover_bytes = 0; } else { const read_tail = self.read_tail; @@ -101,51 +158,16 @@ fn handleRequest( error.HttpVersionNotSupported => { try response.sendClose(.{ .status_text = http.status.http_version_not_supported }); }, - error.MissingLineFeed => { + error.SyntaxError => { try response.sendClose(.{ .status_text = http.status.bad_request }); }, - error.InvalidContentLength => { - try response.sendClose(.{ .status_text = http.status.bad_request }); - }, - error.RouterError => { - const cause = parser.last_router_error; - const cause_name = @errorName(cause); - - // TODO Could really use separate header and content write - // buffers and this code shows why. We can't start writing - // until we know the exact byte length of the response body, - // because the value of "Content-Length" header depends on - // it. I guess we could do preemptive padding and in-place - // patch it later, but two buffers would be ideal anyway. - // We could then use writev syscall and it still will be - // exactly one syscall per response (unless we want to - // sendfile, but then it will be two syscalls either way). - - try response.writer.print("{s}", .{http.status.internal_server_error}); - try response.writer.print("Content-Type: {s}\r\n", .{"text/plain; charset=utf-8"}); - try response.writer.print("Content-Length: {d}\r\n", .{cause_name.len}); - try response.writer.print("\r\n", .{}); - try response.writer.print("{s}", .{cause_name}); - - response.finalize(); - }, - error.HandlerError => { - const cause = parser.last_handler_error; - const cause_name = @errorName(cause); - - try response.writer.print("{s}", .{http.status.internal_server_error}); - try response.writer.print("Content-Type: {s}\r\n", .{"text/plain; charset=utf-8"}); - try response.writer.print("Content-Length: {d}\r\n", .{cause_name.len}); - try response.writer.print("\r\n", .{}); - try response.writer.print("{s}", .{cause_name}); - - response.finalize(); - }, } return false; }; - if (self.read_tail - self.read_head >= self.read_buffer_size and !res.done) { + const done = if (res.result) |result| std.meta.activeTag(result) == .body else false; + + if (self.read_tail - self.read_head >= self.read_buffer_size and !done) { if (parser.state == .body) { try response.sendClose(.{ .status_text = http.status.content_too_large }); } else { @@ -154,11 +176,94 @@ fn handleRequest( return false; } - if (res.done) { - leftover_bytes = bytes_read - res.consumed; - self.read_head = (self.read_tail - leftover_bytes) & ~(self.read_buffer_size - 1); - self.read_tail = self.read_head + leftover_bytes; - return true; + if (res.result) |result| { + switch (result) { + .method => |method| request.method = method, + .pathname => |pathname| request.pathname = pathname, + .header => |header| { + if (ignore) { + break; + } + + if (next_header_index >= self.header_value_buffer.len or self.header_hash_map.available == 0) { + try response.send(.{ .status_text = http.status.request_header_fields_too_large }); + ignore = true; + } else { + const entry = self.header_hash_map.getOrPutAssumeCapacity(header.name); + const header_value = &self.header_value_buffer[next_header_index]; + header_value.* = .{ .node = .{}, .value = header.value }; + next_header_index += 1; + + if (!entry.found_existing) { + entry.value_ptr.* = .{ + .len = 0, + .list = .{}, + }; + } + + entry.value_ptr.list.prepend(&header_value.node); + entry.value_ptr.len += 1; + } + }, + .end_of_headers => {}, + .body => |body| { + request.body = body; + + if (!ignore) { + request_handler.handle(&request, &response) catch |err| { + if (response.state == .init) { + response.header_writer.end = 0; + response.body_writer.end = 0; + + const error_name = @errorName(err); + + try response.body_writer.print("Internal Server Error\n{s}\n", .{error_name}); + + try response.header_writer.writeAll(http.status.internal_server_error); + try response.header_writer.writeAll("Content-Type: text/plain; charset=utf-8\r\n"); + try response.header_writer.print("Content-Length: {d}\r\n", .{response.body_writer.end}); + try response.header_writer.writeAll("\r\n"); + + response.sendHeadersAndBody(); + } + }; + } + + if (response.state == .init) { + const no_headers = response.header_writer.end > 0; + const no_body = response.body_writer.end > 0; + + if (no_headers) { + if (no_body) { + try response.header_writer.writeAll(http.status.no_content); + try response.header_writer.writeAll("\r\n"); + + response.sendHeadersOnly(); + } else { + try response.header_writer.writeAll(http.status.ok); + try response.header_writer.writeAll("Content-Type: application/octet-stream"); + try response.header_writer.print("Content-Length: {d}\r\n", .{response.body_writer.end}); + try response.header_writer.writeAll("\r\n"); + + response.sendHeadersAndBody(); + } + } else { + if (no_body) { + response.sendHeadersOnly(); + } else { + response.sendHeadersAndBody(); + } + } + } + + leftover_bytes = bytes_read - res.consumed; + self.read_head = (self.read_tail - leftover_bytes) & ~(self.read_buffer_size - 1); + self.read_tail = self.read_head + leftover_bytes; + return true; + }, + } } + + leftover_bytes = bytes_read - res.consumed; } } diff --git a/packages/web/src/http.zig b/packages/web/src/http.zig index 9b50e69..2efc51e 100644 --- a/packages/web/src/http.zig +++ b/packages/web/src/http.zig @@ -5,5 +5,4 @@ pub const Header = @import("http/Header.zig"); pub const KnownFieldName = @import("http/KnownFieldName.zig").KnownFieldName; pub const Method = @import("http/Method.zig").Method; pub const Parser = @import("http/Parser.zig"); -pub const Route = @import("http/Route.zig"); pub const status = @import("http/status.zig"); diff --git a/packages/web/src/http/FieldName.zig b/packages/web/src/http/FieldName.zig index 7bcfa14..d2aa890 100644 --- a/packages/web/src/http/FieldName.zig +++ b/packages/web/src/http/FieldName.zig @@ -1,44 +1,90 @@ const std = @import("std"); const KnownFieldName = @import("KnownFieldName.zig").KnownFieldName; -const ShortString = @import("../ShortString.zig"); -pub const FieldName = union(enum) { - known: KnownFieldName, - short: ShortString, - long: []const u8, +const Wyhash = std.hash.Wyhash; + +pub const FieldName = extern struct { + data: [16]u8 align(8), + + const tag_known: u8 = 0x00; + const tag_long: u8 = 0x01; + const tag_short_bias: u8 = 0x02; pub fn init(name: []const u8) FieldName { + var data: [16]u8 = @splat(0); if (KnownFieldName.isKnownFieldName(name)) |known| { - return initKnown(known); - } else if (ShortString.isShortString(name)) |short| { - return initShort(short); + data[0] = tag_known; + @as(*KnownFieldName, @ptrCast(data[8..16])).* = known; + } else if (name.len <= 15) { + data[0] = @intCast(name.len + tag_short_bias); + @memcpy(data[1..][0..name.len], name); } else { - return initOther(name); + data[0] = tag_long; + @as(*u32, @ptrCast(data[4..8])).* = @intCast(name.len); + @as(*usize, @ptrCast(data[8..16])).* = @intFromPtr(name.ptr); } + + return .{ .data = data }; } pub fn initKnown(known: KnownFieldName) FieldName { - return .{ .known = known }; + var data: [16]u8 = @splat(0); + + data[0] = tag_known; + @as(*KnownFieldName, @ptrCast(data[8..16])).* = known; + + return .{ .data = data }; } - pub fn initShort(short: ShortString) FieldName { - return .{ .short = short }; + fn getKnown(self: FieldName) KnownFieldName { + std.debug.assert(self.data[0] == tag_known); + return @bitCast(self.data[8..16].*); } - pub fn initOther(other: []const u8) FieldName { - return .{ .other = other }; + fn getLong(self: FieldName) []const u8 { + std.debug.assert(self.data[0] == tag_long); + const len: u32 = @bitCast(self.data[4..8].*); + const intptr: usize = @bitCast(self.data[8..16].*); + const ptr: [*]const u8 = @ptrFromInt(intptr); + return ptr[0..len]; + } + + fn getShort(self: FieldName) []const u8 { + std.debug.assert(self.data[0] >= tag_short_bias); + const len: u8 = self.data[0] - tag_short_bias; + const str = self.data[1..][0..len]; + return str; + } + + pub fn hash(self: FieldName) u32 { + return switch (self.data[0]) { + tag_known => Wyhash.hash(0, self.data[8..16]), + tag_long => Wyhash.hash(1, self.getLong()), + else => Wyhash.hash(2, self.getShort()), + }; } pub fn eql(a: FieldName, b: FieldName) bool { - const tag_a = std.meta.activeTag(a); - const tag_b = std.meta.activeTag(b); - if (tag_a != tag_b) return false; + const a_tag = a.data[0]; + const b_tag = b.data[0]; - return switch (a) { - .known => |x| x == b.known, - .short => |x| ShortString.eql(x, b.short), - .long => |x| std.mem.eql(x, b.long), + if (a_tag != b_tag) return false; + + return switch (a_tag) { + tag_known => a.getKnown() == b.getKnown(), + tag_long => std.mem.eql(u8, a.getLong(), b.getLong()), + else => std.mem.eql(u8, a.getShort(), b.getShort()), }; } + + pub const HashMapContext = struct { + pub fn hash(_: HashMapContext, key: FieldName) u32 { + return key.hash(); + } + + pub fn eql(_: HashMapContext, a: FieldName, b: FieldName, _: usize) bool { + return FieldName.eql(a, b); + } + }; }; diff --git a/packages/web/src/http/Header.zig b/packages/web/src/http/Header.zig index 7be446a..451e2e8 100644 --- a/packages/web/src/http/Header.zig +++ b/packages/web/src/http/Header.zig @@ -19,8 +19,5 @@ pub fn isNamed(self: Header, name: FieldName) bool { } pub fn isNamedKnown(self: Header, known: KnownFieldName) bool { - return switch (self.name) { - .known => |x| x == known, - else => false, - }; + return FieldName.eql(self.name, .initKnonw(known)); } diff --git a/packages/web/src/http/KnownFieldName.zig b/packages/web/src/http/KnownFieldName.zig index 6710bb9..d0377d9 100644 --- a/packages/web/src/http/KnownFieldName.zig +++ b/packages/web/src/http/KnownFieldName.zig @@ -1,6 +1,6 @@ const std = @import("std"); -pub const KnownFieldName = enum { +pub const KnownFieldName = enum(u64) { // --- STANDARD FIELD NAMES ------------------------------------------------ @@ -11,7 +11,7 @@ pub const KnownFieldName = enum { // Some of them might be obsoleted or deprecated; they are included here // nonetheless. // - // When the list was retrieved, its "Last Updated" date was 2026-03-06. + // When the list was last retrieved, its "Last Updated" date was 2026-03-06. @"A-IM", Accept, diff --git a/packages/web/src/http/Parser.zig b/packages/web/src/http/Parser.zig index 96fecf6..6a384b7 100644 --- a/packages/web/src/http/Parser.zig +++ b/packages/web/src/http/Parser.zig @@ -7,10 +7,11 @@ //! //! During a single ingestion, the parser can return one of the following: //! -//! - route of type `Route`, i.e. HTTP method (aka verb) with pathname +//! - method of type `Method`, i.e. HTTP method (aka verb) +//! - pathname of type `[]const u8` //! - header of type `Header`, i.e. a field name with a value //! - end_of_headers of type `void`, i.e. a marker which informs the user of -//! this parser that there will be no more headers; this moment can be used by +//! this parser that there will be no more headers; this result can be used by //! the user to make decisions about further processing of the request based //! on the full knowledge of all the headers //! - body of type `[]const u8`, i.e. a slice to the request body (or @@ -48,7 +49,6 @@ const Parser = @This(); const FieldName = @import("FieldName.zig").FieldName; const Header = @import("Header.zig"); const Method = @import("Method.zig").Method; -const Route = @import("Route.zig"); pub const Error = error{ MethodNotSupported, @@ -57,13 +57,18 @@ pub const Error = error{ }; pub const Result = union(enum) { - route: Route, + method: Method, + pathname: []const u8, header: Header, end_of_headers: void, body: []const u8, - pub fn initRoute(route: Route) Result { - return .{ .route = route }; + pub fn initMethod(method: Method) Result { + return .{ .method = method }; + } + + pub fn initPathname(pathname: []const u8) Result { + return .{ .pathname = pathname }; } pub fn initHeader(header: Header) Result { @@ -115,8 +120,8 @@ pub const State = union(enum) { method_optio: void, method_connec: void, method_option: void, - method_complete: Method, - pathname: Route, + method_complete: void, + pathname: []const u8, pathname_complete: void, version_h: void, version_ht: void, @@ -135,12 +140,8 @@ pub const State = union(enum) { body: []const u8, done: void, - pub fn initMethodComplete(method: Method) State { - return .{ .method_complete = method }; - } - - pub fn initPathname(route: Route) State { - return .{ .pathname = route }; + pub fn initPathname(pathname: []const u8) State { + return .{ .pathname = pathname }; } pub fn initHeaderName(name: []const u8) State { @@ -280,7 +281,10 @@ fn consumeChar(self: *Parser, char_ptr: *const u8) Error!?Result { else => return error.MethodNotSupported, }, .method_ge => switch (char) { - 'T' => self.state = .initMethodComplete(.GET), + 'T' => { + self.state = .method_complete; + return .initMethod(.GET); + }, else => return error.MethodNotSupported, }, .method_he => switch (char) { @@ -300,7 +304,10 @@ fn consumeChar(self: *Parser, char_ptr: *const u8) Error!?Result { else => return error.MethodNotSupported, }, .method_pu => switch (char) { - 'T' => self.state = .initMethodComplete(.PUT), + 'T' => { + self.state = .method_complete; + return .initMethod(.PUT); + }, else => return error.MethodNotSupported, }, .method_tr => switch (char) { @@ -316,7 +323,10 @@ fn consumeChar(self: *Parser, char_ptr: *const u8) Error!?Result { else => return error.MethodNotSupported, }, .method_hea => switch (char) { - 'D' => self.state = .initHeaderName(.HEAD), + 'D' => { + self.state = .method_complete; + return .initMethod(.HEAD); + }, else => return error.MethodNotSupported, }, .method_opt => switch (char) { @@ -328,7 +338,10 @@ fn consumeChar(self: *Parser, char_ptr: *const u8) Error!?Result { else => return error.MethodNotSupported, }, .method_pos => switch (char) { - 'T' => self.state = .initHeaderName(.POST), + 'T' => { + self.state = .method_complete; + return .initMethod(.POST); + }, else => return error.MethodNotSupported, }, .method_tra => switch (char) { @@ -348,11 +361,17 @@ fn consumeChar(self: *Parser, char_ptr: *const u8) Error!?Result { else => return error.MethodNotSupported, }, .method_patc => switch (char) { - 'H' => self.state = .initMethodComplete(.PATCH), + 'H' => { + self.state = .method_complete; + return .initMethod(.PATCH); + }, else => return error.MethodNotSupported, }, .method_trac => switch (char) { - 'E' => self.state = .initMethodComplete(.TRACE), + 'E' => { + self.state = .method_complete; + return .initMethod(.TRACE); + }, else => return error.MethodNotSupported, }, .method_conne => switch (char) { @@ -360,7 +379,10 @@ fn consumeChar(self: *Parser, char_ptr: *const u8) Error!?Result { else => return error.MethodNotSupported, }, .method_delet => switch (char) { - 'E' => self.state = .initMethodComplete(.DELETE), + 'E' => { + self.state = .method_complete; + return .initMethod(.DELETE); + }, else => return error.MethodNotSupported, }, .method_optio => switch (char) { @@ -368,23 +390,29 @@ fn consumeChar(self: *Parser, char_ptr: *const u8) Error!?Result { else => return error.MethodNotSupported, }, .method_connec => switch (char) { - 'T' => self.state = .initMethodComplete(.CONNECT), + 'T' => { + self.state = .method_complete; + return .initMethod(.CONNECT); + }, else => return error.MethodNotSupported, }, .method_option => switch (char) { - 'S' => self.state = .initMethodComplete(.OPTIONS), + 'S' => { + self.state = .method_complete; + return .initMethod(.OPTIONS); + }, else => return error.MethodNotSupported, }, - .method_complete => |method| switch (char) { - ' ' => self.state = .initPathname(.init(method, next_char_slice)), + .method_complete => switch (char) { + ' ' => self.state = .initPathname(next_char_slice), else => return error.MethodNotSupported, }, - .pathname => |route| switch (char) { + .pathname => |pathname| switch (char) { ' ' => { self.state = .pathname_complete; - return .initRoute(route); + return .initPathname(pathname); }, - else => self.state = .initPathname(extendRoute(route)), + else => self.state = .initPathname(extendSlice(pathname)), }, .pathname_complete => switch (char) { 'H' => self.state = .version_h, @@ -503,13 +531,6 @@ fn extendSliceBy(slice: []const u8, n: usize) []const u8 { return slice.ptr[0 .. slice.len + n]; } -fn extendRoute(route: Route) Route { - return .{ - .method = route.method, - .pathname = extendSlice(route.pathname), - }; -} - fn extendHeader(header: Header) Header { return .{ .name = header.name, @@ -597,10 +618,10 @@ pub fn consumeVec(self: *Parser, vec_ptr: *const [vec_len]u8) Error!ConsumeResul inline for (@typeInfo(patterns.methods).@"struct".decls) |decl| { const pattern: Pattern = @field(patterns.methods, decl.name); if (pattern.check(vec)) { - self.state = .methodComplete(@field(Method, decl.name)); + self.state = .method_complete; return .{ .consumed = pattern.len, - .done = false, + .result = .initMethod(@field(Method, decl.name)), }; } } @@ -612,14 +633,14 @@ pub fn consumeVec(self: *Parser, vec_ptr: *const [vec_len]u8) Error!ConsumeResul // Delegate to `consumeChar`. return .{ .consumed = 0, - .done = false, + .result = null, }; } self.state = .pathname(s.method, s.pathname.ptr[0 .. s.pathname.len + vec_len]); return .{ .consumed = vec_len, - .done = false, + .result = null, }; }, .pathname_complete => { @@ -627,7 +648,7 @@ pub fn consumeVec(self: *Parser, vec_ptr: *const [vec_len]u8) Error!ConsumeResul self.state = .header_name_start; return .{ .consumed = patterns.@"version_http/1.1".len, - .done = false, + .result = null, }; } else { return error.HttpVersionNotSupported; @@ -638,21 +659,21 @@ pub fn consumeVec(self: *Parser, vec_ptr: *const [vec_len]u8) Error!ConsumeResul // Delegate to `consumeChar`. return .{ .consumed = 0, - .done = false, + .result = null, }; } self.state = .headerValue(s.name, s.value.ptr[0 .. s.value.len + vec_len]); return .{ .consumed = vec_len, - .done = false, + .result = null, }; }, else => { // Delegate to `consumeChar`. return .{ .consumed = 0, - .done = false, + .result = null, }; }, } diff --git a/packages/web/src/http/Route.zig b/packages/web/src/http/Route.zig deleted file mode 100644 index 7219255..0000000 --- a/packages/web/src/http/Route.zig +++ /dev/null @@ -1,14 +0,0 @@ -const std = @import("std"); -const Route = @This(); - -const Method = @import("Method.zig").Method; - -method: Method, -pathname: []const u8, - -pub fn init(method: Method, pathname: []const u8) Route { - return .{ - .method = method, - .pathname = pathname, - }; -} diff --git a/packages/web/src/main.zig b/packages/web/src/main.zig index 8d419c6..8f0cf55 100644 --- a/packages/web/src/main.zig +++ b/packages/web/src/main.zig @@ -17,114 +17,49 @@ fn interruptionHandler(signal: i32) callconv(.c) void { } } -const Router = struct { - allocator: std.mem.Allocator, - - fn init(allocator: std.mem.Allocator) Router { - return .{ - .allocator = allocator, - }; - } - - fn interface(self: *Router) web.RequestRouter { - return .{ - .ptr = self, - .vtable = &.{ - .route = onRoute, - }, - }; - } - - fn onRoute(ctx: *anyopaque, route: web.Route) !web.RequestHandler { - const self: *Router = @ptrCast(@alignCast(ctx)); - - const handler = try self.allocator.create(Handler); - handler.* = try .init(self.allocator, route); - return handler.interface(); - } -}; - const Handler = struct { - allocator: std.mem.Allocator, + fn handle(_: *anyopaque, request: *web.Request, response: *web.Response) !void { + if (!std.mem.eql(request.pathname, "/")) { + try response.body_writer.writeAll("Not Found\n"); - route: web.Route, - uuid: UUID, - timer: std.time.Timer, + try response.header_writer.writeAll(web.http.status.not_found); + try response.header_writer.writeAll("Content-Type: text/plain; charset=utf-8\r\n"); + try response.header_writer.print("Content-Length: {d}\r\n", .{response.body_writer.end}); + try response.header_writer.writeAll("\r\n"); - accept: ?[]const u8 = null, - accept_encoding: ?[]const u8 = null, - accept_language: ?[]const u8 = null, - user_agent: ?[]const u8 = null, - - fn init(allocator: std.mem.Allocator, route: web.Route) !Handler { - return .{ - .allocator = allocator, - - .route = route, - .uuid = UUID.v7(), - .timer = try .start(), - }; - } - - fn interface(self: *Handler) web.RequestHandler { - return .{ - .ptr = self, - .vtable = &.{ - .header = onHeader, - .body = onBody, - .finalize = onFinalize, - }, - }; - } - - fn onHeader(ctx: *anyopaque, response: *web.Response, header: web.http.Header) !void { - const self: *Handler = @ptrCast(@alignCast(ctx)); - - switch (header.name) { - .known => |k| switch (k) { - .Accept => { - self.accept = header.value; - }, - .@"Accept-Encoding" => { - self.accept_encoding = header.value; - }, - .@"Accept-Language" => { - self.accept_language = header.value; - }, - .@"User-Agent" => { - self.user_agent = header.value; - }, - else => {}, - }, - .other => {}, + response.sendHeadersAndBody(); + return; } - _ = response; + if (!std.mem.eql(request.method, "GET")) { + try response.body_writer.writeAll("Method Not Allowed\n"); + + try response.header_writer.writeAll(web.http.status.method_not_allowed); + try response.header_writer.writeAll("Content-Type: text/plain; charset=utf-8\r\n"); + try response.header_writer.print("Content-Length: {d}\r\n", .{response.body_writer.end}); + try response.header_writer.writeAll("\r\n"); + + response.sendHeadersAndBody(); + return; + } + + try response.body_writer.writeAll("{\"ok\":true}\n"); + + try response.header_writer.writeAll(web.http.status.ok); + try response.header_writer.writeAll("Content-Type: application/json\r\n"); + try response.header_writer.writeAll("Content-Length: {d}\r\n", .{response.body_writer.end}); + try response.header_writer.writeAll("\r\n"); + + response.sendHeadersAndBody(); } - fn onBody(ctx: *anyopaque, response: *web.Response, body: []const u8) !void { - const self: *Handler = @ptrCast(@alignCast(ctx)); - - try response.sendResponse(.{ - .media_type = "application/json", - .response_body = "{\"ok\":true}\r\n", - }); - - _ = self; - _ = body; - } - - fn onFinalize(ctx: *anyopaque) void { - const self: *Handler = @ptrCast(@alignCast(ctx)); - - const time_ns = self.timer.read(); - const time_us_ceil = (time_ns + std.time.ns_per_us - 1) / std.time.ns_per_us; - - const rps_floor = std.time.ns_per_s / time_ns; - - std.log.info("{s} {s} (lat = {} µs, rlat = {} rps)", .{ @tagName(self.route.method), self.route.pathname, time_us_ceil, rps_floor }); - - self.allocator.destroy(self); + fn interface() web.RequestHandler { + return .{ + .ptr = undefined, + .vtable = &.{ + .handle = handle, + }, + }; } }; @@ -135,8 +70,6 @@ pub fn main() !void { defer _ = gpa.deinit(); const allocator = gpa.allocator(); - var router: Router = .init(allocator); - _ = ssl.c_ssl.SSL_library_init(); _ = ssl.c_ssl.OpenSSL_add_all_algorithms(); _ = ssl.c_ssl.SSL_load_error_strings(); @@ -152,7 +85,7 @@ pub fn main() !void { try ssl_ctx.checkPrivateKey(); var server = try web.Server.init(allocator, .{ - .request_router = router.interface(), + .request_handler = Handler.interface(), .address = .initIp4(.{ 127, 0, 0, 1 }, 8000), .ssl_ctx = ssl_ctx, }); diff --git a/packages/web/src/openssl/Ssl.zig b/packages/web/src/openssl/Ssl.zig index aba1fad..b487d6c 100644 --- a/packages/web/src/openssl/Ssl.zig +++ b/packages/web/src/openssl/Ssl.zig @@ -39,6 +39,20 @@ pub const Ssl = opaque { } } + pub fn sendfile(self: *Ssl, fd: FileDescriptor, offset: usize, size: usize) !usize { + const res = import.SSL_sendfile(self, fd, offset, size, 0); + return if (res <= 0) error.OpenSslError else @intCast(res); + } + + pub fn sendfileAll(self: *Ssl, fd: FileDescriptor, offset: usize, size: usize) !void { + var total_bytes_sent: usize = 0; + + while (total_bytes_sent < size) { + const bytes_written = try self.sendfile(fd, offset + total_bytes_sent, size - total_bytes_sent); + total_bytes_sent += bytes_written; + } + } + pub inline fn setFd(self: *Ssl, fd: FileDescriptor) !void { const res = import.SSL_set_fd(self, @intFromEnum(fd)); if (res <= 0) { diff --git a/packages/web/src/root.zig b/packages/web/src/root.zig index a40d924..5906c24 100644 --- a/packages/web/src/root.zig +++ b/packages/web/src/root.zig @@ -10,10 +10,9 @@ pub const FileDescriptor = @import("FileDescriptor.zig").FileDescriptor; pub const http = @import("http.zig"); pub const Id = @import("Id.zig").Id; pub const openssl = @import("openssl.zig"); +pub const Request = @import("Request.zig"); pub const RequestHandler = @import("RequestHandler.zig"); -pub const RequestRouter = @import("RequestRouter.zig"); pub const Response = @import("Response.zig"); -pub const Route = @import("Route.zig"); pub const Server = @import("Server.zig"); pub const UUID = @import("UUID.zig"); pub const Worker = @import("Worker.zig");