web: some stuff
This commit is contained in:
@@ -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",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
56
packages/web/src/RequestHandler.zig
Normal file
56
packages/web/src/RequestHandler.zig
Normal 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);
|
||||||
|
}
|
||||||
16
packages/web/src/RequestRouter.zig
Normal file
16
packages/web/src/RequestRouter.zig
Normal 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);
|
||||||
|
}
|
||||||
70
packages/web/src/Response.zig
Normal file
70
packages/web/src/Response.zig
Normal 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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
14
packages/web/src/Route.zig
Normal file
14
packages/web/src/Route.zig
Normal 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
249
packages/web/src/Server.zig
Normal 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);
|
||||||
|
}
|
||||||
338
packages/web/src/http/Header.zig
Normal file
338
packages/web/src/http/Header.zig
Normal 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
13
packages/web/src/http/Method.zig
Normal file
13
packages/web/src/http/Method.zig
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub const Method = enum {
|
||||||
|
CONNECT,
|
||||||
|
DELETE,
|
||||||
|
GET,
|
||||||
|
HEAD,
|
||||||
|
OPTIONS,
|
||||||
|
PATCH,
|
||||||
|
POST,
|
||||||
|
PUT,
|
||||||
|
TRACE,
|
||||||
|
};
|
||||||
@@ -1,24 +1,23 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const Parser = @This();
|
const Parser = @This();
|
||||||
|
|
||||||
const Callbacks = struct {
|
const Header = @import("Header.zig");
|
||||||
self: ?*anyopaque = null,
|
const Method = @import("Method.zig").Method;
|
||||||
|
const Response = @import("../Response.zig");
|
||||||
route: ?*const fn (self: ?*anyopaque, route: Route) void = null,
|
const RequestHandler = @import("../RequestHandler.zig");
|
||||||
header: ?*const fn (self: ?*anyopaque, name: []const u8, value: []const u8) void = null,
|
const RequestRouter = @import("../RequestRouter.zig");
|
||||||
body: ?*const fn (self: ?*anyopaque, body: []const u8) void = null,
|
|
||||||
|
|
||||||
pub const null_callbacks: Callbacks = .{};
|
|
||||||
};
|
|
||||||
|
|
||||||
const Error = error{
|
const Error = error{
|
||||||
MethodNotSupported,
|
MethodNotSupported,
|
||||||
HttpVersionNotSupported,
|
HttpVersionNotSupported,
|
||||||
MissingLineFeed,
|
MissingLineFeed,
|
||||||
InvalidContentLength,
|
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 {
|
const Pattern = struct {
|
||||||
value: Vec,
|
value: Vec,
|
||||||
@@ -26,13 +25,13 @@ const Pattern = struct {
|
|||||||
len: u6,
|
len: u6,
|
||||||
|
|
||||||
pub fn init(comptime prefix: []const u8) Pattern {
|
pub fn init(comptime prefix: []const u8) Pattern {
|
||||||
if (prefix.len > 32) {
|
if (prefix.len > vec_len) {
|
||||||
@compileError("Prefix length is too high");
|
@compileError("Prefix length is too high");
|
||||||
}
|
}
|
||||||
|
|
||||||
var value: [32]u8 = undefined;
|
var value: [vec_len]u8 = undefined;
|
||||||
var mask: [32]u8 = undefined;
|
var mask: [vec_len]u8 = undefined;
|
||||||
for (0..32) |i| {
|
for (0..vec_len) |i| {
|
||||||
if (i < prefix.len) {
|
if (i < prefix.len) {
|
||||||
value[i] = prefix[i];
|
value[i] = prefix[i];
|
||||||
mask[i] = 0xFF;
|
mask[i] = 0xFF;
|
||||||
@@ -49,14 +48,24 @@ const Pattern = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const patterns = struct {
|
const patterns = struct {
|
||||||
method_delete: Pattern.init("DELETE "),
|
pub const methods = struct {
|
||||||
method_get: Pattern.init("GET "),
|
// NOTE These patterns are arranged in a specific order, such that the
|
||||||
method_head: Pattern.init("HEAD "),
|
// first ones are the most common (based on vibes only).
|
||||||
method_patch: Pattern.init("PATCH "),
|
|
||||||
method_post: Pattern.init("POST "),
|
|
||||||
method_put: Pattern.init("PUT "),
|
|
||||||
|
|
||||||
@"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 {
|
inline fn hasSpace(vec: Vec) bool {
|
||||||
@@ -98,6 +107,8 @@ const State = union(enum) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init: void,
|
init: void,
|
||||||
|
// TODO Add all methods here and in `consumeChar` (they are covered by
|
||||||
|
// `consumeVec`, though)
|
||||||
method_d: void,
|
method_d: void,
|
||||||
method_g: void,
|
method_g: void,
|
||||||
method_h: void,
|
method_h: void,
|
||||||
@@ -145,30 +156,20 @@ const ConsumeCharResult = enum {
|
|||||||
done,
|
done,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Method = enum {
|
request_router: RequestRouter,
|
||||||
DELETE,
|
response: *Response,
|
||||||
GET,
|
|
||||||
HEAD,
|
|
||||||
PATCH,
|
|
||||||
POST,
|
|
||||||
PUT,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Route = struct {
|
|
||||||
method: Method,
|
|
||||||
pathname: []const u8,
|
|
||||||
};
|
|
||||||
|
|
||||||
callbacks: Callbacks,
|
|
||||||
state: State,
|
state: State,
|
||||||
current_header_is_content_length: bool,
|
|
||||||
content_length: usize,
|
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 .{
|
return .{
|
||||||
.callbacks = callbacks,
|
.request_router = request_router,
|
||||||
|
.response = response,
|
||||||
.state = .init,
|
.state = .init,
|
||||||
.current_header_is_content_length = false,
|
|
||||||
.content_length = 0,
|
.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;
|
const done = new_body.len >= self.content_length;
|
||||||
|
|
||||||
if (done) {
|
if (done) {
|
||||||
if (self.callbacks.body) |bodyCallback| {
|
self.request_handler.?.rawBody(self.response, new_body) catch |err| {
|
||||||
bodyCallback(self.callbacks.self, new_body);
|
self.last_handler_error = err;
|
||||||
}
|
return error.HandlerError;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
@@ -197,9 +199,25 @@ pub fn consume(self: *Parser, chars: []const u8) Error!ConsumeResult {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
else => {
|
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;
|
i += 1;
|
||||||
if (res == .done) return .{
|
if (char_res == .done) return .{
|
||||||
.consumed = i,
|
.consumed = i,
|
||||||
.done = true,
|
.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 {
|
pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult {
|
||||||
const c = c_ptr.*;
|
const c = c_ptr.*;
|
||||||
const c_slice = @as([*]const u8, @ptrCast(c_ptr))[0..1];
|
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) {
|
.pathname_state => |s| switch (c) {
|
||||||
' ' => {
|
' ' => {
|
||||||
self.state = .pathname_complete;
|
self.state = .pathname_complete;
|
||||||
if (self.callbacks.route) |routeCallback| {
|
self.request_handler = self.request_router.rawRoute(.init(s.method, s.pathname)) catch |err| {
|
||||||
routeCallback(self.callbacks.self, .{
|
self.last_router_error = err;
|
||||||
.method = s.method,
|
return error.RouterError;
|
||||||
.pathname = s.pathname,
|
};
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
else => self.state = .pathname(s.method, s.pathname.ptr[0 .. s.pathname.len + 1]),
|
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) {
|
.header_name => |name| switch (c) {
|
||||||
':' => {
|
':' => {
|
||||||
self.state = .headerValue(name, @as([*]const u8, @ptrCast(c_ptr))[1..1]);
|
self.state = .headerValue(name, @as([*]const u8, @ptrCast(c_ptr))[1..1]);
|
||||||
self.current_header_is_content_length = std.ascii.eqlIgnoreCase(name, "Content-Length");
|
|
||||||
},
|
},
|
||||||
else => self.state = .{ .header_name = name.ptr[0 .. name.len + 1] },
|
else => self.state = .{ .header_name = name.ptr[0 .. name.len + 1] },
|
||||||
},
|
},
|
||||||
.header_value => |s| switch (c) {
|
.header_value => |s| switch (c) {
|
||||||
'\r' => {
|
'\r' => {
|
||||||
self.state = .header_line_end;
|
self.state = .header_line_end;
|
||||||
const value_trimmed = std.mem.trim(u8, s.value, " \t");
|
const header: Header = .init(s.name, 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;
|
if (header.isKnown(.@"Content-Length")) {
|
||||||
self.current_header_is_content_length = false;
|
self.content_length = std.fmt.parseInt(usize, header.value, 10) catch return error.InvalidContentLength;
|
||||||
}
|
|
||||||
if (self.callbacks.header) |headerCallback| {
|
|
||||||
headerCallback(self.callbacks.self, s.name, value_trimmed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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]),
|
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) {
|
.headers_end => switch (c) {
|
||||||
'\n' => {
|
'\n' => {
|
||||||
if (self.content_length == 0) {
|
if (self.content_length == 0) {
|
||||||
if (self.callbacks.body) |bodyCallback| {
|
self.handler.rawBody(self.request, &.{});
|
||||||
bodyCallback(self.callbacks.self, &.{});
|
|
||||||
}
|
|
||||||
return .done;
|
return .done;
|
||||||
}
|
}
|
||||||
self.state = .{ .body = @as([*]const u8, @ptrCast(c_ptr))[1..1] };
|
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];
|
const new_body = body.ptr[0 .. body.len + 1];
|
||||||
self.state = .{ .body = new_body };
|
self.state = .{ .body = new_body };
|
||||||
if (new_body.len >= self.content_length) {
|
if (new_body.len >= self.content_length) {
|
||||||
if (self.callbacks.body) |bodyCallback| {
|
self.handler.rawBody(self.request, new_body);
|
||||||
bodyCallback(self.callbacks.self, new_body);
|
|
||||||
}
|
|
||||||
return .done;
|
return .done;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
164
packages/web/src/main.zig
Normal file
164
packages/web/src/main.zig
Normal 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);
|
||||||
|
}
|
||||||
4
packages/web/src/openssl.zig
Normal file
4
packages/web/src/openssl.zig
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub const err = @import("openssl/err.zig");
|
||||||
|
pub const ssl = @import("openssl/ssl.zig");
|
||||||
7309
packages/web/src/openssl/err.zig
Normal file
7309
packages/web/src/openssl/err.zig
Normal file
File diff suppressed because it is too large
Load Diff
30575
packages/web/src/openssl/ssl.zig
Normal file
30575
packages/web/src/openssl/ssl.zig
Normal file
File diff suppressed because it is too large
Load Diff
19
packages/web/src/root.zig
Normal file
19
packages/web/src/root.zig
Normal 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");
|
||||||
Reference in New Issue
Block a user