Compare commits
3 Commits
6315589fa1
...
712e214f61
| Author | SHA1 | Date | |
|---|---|---|---|
| 712e214f61 | |||
| fe4a585b6b | |||
| 9a4932e629 |
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,6 +105,39 @@ 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, @intCast(opt.len));
|
||||||
return switch (errno(rc)) {
|
return switch (errno(rc)) {
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
66
packages/web/src/Request.zig
Normal file
66
packages/web/src/Request.zig
Normal 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;
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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),
|
||||||
|
.body_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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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 {
|
||||||
@@ -89,7 +103,7 @@ pub fn init(allocator: std.mem.Allocator, options: Options) !Server {
|
|||||||
|
|
||||||
// Allocate and remap read buffers
|
// Allocate and remap read buffers
|
||||||
|
|
||||||
const single_read_buffer_size = @as(usize, options.read_buffer_pages) * huge_page_size;
|
const single_read_buffer_size = @as(usize, options.read_buffer_huge_pages) * huge_page_size;
|
||||||
const all_read_buffers_size = worker_count * single_read_buffer_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_single_read_buffer_size = 2 * single_read_buffer_size;
|
||||||
@@ -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.body_write_buffer_huge_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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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_value_buffer = try allocator.alloc(Request.HeaderValue, options.max_header_fields);
|
||||||
|
errdefer allocator.free(header_value_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_value_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 {
|
||||||
var response: Response = .init(connection, self.write_buffer);
|
self.header_hash_map.clearRetainingCapacity();
|
||||||
var parser: http.Parser = .init(request_router, &response);
|
|
||||||
defer {
|
var request: Request = .{
|
||||||
if (parser.request_handler) |rh| {
|
.method = undefined,
|
||||||
rh.rawFinalize();
|
.pathname = undefined,
|
||||||
}
|
.headers = &self.header_hash_map,
|
||||||
}
|
.body = undefined,
|
||||||
|
};
|
||||||
|
var response: Response = .init(connection, self.header_write_buffer, self.body_write_buffer);
|
||||||
|
var parser: http.Parser = .init();
|
||||||
|
|
||||||
|
var next_header_index: usize = 0;
|
||||||
|
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;
|
||||||
@@ -95,70 +152,129 @@ fn handleRequest(
|
|||||||
|
|
||||||
const res = parser.consume(chunk) catch |err| {
|
const res = parser.consume(chunk) catch |err| {
|
||||||
switch (err) {
|
switch (err) {
|
||||||
error.MethodNotSupported => {
|
error.MethodNotSupported => try closeWith(&response, http.status.method_not_allowed),
|
||||||
try response.sendClose(.{ .status_text = http.status.method_not_allowed });
|
error.HttpVersionNotSupported => try closeWith(&response, http.status.http_version_not_supported),
|
||||||
},
|
error.SyntaxError => try closeWith(&response, http.status.bad_request),
|
||||||
error.HttpVersionNotSupported => {
|
|
||||||
try response.sendClose(.{ .status_text = http.status.http_version_not_supported });
|
|
||||||
},
|
|
||||||
error.MissingLineFeed => {
|
|
||||||
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 closeWith(&response, http.status.content_too_large);
|
||||||
} else {
|
} else {
|
||||||
try response.sendClose(.{ .status_text = http.status.request_header_fields_too_large });
|
try closeWith(&response, http.status.request_header_fields_too_large);
|
||||||
}
|
}
|
||||||
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| blk: {
|
||||||
|
if (ignore) {
|
||||||
|
break :blk;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (next_header_index >= self.header_value_buffer.len or self.header_hash_map.available == 0) {
|
||||||
|
// TODO Here, we could ignore, but make sure this does
|
||||||
|
// not clash with the other "request too long" checks
|
||||||
|
// (i.e. be careful not to double respond).
|
||||||
|
_ = &ignore;
|
||||||
|
|
||||||
|
try closeWith(&response, http.status.request_header_fields_too_large);
|
||||||
|
return false;
|
||||||
|
} 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn closeWith(response: *Response, status_line: []const u8) !void {
|
||||||
|
// This function is meant to be called before a request handler gets to do
|
||||||
|
// anything.
|
||||||
|
std.debug.assert(response.header_writer.end == 0);
|
||||||
|
std.debug.assert(response.body_writer.end == 0);
|
||||||
|
std.debug.assert(response.state == .init);
|
||||||
|
|
||||||
|
try response.header_writer.writeAll(status_line);
|
||||||
|
try response.header_writer.writeAll("Connection: close\r\n");
|
||||||
|
try response.header_writer.writeAll("\r\n");
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub const FieldName = @import("http/FieldName.zig").FieldName;
|
||||||
pub const Header = @import("http/Header.zig");
|
pub const Header = @import("http/Header.zig");
|
||||||
|
pub const KnownFieldName = @import("http/KnownFieldName.zig").KnownFieldName;
|
||||||
pub const Method = @import("http/Method.zig").Method;
|
pub const Method = @import("http/Method.zig").Method;
|
||||||
pub const Parser = @import("http/Parser.zig");
|
pub const Parser = @import("http/Parser.zig");
|
||||||
pub const status = @import("http/status.zig");
|
pub const status = @import("http/status.zig");
|
||||||
|
|||||||
91
packages/web/src/http/FieldName.zig
Normal file
91
packages/web/src/http/FieldName.zig
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const KnownFieldName = @import("KnownFieldName.zig").KnownFieldName;
|
||||||
|
|
||||||
|
const Wyhash = std.hash.Wyhash;
|
||||||
|
|
||||||
|
pub const FieldName = extern struct {
|
||||||
|
data: [16]u8 align(8),
|
||||||
|
|
||||||
|
const tag_known: u8 = 0x00;
|
||||||
|
const tag_long: u8 = 0x01;
|
||||||
|
const tag_short_bias: u8 = 0x02;
|
||||||
|
|
||||||
|
pub fn init(name: []const u8) FieldName {
|
||||||
|
var data: [16]u8 align(8) = @splat(0);
|
||||||
|
if (KnownFieldName.isKnownFieldName(name)) |known| {
|
||||||
|
data[0] = tag_known;
|
||||||
|
@as(*KnownFieldName, @ptrCast(data[8..16])).* = known;
|
||||||
|
} else if (name.len <= 15) {
|
||||||
|
data[0] = @intCast(name.len + tag_short_bias);
|
||||||
|
@memcpy(data[1..][0..name.len], name);
|
||||||
|
} else {
|
||||||
|
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 {
|
||||||
|
var data: [16]u8 align(8) = @splat(0);
|
||||||
|
|
||||||
|
data[0] = tag_known;
|
||||||
|
@as(*KnownFieldName, @ptrCast(data[8..16])).* = known;
|
||||||
|
|
||||||
|
return .{ .data = data };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getKnown(self: FieldName) KnownFieldName {
|
||||||
|
std.debug.assert(self.data[0] == tag_known);
|
||||||
|
const intval: u64 = @bitCast(self.data[8..16].*);
|
||||||
|
return @enumFromInt(intval);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getLong(self: FieldName) []const u8 {
|
||||||
|
std.debug.assert(self.data[0] == tag_long);
|
||||||
|
const len: u32 = @bitCast(self.data[4..8].*);
|
||||||
|
const intptr: usize = @bitCast(self.data[8..16].*);
|
||||||
|
const ptr: [*]const u8 = @ptrFromInt(intptr);
|
||||||
|
return ptr[0..len];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getShort(self: FieldName) []const u8 {
|
||||||
|
std.debug.assert(self.data[0] >= tag_short_bias);
|
||||||
|
const len: u8 = self.data[0] - tag_short_bias;
|
||||||
|
const str = self.data[1..][0..len];
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hash(self: FieldName) u64 {
|
||||||
|
return switch (self.data[0]) {
|
||||||
|
tag_known => Wyhash.hash(0, self.data[8..16]),
|
||||||
|
tag_long => Wyhash.hash(1, self.getLong()),
|
||||||
|
else => Wyhash.hash(2, self.getShort()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eql(a: FieldName, b: FieldName) bool {
|
||||||
|
const a_tag = a.data[0];
|
||||||
|
const b_tag = b.data[0];
|
||||||
|
|
||||||
|
if (a_tag != b_tag) return false;
|
||||||
|
|
||||||
|
return switch (a_tag) {
|
||||||
|
tag_known => a.getKnown() == b.getKnown(),
|
||||||
|
tag_long => std.mem.eql(u8, a.getLong(), b.getLong()),
|
||||||
|
else => std.mem.eql(u8, a.getShort(), b.getShort()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const HashMapContext = struct {
|
||||||
|
pub fn hash(_: HashMapContext, key: FieldName) u64 {
|
||||||
|
return key.hash();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eql(_: HashMapContext, a: FieldName, b: FieldName) bool {
|
||||||
|
return FieldName.eql(a, b);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,339 +1,23 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const Header = @This();
|
const Header = @This();
|
||||||
|
|
||||||
name: Name,
|
const FieldName = @import("FieldName.zig").FieldName;
|
||||||
|
const KnownFieldName = @import("KnownFieldName.zig").KnownFieldName;
|
||||||
|
|
||||||
|
name: FieldName,
|
||||||
value: []const u8,
|
value: []const u8,
|
||||||
|
|
||||||
pub fn init(name: []const u8, value: []const u8) Header {
|
pub fn init(name: FieldName, value: []const u8) Header {
|
||||||
return .{
|
return .{
|
||||||
.name = .init(name),
|
.name = name,
|
||||||
.value = value,
|
.value = value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn initKnown(known: Name.Known, value: []const u8) Header {
|
pub fn isNamed(self: Header, name: FieldName) bool {
|
||||||
return .{
|
return FieldName.eql(self.name, name);
|
||||||
.name = .initKnown(known),
|
|
||||||
.value = value,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn isKnown(self: Header, known: Name.Known) bool {
|
pub fn isNamedKnown(self: Header, known: KnownFieldName) bool {
|
||||||
return switch (self.name) {
|
return FieldName.eql(self.name, .initKnown(known));
|
||||||
.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: {
|
|
||||||
@setEvalBranchQuota(20000);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|||||||
367
packages/web/src/http/KnownFieldName.zig
Normal file
367
packages/web/src/http/KnownFieldName.zig
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub const KnownFieldName = enum(u64) {
|
||||||
|
|
||||||
|
// --- STANDARD FIELD NAMES ------------------------------------------------
|
||||||
|
|
||||||
|
// These are all names listed under:
|
||||||
|
//
|
||||||
|
// https://www.iana.org/assignments/http-fields/http-fields.xhtml
|
||||||
|
//
|
||||||
|
// Some of them might be obsoleted or deprecated; they are included here
|
||||||
|
// nonetheless.
|
||||||
|
//
|
||||||
|
// When the list was last retrieved, its "Last Updated" date was 2026-03-06.
|
||||||
|
|
||||||
|
@"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",
|
||||||
|
|
||||||
|
// --- NON-STANDARD FIELD NAMES --------------------------------------------
|
||||||
|
|
||||||
|
// These names include, but are not limited to:
|
||||||
|
//
|
||||||
|
// - Cloudflare HTTP headers
|
||||||
|
// https://developers.cloudflare.com/fundamentals/reference/http-headers/
|
||||||
|
// - Entries from MDN marked as "non-standard", but not "deprecated"
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Attribution-Reporting-Register-Trigger
|
||||||
|
|
||||||
|
@"Cf-Cache-Status",
|
||||||
|
@"CF-Connecting-IP",
|
||||||
|
@"CF-Connecting-IPv6",
|
||||||
|
@"CF-Connecting-O2O",
|
||||||
|
@"CF-EW-Via",
|
||||||
|
@"CF-IPCountry",
|
||||||
|
@"CF-Pseudo-IPv4",
|
||||||
|
@"Cf-Ray",
|
||||||
|
@"CF-Visitor",
|
||||||
|
@"CF-Worker",
|
||||||
|
@"Idempotency-Key",
|
||||||
|
@"True-Client-IP",
|
||||||
|
@"X-Accel-Buffering",
|
||||||
|
@"X-Accel-Charset",
|
||||||
|
@"X-Accel-Limit-Rate",
|
||||||
|
@"X-Accel-Redirect",
|
||||||
|
@"X-API-Key",
|
||||||
|
@"X-Correlation-ID",
|
||||||
|
@"X-DNS-Prefetch-Control",
|
||||||
|
@"X-Forwarded-For",
|
||||||
|
@"X-Forwarded-Host",
|
||||||
|
@"X-Forwarded-Proto",
|
||||||
|
@"X-Permitted-Cross-Domain-Policies",
|
||||||
|
@"X-Powered-By",
|
||||||
|
@"X-Request-ID",
|
||||||
|
@"X-Robots-Tag",
|
||||||
|
|
||||||
|
// --- EXPERIMENTAL FIELD NAMES --------------------------------------------
|
||||||
|
|
||||||
|
@"Sec-CH-Device-Memory",
|
||||||
|
@"Sec-CH-DPR",
|
||||||
|
@"Sec-CH-Prefers-Color-Scheme",
|
||||||
|
@"Sec-CH-Prefers-Reduced-Motion",
|
||||||
|
@"Sec-CH-Prefers-Reduced-Transparency",
|
||||||
|
@"Sec-CH-UA-Arch",
|
||||||
|
@"Sec-CH-UA-Bitness",
|
||||||
|
@"Sec-CH-UA-Form-Factors",
|
||||||
|
@"Sec-CH-UA-Full-Versi",
|
||||||
|
@"Sec-CH-UA-Full-Version-List",
|
||||||
|
@"Sec-CH-UA-Mobile",
|
||||||
|
@"Sec-CH-UA-Model",
|
||||||
|
@"Sec-CH-UA-Platform-Version",
|
||||||
|
@"Sec-CH-UA-Platform",
|
||||||
|
@"Sec-CH-UA-WoW64",
|
||||||
|
@"Sec-CH-UA",
|
||||||
|
@"Sec-CH-Viewport-Height",
|
||||||
|
@"Sec-CH-Viewport-Width",
|
||||||
|
@"Sec-CH-Width",
|
||||||
|
|
||||||
|
/// Maps **lowercased** header names to enum values.
|
||||||
|
pub const map: std.StaticStringMap(KnownFieldName) = blk: {
|
||||||
|
@setEvalBranchQuota(20000);
|
||||||
|
const fields = @typeInfo(KnownFieldName).@"enum".fields;
|
||||||
|
|
||||||
|
var kvs_list: [fields.len]struct { []const u8, KnownFieldName } = 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(KnownFieldName, 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_field_name_len = blk: {
|
||||||
|
const fields = @typeInfo(KnownFieldName).@"enum".fields;
|
||||||
|
|
||||||
|
var max_len: usize = 0;
|
||||||
|
for (fields) |field| {
|
||||||
|
max_len = @max(max_len, field.name.len);
|
||||||
|
}
|
||||||
|
break :blk max_len;
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn isKnownFieldName(name: []const u8) ?KnownFieldName {
|
||||||
|
if (name.len > max_known_field_name_len) {
|
||||||
|
@branchHint(.unlikely);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var name_lowercase_buf: [max_known_field_name_len]u8 = undefined;
|
||||||
|
const name_lowercase = std.ascii.lowerString(&name_lowercase_buf, name);
|
||||||
|
return map.get(name_lowercase);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,22 +1,546 @@
|
|||||||
|
//! HTTP/1.1 parser.
|
||||||
|
//!
|
||||||
|
//! This parser is *streaming*, meaning it can gracefully consume partial HTTP
|
||||||
|
//! request bytes. An instance of this parser is meant for parsing a singular
|
||||||
|
//! request. Once the request if fully completed, a new instance of the parser
|
||||||
|
//! should be initialized.
|
||||||
|
//!
|
||||||
|
//! During a single ingestion, the parser can return one of the following:
|
||||||
|
//!
|
||||||
|
//! - method of type `Method`, i.e. HTTP method (aka verb)
|
||||||
|
//! - pathname of type `[]const u8`
|
||||||
|
//! - header of type `Header`, i.e. a field name with a value
|
||||||
|
//! - end_of_headers of type `void`, i.e. a marker which informs the user of
|
||||||
|
//! this parser that there will be no more headers; this result can be used by
|
||||||
|
//! the user to make decisions about further processing of the request based
|
||||||
|
//! on the full knowledge of all the headers
|
||||||
|
//! - body of type `[]const u8`, i.e. a slice to the request body (or
|
||||||
|
//! zero-length slice if there is no request body)
|
||||||
|
//!
|
||||||
|
//! The first result returned from the parser will always be the route. Then,
|
||||||
|
//! one or more headers will follow terminated with end_of_headers marker. The
|
||||||
|
//! parser will always finish with a single body result.
|
||||||
|
//!
|
||||||
|
//! Parser methods stop processing at the first result. Therefore, if any result
|
||||||
|
//! is returned, the provided bytes might have been only partially consumed and
|
||||||
|
//! the methods must be repeatedly called until all of the bytes are consumed.
|
||||||
|
//! When the body is returned, the parser is finished and should be no longer
|
||||||
|
//! used. If the body was returned, but the bytes were not fully consumed, it
|
||||||
|
//! means that the remainder belongs to a subsequent HTTP request.
|
||||||
|
//!
|
||||||
|
//! When an error is returned from the parser, the HTTP request should be
|
||||||
|
//! considered malformed. You may choose to respond to it, but the request must
|
||||||
|
//! no longer be parsed and the connection should be closed.
|
||||||
|
//!
|
||||||
|
//! The parser is not involved in any HTTP semantics, only its syntax. It is up
|
||||||
|
//! to the user of this parser to respect all of the HTTP standards (if they
|
||||||
|
//! even choose to). For example, none of the header field valuess are verified.
|
||||||
|
//! The only exception is `Content-Length`. The parser must know the value to
|
||||||
|
//! determine the length of the request body. If the value fails to parse as a
|
||||||
|
//! decimal non-negative integer, a syntax error is returned. Note that
|
||||||
|
//! according to [RFC 9110, Section 8.6: HTTP Semantics](https://datatracker.ietf.org/doc/html/rfc9110#section-8.6),
|
||||||
|
//! `Content-Length` header field value consisting of the same decimal value
|
||||||
|
//! repeated as a comma-separated list (e.g. `Content-Length: 42, 42`) MAY be
|
||||||
|
//! accepted. This parser chooses not to accept it.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const Parser = @This();
|
const Parser = @This();
|
||||||
|
|
||||||
|
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 Response = @import("../Response.zig");
|
|
||||||
const RequestHandler = @import("../RequestHandler.zig");
|
|
||||||
const RequestRouter = @import("../RequestRouter.zig");
|
|
||||||
|
|
||||||
const Error = error{
|
pub const Error = error{
|
||||||
MethodNotSupported,
|
MethodNotSupported,
|
||||||
HttpVersionNotSupported,
|
HttpVersionNotSupported,
|
||||||
MissingLineFeed,
|
SyntaxError,
|
||||||
InvalidContentLength,
|
|
||||||
RouterError,
|
|
||||||
HandlerError,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Vec: type = @Vector(std.simd.suggestVectorLength(u8).?, u8);
|
pub const Result = union(enum) {
|
||||||
|
method: Method,
|
||||||
|
pathname: []const u8,
|
||||||
|
header: Header,
|
||||||
|
end_of_headers: void,
|
||||||
|
body: []const u8,
|
||||||
|
|
||||||
|
pub fn initMethod(method: Method) Result {
|
||||||
|
return .{ .method = method };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initPathname(pathname: []const u8) Result {
|
||||||
|
return .{ .pathname = pathname };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initHeader(header: Header) Result {
|
||||||
|
return .{ .header = header };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initBody(body: []const u8) Result {
|
||||||
|
return .{ .body = body };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ConsumeResult = struct {
|
||||||
|
consumed: usize,
|
||||||
|
result: ?Result,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const State = union(enum) {
|
||||||
|
init: void,
|
||||||
|
method_c: void,
|
||||||
|
method_d: void,
|
||||||
|
method_g: void,
|
||||||
|
method_h: void,
|
||||||
|
method_o: void,
|
||||||
|
method_p: void,
|
||||||
|
method_t: void,
|
||||||
|
method_co: void,
|
||||||
|
method_de: void,
|
||||||
|
method_ge: void,
|
||||||
|
method_he: void,
|
||||||
|
method_op: void,
|
||||||
|
method_pa: void,
|
||||||
|
method_po: void,
|
||||||
|
method_pu: void,
|
||||||
|
method_tr: void,
|
||||||
|
method_con: void,
|
||||||
|
method_del: void,
|
||||||
|
method_hea: void,
|
||||||
|
method_opt: void,
|
||||||
|
method_pat: void,
|
||||||
|
method_pos: void,
|
||||||
|
method_tra: void,
|
||||||
|
method_conn: void,
|
||||||
|
method_dele: void,
|
||||||
|
method_opti: void,
|
||||||
|
method_patc: void,
|
||||||
|
method_trac: void,
|
||||||
|
method_conne: void,
|
||||||
|
method_delet: void,
|
||||||
|
method_optio: void,
|
||||||
|
method_connec: void,
|
||||||
|
method_option: void,
|
||||||
|
method_complete: void,
|
||||||
|
pathname: []const u8,
|
||||||
|
pathname_complete: void,
|
||||||
|
version_h: void,
|
||||||
|
version_ht: void,
|
||||||
|
version_htt: void,
|
||||||
|
version_http: void,
|
||||||
|
@"version_http/": void,
|
||||||
|
@"version_http/1": void,
|
||||||
|
@"version_http/1.": void,
|
||||||
|
version_complete: void,
|
||||||
|
start_line_end: void,
|
||||||
|
header_name_start: void,
|
||||||
|
header_name: []const u8,
|
||||||
|
header_value: Header,
|
||||||
|
header_line_end: void,
|
||||||
|
headers_end: void,
|
||||||
|
body: []const u8,
|
||||||
|
done: void,
|
||||||
|
|
||||||
|
pub fn initPathname(pathname: []const u8) State {
|
||||||
|
return .{ .pathname = pathname };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initHeaderName(name: []const u8) State {
|
||||||
|
return .{ .header_name = name };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initHeaderValue(header: Header) State {
|
||||||
|
return .{ .header_value = header };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initBody(body: []const u8) State {
|
||||||
|
return .{ .body = body };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
state: State,
|
||||||
|
content_length: ?usize,
|
||||||
|
|
||||||
|
pub fn init() Parser {
|
||||||
|
return .{
|
||||||
|
.state = .init,
|
||||||
|
.content_length = null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn consume(self: *Parser, chars: []const u8) Error!ConsumeResult {
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < chars.len) {
|
||||||
|
switch (self.state) {
|
||||||
|
.body => |body| {
|
||||||
|
const content_length = self.content_length.?;
|
||||||
|
const to_consume = @min(chars.len - i, content_length - body.len);
|
||||||
|
|
||||||
|
const new_body = extendSliceBy(body, to_consume);
|
||||||
|
i += to_consume;
|
||||||
|
|
||||||
|
if (new_body.len >= content_length) {
|
||||||
|
self.state = .done;
|
||||||
|
return .{
|
||||||
|
.consumed = i,
|
||||||
|
.result = .initBody(new_body),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
self.state = .initBody(new_body);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => {
|
||||||
|
// TODO fix
|
||||||
|
// if (chars.len - i >= vec_len) {
|
||||||
|
// const vec_res = try self.consumeVec(chars[i..][0..vec_len]);
|
||||||
|
// i += vec_res.consumed;
|
||||||
|
|
||||||
|
// if (vec_res.result) |result| {
|
||||||
|
// return .{
|
||||||
|
// .consumed = i,
|
||||||
|
// .result = result,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (vec_res.consumed > 0) {
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
const maybe_result = try self.consumeChar(&chars[i]);
|
||||||
|
i += 1;
|
||||||
|
|
||||||
|
if (maybe_result) |result| {
|
||||||
|
return .{
|
||||||
|
.consumed = i,
|
||||||
|
.result = result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std.debug.assert(i == chars.len);
|
||||||
|
return .{
|
||||||
|
.consumed = chars.len,
|
||||||
|
.result = null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn consumeChar(self: *Parser, char_ptr: *const u8) Error!?Result {
|
||||||
|
const char = char_ptr.*;
|
||||||
|
const char_slice: *const [1]u8 = @ptrCast(char_ptr);
|
||||||
|
const next_char_slice = @as([*]const u8, @ptrCast(char_ptr))[1..1];
|
||||||
|
|
||||||
|
switch (self.state) {
|
||||||
|
.init => switch (char) {
|
||||||
|
'C' => self.state = .method_c,
|
||||||
|
'D' => self.state = .method_d,
|
||||||
|
'G' => self.state = .method_g,
|
||||||
|
'H' => self.state = .method_h,
|
||||||
|
'O' => self.state = .method_o,
|
||||||
|
'P' => self.state = .method_p,
|
||||||
|
'T' => self.state = .method_t,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_c => switch (char) {
|
||||||
|
'O' => self.state = .method_co,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_d => switch (char) {
|
||||||
|
'E' => self.state = .method_de,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_g => switch (char) {
|
||||||
|
'E' => self.state = .method_ge,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_h => switch (char) {
|
||||||
|
'E' => self.state = .method_he,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_o => switch (char) {
|
||||||
|
'P' => self.state = .method_op,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_p => switch (char) {
|
||||||
|
'A' => self.state = .method_pa,
|
||||||
|
'O' => self.state = .method_po,
|
||||||
|
'U' => self.state = .method_pu,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_t => switch (char) {
|
||||||
|
'R' => self.state = .method_tr,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_co => switch (char) {
|
||||||
|
'N' => self.state = .method_con,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_de => switch (char) {
|
||||||
|
'L' => self.state = .method_del,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_ge => switch (char) {
|
||||||
|
'T' => {
|
||||||
|
self.state = .method_complete;
|
||||||
|
return .initMethod(.GET);
|
||||||
|
},
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_he => switch (char) {
|
||||||
|
'A' => self.state = .method_hea,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_op => switch (char) {
|
||||||
|
'T' => self.state = .method_opt,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_pa => switch (char) {
|
||||||
|
'T' => self.state = .method_pat,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_po => switch (char) {
|
||||||
|
'S' => self.state = .method_pos,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_pu => switch (char) {
|
||||||
|
'T' => {
|
||||||
|
self.state = .method_complete;
|
||||||
|
return .initMethod(.PUT);
|
||||||
|
},
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_tr => switch (char) {
|
||||||
|
'A' => self.state = .method_tra,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_con => switch (char) {
|
||||||
|
'N' => self.state = .method_conn,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_del => switch (char) {
|
||||||
|
'E' => self.state = .method_dele,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_hea => switch (char) {
|
||||||
|
'D' => {
|
||||||
|
self.state = .method_complete;
|
||||||
|
return .initMethod(.HEAD);
|
||||||
|
},
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_opt => switch (char) {
|
||||||
|
'I' => self.state = .method_opti,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_pat => switch (char) {
|
||||||
|
'C' => self.state = .method_patc,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_pos => switch (char) {
|
||||||
|
'T' => {
|
||||||
|
self.state = .method_complete;
|
||||||
|
return .initMethod(.POST);
|
||||||
|
},
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_tra => switch (char) {
|
||||||
|
'C' => self.state = .method_trac,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_conn => switch (char) {
|
||||||
|
'E' => self.state = .method_conne,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_dele => switch (char) {
|
||||||
|
'T' => self.state = .method_delet,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_opti => switch (char) {
|
||||||
|
'O' => self.state = .method_optio,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_patc => switch (char) {
|
||||||
|
'H' => {
|
||||||
|
self.state = .method_complete;
|
||||||
|
return .initMethod(.PATCH);
|
||||||
|
},
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_trac => switch (char) {
|
||||||
|
'E' => {
|
||||||
|
self.state = .method_complete;
|
||||||
|
return .initMethod(.TRACE);
|
||||||
|
},
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_conne => switch (char) {
|
||||||
|
'C' => self.state = .method_connec,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_delet => switch (char) {
|
||||||
|
'E' => {
|
||||||
|
self.state = .method_complete;
|
||||||
|
return .initMethod(.DELETE);
|
||||||
|
},
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_optio => switch (char) {
|
||||||
|
'N' => self.state = .method_option,
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_connec => switch (char) {
|
||||||
|
'T' => {
|
||||||
|
self.state = .method_complete;
|
||||||
|
return .initMethod(.CONNECT);
|
||||||
|
},
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_option => switch (char) {
|
||||||
|
'S' => {
|
||||||
|
self.state = .method_complete;
|
||||||
|
return .initMethod(.OPTIONS);
|
||||||
|
},
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.method_complete => switch (char) {
|
||||||
|
' ' => self.state = .initPathname(next_char_slice),
|
||||||
|
else => return error.MethodNotSupported,
|
||||||
|
},
|
||||||
|
.pathname => |pathname| switch (char) {
|
||||||
|
' ' => {
|
||||||
|
self.state = .pathname_complete;
|
||||||
|
return .initPathname(pathname);
|
||||||
|
},
|
||||||
|
else => self.state = .initPathname(extendSlice(pathname)),
|
||||||
|
},
|
||||||
|
.pathname_complete => switch (char) {
|
||||||
|
'H' => self.state = .version_h,
|
||||||
|
else => return error.HttpVersionNotSupported,
|
||||||
|
},
|
||||||
|
.version_h => switch (char) {
|
||||||
|
'T' => self.state = .version_ht,
|
||||||
|
else => return error.HttpVersionNotSupported,
|
||||||
|
},
|
||||||
|
.version_ht => switch (char) {
|
||||||
|
'T' => self.state = .version_htt,
|
||||||
|
else => return error.HttpVersionNotSupported,
|
||||||
|
},
|
||||||
|
.version_htt => switch (char) {
|
||||||
|
'P' => self.state = .version_http,
|
||||||
|
else => return error.HttpVersionNotSupported,
|
||||||
|
},
|
||||||
|
.version_http => switch (char) {
|
||||||
|
'/' => self.state = .@"version_http/",
|
||||||
|
else => return error.HttpVersionNotSupported,
|
||||||
|
},
|
||||||
|
.@"version_http/" => switch (char) {
|
||||||
|
'1' => self.state = .@"version_http/1",
|
||||||
|
else => return error.HttpVersionNotSupported,
|
||||||
|
},
|
||||||
|
.@"version_http/1" => switch (char) {
|
||||||
|
'.' => self.state = .@"version_http/1.",
|
||||||
|
else => return error.HttpVersionNotSupported,
|
||||||
|
},
|
||||||
|
.@"version_http/1." => switch (char) {
|
||||||
|
'1' => self.state = .version_complete,
|
||||||
|
else => return error.HttpVersionNotSupported,
|
||||||
|
},
|
||||||
|
.version_complete => switch (char) {
|
||||||
|
'\r' => self.state = .start_line_end,
|
||||||
|
else => return error.HttpVersionNotSupported,
|
||||||
|
},
|
||||||
|
.start_line_end => switch (char) {
|
||||||
|
'\n' => self.state = .header_name_start,
|
||||||
|
else => return error.SyntaxError,
|
||||||
|
},
|
||||||
|
.header_name_start => switch (char) {
|
||||||
|
'\r' => {
|
||||||
|
self.state = .headers_end;
|
||||||
|
return .end_of_headers;
|
||||||
|
},
|
||||||
|
else => self.state = .initHeaderName(char_slice),
|
||||||
|
},
|
||||||
|
.header_name => |name| switch (char) {
|
||||||
|
':' => self.state = .initHeaderValue(.init(.init(name), next_char_slice)),
|
||||||
|
else => self.state = .initHeaderName(extendSlice(name)),
|
||||||
|
},
|
||||||
|
.header_value => |untrimmed_header| switch (char) {
|
||||||
|
'\r' => {
|
||||||
|
self.state = .header_line_end;
|
||||||
|
const header: Header = .init(
|
||||||
|
untrimmed_header.name,
|
||||||
|
std.mem.trim(u8, untrimmed_header.value, " \t"),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (header.isNamedKnown(.@"Content-Length")) {
|
||||||
|
const content_length = std.fmt.parseInt(usize, header.value, 10) catch return error.SyntaxError;
|
||||||
|
if (self.content_length) |current_content_length| {
|
||||||
|
@branchHint(.unlikely);
|
||||||
|
// Accept multiple `Content-Length` headers as long as
|
||||||
|
// they have the exact same value.
|
||||||
|
if (content_length != current_content_length) {
|
||||||
|
return error.SyntaxError;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.content_length = content_length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return .initHeader(header);
|
||||||
|
},
|
||||||
|
else => self.state = .initHeaderValue(extendHeader(untrimmed_header)),
|
||||||
|
},
|
||||||
|
.header_line_end => switch (char) {
|
||||||
|
'\n' => self.state = .header_name_start,
|
||||||
|
else => return error.SyntaxError,
|
||||||
|
},
|
||||||
|
.headers_end => switch (char) {
|
||||||
|
'\n' => {
|
||||||
|
const content_length = self.content_length orelse 0;
|
||||||
|
if (content_length == 0) {
|
||||||
|
self.state = .done;
|
||||||
|
return .initBody(&.{});
|
||||||
|
} else {
|
||||||
|
self.state = .initBody(next_char_slice);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => return error.SyntaxError,
|
||||||
|
},
|
||||||
|
.body => |body| {
|
||||||
|
const content_length = self.content_length.?;
|
||||||
|
const new_body = extendSlice(body);
|
||||||
|
if (new_body.len >= content_length) {
|
||||||
|
self.state = .done;
|
||||||
|
return .initBody(new_body);
|
||||||
|
} else {
|
||||||
|
self.state = .initBody(new_body);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.done => unreachable,
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extendSlice(slice: []const u8) []const u8 {
|
||||||
|
return slice.ptr[0 .. slice.len + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extendSliceBy(slice: []const u8, n: usize) []const u8 {
|
||||||
|
return slice.ptr[0 .. slice.len + n];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extendHeader(header: Header) Header {
|
||||||
|
return .{
|
||||||
|
.name = header.name,
|
||||||
|
.value = extendSlice(header.value),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- SIMD --------------------------------------------------------------------
|
||||||
|
|
||||||
|
const Vec = @Vector(std.simd.suggestVectorLength(u8).?, u8);
|
||||||
const vec_len = @typeInfo(Vec).vector.len;
|
const vec_len = @typeInfo(Vec).vector.len;
|
||||||
|
|
||||||
const Pattern = struct {
|
const Pattern = struct {
|
||||||
@@ -85,159 +609,8 @@ inline fn hasCRLF(vec: Vec) bool {
|
|||||||
return @reduce(.Or, has_cr | has_lf);
|
return @reduce(.Or, has_cr | has_lf);
|
||||||
}
|
}
|
||||||
|
|
||||||
const State = union(enum) {
|
/// May return with `.consumed == 0`, in which case the parsing should be
|
||||||
pub fn methodComplete(method: Method) State {
|
/// retried with non-SIMD method.
|
||||||
return .{
|
|
||||||
.method_complete = .{
|
|
||||||
.method = method,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pathname(method: Method, p: []const u8) State {
|
|
||||||
return .{
|
|
||||||
.pathname_state = .{
|
|
||||||
.method = method,
|
|
||||||
.pathname = p,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn headerValue(name: []const u8, value: []const u8) State {
|
|
||||||
return .{
|
|
||||||
.header_value = .{
|
|
||||||
.name = name,
|
|
||||||
.value = value,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
method_p: void,
|
|
||||||
method_de: void,
|
|
||||||
method_ge: void,
|
|
||||||
method_he: void,
|
|
||||||
method_pa: void,
|
|
||||||
method_po: void,
|
|
||||||
method_pu: void,
|
|
||||||
method_del: void,
|
|
||||||
method_hea: void,
|
|
||||||
method_pat: void,
|
|
||||||
method_pos: void,
|
|
||||||
method_dele: void,
|
|
||||||
method_patc: void,
|
|
||||||
method_delet: void,
|
|
||||||
method_complete: struct { method: Method },
|
|
||||||
pathname_state: struct { method: Method, pathname: []const u8 },
|
|
||||||
pathname_complete: void,
|
|
||||||
version_h: void,
|
|
||||||
version_ht: void,
|
|
||||||
version_htt: void,
|
|
||||||
version_http: void,
|
|
||||||
@"version_http/": void,
|
|
||||||
@"version_http/1": void,
|
|
||||||
@"version_http/1.": void,
|
|
||||||
version_complete: void,
|
|
||||||
start_line_end: void,
|
|
||||||
header_name_start: void,
|
|
||||||
header_name: []const u8,
|
|
||||||
header_value: struct { name: []const u8, value: []const u8 },
|
|
||||||
header_line_end: void,
|
|
||||||
headers_end: void,
|
|
||||||
body: []const u8,
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConsumeResult = struct {
|
|
||||||
consumed: usize,
|
|
||||||
done: bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConsumeCharResult = enum {
|
|
||||||
not_done,
|
|
||||||
done,
|
|
||||||
};
|
|
||||||
|
|
||||||
request_router: RequestRouter,
|
|
||||||
response: *Response,
|
|
||||||
state: State,
|
|
||||||
content_length: usize,
|
|
||||||
|
|
||||||
request_handler: ?RequestHandler = null,
|
|
||||||
last_router_error: anyerror = undefined,
|
|
||||||
last_handler_error: anyerror = undefined,
|
|
||||||
|
|
||||||
pub fn init(request_router: RequestRouter, response: *Response) Parser {
|
|
||||||
return .{
|
|
||||||
.request_router = request_router,
|
|
||||||
.response = response,
|
|
||||||
.state = .init,
|
|
||||||
.content_length = 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn consume(self: *Parser, chars: []const u8) Error!ConsumeResult {
|
|
||||||
var i: usize = 0;
|
|
||||||
while (i < chars.len) {
|
|
||||||
switch (self.state) {
|
|
||||||
.body => |body| {
|
|
||||||
const to_consume = @min(chars.len - i, self.content_length - body.len);
|
|
||||||
const new_body = body.ptr[0 .. body.len + to_consume];
|
|
||||||
self.state = .{ .body = new_body };
|
|
||||||
i += to_consume;
|
|
||||||
|
|
||||||
const done = new_body.len >= self.content_length;
|
|
||||||
|
|
||||||
if (done) {
|
|
||||||
self.request_handler.?.rawBody(self.response, new_body) catch |err| {
|
|
||||||
self.last_handler_error = err;
|
|
||||||
return error.HandlerError;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.consumed = i,
|
|
||||||
.done = done,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
else => {
|
|
||||||
// TODO Fix
|
|
||||||
// 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 (char_res == .done) return .{
|
|
||||||
.consumed = i,
|
|
||||||
.done = true,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.consumed = chars.len,
|
|
||||||
.done = false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn consumeVec(self: *Parser, vec_ptr: *const [vec_len]u8) Error!ConsumeResult {
|
pub fn consumeVec(self: *Parser, vec_ptr: *const [vec_len]u8) Error!ConsumeResult {
|
||||||
const vec: Vec = vec_ptr.*;
|
const vec: Vec = vec_ptr.*;
|
||||||
switch (self.state) {
|
switch (self.state) {
|
||||||
@@ -245,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)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -260,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 => {
|
||||||
@@ -275,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;
|
||||||
@@ -286,216 +659,22 @@ 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,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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];
|
|
||||||
switch (self.state) {
|
|
||||||
.init => switch (c) {
|
|
||||||
'D' => self.state = .method_d,
|
|
||||||
'G' => self.state = .method_g,
|
|
||||||
'H' => self.state = .method_h,
|
|
||||||
'P' => self.state = .method_p,
|
|
||||||
else => return error.MethodNotSupported,
|
|
||||||
},
|
|
||||||
.method_d => switch (c) {
|
|
||||||
'E' => self.state = .method_de,
|
|
||||||
else => return error.MethodNotSupported,
|
|
||||||
},
|
|
||||||
.method_g => switch (c) {
|
|
||||||
'E' => self.state = .method_ge,
|
|
||||||
else => return error.MethodNotSupported,
|
|
||||||
},
|
|
||||||
.method_h => switch (c) {
|
|
||||||
'E' => self.state = .method_he,
|
|
||||||
else => return error.MethodNotSupported,
|
|
||||||
},
|
|
||||||
.method_p => switch (c) {
|
|
||||||
'A' => self.state = .method_pa,
|
|
||||||
'O' => self.state = .method_po,
|
|
||||||
'U' => self.state = .method_pu,
|
|
||||||
else => return error.MethodNotSupported,
|
|
||||||
},
|
|
||||||
.method_de => switch (c) {
|
|
||||||
'L' => self.state = .method_del,
|
|
||||||
else => return error.MethodNotSupported,
|
|
||||||
},
|
|
||||||
.method_ge => switch (c) {
|
|
||||||
'T' => self.state = .methodComplete(.GET),
|
|
||||||
else => return error.MethodNotSupported,
|
|
||||||
},
|
|
||||||
.method_he => switch (c) {
|
|
||||||
'A' => self.state = .method_hea,
|
|
||||||
else => return error.MethodNotSupported,
|
|
||||||
},
|
|
||||||
.method_pa => switch (c) {
|
|
||||||
'T' => self.state = .method_pat,
|
|
||||||
else => return error.MethodNotSupported,
|
|
||||||
},
|
|
||||||
.method_po => switch (c) {
|
|
||||||
'S' => self.state = .method_pos,
|
|
||||||
else => return error.MethodNotSupported,
|
|
||||||
},
|
|
||||||
.method_pu => switch (c) {
|
|
||||||
'T' => self.state = .methodComplete(.PUT),
|
|
||||||
else => return error.MethodNotSupported,
|
|
||||||
},
|
|
||||||
.method_del => switch (c) {
|
|
||||||
'E' => self.state = .method_dele,
|
|
||||||
else => return error.MethodNotSupported,
|
|
||||||
},
|
|
||||||
.method_hea => switch (c) {
|
|
||||||
'D' => self.state = .methodComplete(.HEAD),
|
|
||||||
else => return error.MethodNotSupported,
|
|
||||||
},
|
|
||||||
.method_pat => switch (c) {
|
|
||||||
'C' => self.state = .method_patc,
|
|
||||||
else => return error.MethodNotSupported,
|
|
||||||
},
|
|
||||||
.method_pos => switch (c) {
|
|
||||||
'T' => self.state = .methodComplete(.POST),
|
|
||||||
else => return error.MethodNotSupported,
|
|
||||||
},
|
|
||||||
.method_dele => switch (c) {
|
|
||||||
'T' => self.state = .method_delet,
|
|
||||||
else => return error.MethodNotSupported,
|
|
||||||
},
|
|
||||||
.method_patc => switch (c) {
|
|
||||||
'H' => self.state = .methodComplete(.PATCH),
|
|
||||||
else => return error.MethodNotSupported,
|
|
||||||
},
|
|
||||||
.method_delet => switch (c) {
|
|
||||||
'E' => self.state = .methodComplete(.DELETE),
|
|
||||||
else => return error.MethodNotSupported,
|
|
||||||
},
|
|
||||||
.method_complete => |s| switch (c) {
|
|
||||||
' ' => self.state = .pathname(s.method, @as([*]const u8, @ptrCast(c_ptr))[1..1]),
|
|
||||||
else => return error.MethodNotSupported,
|
|
||||||
},
|
|
||||||
.pathname_state => |s| switch (c) {
|
|
||||||
' ' => {
|
|
||||||
self.state = .pathname_complete;
|
|
||||||
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]),
|
|
||||||
},
|
|
||||||
.pathname_complete => switch (c) {
|
|
||||||
'H' => self.state = .version_h,
|
|
||||||
else => return error.HttpVersionNotSupported,
|
|
||||||
},
|
|
||||||
.version_h => switch (c) {
|
|
||||||
'T' => self.state = .version_ht,
|
|
||||||
else => return error.HttpVersionNotSupported,
|
|
||||||
},
|
|
||||||
.version_ht => switch (c) {
|
|
||||||
'T' => self.state = .version_htt,
|
|
||||||
else => return error.HttpVersionNotSupported,
|
|
||||||
},
|
|
||||||
.version_htt => switch (c) {
|
|
||||||
'P' => self.state = .version_http,
|
|
||||||
else => return error.HttpVersionNotSupported,
|
|
||||||
},
|
|
||||||
.version_http => switch (c) {
|
|
||||||
'/' => self.state = .@"version_http/",
|
|
||||||
else => return error.HttpVersionNotSupported,
|
|
||||||
},
|
|
||||||
.@"version_http/" => switch (c) {
|
|
||||||
'1' => self.state = .@"version_http/1",
|
|
||||||
else => return error.HttpVersionNotSupported,
|
|
||||||
},
|
|
||||||
.@"version_http/1" => switch (c) {
|
|
||||||
'.' => self.state = .@"version_http/1.",
|
|
||||||
else => return error.HttpVersionNotSupported,
|
|
||||||
},
|
|
||||||
.@"version_http/1." => switch (c) {
|
|
||||||
'1' => self.state = .version_complete,
|
|
||||||
else => return error.HttpVersionNotSupported,
|
|
||||||
},
|
|
||||||
.version_complete => switch (c) {
|
|
||||||
'\r' => self.state = .start_line_end,
|
|
||||||
else => return error.HttpVersionNotSupported,
|
|
||||||
},
|
|
||||||
.start_line_end => switch (c) {
|
|
||||||
'\n' => self.state = .header_name_start,
|
|
||||||
else => return error.MissingLineFeed,
|
|
||||||
},
|
|
||||||
.header_name_start => switch (c) {
|
|
||||||
'\r' => self.state = .headers_end,
|
|
||||||
else => self.state = .{ .header_name = c_slice },
|
|
||||||
},
|
|
||||||
.header_name => |name| switch (c) {
|
|
||||||
':' => {
|
|
||||||
self.state = .headerValue(name, @as([*]const u8, @ptrCast(c_ptr))[1..1]);
|
|
||||||
},
|
|
||||||
else => self.state = .{ .header_name = name.ptr[0 .. name.len + 1] },
|
|
||||||
},
|
|
||||||
.header_value => |s| switch (c) {
|
|
||||||
'\r' => {
|
|
||||||
self.state = .header_line_end;
|
|
||||||
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(self.response, 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]),
|
|
||||||
},
|
|
||||||
.header_line_end => switch (c) {
|
|
||||||
'\n' => self.state = .header_name_start,
|
|
||||||
else => return error.MissingLineFeed,
|
|
||||||
},
|
|
||||||
.headers_end => switch (c) {
|
|
||||||
'\n' => {
|
|
||||||
if (self.content_length == 0) {
|
|
||||||
self.request_handler.?.rawBody(self.response, &.{}) catch |err| {
|
|
||||||
self.last_handler_error = err;
|
|
||||||
return error.HandlerError;
|
|
||||||
};
|
|
||||||
return .done;
|
|
||||||
}
|
|
||||||
self.state = .{ .body = @as([*]const u8, @ptrCast(c_ptr))[1..1] };
|
|
||||||
},
|
|
||||||
else => return error.MissingLineFeed,
|
|
||||||
},
|
|
||||||
.body => |body| {
|
|
||||||
const new_body = body.ptr[0 .. body.len + 1];
|
|
||||||
self.state = .{ .body = new_body };
|
|
||||||
if (new_body.len >= self.content_length) {
|
|
||||||
self.request_handler.?.rawBody(self.response, new_body) catch |err| {
|
|
||||||
self.last_handler_error = err;
|
|
||||||
return error.HandlerError;
|
|
||||||
};
|
|
||||||
return .done;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return .not_done;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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(u8, 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 (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.print("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,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
Reference in New Issue
Block a user