web: Secondary utils and helpers
This commit is contained in:
8
.vscode/settings.json
vendored
Normal file
8
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"files.exclude": {
|
||||||
|
"**/.zig-cache": true,
|
||||||
|
},
|
||||||
|
"files.associations": {
|
||||||
|
"**/*.{c,h}": "c",
|
||||||
|
},
|
||||||
|
}
|
||||||
21
packages/web/build.zig
Normal file
21
packages/web/build.zig
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub fn build(b: *std.Build) void {
|
||||||
|
const target = b.standardTargetOptions(.{});
|
||||||
|
const optimize = b.standardOptimizeOption(.{});
|
||||||
|
|
||||||
|
const module = b.addModule("http", .{
|
||||||
|
.root_source_file = b.path("src/root.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tests = b.addTest(.{
|
||||||
|
.root_module = module,
|
||||||
|
});
|
||||||
|
|
||||||
|
const run_tests = b.addRunArtifact(tests);
|
||||||
|
|
||||||
|
const test_step = b.step("test", "Run tests");
|
||||||
|
test_step.dependOn(&run_tests.step);
|
||||||
|
}
|
||||||
11
packages/web/build.zig.zon
Normal file
11
packages/web/build.zig.zon
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
.{
|
||||||
|
.name = .web,
|
||||||
|
.version = "0.0.0",
|
||||||
|
.minimum_zig_version = "0.15.2",
|
||||||
|
.paths = .{
|
||||||
|
"src",
|
||||||
|
"build.zig",
|
||||||
|
"build.zig.zon",
|
||||||
|
},
|
||||||
|
.fingerprint = 0x15c938517148d390,
|
||||||
|
}
|
||||||
15
packages/web/src/Connection.zig
Normal file
15
packages/web/src/Connection.zig
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Connection = @This();
|
||||||
|
|
||||||
|
const FileDescriptor = @import("FileDescriptor.zig").FileDescriptor;
|
||||||
|
|
||||||
|
const linux = std.os.linux;
|
||||||
|
|
||||||
|
address: std.net.Address,
|
||||||
|
fd: FileDescriptor,
|
||||||
|
node: std.DoublyLinkedList.Node = .{},
|
||||||
|
|
||||||
|
pub fn deinit(self: *Connection) void {
|
||||||
|
self.fd.close();
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
133
packages/web/src/FileDescriptor.zig
Normal file
133
packages/web/src/FileDescriptor.zig
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const linux = std.os.linux;
|
||||||
|
const errno = linux.E.init;
|
||||||
|
|
||||||
|
pub const FileDescriptor = enum(i32) {
|
||||||
|
stdin = 0,
|
||||||
|
stdout = 1,
|
||||||
|
stderr = 2,
|
||||||
|
_,
|
||||||
|
|
||||||
|
pub fn memfd_create(name: [*:0]const u8, flags: u32) FileDescriptor {
|
||||||
|
const rc = linux.memfd_create(name, flags);
|
||||||
|
return switch (errno(rc)) {
|
||||||
|
.SUCCESS => @enumFromInt(@as(i32, @intCast(rc))),
|
||||||
|
else => error.SystemError,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn socket(domain: u32, socket_type: u32, protocol: u32) FileDescriptor {
|
||||||
|
const rc = linux.socket(domain, socket_type, protocol);
|
||||||
|
return switch (errno(rc)) {
|
||||||
|
.SUCCESS => @enumFromInt(@as(i32, @intCast(rc))),
|
||||||
|
else => error.SystemError,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close(self: FileDescriptor) void {
|
||||||
|
_ = linux.close(@intFromEnum(self));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn accept(self: FileDescriptor, noalias addr: ?*linux.sockaddr, noalias len: ?*linux.socklen_t) !FileDescriptor {
|
||||||
|
const rc = linux.accept(@intFromEnum(self), &addr, &len);
|
||||||
|
return switch (errno(rc)) {
|
||||||
|
.SUCCESS => @enumFromInt(@as(i32, @intCast(rc))),
|
||||||
|
.CONNABORTED => return error.ConnectionAborted,
|
||||||
|
.PERM => return error.BlockedByFirewall,
|
||||||
|
else => return error.SystemError,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bind(self: FileDescriptor, addr: *const linux.sockaddr, len: linux.socklen_t) !void {
|
||||||
|
const rc = linux.bind(@intFromEnum(self), addr, len);
|
||||||
|
return switch (errno(rc)) {
|
||||||
|
.SUCCESS => {},
|
||||||
|
.ACCES, .PERM => error.AccessDenied,
|
||||||
|
.ADDRINUSE => error.AddressInUse,
|
||||||
|
.ADDRNOTAVAIL => error.AddressNotAvailable,
|
||||||
|
.AFNOSUPPORT => error.AddressFamilyNotSupported,
|
||||||
|
.LOOP => error.SymLinkLoop,
|
||||||
|
.NAMETOOLONG => error.NameTooLong,
|
||||||
|
.NOENT => error.FileNotFound,
|
||||||
|
.NOTDIR => error.NotDir,
|
||||||
|
.ROFS => error.ReadOnlyFileSystem,
|
||||||
|
else => error.SystemError,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ftruncate(self: FileDescriptor, length: i64) !void {
|
||||||
|
const rc = linux.ftruncate(@intFromEnum(self), length);
|
||||||
|
return switch (errno(rc)) {
|
||||||
|
.SUCCESS => {},
|
||||||
|
else => error.SystemError,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getsockname(self: FileDescriptor, noalias addr: *linux.sockaddr, noalias len: *linux.socklen_t) !void {
|
||||||
|
const rc = linux.getsockname(@intFromEnum(self), addr, len);
|
||||||
|
return switch (errno(rc)) {
|
||||||
|
.SUCCESS => {},
|
||||||
|
else => error.SystemError,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn listen(self: FileDescriptor, backlog: u32) !void {
|
||||||
|
const rc = linux.listen(@intFromEnum(self), backlog);
|
||||||
|
return switch (errno(rc)) {
|
||||||
|
.SUCCESS => {},
|
||||||
|
.ADDRINUSE => error.AddressInUse,
|
||||||
|
else => error.SystemError,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read(self: FileDescriptor, buf: []u8) !usize {
|
||||||
|
while (true) {
|
||||||
|
const rc = linux.read(@intFromEnum(self), buf.ptr, @intCast(buf.len));
|
||||||
|
switch (errno(rc)) {
|
||||||
|
.SUCCESS => return rc,
|
||||||
|
.INTR => continue,
|
||||||
|
else => return error.SystemError,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn readAll(self: FileDescriptor, buf: []u8) !void {
|
||||||
|
var total_bytes_read: usize = 0;
|
||||||
|
|
||||||
|
while (total_bytes_read < buf.len) {
|
||||||
|
const chunk = buf[total_bytes_read..];
|
||||||
|
const bytes_read = try self.read(chunk);
|
||||||
|
total_bytes_read += bytes_read;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setsockopt(self: FileDescriptor, level: i32, optname: u32, opt: []const u8) !void {
|
||||||
|
const rc = linux.setsockopt(@intFromEnum(self), level, optname, opt.ptr, @intCast(opt.len));
|
||||||
|
return switch (errno(rc)) {
|
||||||
|
.SUCCESS => {},
|
||||||
|
else => error.SystemError,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(self: FileDescriptor, buf: []const u8) !usize {
|
||||||
|
while (true) {
|
||||||
|
const rc = linux.write(@intFromEnum(self), buf.ptr, @intCast(buf.len));
|
||||||
|
switch (errno(rc)) {
|
||||||
|
.SUCCESS => return rc,
|
||||||
|
.INTR => continue,
|
||||||
|
else => return error.SystemError,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn writeAll(self: FileDescriptor, buf: []const u8) !void {
|
||||||
|
var total_bytes_written: usize = 0;
|
||||||
|
|
||||||
|
while (total_bytes_written < buf.len) {
|
||||||
|
const chunk = buf[total_bytes_written..];
|
||||||
|
const bytes_written = try self.write(chunk);
|
||||||
|
total_bytes_written += bytes_written;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
41
packages/web/src/Id.zig
Normal file
41
packages/web/src/Id.zig
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const UUID = @import("UUID.zig");
|
||||||
|
|
||||||
|
const decoder = &std.base64.url_safe_no_pad.Decoder;
|
||||||
|
const encoder = &std.base64.url_safe_no_pad.Encoder;
|
||||||
|
|
||||||
|
pub fn Id(comptime _tag: @Type(.enum_literal)) type {
|
||||||
|
return struct {
|
||||||
|
pub const tag = _tag;
|
||||||
|
|
||||||
|
uuid: UUID,
|
||||||
|
|
||||||
|
pub fn init(uuid: UUID) @This() {
|
||||||
|
return .{ .uuid = uuid };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eql(a: @This(), b: @This()) bool {
|
||||||
|
return std.mem.eql(u8, &a.bytes, &b.bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode(encoded: *const [22]u8) !@This() {
|
||||||
|
var bytes: [16]u8 = undefined;
|
||||||
|
decoder.decode(&bytes, encoded) catch |err| switch (err) {
|
||||||
|
error.InvalidCharacter => return error.InvalidId,
|
||||||
|
error.InvalidPadding, error.NoSpaceLeft => unreachable,
|
||||||
|
};
|
||||||
|
return .{ .bytes = bytes };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode(self: @This()) [22]u8 {
|
||||||
|
var text: [22]u8 = undefined;
|
||||||
|
self.encodeInto(&text);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encodeInto(self: @This(), text: *[22]u8) void {
|
||||||
|
std.base64.url_safe_no_pad.Encoder.encode(text, self.bytes);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
87
packages/web/src/UUID.zig
Normal file
87
packages/web/src/UUID.zig
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const UUID = @This();
|
||||||
|
|
||||||
|
bytes: [16]u8,
|
||||||
|
|
||||||
|
pub const zero: UUID = .{ .bytes = @splat(0) };
|
||||||
|
|
||||||
|
pub fn v4() UUID {
|
||||||
|
var bytes: [16]u8 = undefined;
|
||||||
|
std.crypto.random.bytes(&bytes);
|
||||||
|
bytes[6] = (bytes[6] & 0x0F) | 0x40;
|
||||||
|
bytes[8] = (bytes[8] & 0x3F) | 0x80;
|
||||||
|
return .{ .bytes = bytes };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const namespaces = struct {
|
||||||
|
/// Name string is a fully-qualified domain name
|
||||||
|
///
|
||||||
|
/// `6ba7b810-9dad-11d1-80b4-00c04fd430c8`
|
||||||
|
pub const dns: UUID = .{ .bytes = .{ 0x6B, 0xA7, 0xB8, 0x10, 0x9D, 0xAD, 0x11, 0xD1, 0x80, 0xB4, 0x00, 0xC0, 0x4F, 0xD4, 0x30, 0xC8 } };
|
||||||
|
/// Name string is a URL
|
||||||
|
///
|
||||||
|
/// `6ba7b811-9dad-11d1-80b4-00c04fd430c8`
|
||||||
|
pub const url: UUID = .{ .bytes = .{ 0x6B, 0xA7, 0xB8, 0x11, 0x9D, 0xAD, 0x11, 0xD1, 0x80, 0xB4, 0x00, 0xC0, 0x4F, 0xD4, 0x30, 0xC8 } };
|
||||||
|
/// Name string is an ISO OID
|
||||||
|
///
|
||||||
|
/// `6ba7b812-9dad-11d1-80b4-00c04fd430c8`
|
||||||
|
pub const oid: UUID = .{ .bytes = .{ 0x6B, 0xA7, 0xB8, 0x12, 0x9D, 0xAD, 0x11, 0xD1, 0x80, 0xB4, 0x00, 0xC0, 0x4F, 0xD4, 0x30, 0xC8 } };
|
||||||
|
/// Name string is an X.500 DN (in DER or a text output format)
|
||||||
|
///
|
||||||
|
/// `6ba7b814-9dad-11d1-80b4-00c04fd430c8`
|
||||||
|
pub const x500: UUID = .{ .bytes = .{ 0x6B, 0xA7, 0xB8, 0x14, 0x9D, 0xAD, 0x11, 0xD1, 0x80, 0xB4, 0x00, 0xC0, 0x4F, 0xD4, 0x30, 0xC8 } };
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn v5(namespace: UUID, name: []const u8) UUID {
|
||||||
|
const hash = blk: {
|
||||||
|
var hasher = std.crypto.hash.Sha1.init(.{});
|
||||||
|
hasher.update(namespace);
|
||||||
|
hasher.update(name);
|
||||||
|
break :blk hasher.finalResult();
|
||||||
|
};
|
||||||
|
|
||||||
|
var bytes: [16]u8 = hash[0..16].*;
|
||||||
|
bytes[6] = (bytes[6] & 0x0F) | 0x50;
|
||||||
|
bytes[8] = (bytes[8] & 0x3F) | 0x80;
|
||||||
|
|
||||||
|
return .{ .bytes = bytes };
|
||||||
|
}
|
||||||
|
|
||||||
|
var v7_lock: std.Thread.Mutex = .{};
|
||||||
|
var v7_last_timestamp: std.atomic.Value(u64) = .{ .raw = 0 };
|
||||||
|
var v7_counter: std.atomic.Value(u32) = .{ .raw = 0 };
|
||||||
|
|
||||||
|
fn getCount(timestamp: u64) u32 {
|
||||||
|
v7_lock.lock();
|
||||||
|
defer v7_lock.unlock();
|
||||||
|
|
||||||
|
if (v7_last_timestamp.swap(timestamp, .monotonic) != timestamp) {
|
||||||
|
v7_counter.store(0, .monotonic);
|
||||||
|
}
|
||||||
|
|
||||||
|
return v7_counter.fetchAdd(1, .monotonic) % 4096;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn v7() UUID {
|
||||||
|
const timestamp: u64 = @intCast(@max(0, std.time.milliTimestamp()));
|
||||||
|
const count = getCount(timestamp);
|
||||||
|
const random = blk: {
|
||||||
|
var bytes: [8]u8 = undefined;
|
||||||
|
std.crypto.random.bytes(&bytes);
|
||||||
|
break :blk bytes;
|
||||||
|
};
|
||||||
|
|
||||||
|
var bytes: [16]u8 = undefined;
|
||||||
|
bytes[0] = @truncate(timestamp >> 40);
|
||||||
|
bytes[1] = @truncate(timestamp >> 32);
|
||||||
|
bytes[2] = @truncate(timestamp >> 24);
|
||||||
|
bytes[3] = @truncate(timestamp >> 16);
|
||||||
|
bytes[4] = @truncate(timestamp >> 8);
|
||||||
|
bytes[5] = @truncate(timestamp);
|
||||||
|
bytes[6] = (@as(u8, 7) << 4) | @as(u8, @truncate((count >> 8) & 0x0F));
|
||||||
|
bytes[7] = @truncate(count);
|
||||||
|
bytes[8] = (random[0] & 0x3F) | 0x80;
|
||||||
|
@memcpy(bytes[9..16], random[1..8]);
|
||||||
|
|
||||||
|
return .{ .bytes = bytes };
|
||||||
|
}
|
||||||
407
packages/web/src/http/Parser.zig
Normal file
407
packages/web/src/http/Parser.zig
Normal file
@@ -0,0 +1,407 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Parser = @This();
|
||||||
|
|
||||||
|
const Callbacks = struct {
|
||||||
|
self: ?*anyopaque = null,
|
||||||
|
|
||||||
|
route: ?*const fn (self: ?*anyopaque, route: Route) void = null,
|
||||||
|
header: ?*const fn (self: ?*anyopaque, name: []const u8, value: []const u8) void = null,
|
||||||
|
body: ?*const fn (self: ?*anyopaque, body: []const u8) void = null,
|
||||||
|
|
||||||
|
pub const null_callbacks: Callbacks = .{};
|
||||||
|
};
|
||||||
|
|
||||||
|
const Error = error{
|
||||||
|
MethodNotSupported,
|
||||||
|
HttpVersionNotSupported,
|
||||||
|
MissingLineFeed,
|
||||||
|
InvalidContentLength,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Vec = @Vector(32, u8);
|
||||||
|
|
||||||
|
const Pattern = struct {
|
||||||
|
value: Vec,
|
||||||
|
mask: Vec,
|
||||||
|
len: u6,
|
||||||
|
|
||||||
|
pub fn init(comptime prefix: []const u8) Pattern {
|
||||||
|
if (prefix.len > 32) {
|
||||||
|
@compileError("Prefix length is too high");
|
||||||
|
}
|
||||||
|
|
||||||
|
var value: [32]u8 = undefined;
|
||||||
|
var mask: [32]u8 = undefined;
|
||||||
|
for (0..32) |i| {
|
||||||
|
if (i < prefix.len) {
|
||||||
|
value[i] = prefix[i];
|
||||||
|
mask[i] = 0xFF;
|
||||||
|
} else {
|
||||||
|
value[i] = 0x00;
|
||||||
|
mask[i] = 0x00;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn check(self: Pattern, vec: Vec) bool {
|
||||||
|
return @reduce(.And, vec & self.mask == self.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const patterns = struct {
|
||||||
|
method_delete: Pattern.init("DELETE "),
|
||||||
|
method_get: Pattern.init("GET "),
|
||||||
|
method_head: Pattern.init("HEAD "),
|
||||||
|
method_patch: Pattern.init("PATCH "),
|
||||||
|
method_post: Pattern.init("POST "),
|
||||||
|
method_put: Pattern.init("PUT "),
|
||||||
|
|
||||||
|
@"version_http/1.1": Pattern.init("HTTP/1.1\r\n"),
|
||||||
|
};
|
||||||
|
|
||||||
|
inline fn hasSpace(vec: Vec) bool {
|
||||||
|
const has_space = vec == @as(Vec, @splat(' '));
|
||||||
|
return @reduce(.Or, has_space);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn hasCRLF(vec: Vec) bool {
|
||||||
|
const has_cr = vec == @as(Vec, @splat('\r'));
|
||||||
|
const has_lf = vec == @as(Vec, @splat('\n'));
|
||||||
|
return @reduce(.Or, has_cr | has_lf);
|
||||||
|
}
|
||||||
|
|
||||||
|
const State = union(enum) {
|
||||||
|
pub fn methodComplete(method: Method) State {
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Method = enum {
|
||||||
|
DELETE,
|
||||||
|
GET,
|
||||||
|
HEAD,
|
||||||
|
PATCH,
|
||||||
|
POST,
|
||||||
|
PUT,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Route = struct {
|
||||||
|
method: Method,
|
||||||
|
pathname: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
callbacks: Callbacks,
|
||||||
|
state: State,
|
||||||
|
current_header_is_content_length: bool,
|
||||||
|
content_length: usize,
|
||||||
|
|
||||||
|
pub fn init(callbacks: Callbacks) Parser {
|
||||||
|
return .{
|
||||||
|
.callbacks = callbacks,
|
||||||
|
.state = .init,
|
||||||
|
.current_header_is_content_length = false,
|
||||||
|
.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) {
|
||||||
|
if (self.callbacks.body) |bodyCallback| {
|
||||||
|
bodyCallback(self.callbacks.self, new_body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.consumed = i,
|
||||||
|
.done = done,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
else => {
|
||||||
|
const res = try self.consumeChar(&chars[i]);
|
||||||
|
i += 1;
|
||||||
|
if (res == .done) return .{
|
||||||
|
.consumed = i,
|
||||||
|
.done = true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.consumed = chars.len,
|
||||||
|
.done = false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn consumeChar(self: *Parser, c_ptr: *const u8) Error!ConsumeCharResult {
|
||||||
|
const c = c_ptr.*;
|
||||||
|
const c_slice = @as([*]const u8, @ptrCast(c_ptr))[0..1];
|
||||||
|
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;
|
||||||
|
if (self.callbacks.route) |routeCallback| {
|
||||||
|
routeCallback(self.callbacks.self, .{
|
||||||
|
.method = s.method,
|
||||||
|
.pathname = s.pathname,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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]);
|
||||||
|
self.current_header_is_content_length = std.ascii.eqlIgnoreCase(name, "Content-Length");
|
||||||
|
},
|
||||||
|
else => self.state = .{ .header_name = name.ptr[0 .. name.len + 1] },
|
||||||
|
},
|
||||||
|
.header_value => |s| switch (c) {
|
||||||
|
'\r' => {
|
||||||
|
self.state = .header_line_end;
|
||||||
|
const value_trimmed = std.mem.trim(u8, s.value, " \t");
|
||||||
|
if (self.current_header_is_content_length) {
|
||||||
|
self.content_length = std.fmt.parseInt(usize, value_trimmed, 10) catch return error.InvalidContentLength;
|
||||||
|
self.current_header_is_content_length = false;
|
||||||
|
}
|
||||||
|
if (self.callbacks.header) |headerCallback| {
|
||||||
|
headerCallback(self.callbacks.self, s.name, value_trimmed);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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) {
|
||||||
|
if (self.callbacks.body) |bodyCallback| {
|
||||||
|
bodyCallback(self.callbacks.self, &.{});
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
if (self.callbacks.body) |bodyCallback| {
|
||||||
|
bodyCallback(self.callbacks.self, new_body);
|
||||||
|
}
|
||||||
|
return .done;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return .not_done;
|
||||||
|
}
|
||||||
58
packages/web/src/http/status.zig
Normal file
58
packages/web/src/http/status.zig
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
pub const ok = "HTTP/1.1 200 OK\r\n";
|
||||||
|
pub const created = "HTTP/1.1 201 Created\r\n";
|
||||||
|
pub const accepted = "HTTP/1.1 202 Accepted\r\n";
|
||||||
|
pub const non_authoritative_information = "HTTP/1.1 203 Non-Authoritative Information\r\n";
|
||||||
|
pub const no_content = "HTTP/1.1 204 No Content\r\n";
|
||||||
|
pub const reset_content = "HTTP/1.1 205 Reset Content\r\n";
|
||||||
|
pub const partial_content = "HTTP/1.1 206 Partial Content\r\n";
|
||||||
|
pub const multi_status = "HTTP/1.1 207 Multi-Status\r\n";
|
||||||
|
pub const already_reported = "HTTP/1.1 208 Already Reported\r\n";
|
||||||
|
|
||||||
|
pub const multiple_choices = "HTTP/1.1 300 Multiple Choices\r\n";
|
||||||
|
pub const moved_permanently = "HTTP/1.1 301 Moved Permanently\r\n";
|
||||||
|
pub const found = "HTTP/1.1 302 Found\r\n";
|
||||||
|
pub const see_other = "HTTP/1.1 303 See Other\r\n";
|
||||||
|
pub const not_modified = "HTTP/1.1 304 Not Modified\r\n";
|
||||||
|
pub const temporary_redirect = "HTTP/1.1 307 Temporary Redirect\r\n";
|
||||||
|
pub const permanent_redirect = "HTTP/1.1 308 Permanent Redirect\r\n";
|
||||||
|
|
||||||
|
pub const bad_request = "HTTP/1.1 400 Bad Request\r\n";
|
||||||
|
pub const unauthorized = "HTTP/1.1 401 Unauthorized\r\n";
|
||||||
|
pub const payment_required = "HTTP/1.1 402 Payment Required\r\n";
|
||||||
|
pub const forbidden = "HTTP/1.1 403 Forbidden\r\n";
|
||||||
|
pub const not_found = "HTTP/1.1 404 Not Found\r\n";
|
||||||
|
pub const method_not_allowed = "HTTP/1.1 405 Method Not Allowed\r\n";
|
||||||
|
pub const not_acceptable = "HTTP/1.1 406 Not Acceptable\r\n";
|
||||||
|
pub const proxy_authentication_required = "HTTP/1.1 407 Proxy Authentication Required\r\n";
|
||||||
|
pub const request_timeout = "HTTP/1.1 408 Request Timeout\r\n";
|
||||||
|
pub const conflict = "HTTP/1.1 409 Conflict\r\n";
|
||||||
|
pub const gone = "HTTP/1.1 410 Gone\r\n";
|
||||||
|
pub const length_required = "HTTP/1.1 411 Length Required\r\n";
|
||||||
|
pub const precondition_failed = "HTTP/1.1 412 Precondition Failed\r\n";
|
||||||
|
pub const content_too_large = "HTTP/1.1 413 Content Too Large\r\n";
|
||||||
|
pub const uri_too_long = "HTTP/1.1 414 URI Too Long\r\n";
|
||||||
|
pub const unsupported_media_type = "HTTP/1.1 415 Unsupported Media Type\r\n";
|
||||||
|
pub const range_not_satisfiable = "HTTP/1.1 416 Range Not Satisfiable\r\n";
|
||||||
|
pub const expectation_failed = "HTTP/1.1 417 Expectation Failed\r\n";
|
||||||
|
pub const im_a_teapot = "HTTP/1.1 418 I'm a teapot\r\n";
|
||||||
|
pub const misdirected_request = "HTTP/1.1 421 Misdirected Request\r\n";
|
||||||
|
pub const unprocessable_content = "HTTP/1.1 422 Unprocessable Content\r\n";
|
||||||
|
pub const locked = "HTTP/1.1 423 Locked\r\n";
|
||||||
|
pub const failed_dependency = "HTTP/1.1 424 Failed Dependency\r\n";
|
||||||
|
pub const upgrade_required = "HTTP/1.1 426 Upgrade Required\r\n";
|
||||||
|
pub const precondition_required = "HTTP/1.1 428 Precondition Required\r\n";
|
||||||
|
pub const too_many_requests = "HTTP/1.1 429 Too Many Requests\r\n";
|
||||||
|
pub const request_header_fields_too_large = "HTTP/1.1 431 Request Header Fields Too Large\r\n";
|
||||||
|
pub const unavailable_for_legal_reasons = "HTTP/1.1 451 Unavailable For Legal Reasons\r\n";
|
||||||
|
|
||||||
|
pub const internal_server_error = "HTTP/1.1 500 Internal Server Error\r\n";
|
||||||
|
pub const not_implemented = "HTTP/1.1 501 Not Implemented\r\n";
|
||||||
|
pub const bad_gateway = "HTTP/1.1 502 Bad Gateway\r\n";
|
||||||
|
pub const service_unavailable = "HTTP/1.1 503 Service Unavailable\r\n";
|
||||||
|
pub const gateway_timeout = "HTTP/1.1 504 Gateway Timeout\r\n";
|
||||||
|
pub const http_version_not_supported = "HTTP/1.1 505 HTTP Version Not Supported\r\n";
|
||||||
|
pub const variant_also_negotiates = "HTTP/1.1 506 Variant Also Negotiates\r\n";
|
||||||
|
pub const insufficient_storage = "HTTP/1.1 507 Insufficient Storage\r\n";
|
||||||
|
pub const loop_detected = "HTTP/1.1 508 Loop Detected\r\n";
|
||||||
|
pub const not_extended = "HTTP/1.1 510 Not Extended\r\n";
|
||||||
|
pub const network_authentication_required = "HTTP/1.1 511 Network Authentication Required\r\n";
|
||||||
Reference in New Issue
Block a user