web: some stuff

This commit is contained in:
2026-03-07 21:08:22 +01:00
parent f02ece22fa
commit 66d49ea8d5
14 changed files with 38964 additions and 101 deletions

View File

@@ -1,36 +0,0 @@
{
"folders": [
{
"name": "cjit",
"path": "packages/cjit"
},
{
"name": "js",
"path": "packages/js"
},
{
"name": "media",
"path": "packages/media"
},
{
"name": "myid",
"path": "packages/myid"
},
{
"name": "vecmath",
"path": "packages/vecmath"
},
{
"name": "x11",
"path": "packages/x11"
}
],
"settings": {
"files.exclude": {
"**/.zig-cache": true,
},
"files.associations": {
"**/packages/tcc/vendor/*.{def,h}": "c",
},
},
}

View File

@@ -0,0 +1,56 @@
const std = @import("std");
const RequestHandler = @This();
const Header = @import("http/Header.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,
};
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);
}

View File

@@ -0,0 +1,16 @@
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

@@ -0,0 +1,70 @@
const std = @import("std");
const Response = @This();
const Connection = @import("Connection.zig");
const http = @import("http.zig");
pub const State = union(enum) {
init: void,
sent: void,
errored: anyerror,
};
connection: Connection,
writer: std.Io.Writer,
state: State,
pub fn init(connection: Connection, write_buffer: []u8) Response {
return .{
.connection = connection,
.writer = .fixed(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 sendResponseEmpty(self: *Response, options: ResponseEmptyOptions) !void {
try self.writer.print("{s}", .{options.status_text});
try self.writer.print("\r\n", .{});
self.finalize();
}
pub fn sendResponseClose(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);
if (self.connection.fd.writeAll(self.writer.buffered())) {
self.state = .sent;
} else |err| {
self.state = .{ .errored = err };
}
}

View File

@@ -0,0 +1,14 @@
const std = @import("std");
const Route = @This();
const Method = @import("http/Method.zig").Method;
method: Method,
pathname: []const u8,
pub fn init(method: Method, pathname: []const u8) Route {
return .{
.method = method,
.pathname = pathname,
};
}

249
packages/web/src/Server.zig Normal file
View File

@@ -0,0 +1,249 @@
const std = @import("std");
const Server = @This();
const Connection = @import("Connection.zig");
const FileDescriptor = @import("FileDescriptor.zig").FileDescriptor;
const http = @import("http.zig");
const RequestRouter = @import("RequestRouter.zig");
const Worker = @import("Worker.zig");
const linux = std.os.linux;
const errno = linux.E.init;
fd: FileDescriptor,
address: std.net.Address,
workers: []Worker,
request_router: RequestRouter,
connection_queue: std.DoublyLinkedList,
// NOTE Connection pool has no need for being doubly-linked, but the queue has
// (as it's FIFO) and we want a single intrusive Node to be able to participate
// in both lists. This is possible because a connection will never belong to
// both lists at the same time.
connection_pool: std.DoublyLinkedList,
connection_buffer: []Connection,
mutex: std.Thread.Mutex,
cond_connection_queued: std.Thread.Condition,
cond_connection_freed: std.Thread.Condition,
/// 2 MiB
const huge_page_size = 2 * 1024 * 1024;
pub const Options = struct {
request_router: RequestRouter,
address: std.net.Address = .initIp4(.{ 127, 0, 0, 1 }, 80),
max_connections: u32 = 128,
/// 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 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,
};
pub fn init(allocator: std.mem.Allocator, options: Options) !Server {
const worker_count = if (options.worker_count > 0) options.worker_count else try std.Thread.getCpuCount();
// Create socket fd
const fd: FileDescriptor = try .socket(
options.address.any.family,
linux.SOCK.STREAM | linux.SOCK.CLOEXEC,
if (options.address.any.family == linux.AF.UNIX) 0 else linux.IPPROTO.TCP,
);
errdefer fd.close();
const opt = std.mem.toBytes(@as(c_int, 1));
try fd.setsockopt(linux.SOL.SOCKET, linux.SO.REUSEADDR, &opt);
try fd.setsockopt(linux.SOL.SOCKET, linux.SO.REUSEPORT, &opt);
var socklen = options.address.getOsSockLen();
try fd.bind(&options.address.any, socklen);
try fd.listen(options.kernel_backlog);
var listen_address = options.address;
try fd.getsockname(fd, &listen_address, &socklen);
// Allocate workers
const workers = try allocator.alloc(Worker, worker_count);
errdefer allocator.free(workers);
// Allocate connection pool
const connection_buffer = try allocator.alloc(Connection, options.max_connections);
errdefer allocator.free(connection_buffer);
// Allocate and remap read buffers
const single_read_buffer_size = @as(usize, options.read_buffer_pages) * huge_page_size;
const all_read_buffers_size = worker_count * single_read_buffer_size;
const double_single_read_buffer_size = 2 * single_read_buffer_size;
const double_all_read_buffers_size = 2 * all_read_buffers_size;
const read_buffer_fd: FileDescriptor = try .memfd_create("read_buffer", 0);
defer read_buffer_fd.close();
try read_buffer_fd.ftruncate(all_read_buffers_size);
const read_buffer_ptr = try errOrPtr(linux.mmap(
null,
double_all_read_buffers_size,
linux.PROT.NONE,
linux.MAP{ .TYPE = .PRIVATE, .ANONYMOUS = true },
-1,
0,
));
errdefer _ = linux.munmap(read_buffer_ptr, double_all_read_buffers_size);
_ = linux.madvise(read_buffer_ptr, double_all_read_buffers_size, linux.MADV.HUGEPAGE);
for (0..worker_count) |i| {
const offset = i * single_read_buffer_size;
const double_offset = i * double_single_read_buffer_size;
try err(linux.mmap(
read_buffer_ptr + double_offset,
single_read_buffer_size,
linux.PROT.READ | linux.PROT.WRITE,
linux.MAP{ .TYPE = .SHARED, .FIXED = true },
@intFromEnum(read_buffer_fd),
offset,
));
try err(linux.mmap(
read_buffer_ptr + double_offset + single_read_buffer_size,
single_read_buffer_size,
linux.PROT.READ | linux.PROT.WRITE,
linux.MAP{ .TYPE = .SHARED, .FIXED = true },
@intFromEnum(read_buffer_fd),
offset,
));
}
// Allocate 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 write_buffer_ptr = try errOrPtr(linux.mmap(
null,
all_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);
// Initialize workers
for (workers, 0..) |*worker, i| {
const read_offset = i * double_single_read_buffer_size;
const write_offset = i * single_write_buffer_size;
worker.* = .{
.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],
};
}
// Fill connection pool
var connection_pool: std.DoublyLinkedList = .{};
for (connection_buffer) |*c| {
connection_pool.prepend(c.node);
}
return .{
.fd = fd,
.address = listen_address,
.workers = workers,
.request_router = options.request_router,
.connection_queue = .{},
.connection_pool = connection_pool,
.connection_buffer = connection_buffer,
.mutex = .{},
.cond_connection_queued = .{},
.cond_connection_freed = .{},
};
}
pub fn deinit(self: *Server, allocator: std.mem.Allocator) void {
// TODO Deinitialize workers
self.fd.close();
allocator.free(self.connection_buffer);
self.* = undefined;
}
/// This method block until the server is stopped, which is achieved by storing
/// `false` into `running`. You should use another thread or interruption
/// handler to be able to stop the server.
pub fn listen(self: *Server, running: *const std.atomic.Value(bool)) !void {
var worker_running: std.atomic.Value(bool) = .init(running.load(.acquire));
var spawned: usize = 0;
defer {
worker_running.store(false, .release);
self.cond_connection_queued.broadcast();
for (self.workers[0..spawned]) |*worker| {
worker.thread.join();
}
}
for (self.workers) |*worker| {
worker.thread = try std.Thread.spawn(.{}, Worker.worker, .{ worker, self, &worker_running });
spawned += 1;
}
while (running.load(.acquire)) {
var address: std.net.Address = undefined;
var address_size: u32 = @sizeOf(std.net.Address);
const fd = self.fd.accept(&address.any, &address_size) catch |e| {
std.log.err("Error while accepting connection: {}", .{e});
continue;
};
{
self.mutex.lock();
defer self.mutex.unlock();
while (true) {
if (self.connection_pool.pop()) |node| {
const connection: *Connection = @fieldParentPtr("node", node);
connection.fd = fd;
connection.address = address;
self.connection_queue.prepend(node);
break;
}
self.cond_connection_freed.wait(&self.mutex);
}
}
self.cond_connection_queued.signal();
}
}
fn err(rc: usize) !void {
const e = errno(rc);
return if (e != .SUCCESS) error.SystemError else rc;
}
fn errOrPtr(rc: usize) ![*]u8 {
const e = errno(rc);
return if (e != .SUCCESS) error.SystemError else @ptrFromInt(rc);
}

View File

@@ -0,0 +1,338 @@
const std = @import("std");
const Header = @This();
name: Name,
value: []const u8,
pub fn init(name: []const u8, value: []const u8) Header {
return .{
.name = .init(name),
.value = value,
};
}
pub fn initKnown(known: Name.Known, value: []const u8) Header {
return .{
.name = .initKnown(known),
.value = value,
};
}
pub fn isKnown(self: Header, known: Name.Known) bool {
return switch (self.name) {
.known => |x| x == known,
.other => false,
};
}
pub const Name = union(enum) {
known: Known,
other: []const u8,
pub fn initKnown(known: Known) Name {
return .{ .known = known };
}
pub fn initOther(other: []const u8) Name {
return .{ .other = other };
}
pub fn init(name: []const u8) Name {
return if (Known.isKnown(name)) |known| .initKnown(known) else .initOther(name);
}
pub const Known = enum {
@"A-IM",
Accept,
@"Accept-Additions",
@"Accept-CH",
@"Accept-Charset",
@"Accept-Datetime",
@"Accept-Encoding",
@"Accept-Features",
@"Accept-Language",
@"Accept-Patch",
@"Accept-Post",
@"Accept-Query",
@"Accept-Ranges",
@"Accept-Signature",
@"Access-Control",
@"Access-Control-Allow-Credentials",
@"Access-Control-Allow-Headers",
@"Access-Control-Allow-Methods",
@"Access-Control-Allow-Origin",
@"Access-Control-Expose-Headers",
@"Access-Control-Max-Age",
@"Access-Control-Request-Headers",
@"Access-Control-Request-Method",
@"Activate-Storage-Access",
Age,
Allow,
ALPN,
@"Alt-Svc",
@"Alt-Used",
Alternates,
@"AMP-Cache-Transform",
@"Apply-To-Redirect-Ref",
@"Authentication-Control",
@"Authentication-Info",
Authorization,
@"Available-Dictionary",
@"C-Ext",
@"C-Man",
@"C-Opt",
@"C-PEP",
@"C-PEP-Info",
@"Cache-Control",
@"Cache-Group-Invalidation",
@"Cache-Groups",
@"Cache-Status",
@"Cal-Managed-ID",
@"CalDAV-Timezones",
@"Capsule-Protocol",
@"CDN-Cache-Control",
@"CDN-Loop",
@"Cert-Not-After",
@"Cert-Not-Before",
@"Clear-Site-Data",
@"Client-Cert",
@"Client-Cert-Chain",
Close,
@"CMCD-Object",
@"CMCD-Request",
@"CMCD-Session",
@"CMCD-Status",
@"CMSD-Dynamic",
@"CMSD-Static",
@"Concealed-Auth-Export",
@"Configuration-Context",
Connection,
@"Content-Base",
@"Content-Digest",
@"Content-Disposition",
@"Content-Encoding",
@"Content-ID",
@"Content-Language",
@"Content-Length",
@"Content-Location",
@"Content-MD5",
@"Content-Range",
@"Content-Script-Type",
@"Content-Security-Policy",
@"Content-Security-Policy-Report-Only",
@"Content-Style-Type",
@"Content-Type",
@"Content-Version",
Cookie,
Cookie2,
@"Cross-Origin-Embedder-Policy",
@"Cross-Origin-Embedder-Policy-Report-Only",
@"Cross-Origin-Opener-Policy",
@"Cross-Origin-Opener-Policy-Report-Only",
@"Cross-Origin-Resource-Policy",
@"CTA-Common-Access-Token",
DASL,
Date,
DAV,
@"Default-Style",
@"Delta-Base",
Deprecation,
Depth,
@"Derived-From",
Destination,
@"Detached-JWS",
@"Differential-ID",
@"Dictionary-ID",
Digest,
DPoP,
@"DPoP-Nonce",
@"Early-Data",
@"EDIINT-Features",
ETag,
Expect,
@"Expect-CT",
Expires,
Ext,
Forwarded,
From,
GetProfile,
Hobareg,
Host,
@"HTTP2-Settings",
If,
@"If-Match",
@"If-Modified-Since",
@"If-None-Match",
@"If-Range",
@"If-Schedule-Tag-Match",
@"If-Unmodified-Since",
IM,
@"Include-Referred-Token-Binding-ID",
Incremental,
Isolation,
@"Keep-Alive",
Label,
@"Last-Event-ID",
@"Last-Modified",
Link,
@"Link-Template",
Location,
@"Lock-Token",
Man,
@"Max-Forwards",
@"Memento-Datetime",
Meter,
@"Method-Check",
@"Method-Check-Expires",
@"MIME-Version",
Negotiate,
NEL,
@"OData-EntityId",
@"OData-Isolation",
@"OData-MaxVersion",
@"OData-Version",
Opt,
@"Optional-WWW-Authenticate",
@"Ordering-Type",
Origin,
@"Origin-Agent-Cluster",
OSCORE,
@"OSLC-Core-Version",
Overwrite,
P3P,
PEP,
@"PEP-Info",
@"Permissions-Policy",
@"PICS-Label",
@"Ping-From",
@"Ping-To",
Position,
Pragma,
Prefer,
@"Preference-Applied",
Priority,
ProfileObject,
Protocol,
@"Protocol-Info",
@"Protocol-Query",
@"Protocol-Request",
@"Proxy-Authenticate",
@"Proxy-Authentication-Info",
@"Proxy-Authorization",
@"Proxy-Features",
@"Proxy-Instruction",
@"Proxy-Status",
Public,
@"Public-Key-Pins",
@"Public-Key-Pins-Report-Only",
Range,
@"Redirect-Ref",
Referer,
@"Referer-Root",
@"Referrer-Policy",
Refresh,
@"Repeatability-Client-ID",
@"Repeatability-First-Sent",
@"Repeatability-Request-ID",
@"Repeatability-Result",
@"Replay-Nonce",
@"Reporting-Endpoints",
@"Repr-Digest",
@"Retry-After",
Safe,
@"Schedule-Reply",
@"Schedule-Tag",
@"Sec-Fetch-Dest",
@"Sec-Fetch-Mode",
@"Sec-Fetch-Site",
@"Sec-Fetch-Storage-Access",
@"Sec-Fetch-User",
@"Sec-GPC",
@"Sec-Purpose",
@"Sec-Token-Binding",
@"Sec-WebSocket-Accept",
@"Sec-WebSocket-Extensions",
@"Sec-WebSocket-Key",
@"Sec-WebSocket-Protocol",
@"Sec-WebSocket-Version",
@"Security-Scheme",
Server,
@"Server-Timing",
@"Set-Cookie",
@"Set-Cookie2",
@"Set-Txn",
SetProfile,
Signature,
@"Signature-Input",
SLUG,
SoapAction,
@"Status-URI",
@"Strict-Transport-Security",
Sunset,
@"Surrogate-Capability",
@"Surrogate-Control",
TCN,
TE,
Timeout,
@"Timing-Allow-Origin",
Topic,
Traceparent,
Tracestate,
Trailer,
@"Transfer-Encoding",
TTL,
Upgrade,
Urgency,
URI,
@"Use-As-Dictionary",
@"User-Agent",
@"Variant-Vary",
Vary,
Via,
@"Want-Content-Digest",
@"Want-Digest",
@"Want-Repr-Digest",
Warning,
@"WWW-Authenticate",
@"X-Content-Type-Options",
@"X-Frame-Options",
/// Maps **lowercased** header names to enum values.
pub const map: std.StaticStringMap(Known) = blk: {
const fields = @typeInfo(Known).@"enum".fields;
var kvs_list: [fields.len]struct { []const u8, Known } = undefined;
for (fields, 0..) |field, i| {
var name_buf: [field.name.len]u8 = undefined;
_ = std.ascii.lowerString(&name_buf, field.name);
const name = name_buf;
kvs_list[i] = .{ &name, @field(Known, field.name) };
}
break :blk .initComptime(kvs_list);
};
/// The maximum length of all known header names. Any header name longer
/// than this cannot be a known header name.
pub const max_known_name_len = blk: {
const fields = @typeInfo(Known).@"enum".fields;
var max_len: usize = 0;
for (fields) |field| {
max_len = @max(max_len, field.name.len);
}
break :blk max_len;
};
pub fn isKnown(name: []const u8) ?Known {
if (name.len > max_known_name_len) {
@branchHint(.unlikely);
return null;
}
var name_lowercase_buf: [max_known_name_len]u8 = undefined;
const name_lowercase = std.ascii.lowerString(&name_lowercase_buf, name);
return map.get(name_lowercase);
}
};
};

View File

@@ -0,0 +1,13 @@
const std = @import("std");
pub const Method = enum {
CONNECT,
DELETE,
GET,
HEAD,
OPTIONS,
PATCH,
POST,
PUT,
TRACE,
};

View File

@@ -1,24 +1,23 @@
const std = @import("std");
const Parser = @This();
const Callbacks = struct {
self: ?*anyopaque = null,
route: ?*const fn (self: ?*anyopaque, route: Route) void = null,
header: ?*const fn (self: ?*anyopaque, name: []const u8, value: []const u8) void = null,
body: ?*const fn (self: ?*anyopaque, body: []const u8) void = null,
pub const null_callbacks: Callbacks = .{};
};
const Header = @import("Header.zig");
const Method = @import("Method.zig").Method;
const Response = @import("../Response.zig");
const RequestHandler = @import("../RequestHandler.zig");
const RequestRouter = @import("../RequestRouter.zig");
const Error = error{
MethodNotSupported,
HttpVersionNotSupported,
MissingLineFeed,
InvalidContentLength,
RouterError,
HandlerError,
};
const Vec = @Vector(32, u8);
const Vec: type = @Vector(std.simd.suggestVectorLength(u8).?, u8);
const vec_len = @typeInfo(Vec).vector.len;
const Pattern = struct {
value: Vec,
@@ -26,13 +25,13 @@ const Pattern = struct {
len: u6,
pub fn init(comptime prefix: []const u8) Pattern {
if (prefix.len > 32) {
if (prefix.len > vec_len) {
@compileError("Prefix length is too high");
}
var value: [32]u8 = undefined;
var mask: [32]u8 = undefined;
for (0..32) |i| {
var value: [vec_len]u8 = undefined;
var mask: [vec_len]u8 = undefined;
for (0..vec_len) |i| {
if (i < prefix.len) {
value[i] = prefix[i];
mask[i] = 0xFF;
@@ -49,14 +48,24 @@ const Pattern = struct {
};
const patterns = struct {
method_delete: Pattern.init("DELETE "),
method_get: Pattern.init("GET "),
method_head: Pattern.init("HEAD "),
method_patch: Pattern.init("PATCH "),
method_post: Pattern.init("POST "),
method_put: Pattern.init("PUT "),
pub const methods = struct {
// NOTE These patterns are arranged in a specific order, such that the
// first ones are the most common (based on vibes only).
@"version_http/1.1": Pattern.init("HTTP/1.1\r\n"),
pub const GET = Pattern.init("GET ");
pub const POST = Pattern.init("POST ");
pub const HEAD = Pattern.init("HEAD ");
pub const PUT = Pattern.init("PUT ");
pub const DELETE = Pattern.init("DELETE ");
pub const PATCH = Pattern.init("PATCH ");
pub const OPTIONS = Pattern.init("OPTIONS ");
pub const CONNECT = Pattern.init("CONNECT ");
pub const TRACE = Pattern.init("TRACE ");
};
pub const @"version_http/1.1" = Pattern.init("HTTP/1.1\r\n");
};
inline fn hasSpace(vec: Vec) bool {
@@ -98,6 +107,8 @@ const State = union(enum) {
}
init: void,
// TODO Add all methods here and in `consumeChar` (they are covered by
// `consumeVec`, though)
method_d: void,
method_g: void,
method_h: void,
@@ -145,30 +156,20 @@ const ConsumeCharResult = enum {
done,
};
pub const Method = enum {
DELETE,
GET,
HEAD,
PATCH,
POST,
PUT,
};
pub const Route = struct {
method: Method,
pathname: []const u8,
};
callbacks: Callbacks,
request_router: RequestRouter,
response: *Response,
state: State,
current_header_is_content_length: bool,
content_length: usize,
pub fn init(callbacks: Callbacks) Parser {
request_handler: ?RequestHandler = null,
last_router_error: anyerror = undefined,
last_handler_error: anyerror = undefined,
pub fn init(request_router: RequestRouter, response: *Response) Parser {
return .{
.callbacks = callbacks,
.request_router = request_router,
.response = response,
.state = .init,
.current_header_is_content_length = false,
.content_length = 0,
};
}
@@ -186,9 +187,10 @@ pub fn consume(self: *Parser, chars: []const u8) Error!ConsumeResult {
const done = new_body.len >= self.content_length;
if (done) {
if (self.callbacks.body) |bodyCallback| {
bodyCallback(self.callbacks.self, new_body);
}
self.request_handler.?.rawBody(self.response, new_body) catch |err| {
self.last_handler_error = err;
return error.HandlerError;
};
}
return .{
@@ -197,9 +199,25 @@ pub fn consume(self: *Parser, chars: []const u8) Error!ConsumeResult {
};
},
else => {
const res = try self.consumeChar(&chars[i]);
if (chars.len - i >= vec_len) {
const vec_res = try self.consumeVec(chars[i..][0..vec_len]);
i += vec_res.consumed;
if (vec_res.done) {
return .{
.consumed = i,
.done = true,
};
}
if (vec_res.consumed > 0) {
continue;
}
}
const char_res = try self.consumeChar(&chars[i]);
i += 1;
if (res == .done) return .{
if (char_res == .done) return .{
.consumed = i,
.done = true,
};
@@ -213,6 +231,65 @@ pub fn consume(self: *Parser, chars: []const u8) Error!ConsumeResult {
};
}
pub fn consumeVec(self: *Parser, vec: *const [vec_len]u8) Error!ConsumeResult {
switch (self.state) {
.init => {
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));
return .{
.consumed = pattern.len,
.done = false,
};
}
}
return error.MethodNotSupported;
},
.pathname_state => |s| {
if (hasSpace(vec)) {
// Delegate to `consumeChar`.
return .{
.consumed = 0,
.done = false,
};
}
self.state = .pathname(s.method, s.pathname.ptr[0 .. s.pathname.len + vec_len]);
},
.pathname_complete => {
if (patterns.@"version_http/1.1".check(vec)) {
self.state = .header_name_start;
return .{
.consumed = patterns.@"version_http/1.1".len,
.done = false,
};
} else {
return error.HttpVersionNotSupported;
}
},
.header_value => |s| {
if (hasCRLF(vec)) {
// Delegate to `consumeChar`.
return .{
.consumed = 0,
.done = false,
};
}
self.state = .headerValue(s.name, s.value.ptr[0 .. s.value.len + vec_len]);
},
else => {
// Delegate to `consumeChar`.
return .{
.consumed = 0,
.done = false,
};
},
}
}
pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult {
const c = c_ptr.*;
const c_slice = @as([*]const u8, @ptrCast(c_ptr))[0..1];
@@ -301,12 +378,10 @@ pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult {
.pathname_state => |s| switch (c) {
' ' => {
self.state = .pathname_complete;
if (self.callbacks.route) |routeCallback| {
routeCallback(self.callbacks.self, .{
.method = s.method,
.pathname = s.pathname,
});
}
self.request_handler = self.request_router.rawRoute(.init(s.method, s.pathname)) catch |err| {
self.last_router_error = err;
return error.RouterError;
};
},
else => self.state = .pathname(s.method, s.pathname.ptr[0 .. s.pathname.len + 1]),
},
@@ -357,21 +432,22 @@ pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult {
.header_name => |name| switch (c) {
':' => {
self.state = .headerValue(name, @as([*]const u8, @ptrCast(c_ptr))[1..1]);
self.current_header_is_content_length = std.ascii.eqlIgnoreCase(name, "Content-Length");
},
else => self.state = .{ .header_name = name.ptr[0 .. name.len + 1] },
},
.header_value => |s| switch (c) {
'\r' => {
self.state = .header_line_end;
const value_trimmed = std.mem.trim(u8, s.value, " \t");
if (self.current_header_is_content_length) {
self.content_length = std.fmt.parseInt(usize, value_trimmed, 10) catch return error.InvalidContentLength;
self.current_header_is_content_length = false;
}
if (self.callbacks.header) |headerCallback| {
headerCallback(self.callbacks.self, s.name, value_trimmed);
const header: Header = .init(s.name, std.mem.trim(u8, s.value, " \t"));
if (header.isKnown(.@"Content-Length")) {
self.content_length = std.fmt.parseInt(usize, header.value, 10) catch return error.InvalidContentLength;
}
self.request_handler.rawHeader(header) catch |err| {
self.last_handler_error = err;
return error.HandlerError;
};
},
else => self.state = .headerValue(s.name, s.value.ptr[0 .. s.value.len + 1]),
},
@@ -382,9 +458,7 @@ pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult {
.headers_end => switch (c) {
'\n' => {
if (self.content_length == 0) {
if (self.callbacks.body) |bodyCallback| {
bodyCallback(self.callbacks.self, &.{});
}
self.handler.rawBody(self.request, &.{});
return .done;
}
self.state = .{ .body = @as([*]const u8, @ptrCast(c_ptr))[1..1] };
@@ -395,9 +469,7 @@ pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult {
const new_body = body.ptr[0 .. body.len + 1];
self.state = .{ .body = new_body };
if (new_body.len >= self.content_length) {
if (self.callbacks.body) |bodyCallback| {
bodyCallback(self.callbacks.self, new_body);
}
self.handler.rawBody(self.request, new_body);
return .done;
}
},

164
packages/web/src/main.zig Normal file
View File

@@ -0,0 +1,164 @@
const std = @import("std");
const http = @import("http.zig");
const Connection = @import("Connection.zig");
const Response = @import("Response.zig");
const RequestHandler = @import("RequestHandler.zig");
const RequestRouter = @import("RequestRouter.zig");
const Route = @import("Route.zig");
const Server = @import("Server.zig");
const UUID = @import("UUID.zig");
const Worker = @import("Worker.zig");
const linux = std.os.linux;
const errno = linux.E.init;
var running: std.atomic.Value(bool) = .init(true);
fn interruptionHandler(signal: i32) callconv(.c) void {
switch (signal) {
linux.SIG.INT, linux.SIG.TERM => {
running.store(false, .release);
},
else => {},
}
}
const Router = struct {
allocator: std.mem.Allocator,
fn init(allocator: std.mem.Allocator) Router {
return .{
.allocator = allocator,
};
}
fn interface(self: *Router) RequestRouter {
return .{
.ptr = self,
.vtable = &.{
.route = onRoute,
},
};
}
fn onRoute(ctx: *anyopaque, route: Route) !RequestHandler {
const self: *Router = @ptrCast(@alignCast(ctx));
const handler = try self.allocator.create(Handler);
handler.* = .init(self.allocator, route);
return handler.interface();
}
};
const Handler = struct {
allocator: std.mem.Allocator,
route: Route,
uuid: UUID,
timer: std.time.Timer,
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: Route) Handler {
return .{
.allocator = allocator,
.route = route,
.uuid = UUID.v7(),
.timer = .start(),
};
}
fn interface(self: *Handler) RequestHandler {
return .{
.ptr = self,
.vtable = &.{
.header = onHeader,
.body = onBody,
.finalize = onFinalize,
},
};
}
fn onHeader(ctx: *anyopaque, response: *Response, header: 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;
},
},
.other => {},
}
_ = response;
}
fn onBody(ctx: *anyopaque, response: *Response, body: []const u8) !void {
const self: *Handler = @ptrCast(@alignCast(ctx));
try response.sendResponse(.{
.media_type = "application/json",
.response_body = "{\"ok\":true}",
});
_ = self;
_ = body;
}
fn onFinalize(ctx: *anyopaque) void {
const self: *Handler = @ptrCast(@alignCast(ctx));
const time_ns = self.timer.read();
const time_us = time_ns / std.time.ns_per_us;
std.log.info("{s} {s} [{d}]", .{ @tagName(self.route.method), self.route.pathname, time_us });
self.allocator.destroy(self);
}
};
pub fn main() !void {
var gpa: std.heap.GeneralPurposeAllocator(.{
.thread_safe = true,
}) = .init;
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var router: Router = .init(allocator);
var server = try Server.init(allocator, .{
.request_router = router.interface(),
});
defer server.deinit(allocator);
const sigaction: linux.Sigaction = .{
.handler = .{ .handler = interruptionHandler },
.mask = linux.sigemptyset(),
.flags = linux.SA.RESETHAND,
};
const rc = linux.sigaction(linux.SIG.INT, &sigaction, null);
switch (errno(rc)) {
.SUCCESS => {},
else => |e| {
std.log.err("Error while estabilishing interruption handler: {s}", .{@tagName(e)});
return error.SystemError;
},
}
try server.listen(&running);
}

View File

@@ -0,0 +1,4 @@
const std = @import("std");
pub const err = @import("openssl/err.zig");
pub const ssl = @import("openssl/ssl.zig");

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

19
packages/web/src/root.zig Normal file
View File

@@ -0,0 +1,19 @@
const std = @import("std");
comptime {
// Only Linux supported
std.debug.assert(@import("builtin").os.tag == .linux);
}
pub const Connection = @import("Connection.zig");
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 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");