web: unnecessary refactor before compilation

This commit is contained in:
2026-03-12 02:47:54 +01:00
parent 9a4932e629
commit fe4a585b6b
19 changed files with 754 additions and 414 deletions

View File

@@ -4,13 +4,17 @@ const Connection = @This();
const FileDescriptor = @import("FileDescriptor.zig").FileDescriptor; const FileDescriptor = @import("FileDescriptor.zig").FileDescriptor;
const openssl = @import("openssl.zig"); 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, address: std.net.Address,
fd: FileDescriptor, fd: FileDescriptor,
ssl: ?*openssl.Ssl, ssl: ?*openssl.Ssl,
node: std.DoublyLinkedList.Node = .{}, node: std.DoublyLinkedList.Node = .{},
// TODO Consider proper usage of `send` syscall with `MSG_MORE` flag and setting
// the `TCP_CORK` option.
pub fn reinit( pub fn reinit(
self: *Connection, self: *Connection,
address: std.net.Address, 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 { pub fn readAll(self: *const Connection, buf: []u8) !void {
if (self.ssl) |ssl| { if (self.ssl) |ssl| {
try ssl.readall(buf); try ssl.readAll(buf);
} else { } else {
try self.fd.readAll(buf); 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 { pub fn write(self: *const Connection, buf: []const u8) !usize {
if (self.ssl) |ssl| { if (self.ssl) |ssl| {
const bytes_written = try ssl.write(buf); 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); 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);
}
}

View File

@@ -3,6 +3,9 @@ const std = @import("std");
const linux = std.os.linux; const linux = std.os.linux;
const errno = linux.E.init; const errno = linux.E.init;
const iovec = std.posix.iovec;
const iovec_const = std.posix.iovec_const;
pub const FileDescriptor = enum(i32) { pub const FileDescriptor = enum(i32) {
stdin = 0, stdin = 0,
stdout = 1, stdout = 1,
@@ -83,7 +86,7 @@ pub const FileDescriptor = enum(i32) {
pub fn read(self: FileDescriptor, buf: []u8) !usize { pub fn read(self: FileDescriptor, buf: []u8) !usize {
while (true) { 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)) { switch (errno(rc)) {
.SUCCESS => return rc, .SUCCESS => return rc,
.INTR => continue, .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 { 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)) { return switch (errno(rc)) {
.SUCCESS => {}, .SUCCESS => {},
else => error.SystemError, else => error.SystemError,
@@ -112,7 +148,7 @@ pub const FileDescriptor = enum(i32) {
pub fn write(self: FileDescriptor, buf: []const u8) !usize { pub fn write(self: FileDescriptor, buf: []const u8) !usize {
while (true) { 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)) { switch (errno(rc)) {
.SUCCESS => return rc, .SUCCESS => return rc,
.INTR => continue, .INTR => continue,
@@ -130,4 +166,68 @@ pub const FileDescriptor = enum(i32) {
total_bytes_written += bytes_written; 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;
}
}
}; };

View File

@@ -35,7 +35,7 @@ pub fn Id(comptime _tag: @Type(.enum_literal)) type {
} }
pub fn encodeInto(self: @This(), text: *[22]u8) void { 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);
} }
}; };
} }

View File

@@ -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;
}

View File

@@ -1,56 +1,16 @@
const std = @import("std"); const std = @import("std");
const RequestHandler = @This(); const RequestHandler = @This();
const Header = @import("http/Header.zig"); const Request = @import("Request.zig");
const Response = @import("Response.zig"); const Response = @import("Response.zig");
const Route = @import("Route.zig");
const Worker = @import("Worker.zig");
ptr: *anyopaque, ptr: *anyopaque,
vtable: *const VTable, vtable: *const VTable,
pub const VTable = struct { pub const VTable = struct {
/// Called multiple times (could be zero) for each header in the request. handle: *const fn (self: *anyopaque, request: *Request, response: *Response) anyerror!void,
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,
}; };
pub fn noHeader(self: *anyopaque, response: *Response, header: Header) anyerror!void { pub inline fn handle(self: RequestHandler, request: *Request, response: *Response) anyerror!void {
_ = self; try self.vtable.handle(self.ptr, request, response);
_ = 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);
} }

View File

@@ -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);
}

View File

@@ -2,69 +2,84 @@ const std = @import("std");
const Response = @This(); const Response = @This();
const Connection = @import("Connection.zig"); const Connection = @import("Connection.zig");
const FileDescriptor = @import("FileDescriptor.zig").FileDescriptor;
const http = @import("http.zig"); const http = @import("http.zig");
const iovec = std.posix.iovec;
const iovec_const = std.posix.iovec_const;
pub const State = union(enum) { pub const State = union(enum) {
init: void, init: void,
sent: void, sent: void,
errored: anyerror, errored: anyerror,
pub fn initErrored(err: anyerror) State {
return .{ .errored = err };
}
}; };
connection: *Connection, connection: *Connection,
writer: std.Io.Writer, header_writer: std.Io.Writer,
body_writer: std.Io.Writer,
state: State, 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 .{ return .{
.connection = connection, .connection = connection,
.writer = .fixed(write_buffer), .header_writer = .fixed(header_write_buffer),
.writer = .fixed(body_write_buffer),
.state = .init, .state = .init,
}; };
} }
pub const ResponseEmptyOptions = struct { pub fn sendHeadersOnly(self: *Response) void {
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 {
std.debug.assert(self.state == .init); 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; self.state = .sent;
} else |err| { } 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);
} }
} }

View File

@@ -5,7 +5,8 @@ const Connection = @import("Connection.zig");
const FileDescriptor = @import("FileDescriptor.zig").FileDescriptor; const FileDescriptor = @import("FileDescriptor.zig").FileDescriptor;
const http = @import("http.zig"); const http = @import("http.zig");
const openssl = @import("openssl.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 Worker = @import("Worker.zig");
const linux = std.os.linux; const linux = std.os.linux;
@@ -16,7 +17,7 @@ address: std.net.Address,
ssl_ctx: ?*openssl.SslContext, ssl_ctx: ?*openssl.SslContext,
workers: []Worker, workers: []Worker,
threads: []std.Thread, threads: []std.Thread,
request_router: RequestRouter, request_handler: RequestHandler,
connection_queue: std.DoublyLinkedList, connection_queue: std.DoublyLinkedList,
// NOTE Connection pool has no need for being doubly-linked, but the queue has // 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_queued: std.Thread.Condition,
cond_connection_freed: std.Thread.Condition, cond_connection_freed: std.Thread.Condition,
/// 4 kiB
const page_size = 4 * 1024;
/// 2 MiB /// 2 MiB
const huge_page_size = 2 * 1024 * 1024; const huge_page_size = 2 * 1024 * 1024;
pub const Options = struct { pub const Options = struct {
request_router: RequestRouter, request_handler: RequestHandler,
address: std.net.Address = .initIp4(.{ 127, 0, 0, 1 }, 8000), address: std.net.Address = .initIp4(.{ 127, 0, 0, 1 }, 8000),
/// If not `null`, the server will use TLS with the provided OpenSSL /// If not `null`, the server will use TLS with the provided OpenSSL
/// context. /// context.
@@ -43,14 +46,25 @@ pub const Options = struct {
/// The number of worker threads. If set to `0`, the number of worker /// The number of worker threads. If set to `0`, the number of worker
/// threads will be equal to the number of logical CPU cores. /// threads will be equal to the number of logical CPU cores.
worker_count: u32 = 0, 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 /// 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) /// has its own read buffer. An HTTP request (headers and content combined)
/// will be rejected if it is larger than the read buffer. /// will be rejected if it is larger than the read buffer.
read_buffer_pages: u32 = 1, read_buffer_huge_pages: u32 = 1,
/// The number of 2 MiB pages reserved for a single write buffer. Each /// The number of 4 kiB pages reserved for a single header write buffer.
/// worker has its own write buffer. An HTTP response (headers and content /// Each worker has its own header write buffer. The HTTP status line, all
/// combined) must be larger than the write buffer. /// header fields and the CRLF terminator must all fit in the header write
write_buffer_pages: u32 = 1, /// 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 { 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 single_header_write_buffer_size = @as(usize, options.header_write_buffer_pages) * page_size;
const all_write_buffers_size = worker_count * single_write_buffer_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, null,
all_write_buffers_size, all_header_write_buffers_size,
linux.PROT.READ | linux.PROT.WRITE, linux.PROT.READ | linux.PROT.WRITE,
linux.MAP{ .TYPE = .PRIVATE, .ANONYMOUS = true }, linux.MAP{ .TYPE = .PRIVATE, .ANONYMOUS = true },
-1, -1,
0, 0,
)); ));
errdefer _ = linux.munmap(write_buffer_ptr, all_write_buffers_size); errdefer _ = linux.munmap(header_write_buffer_ptr, all_header_write_buffers_size);
_ = linux.madvise(write_buffer_ptr, all_write_buffers_size, linux.MADV.HUGEPAGE);
// 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 // Initialize workers
var workers_initialized: usize = 0;
errdefer {
for (workers[0..workers_initialized]) |*worker| {
worker.deinit(allocator);
}
}
for (workers, 0..) |*worker, i| { for (workers, 0..) |*worker, i| {
const read_offset = i * double_single_read_buffer_size; const read_offset = i * double_single_read_buffer_size;
const write_offset = i * single_write_buffer_size; const header_write_offset = i * single_header_write_buffer_size;
worker.* = .{ 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_ptr = read_buffer_ptr + read_offset,
.read_buffer_size = single_read_buffer_size, .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 // Fill connection pool
@@ -178,7 +219,7 @@ pub fn init(allocator: std.mem.Allocator, options: Options) !Server {
.ssl_ctx = options.ssl_ctx, .ssl_ctx = options.ssl_ctx,
.workers = workers, .workers = workers,
.threads = threads, .threads = threads,
.request_router = options.request_router, .request_handler = options.request_handler,
.connection_queue = .{}, .connection_queue = .{},
.connection_pool = connection_pool, .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 { pub fn deinit(self: *Server, allocator: std.mem.Allocator) void {
// TODO Deinitialize workers const worker_count = self.workers.len;
self.fd.close();
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.threads);
allocator.free(self.connection_buffer); allocator.free(self.connection_buffer);
allocator.free(self.workers); allocator.free(self.workers);
self.fd.close();
self.* = undefined; self.* = undefined;
} }

View File

@@ -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());
}

View File

@@ -4,17 +4,68 @@ const Worker = @This();
const Connection = @import("Connection.zig"); const Connection = @import("Connection.zig");
const FileDescriptor = @import("FileDescriptor.zig").FileDescriptor; const FileDescriptor = @import("FileDescriptor.zig").FileDescriptor;
const http = @import("http.zig"); const http = @import("http.zig");
const Request = @import("Request.zig");
const RequestHandler = @import("RequestHandler.zig"); const RequestHandler = @import("RequestHandler.zig");
const RequestRouter = @import("RequestRouter.zig");
const Response = @import("Response.zig"); const Response = @import("Response.zig");
const Server = @import("Server.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_ptr: [*]u8,
read_buffer_size: usize, read_buffer_size: usize,
read_head: usize, read_head: usize,
read_tail: 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( pub fn worker(
self: *Worker, self: *Worker,
@@ -35,7 +86,7 @@ pub fn worker(
server.cond_connection_freed.signal(); 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}); std.log.err("Error while handling connection: {}", .{err});
}; };
} else { } else {
@@ -46,14 +97,14 @@ pub fn worker(
fn handleConnection( fn handleConnection(
self: *Worker, self: *Worker,
request_router: RequestRouter, request_handler: RequestHandler,
connection: *Connection, connection: *Connection,
running: *const std.atomic.Value(bool), running: *const std.atomic.Value(bool),
) !void { ) !void {
defer connection.deinit(); defer connection.deinit();
while (running.load(.acquire)) { 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}); std.log.err("Error while handling request: {}", .{err});
return err; return err;
}; };
@@ -64,16 +115,22 @@ fn handleConnection(
fn handleRequest( fn handleRequest(
self: *Worker, self: *Worker,
request_router: RequestRouter, request_handler: RequestHandler,
connection: *Connection, connection: *Connection,
) !bool { ) !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 response: Response = .init(connection, self.write_buffer);
var parser: http.Parser = .init(request_router, &response); var parser: http.Parser = .init();
defer {
if (parser.request_handler) |rh| { var next_header_index: usize = 0;
rh.rawFinalize(); var ignore: bool = false;
}
}
var leftover_bytes = self.read_tail - self.read_head; var leftover_bytes = self.read_tail - self.read_head;
const max_read_tail = self.read_head + self.read_buffer_size; const max_read_tail = self.read_head + self.read_buffer_size;
@@ -84,7 +141,7 @@ fn handleRequest(
if (leftover_bytes > 0) { if (leftover_bytes > 0) {
bytes_read = leftover_bytes; 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; leftover_bytes = 0;
} else { } else {
const read_tail = self.read_tail; const read_tail = self.read_tail;
@@ -101,51 +158,16 @@ fn handleRequest(
error.HttpVersionNotSupported => { error.HttpVersionNotSupported => {
try response.sendClose(.{ .status_text = http.status.http_version_not_supported }); try response.sendClose(.{ .status_text = http.status.http_version_not_supported });
}, },
error.MissingLineFeed => { error.SyntaxError => {
try response.sendClose(.{ .status_text = http.status.bad_request }); 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; 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) { if (parser.state == .body) {
try response.sendClose(.{ .status_text = http.status.content_too_large }); try response.sendClose(.{ .status_text = http.status.content_too_large });
} else { } else {
@@ -154,11 +176,94 @@ fn handleRequest(
return false; return false;
} }
if (res.done) { if (res.result) |result| {
leftover_bytes = bytes_read - res.consumed; switch (result) {
self.read_head = (self.read_tail - leftover_bytes) & ~(self.read_buffer_size - 1); .method => |method| request.method = method,
self.read_tail = self.read_head + leftover_bytes; .pathname => |pathname| request.pathname = pathname,
return true; .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;
} }
} }

View File

@@ -5,5 +5,4 @@ pub const Header = @import("http/Header.zig");
pub const KnownFieldName = @import("http/KnownFieldName.zig").KnownFieldName; pub const KnownFieldName = @import("http/KnownFieldName.zig").KnownFieldName;
pub const Method = @import("http/Method.zig").Method; pub const Method = @import("http/Method.zig").Method;
pub const Parser = @import("http/Parser.zig"); pub const Parser = @import("http/Parser.zig");
pub const Route = @import("http/Route.zig");
pub const status = @import("http/status.zig"); pub const status = @import("http/status.zig");

View File

@@ -1,44 +1,90 @@
const std = @import("std"); const std = @import("std");
const KnownFieldName = @import("KnownFieldName.zig").KnownFieldName; const KnownFieldName = @import("KnownFieldName.zig").KnownFieldName;
const ShortString = @import("../ShortString.zig");
pub const FieldName = union(enum) { const Wyhash = std.hash.Wyhash;
known: KnownFieldName,
short: ShortString, pub const FieldName = extern struct {
long: []const u8, 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 { pub fn init(name: []const u8) FieldName {
var data: [16]u8 = @splat(0);
if (KnownFieldName.isKnownFieldName(name)) |known| { if (KnownFieldName.isKnownFieldName(name)) |known| {
return initKnown(known); data[0] = tag_known;
} else if (ShortString.isShortString(name)) |short| { @as(*KnownFieldName, @ptrCast(data[8..16])).* = known;
return initShort(short); } else if (name.len <= 15) {
data[0] = @intCast(name.len + tag_short_bias);
@memcpy(data[1..][0..name.len], name);
} else { } 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 { 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 { fn getKnown(self: FieldName) KnownFieldName {
return .{ .short = short }; std.debug.assert(self.data[0] == tag_known);
return @bitCast(self.data[8..16].*);
} }
pub fn initOther(other: []const u8) FieldName { fn getLong(self: FieldName) []const u8 {
return .{ .other = other }; 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 { pub fn eql(a: FieldName, b: FieldName) bool {
const tag_a = std.meta.activeTag(a); const a_tag = a.data[0];
const tag_b = std.meta.activeTag(b); const b_tag = b.data[0];
if (tag_a != tag_b) return false;
return switch (a) { if (a_tag != b_tag) return false;
.known => |x| x == b.known,
.short => |x| ShortString.eql(x, b.short), return switch (a_tag) {
.long => |x| std.mem.eql(x, b.long), 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);
}
};
}; };

View File

@@ -19,8 +19,5 @@ pub fn isNamed(self: Header, name: FieldName) bool {
} }
pub fn isNamedKnown(self: Header, known: KnownFieldName) bool { pub fn isNamedKnown(self: Header, known: KnownFieldName) bool {
return switch (self.name) { return FieldName.eql(self.name, .initKnonw(known));
.known => |x| x == known,
else => false,
};
} }

View File

@@ -1,6 +1,6 @@
const std = @import("std"); const std = @import("std");
pub const KnownFieldName = enum { pub const KnownFieldName = enum(u64) {
// --- STANDARD FIELD NAMES ------------------------------------------------ // --- STANDARD FIELD NAMES ------------------------------------------------
@@ -11,7 +11,7 @@ pub const KnownFieldName = enum {
// Some of them might be obsoleted or deprecated; they are included here // Some of them might be obsoleted or deprecated; they are included here
// nonetheless. // 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", @"A-IM",
Accept, Accept,

View File

@@ -7,10 +7,11 @@
//! //!
//! During a single ingestion, the parser can return one of the following: //! 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 //! - 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 //! - 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 //! the user to make decisions about further processing of the request based
//! on the full knowledge of all the headers //! on the full knowledge of all the headers
//! - body of type `[]const u8`, i.e. a slice to the request body (or //! - 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 FieldName = @import("FieldName.zig").FieldName;
const Header = @import("Header.zig"); const Header = @import("Header.zig");
const Method = @import("Method.zig").Method; const Method = @import("Method.zig").Method;
const Route = @import("Route.zig");
pub const Error = error{ pub const Error = error{
MethodNotSupported, MethodNotSupported,
@@ -57,13 +57,18 @@ pub const Error = error{
}; };
pub const Result = union(enum) { pub const Result = union(enum) {
route: Route, method: Method,
pathname: []const u8,
header: Header, header: Header,
end_of_headers: void, end_of_headers: void,
body: []const u8, body: []const u8,
pub fn initRoute(route: Route) Result { pub fn initMethod(method: Method) Result {
return .{ .route = route }; return .{ .method = method };
}
pub fn initPathname(pathname: []const u8) Result {
return .{ .pathname = pathname };
} }
pub fn initHeader(header: Header) Result { pub fn initHeader(header: Header) Result {
@@ -115,8 +120,8 @@ pub const State = union(enum) {
method_optio: void, method_optio: void,
method_connec: void, method_connec: void,
method_option: void, method_option: void,
method_complete: Method, method_complete: void,
pathname: Route, pathname: []const u8,
pathname_complete: void, pathname_complete: void,
version_h: void, version_h: void,
version_ht: void, version_ht: void,
@@ -135,12 +140,8 @@ pub const State = union(enum) {
body: []const u8, body: []const u8,
done: void, done: void,
pub fn initMethodComplete(method: Method) State { pub fn initPathname(pathname: []const u8) State {
return .{ .method_complete = method }; return .{ .pathname = pathname };
}
pub fn initPathname(route: Route) State {
return .{ .pathname = route };
} }
pub fn initHeaderName(name: []const u8) State { 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, else => return error.MethodNotSupported,
}, },
.method_ge => switch (char) { .method_ge => switch (char) {
'T' => self.state = .initMethodComplete(.GET), 'T' => {
self.state = .method_complete;
return .initMethod(.GET);
},
else => return error.MethodNotSupported, else => return error.MethodNotSupported,
}, },
.method_he => switch (char) { .method_he => switch (char) {
@@ -300,7 +304,10 @@ fn consumeChar(self: *Parser, char_ptr: *const u8) Error!?Result {
else => return error.MethodNotSupported, else => return error.MethodNotSupported,
}, },
.method_pu => switch (char) { .method_pu => switch (char) {
'T' => self.state = .initMethodComplete(.PUT), 'T' => {
self.state = .method_complete;
return .initMethod(.PUT);
},
else => return error.MethodNotSupported, else => return error.MethodNotSupported,
}, },
.method_tr => switch (char) { .method_tr => switch (char) {
@@ -316,7 +323,10 @@ fn consumeChar(self: *Parser, char_ptr: *const u8) Error!?Result {
else => return error.MethodNotSupported, else => return error.MethodNotSupported,
}, },
.method_hea => switch (char) { .method_hea => switch (char) {
'D' => self.state = .initHeaderName(.HEAD), 'D' => {
self.state = .method_complete;
return .initMethod(.HEAD);
},
else => return error.MethodNotSupported, else => return error.MethodNotSupported,
}, },
.method_opt => switch (char) { .method_opt => switch (char) {
@@ -328,7 +338,10 @@ fn consumeChar(self: *Parser, char_ptr: *const u8) Error!?Result {
else => return error.MethodNotSupported, else => return error.MethodNotSupported,
}, },
.method_pos => switch (char) { .method_pos => switch (char) {
'T' => self.state = .initHeaderName(.POST), 'T' => {
self.state = .method_complete;
return .initMethod(.POST);
},
else => return error.MethodNotSupported, else => return error.MethodNotSupported,
}, },
.method_tra => switch (char) { .method_tra => switch (char) {
@@ -348,11 +361,17 @@ fn consumeChar(self: *Parser, char_ptr: *const u8) Error!?Result {
else => return error.MethodNotSupported, else => return error.MethodNotSupported,
}, },
.method_patc => switch (char) { .method_patc => switch (char) {
'H' => self.state = .initMethodComplete(.PATCH), 'H' => {
self.state = .method_complete;
return .initMethod(.PATCH);
},
else => return error.MethodNotSupported, else => return error.MethodNotSupported,
}, },
.method_trac => switch (char) { .method_trac => switch (char) {
'E' => self.state = .initMethodComplete(.TRACE), 'E' => {
self.state = .method_complete;
return .initMethod(.TRACE);
},
else => return error.MethodNotSupported, else => return error.MethodNotSupported,
}, },
.method_conne => switch (char) { .method_conne => switch (char) {
@@ -360,7 +379,10 @@ fn consumeChar(self: *Parser, char_ptr: *const u8) Error!?Result {
else => return error.MethodNotSupported, else => return error.MethodNotSupported,
}, },
.method_delet => switch (char) { .method_delet => switch (char) {
'E' => self.state = .initMethodComplete(.DELETE), 'E' => {
self.state = .method_complete;
return .initMethod(.DELETE);
},
else => return error.MethodNotSupported, else => return error.MethodNotSupported,
}, },
.method_optio => switch (char) { .method_optio => switch (char) {
@@ -368,23 +390,29 @@ fn consumeChar(self: *Parser, char_ptr: *const u8) Error!?Result {
else => return error.MethodNotSupported, else => return error.MethodNotSupported,
}, },
.method_connec => switch (char) { .method_connec => switch (char) {
'T' => self.state = .initMethodComplete(.CONNECT), 'T' => {
self.state = .method_complete;
return .initMethod(.CONNECT);
},
else => return error.MethodNotSupported, else => return error.MethodNotSupported,
}, },
.method_option => switch (char) { .method_option => switch (char) {
'S' => self.state = .initMethodComplete(.OPTIONS), 'S' => {
self.state = .method_complete;
return .initMethod(.OPTIONS);
},
else => return error.MethodNotSupported, else => return error.MethodNotSupported,
}, },
.method_complete => |method| switch (char) { .method_complete => switch (char) {
' ' => self.state = .initPathname(.init(method, next_char_slice)), ' ' => self.state = .initPathname(next_char_slice),
else => return error.MethodNotSupported, else => return error.MethodNotSupported,
}, },
.pathname => |route| switch (char) { .pathname => |pathname| switch (char) {
' ' => { ' ' => {
self.state = .pathname_complete; 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) { .pathname_complete => switch (char) {
'H' => self.state = .version_h, '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]; 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 { fn extendHeader(header: Header) Header {
return .{ return .{
.name = header.name, .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| { inline for (@typeInfo(patterns.methods).@"struct".decls) |decl| {
const pattern: Pattern = @field(patterns.methods, decl.name); const pattern: Pattern = @field(patterns.methods, decl.name);
if (pattern.check(vec)) { if (pattern.check(vec)) {
self.state = .methodComplete(@field(Method, decl.name)); self.state = .method_complete;
return .{ return .{
.consumed = pattern.len, .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`. // Delegate to `consumeChar`.
return .{ return .{
.consumed = 0, .consumed = 0,
.done = false, .result = null,
}; };
} }
self.state = .pathname(s.method, s.pathname.ptr[0 .. s.pathname.len + vec_len]); self.state = .pathname(s.method, s.pathname.ptr[0 .. s.pathname.len + vec_len]);
return .{ return .{
.consumed = vec_len, .consumed = vec_len,
.done = false, .result = null,
}; };
}, },
.pathname_complete => { .pathname_complete => {
@@ -627,7 +648,7 @@ pub fn consumeVec(self: *Parser, vec_ptr: *const [vec_len]u8) Error!ConsumeResul
self.state = .header_name_start; self.state = .header_name_start;
return .{ return .{
.consumed = patterns.@"version_http/1.1".len, .consumed = patterns.@"version_http/1.1".len,
.done = false, .result = null,
}; };
} else { } else {
return error.HttpVersionNotSupported; return error.HttpVersionNotSupported;
@@ -638,21 +659,21 @@ pub fn consumeVec(self: *Parser, vec_ptr: *const [vec_len]u8) Error!ConsumeResul
// Delegate to `consumeChar`. // Delegate to `consumeChar`.
return .{ return .{
.consumed = 0, .consumed = 0,
.done = false, .result = null,
}; };
} }
self.state = .headerValue(s.name, s.value.ptr[0 .. s.value.len + vec_len]); self.state = .headerValue(s.name, s.value.ptr[0 .. s.value.len + vec_len]);
return .{ return .{
.consumed = vec_len, .consumed = vec_len,
.done = false, .result = null,
}; };
}, },
else => { else => {
// Delegate to `consumeChar`. // Delegate to `consumeChar`.
return .{ return .{
.consumed = 0, .consumed = 0,
.done = false, .result = null,
}; };
}, },
} }

View File

@@ -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,
};
}

View File

@@ -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 { 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, try response.header_writer.writeAll(web.http.status.not_found);
uuid: UUID, try response.header_writer.writeAll("Content-Type: text/plain; charset=utf-8\r\n");
timer: std.time.Timer, 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, response.sendHeadersAndBody();
accept_encoding: ?[]const u8 = null, return;
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; 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 { fn interface() web.RequestHandler {
const self: *Handler = @ptrCast(@alignCast(ctx)); return .{
.ptr = undefined,
try response.sendResponse(.{ .vtable = &.{
.media_type = "application/json", .handle = handle,
.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);
} }
}; };
@@ -135,8 +70,6 @@ pub fn main() !void {
defer _ = gpa.deinit(); defer _ = gpa.deinit();
const allocator = gpa.allocator(); const allocator = gpa.allocator();
var router: Router = .init(allocator);
_ = ssl.c_ssl.SSL_library_init(); _ = ssl.c_ssl.SSL_library_init();
_ = ssl.c_ssl.OpenSSL_add_all_algorithms(); _ = ssl.c_ssl.OpenSSL_add_all_algorithms();
_ = ssl.c_ssl.SSL_load_error_strings(); _ = ssl.c_ssl.SSL_load_error_strings();
@@ -152,7 +85,7 @@ pub fn main() !void {
try ssl_ctx.checkPrivateKey(); try ssl_ctx.checkPrivateKey();
var server = try web.Server.init(allocator, .{ var server = try web.Server.init(allocator, .{
.request_router = router.interface(), .request_handler = Handler.interface(),
.address = .initIp4(.{ 127, 0, 0, 1 }, 8000), .address = .initIp4(.{ 127, 0, 0, 1 }, 8000),
.ssl_ctx = ssl_ctx, .ssl_ctx = ssl_ctx,
}); });

View File

@@ -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 { pub inline fn setFd(self: *Ssl, fd: FileDescriptor) !void {
const res = import.SSL_set_fd(self, @intFromEnum(fd)); const res = import.SSL_set_fd(self, @intFromEnum(fd));
if (res <= 0) { if (res <= 0) {

View File

@@ -10,10 +10,9 @@ pub const FileDescriptor = @import("FileDescriptor.zig").FileDescriptor;
pub const http = @import("http.zig"); pub const http = @import("http.zig");
pub const Id = @import("Id.zig").Id; pub const Id = @import("Id.zig").Id;
pub const openssl = @import("openssl.zig"); pub const openssl = @import("openssl.zig");
pub const Request = @import("Request.zig");
pub const RequestHandler = @import("RequestHandler.zig"); pub const RequestHandler = @import("RequestHandler.zig");
pub const RequestRouter = @import("RequestRouter.zig");
pub const Response = @import("Response.zig"); pub const Response = @import("Response.zig");
pub const Route = @import("Route.zig");
pub const Server = @import("Server.zig"); pub const Server = @import("Server.zig");
pub const UUID = @import("UUID.zig"); pub const UUID = @import("UUID.zig");
pub const Worker = @import("Worker.zig"); pub const Worker = @import("Worker.zig");