Compare commits

...

10 Commits

70 changed files with 2489 additions and 1941 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
.zig-cache .zig-cache
zig-out zig-out
zig-pkg

88
README.md Normal file
View File

@@ -0,0 +1,88 @@
# Building my own castle
I'm building my own castle in Zig ⚡.
This is a collection of experimental, recreational or actually useful libraries,
which I may or may not use as a part of some other projects. Most of them are
work in progress and might remain so indefinitely.
## cjit
JIT compiler for the C language inspired by Fabrice Bellard's Tiny C Compiler
(TCC). Meant to compile x86_64 and aarch64 for Windows and Linux straight to
virtual memory, without the ability to output an executable file or library. C
standard library is not fully supported, only some includes and builtins (see
[src/includes](packages/cjit/src/includes)).
It's in very early stage and cannot be used as of now. Currently, parts of the
tokenizer, x86_64 emit code and relocation logic are implemented. Aarch64 would
come after x86_64 is fully implemented and actually works.
You can see an example of how this library is supposed to be used in
[test/root.zig](packages/cjit/test/root.zig).
## js
Zig bindings for Fabrice Bellard's QuickJS. Original C sources are included and
compiled with Zig's build system. The bindings are not complete, but usable.
## media
Set of utilities for decoding and encoding media files. As of now, no purely Zig
decoder/encoder exists, which would be the goal. There is fairly comprehensive
PNG chunk decoder, though.
Bindings for stb_image.h from Sean T. Barrett's single-file C libraries are
included. They can be used to decode PNG, JPEG and HDR images and support Zig's
allocator interface in a thread-safe manner.
This package is actually used by my other project, codenamed
[voxel-game](/:root/renati/voxel-game).
## myid
Remnants of a project meant to provide a simpler alternative to OpenID Connect
(OIDC). It contains an HTTP server, which has since been improved and resides
in *web* package.
The ideas behind *myid* were developed further in another project I created
using TypeScript and Bun as a runtime. I might backport them back to Zig when I
find the time and motivation, which would be implemented on top of the HTTP
server from the *web* package.
## vecmath
Vector math library with support for vectors, matrices and ×8 SIMD operations
(utilizing SOA layout).
This package is actually used by my other project, codenamed
[voxel-game](/:root/renati/voxel-game) and is probably the most complete package
in this repository.
## web
An HTTP server library and collection of other utilities commonly associated
with web technologies.
The HTTP server implements HTTP version 1.1 using worker threads and supports
SSL via OpenSSL library. The server is very low level and does not provide any
logic to handle proper HTTP semantics. It is up to the user of the HTTP server
library to respect (or not) all HTTP methods and headers. The server is designed
or Linux only and uses Linux syscalls directly in its implementation.
The other utilities include:
- HTTP/1.1 request parser, which is part of the HTTP server, but can be used
independently,
- OpenSSL bindings, which are used by the HTTP server, but can be used
independently (full mechanical C translation is provided and partial manual
Zig translation),
- UUID type and generators for v4, v5 and v7,
- Generic ID type, a wrapper around UUID (or anything that is 16 bytes long)
that can be instantiated for each distinct use of ID with a tag to help avoid
type confusion at compile time.
## x11
Zig bindings for Xlib (aka libX11), a C library implementing a client of the X
Window System protocol. The bindings are not complete.

View File

@@ -195,7 +195,7 @@ const import = struct {
no_add: bool = false, no_add: bool = false,
/// Internal use. /// Internal use.
no_exotic: bool = false, no_exotic: bool = false,
_pad14: u18 = 0, _pad18: u14 = 0,
}; };
pub const JS_EVAL = packed struct(u32) { pub const JS_EVAL = packed struct(u32) {
@@ -1168,7 +1168,7 @@ pub const Value = extern struct {
} }
pub fn getClassId(self: Value) ClassId { pub fn getClassId(self: Value) ClassId {
return .{ .class_id = import.JS_GetClassID(self) }; return .{ .class_id = import.JS_GetClassID(self.value) };
} }
}; };
@@ -1182,7 +1182,7 @@ pub const ClassId = extern struct {
pub const invalid: ClassId = .{ .class_id = 0 }; pub const invalid: ClassId = .{ .class_id = 0 };
pub fn new() ClassId { pub fn new() ClassId {
var class_id: import.JSClassId = 0; var class_id: import.JSClassID = 0;
return .{ .class_id = import.JS_NewClassID(&class_id) }; return .{ .class_id = import.JS_NewClassID(&class_id) };
} }
}; };

View File

@@ -1,15 +1,23 @@
const std = @import("std"); const std = @import("std");
pub fn build(b: *std.Build) void { pub fn build(b: *std.Build) void {
const vm_dep = b.dependency("vecmath", .{}); const target = b.standardTargetOptions(.{});
const vm_module = vm_dep.module("vecmath");
const root_module = b.addModule("media", .{ const vm_dep = b.dependency("vecmath", .{
.target = target,
});
const vm_mod = vm_dep.module("vecmath");
const mod = b.addModule("media", .{
.root_source_file = b.path("src/root.zig"), .root_source_file = b.path("src/root.zig"),
.target = target,
.link_libc = true, .link_libc = true,
.imports = &.{
.{ .name = "vecmath", .module = vm_mod },
},
}); });
root_module.addCSourceFile(.{ mod.addCSourceFile(.{
.file = b.path("src/stbi/stb_image.c"), .file = b.path("src/stbi/stb_image.c"),
.flags = &.{ .flags = &.{
"-std=c17", "-std=c17",
@@ -19,5 +27,12 @@ pub fn build(b: *std.Build) void {
.language = .c, .language = .c,
}); });
root_module.addImport("vecmath", vm_module); const mod_tests = b.addTest(.{
.root_module = mod,
});
const run_mod_tests = b.addRunArtifact(mod_tests);
const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_mod_tests.step);
} }

View File

@@ -1,7 +1,7 @@
.{ .{
.name = .media, .name = .media,
.version = "0.0.0", .version = "0.0.0",
.minimum_zig_version = "0.15.2", .minimum_zig_version = "0.16.0",
.paths = .{ .paths = .{
"src", "src",
"build.zig", "build.zig",

View File

@@ -19,3 +19,7 @@ media_type: []const u8,
/// to contain the entire file, only its beginning. The caller asserts that /// to contain the entire file, only its beginning. The caller asserts that
/// `buffer` is at least `magic_length` bytes long. /// `buffer` is at least `magic_length` bytes long.
isFormat: *const fn (buffer: []const u8) bool, isFormat: *const fn (buffer: []const u8) bool,
test "refAllDecls" {
std.testing.refAllDecls(@This());
}

View File

@@ -9,14 +9,26 @@ pub const Static = struct {
const sample_rate: f64 = @floatFromInt(self.sample_rate); const sample_rate: f64 = @floatFromInt(self.sample_rate);
return @floatCast(samples / sample_rate); return @floatCast(samples / sample_rate);
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
pub const Sample = extern struct { pub const Sample = extern struct {
left: i16, left: i16,
right: i16, right: i16,
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
pub const Stream = struct { pub const Stream = struct {
source: *std.Io.Reader, source: *std.Io.Reader,
sample_rate: u32, sample_rate: u32,
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -19,11 +19,15 @@ pub fn Static(comptime W: u32, comptime H: u32) type {
} }
pub fn fill(self: *@This(), color: vm.Color) void { pub fn fill(self: *@This(), color: vm.Color) void {
@memset(self.data, color); @memset(&self.data, color);
} }
}; };
} }
test "Static - refAllDecls" {
std.testing.refAllDecls(Static(16, 16));
}
pub const Dynamic = struct { pub const Dynamic = struct {
width: u32, width: u32,
height: u32, height: u32,
@@ -65,6 +69,10 @@ pub const Dynamic = struct {
pub fn fill(self: *@This(), color: vm.Color) void { pub fn fill(self: *@This(), color: vm.Color) void {
@memset(self.data[0 .. self.width * self.height], color); @memset(self.data[0 .. self.width * self.height], color);
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
pub const Hdr = struct { pub const Hdr = struct {
@@ -108,4 +116,8 @@ pub const Hdr = struct {
pub fn fill(self: *@This(), color: vm.ColorHdr) void { pub fn fill(self: *@This(), color: vm.ColorHdr) void {
@memset(self.data[0 .. self.width * self.height], color); @memset(self.data[0 .. self.width * self.height], color);
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -20,6 +20,10 @@ const Info = union(enum) {
pub fn makeFull(full: Header) Info { pub fn makeFull(full: Header) Info {
return .{ .full = full }; return .{ .full = full };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
const Header = struct { const Header = struct {
@@ -99,14 +103,18 @@ const Marker = enum(u8) {
return if (@intFromEnum(self) & 0b1111_0000 == 0xE0) @intCast(@intFromEnum(self) & 0b0000_1111) else null; return if (@intFromEnum(self) & 0b1111_0000 == 0xE0) @intCast(@intFromEnum(self) & 0b0000_1111) else null;
} }
/// A standalone marker has no content and are not followed by segment /// A standalone marker has no content and is not followed by segment length
/// length parameter. /// parameter.
pub fn isStandalone(self: Marker) bool { pub fn isStandalone(self: Marker) bool {
return self.isRST() != null or return self.isRST() != null or
self == .SOI or self == .SOI or
self == .EOI or self == .EOI or
self == .TEM; self == .TEM;
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
/// The caller asserts that the buffer is at least `format.magic_length` bytes /// The caller asserts that the buffer is at least `format.magic_length` bytes
@@ -137,3 +145,7 @@ pub fn info(buffer: []const u8) ?Info {
@panic("TODO"); @panic("TODO");
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}

View File

@@ -35,3 +35,7 @@ pub fn info(buffer: []const u8) ?Header {
@panic("TODO"); @panic("TODO");
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}

View File

@@ -27,7 +27,7 @@ pub const Header = struct {
filter_method: FilterMethod, filter_method: FilterMethod,
interlace_method: InterlaceMethod, interlace_method: InterlaceMethod,
pub fn decode(chunk: Chunk) Header { pub fn decode(chunk: Chunk) !Header {
std.debug.assert(chunk.chunk_type == .IHDR); std.debug.assert(chunk.chunk_type == .IHDR);
if (chunk.data.len != 13) return error.InvalidPng; if (chunk.data.len != 13) return error.InvalidPng;
@@ -79,6 +79,10 @@ pub const Header = struct {
.interlace_method = @enumFromInt(interlace_method), .interlace_method = @enumFromInt(interlace_method),
}; };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
pub const BitDepth = enum(u8) { pub const BitDepth = enum(u8) {
@@ -89,7 +93,11 @@ pub const BitDepth = enum(u8) {
@"16" = 16, @"16" = 16,
pub fn range(self: BitDepth) usize { pub fn range(self: BitDepth) usize {
return @as(usize, 2) << @intFromEnum(@intFromEnum(self)); return @as(usize, 2) << @intCast(@intFromEnum(self));
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
} }
}; };
@@ -111,6 +119,10 @@ pub const ColorType = enum(u8) {
pub fn alphaChannelUsed(self: ColorType) bool { pub fn alphaChannelUsed(self: ColorType) bool {
return @intFromEnum(self) & 0b0000_0100 != 0; return @intFromEnum(self) & 0b0000_0100 != 0;
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
pub const CompressionMethod = enum(u8) { pub const CompressionMethod = enum(u8) {
@@ -156,6 +168,10 @@ pub const Palette = struct {
.entries = entries, .entries = entries,
}; };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
// --- tRNS -------------------------------------------------------------------- // --- tRNS --------------------------------------------------------------------
@@ -209,6 +225,10 @@ pub const Transparency = union(enum) {
.rgba => error.InvalidPng, .rgba => error.InvalidPng,
}; };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
const TransparencyPalette = struct { const TransparencyPalette = struct {
@@ -218,6 +238,10 @@ const TransparencyPalette = struct {
pub fn asSlice(self: *const TransparencyPalette) []const u8 { pub fn asSlice(self: *const TransparencyPalette) []const u8 {
return self.entries[0..self.len]; return self.entries[0..self.len];
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
// --- gAMA -------------------------------------------------------------------- // --- gAMA --------------------------------------------------------------------
@@ -245,6 +269,10 @@ const Gamma = struct {
const gamma = std.mem.readInt(u32, chunk.data[0..4], .big); const gamma = std.mem.readInt(u32, chunk.data[0..4], .big);
return .{ .gamma = gamma }; return .{ .gamma = gamma };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
// --- cHRM -------------------------------------------------------------------- // --- cHRM --------------------------------------------------------------------
@@ -274,6 +302,10 @@ const Chromaticities = struct {
.blue_y = std.mem.readInt(u32, chunk.data[28..32], .big), .blue_y = std.mem.readInt(u32, chunk.data[28..32], .big),
}; };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
// --- sRGB -------------------------------------------------------------------- // --- sRGB --------------------------------------------------------------------
@@ -301,6 +333,10 @@ const RenderingIntent = enum(u8) {
if (rendering_intent > 3) return error.InvalidPng; if (rendering_intent > 3) return error.InvalidPng;
return @enumFromInt(rendering_intent); return @enumFromInt(rendering_intent);
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
// --- pHYs -------------------------------------------------------------------- // --- pHYs --------------------------------------------------------------------
@@ -310,7 +346,7 @@ pub const PhysicalPixelDimensions = struct {
pixels_per_unit_y: u32, pixels_per_unit_y: u32,
unit: PhysicalUnit, unit: PhysicalUnit,
pub fn decode(chunk: Chunk) PhysicalPixelDimensions { pub fn decode(chunk: Chunk) !PhysicalPixelDimensions {
std.debug.assert(chunk.chunk_type == .pHYs); std.debug.assert(chunk.chunk_type == .pHYs);
if (chunk.data.len != 9) return error.InvalidPng; if (chunk.data.len != 9) return error.InvalidPng;
@@ -328,6 +364,10 @@ pub const PhysicalPixelDimensions = struct {
.unit = @enumFromInt(unit), .unit = @enumFromInt(unit),
}; };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
pub const PhysicalUnit = enum(u8) { pub const PhysicalUnit = enum(u8) {
@@ -381,41 +421,61 @@ pub const LastModificationTime = struct {
.second = second, .second = second,
}; };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
pub const ChunkType = enum(u32) { pub const ChunkType = enum(u32) {
IHDR = @bitCast("IHDR".*), IHDR = fromName("IHDR"),
PLTE = @bitCast("PLTE".*), PLTE = fromName("PLTE"),
IDAT = @bitCast("IDAT".*), IDAT = fromName("IDAT"),
IEND = @bitCast("IEND".*), IEND = fromName("IEND"),
tRNS = @bitCast("tRNS".*), tRNS = fromName("tRNS"),
gAMA = @bitCast("gAMA".*), gAMA = fromName("gAMA"),
cHRM = @bitCast("cHRM".*), cHRM = fromName("cHRM"),
sRGB = @bitCast("sRGB".*), sRGB = fromName("sRGB"),
iCCP = @bitCast("iCCP".*), iCCP = fromName("iCCP"),
tEXt = @bitCast("tEXt".*), tEXt = fromName("tEXt"),
zTXt = @bitCast("zTXt".*), zTXt = fromName("zTXt"),
iTXt = @bitCast("iTXt".*), iTXt = fromName("iTXt"),
bKGD = @bitCast("bKGD".*), bKGD = fromName("bKGD"),
pHYs = @bitCast("pHYs".*), pHYs = fromName("pHYs"),
sBIT = @bitCast("sBIT".*), sBIT = fromName("sBIT"),
sPLT = @bitCast("sPLT".*), sPLT = fromName("sPLT"),
hIST = @bitCast("hIST".*), hIST = fromName("hIST"),
tIME = @bitCast("tIME".*), tIME = fromName("tIME"),
_, _,
fn fromName(name: *const [4]u8) u32 {
return @bitCast(name.*);
}
pub fn fromBytes(bytes: *const [4]u8) ChunkType {
return @enumFromInt(fromName(bytes));
}
pub fn toBytes(self: ChunkType) [4]u8 {
return @bitCast(@intFromEnum(self));
}
pub fn ancillary(self: ChunkType) bool { pub fn ancillary(self: ChunkType) bool {
return @as([4]u8, @bitCast(self))[0] & 0b0010_0000 != 0; return self.toBytes()[0] & 0b0010_0000 != 0;
} }
pub fn private(self: ChunkType) bool { pub fn private(self: ChunkType) bool {
return @as([4]u8, @bitCast(self))[1] & 0b0010_0000 != 0; return self.toBytes()[1] & 0b0010_0000 != 0;
} }
pub fn safeToCopy(self: ChunkType) bool { pub fn safeToCopy(self: ChunkType) bool {
return @as([4]u8, @bitCast(self))[3] & 0b0010_0000 != 0; return self.toBytes()[3] & 0b0010_0000 != 0;
}
test "refAllDecls" {
std.testing.refAllDecls(@This());
} }
}; };
@@ -460,6 +520,10 @@ pub const StandardKeyword = enum {
pub fn isStandardKeyword(keyword: []const u8) ?StandardKeyword { pub fn isStandardKeyword(keyword: []const u8) ?StandardKeyword {
return map.get(keyword); return map.get(keyword);
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
/// The caller asserts that the buffer is at least `format.magic_length` bytes /// The caller asserts that the buffer is at least `format.magic_length` bytes
@@ -477,7 +541,7 @@ pub fn info(buffer: []const u8) ?Header {
return null; return null;
} }
const chunk, _ = decodeChunk(buffer[format.magic_length..]) catch return null orelse return null; const chunk, _ = (decodeChunk(buffer[format.magic_length..]) catch return null) orelse return null;
if (chunk.chunk_type != .IHDR) { if (chunk.chunk_type != .IHDR) {
return null; return null;
} }
@@ -495,7 +559,7 @@ pub fn decodeChunk(buffer: []const u8) !?struct { Chunk, []const u8 } {
} }
const length = std.mem.readInt(u32, rest[0..4], .big); const length = std.mem.readInt(u32, rest[0..4], .big);
const chunk_type: ChunkType = @bitCast(rest[4..8].*); const chunk_type: ChunkType = .fromBytes(rest[4..8]);
rest = rest[8..]; rest = rest[8..];
if (length > 0x7FFF_FFFF) { if (length > 0x7FFF_FFFF) {
@@ -575,3 +639,7 @@ pub fn encodeChunks(chunks: []const Chunk, writer: *std.Io.Writer) !void {
try writer.writeInt(u32, crc.final(), .big); try writer.writeInt(u32, crc.final(), .big);
} }
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}

View File

@@ -22,6 +22,10 @@ const Header = union(enum) {
pub fn initStatic(static: HeaderStatic) Header { pub fn initStatic(static: HeaderStatic) Header {
return .{ .static = static }; return .{ .static = static };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
const HeaderStreaming = void; const HeaderStreaming = void;
@@ -36,6 +40,10 @@ const HeaderStatic = struct {
const sample_rate: f64 = @floatFromInt(self.sample_rate); const sample_rate: f64 = @floatFromInt(self.sample_rate);
return @floatCast(samples / sample_rate); return @floatCast(samples / sample_rate);
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };
/// The caller asserts that the buffer is at least `format.magic_length` bytes /// The caller asserts that the buffer is at least `format.magic_length` bytes
@@ -73,3 +81,7 @@ pub fn info(buffer: []const u8) ?Header {
}); });
} }
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}

View File

@@ -68,3 +68,7 @@ pub fn info(buffer: []const u8) ?Header {
.color_space = @enumFromInt(color_space), .color_space = @enumFromInt(color_space),
}; };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}

View File

@@ -1,3 +1,5 @@
const std = @import("std");
pub const audio = @import("audio.zig"); pub const audio = @import("audio.zig");
pub const Format = @import("Format.zig"); pub const Format = @import("Format.zig");
pub const image = @import("image.zig"); pub const image = @import("image.zig");
@@ -7,3 +9,7 @@ pub const png = @import("png.zig");
pub const qoa = @import("qoa.zig"); pub const qoa = @import("qoa.zig");
pub const qoi = @import("qoi.zig"); pub const qoi = @import("qoi.zig");
pub const stbi = @import("stbi.zig"); pub const stbi = @import("stbi.zig");
test "refAllDecls" {
std.testing.refAllDecls(@This());
}

View File

@@ -5,17 +5,20 @@ const image = @import("image.zig");
const vm = @import("vecmath"); const vm = @import("vecmath");
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
io: std.Io,
mutex: std.Io.Mutex = .init,
allocations: std.AutoHashMapUnmanaged(*anyopaque, usize) = .empty, allocations: std.AutoHashMapUnmanaged(*anyopaque, usize) = .empty,
mutex: std.Thread.Mutex = .{},
allocated_bytes: usize = 0, allocated_bytes: usize = 0,
const alignment: std.mem.Alignment = .@"16"; const alignment: std.mem.Alignment = .@"16";
const VoidPtr = ?*align(alignment.toByteUnits()) anyopaque; const VoidPtr = ?*align(alignment.toByteUnits()) anyopaque;
const log = std.log.scoped(.stbi); const log = std.log.scoped(.stbi);
pub fn init(allocator: std.mem.Allocator) Self { pub fn init(allocator: std.mem.Allocator, io: std.Io) Self {
return .{ return .{
.allocator = allocator, .allocator = allocator,
.io = io,
}; };
} }
@@ -76,7 +79,7 @@ pub fn loadStaticBuf(self: *Self, comptime W: u32, comptime H: u32, buf: []const
return .{ .data = @as(*const [W * H]vm.Color, @ptrCast(@alignCast(res))).* }; return .{ .data = @as(*const [W * H]vm.Color, @ptrCast(@alignCast(res))).* };
} }
pub fn loadStaticIo(self: *Self, comptime W: u32, comptime H: u32, reader: *std.io.Reader) !image.Static(W, H) { pub fn loadStaticIo(self: *Self, comptime W: u32, comptime H: u32, reader: *std.Io.Reader) !image.Static(W, H) {
current_self = self; current_self = self;
defer current_self = undefined; defer current_self = undefined;
@@ -109,7 +112,7 @@ pub fn loadDynamicBuf(self: *Self, buf: []const u8) !image.Dynamic {
} }
/// On success, must free memory by calling `freeDynamic` method. /// On success, must free memory by calling `freeDynamic` method.
pub fn loadDynamicIo(self: *Self, reader: *std.io.Reader) !image.Dynamic { pub fn loadDynamicIo(self: *Self, reader: *std.Io.Reader) !image.Dynamic {
current_self = self; current_self = self;
defer current_self = undefined; defer current_self = undefined;
@@ -155,7 +158,7 @@ pub fn loadHdrBuf(self: *Self, buf: []const u8) !image.Hdr {
} }
/// On success, must free memory by calling `freeHdr` method. /// On success, must free memory by calling `freeHdr` method.
pub fn loadHdrIo(self: *Self, reader: *std.io.Reader) !image.Hdr { pub fn loadHdrIo(self: *Self, reader: *std.Io.Reader) !image.Hdr {
current_self = self; current_self = self;
defer current_self = undefined; defer current_self = undefined;
@@ -243,8 +246,8 @@ threadlocal var current_self: *Self = undefined;
export fn castle_media_stbi_malloc(size: usize) callconv(.c) VoidPtr { export fn castle_media_stbi_malloc(size: usize) callconv(.c) VoidPtr {
const self = current_self; const self = current_self;
self.mutex.lock(); self.mutex.lock(self.io) catch return null;
defer self.mutex.unlock(); defer self.mutex.unlock(self.io);
self.allocations.ensureUnusedCapacity(self.allocator, 1) catch return null; self.allocations.ensureUnusedCapacity(self.allocator, 1) catch return null;
const memory = self.allocator.alignedAlloc(u8, alignment, size) catch return null; const memory = self.allocator.alignedAlloc(u8, alignment, size) catch return null;
@@ -259,8 +262,8 @@ export fn castle_media_stbi_malloc(size: usize) callconv(.c) VoidPtr {
export fn castle_media_stbi_realloc(maybe_ptr: VoidPtr, size: usize) callconv(.c) VoidPtr { export fn castle_media_stbi_realloc(maybe_ptr: VoidPtr, size: usize) callconv(.c) VoidPtr {
const self = current_self; const self = current_self;
self.mutex.lock(); self.mutex.lock(self.io) catch return null;
defer self.mutex.unlock(); defer self.mutex.unlock(self.io);
// NOTE If we were pedantic, we would consider the fact that we might not // NOTE If we were pedantic, we would consider the fact that we might not
// need unused capacity if the memory doesn't get relocated. // need unused capacity if the memory doesn't get relocated.
@@ -291,8 +294,8 @@ export fn castle_media_stbi_free(maybe_ptr: VoidPtr) callconv(.c) void {
const self = current_self; const self = current_self;
if (maybe_ptr) |ptr| { if (maybe_ptr) |ptr| {
self.mutex.lock(); self.mutex.lockUncancelable(self.io);
defer self.mutex.unlock(); defer self.mutex.unlock(self.io);
const size = self.allocations.fetchRemove(ptr).?.value; const size = self.allocations.fetchRemove(ptr).?.value;
self.allocated_bytes -= size; self.allocated_bytes -= size;
@@ -301,3 +304,7 @@ export fn castle_media_stbi_free(maybe_ptr: VoidPtr) callconv(.c) void {
self.allocator.free(memory); self.allocator.free(memory);
} }
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}

View File

@@ -11,11 +11,19 @@ pub fn build(b: *std.Build) void {
const sqlite_mod = sqlite_dep.module("sqlite"); const sqlite_mod = sqlite_dep.module("sqlite");
const web_dep = b.dependency("web", .{
.target = target,
.optimize = optimize,
});
const web_mod = web_dep.module("web");
const myid_mod = b.addModule("myid", .{ const myid_mod = b.addModule("myid", .{
.root_source_file = b.path("src/root.zig"), .root_source_file = b.path("src/root.zig"),
.target = target, .target = target,
.imports = &.{ .imports = &.{
.{ .name = "sqlite", .module = sqlite_mod }, .{ .name = "sqlite", .module = sqlite_mod },
.{ .name = "web", .module = web_mod },
}, },
}); });

View File

@@ -13,5 +13,8 @@
.url = "git+https://github.com/vrischmann/zig-sqlite#6d90ee900d186a7fbb6066f28ee13beeaf8be345", .url = "git+https://github.com/vrischmann/zig-sqlite#6d90ee900d186a7fbb6066f28ee13beeaf8be345",
.hash = "sqlite-3.48.0-F2R_a5yODgDFvwwsytm7ZONcSqYBo3qv1PmXOtw3tqLA", .hash = "sqlite-3.48.0-F2R_a5yODgDFvwwsytm7ZONcSqYBo3qv1PmXOtw3tqLA",
}, },
.web = .{
.path = "../web",
},
}, },
} }

259
packages/myid/src/data.zig Normal file
View File

@@ -0,0 +1,259 @@
const std = @import("std");
const sqlite = @import("sqlite");
const web = @import("web");
const id = @import("id.zig");
pub const callback_regex = "^https?://([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,}/";
pub const App = struct {
aid: id.App,
name: []const u8,
callbacks: []const []const u8,
};
pub const AppWithSecret = struct {
aid: id.App,
name: []const u8,
callbacks: []const []const u8,
secret: []const u8,
};
pub const CreateAppData = struct {
name: []const u8,
callback: ?[]const []const u8,
};
pub const CreateAppResult = struct {
aid: id.App,
plainSecret: []const u8,
};
pub const User = struct {
uid: id.User,
name: []const u8,
email: []const u8,
admin: bool,
};
pub const UserWithPassword = struct {
uid: id.User,
name: []const u8,
email: []const u8,
admin: bool,
passworD: []const u8,
};
pub const InviteUserData = struct {
name: []const u8,
email: []const u8,
admin: bool,
};
pub const CreateUserData = struct {
name: []const u8,
email: []const u8,
plainPassword: []const u8,
admin: bool,
};
pub const All = struct {
users: []const User,
apps: []const App,
};
pub const Database = struct {
db: sqlite.Db,
allocator: std.mem.Allocator,
pub fn init(path: [:0]const u8, allocator: std.mem.Allocator) !Database {
var db = try sqlite.Db.init(.{
.mode = .{ .File = path },
.open_flags = .{
.create = true,
.write = true,
},
.threading_mode = .MultiThread,
});
errdefer db.deinit();
_ = try db.one(void, "PRAGMA journal_mode = WAL", .{}, .{});
_ = try db.one(void, "PRAGMA foreign_keys = ON", .{}, .{});
return .{
.db = db,
.allocator = allocator,
};
}
pub fn deinit(self: *Database) void {
self.db.deinit();
self.* = undefined;
}
// --- MIGRATION -----------------------------------------------------------
fn getUserVersion(self: *Database) !i32 {
const version = try self.db.one(i32, "PRAGMA user_version", .{}, .{});
return version.?;
}
fn setUserVersion(self: *Database, version: i32) !void {
var buf: [100]u8 = undefined;
const query = std.fmt.bufPrint(&buf, "PRAGMA user_version = {d}", .{version}) catch unreachable;
_ = try self.db.oneDynamic(void, query, .{}, .{});
}
pub fn migrate(self: *Database) !void {
var user_version = try self.getUserVersion();
if (user_version == 0) {
_ = try self.db.exec(
\\CREATE TABLE users (
\\ uid BLOB NOT NULL,
\\ name TEXT NOT NULL,
\\ email TEXT NOT NULL UNIQUE,
\\ password TEXT NOT NULL,
\\ PRIMARY KEY (uid)
\\)
, .{}, .{});
_ = try self.db.exec(
\\CREATE TABLE apps (
\\ aid BLOB NOT NULL,
\\ name TEXT NOT NULL,
\\ secret TEXT NOT NULL,
\\ PRIMARY KEY (aid)
\\)
, .{}, .{});
_ = try self.db.exec(
\\CREATE TABLE app_callbacks (
\\ aid BLOB NOT NULL,
\\ callback TEXT NOT NULL,
\\ PRIMARY KEY (aid, callback),
\\ FOREIGN KEY (aid) REFERENCES apps (aid)
\\ ON UPDATE CASCADE
\\ ON DELETE CASCADE
\\)
, .{}, .{});
user_version += 1;
try self.setUserVersion(user_version);
}
}
// --- USERS ---------------------------------------------------------------
pub fn createUser(self: *Database, name: []const u8, email: []const u8, plain_password: []const u8) !id.User {
const uid = id.User.init(web.UUID.v7());
var password_buf: [1000]u8 = undefined;
const password = try std.crypto.pwhash.argon2.strHash(plain_password, .{
.allocator = self.allocator,
.mode = .argon2id,
.params = .owasp_2id,
}, &password_buf);
try self.db.exec("INSERT INTO users (uid, name, email, password) VALUES (?, ?, ?, ?)", .{}, .{
.uid = uid.bytes,
.name = name,
.email = email,
.password = password,
});
return uid;
}
// --- APPS ----------------------------------------------------------------
pub fn createApp(self: *Database, name: []const u8, callbacks: []const []const u8) !CreateAppResult {
_ = try self.db.exec("BEGIN", .{}, .{});
errdefer self.db.exec("ROLLBACK", .{}, .{}) catch unreachable;
const secret_bytes = blk: {
var bytes: [24]u8 = undefined;
std.crypto.random.bytes(&bytes);
break :blk bytes;
};
var plain_secret_buf: [32]u8 = undefined;
const plain_secret = std.base64.url_safe_no_pad.Encoder.encode(&plain_secret_buf, &secret_bytes);
std.debug.assert(plain_secret_buf.len == plain_secret.len);
const aid = id.App.init(web.UUID.v7());
var secret_buf: [1000]u8 = undefined;
const secret = try std.crypto.pwhash.argon2.strHash(plain_secret, .{
.allocator = self.allocator,
.mode = .argon2id,
.params = .owasp_2id,
}, &secret_buf);
try self.db.exec("INSERT INTO apps (aid, name, secret) VALUES (?, ?, ?)", .{}, .{
.aid = aid.bytes,
.name = name,
.secret = secret,
});
var insert_callback = try self.db.prepare("INSERT INTO app_callbacks (aid, callback) VALUES (?, ?)");
defer insert_callback.deinit();
for (callbacks) |callback| {
try insert_callback.exec(.{}, .{ .aid = aid.bytes, .callback = callback });
insert_callback.reset();
}
try self.db.exec("COMMIT", .{}, .{});
return .{
.aid = aid,
.plain_secret = plain_secret_buf,
};
}
};
test "user version" {
var db = try Database.init(":memory:", std.testing.allocator);
defer db.deinit();
try std.testing.expectEqual(0, try db.getUserVersion());
try db.setUserVersion(1);
try std.testing.expectEqual(1, try db.getUserVersion());
}
test "migrate" {
var db = try Database.init(":memory:", std.testing.allocator);
defer db.deinit();
try db.migrate();
}
test "create user" {
var db = try Database.init(":memory:", std.testing.allocator);
defer db.deinit();
try db.migrate();
const uid = try db.createUser("admin", "admin@test.invalid", "admin");
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
const maybe_user = try db.db.oneAlloc(struct {
name: []const u8,
email: []const u8,
password: []const u8,
}, arena.allocator(), "SELECT name, email, password FROM users WHERE uid = ?", .{}, .{
.uid = uid.bytes,
});
defer arena.deinit();
try std.testing.expect(maybe_user != null);
if (maybe_user) |user| {
try std.testing.expectEqualSlices(u8, "admin", user.name);
}
}
test "create app" {
var db = try Database.init(":memory:", std.testing.allocator);
defer db.deinit();
try db.migrate();
_ = try db.createApp("app", &.{
"http://localhost:3000/callback",
"https://example.com/callback",
});
}

View File

@@ -1,2 +0,0 @@
pub const database_path = "db.sqlite3";
pub const socket_path = "myid.sock";

View File

@@ -1,206 +0,0 @@
const std = @import("std");
const main = @import("main.zig");
const Parser = @import("http/Parser.zig");
threadlocal var read_buffer: [2 * 1024 * 1024]u8 = undefined;
threadlocal var write_buffer: [2 * 1024 * 1024]u8 = undefined;
const log = std.log.scoped(.http);
const status = struct {
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";
};
const ResponseEmptyOptions = struct {
status_text: []const u8 = status.ok,
};
const ResponseOptions = struct {
status_text: []const u8 = status.ok,
media_type: []const u8 = "text/plain; charset=utf-8",
response_body: []const u8,
};
fn makeResponseEmpty(options: ResponseEmptyOptions) ![]const u8 {
var fbs = std.io.fixedBufferStream(&write_buffer);
const writer = fbs.writer();
try writer.print("{s}", .{options.status_text});
try writer.print("\r\n", .{});
return fbs.getWritten();
}
fn makeResponseClose(options: ResponseEmptyOptions) ![]const u8 {
var fbs = std.io.fixedBufferStream(&write_buffer);
const writer = fbs.writer();
try writer.print("{s}", .{options.status_text});
try writer.print("Connection: close\r\n", .{});
try writer.print("\r\n", .{});
return fbs.getWritten();
}
fn makeResponse(options: ResponseOptions) ![]const u8 {
var fbs = std.io.fixedBufferStream(&write_buffer);
const writer = fbs.writer();
try writer.print("{s}", .{options.status_text});
try writer.print("Content-Type: {s}\r\n", .{options.media_type});
try writer.print("Content-Length: {d}\r\n", .{options.response_body.len});
try writer.print("\r\n", .{});
try writer.print("{s}", .{options.response_body});
return fbs.getWritten();
}
pub fn process(conn: std.net.Server.Connection) !void {
defer conn.stream.close();
var leftover_bytes: usize = 0;
while (true) {
const start = try std.time.Instant.now();
var route: Parser.Route = undefined;
var parser = Parser.init(.{
.self = &route,
.route = routeCallback,
});
var total_bytes_read: usize = 0;
while (true) {
var bytes_read: usize = undefined;
var chars: []const u8 = undefined;
if (leftover_bytes > 0) {
bytes_read = leftover_bytes;
chars = read_buffer[0..leftover_bytes];
leftover_bytes = 0;
} else {
bytes_read = try conn.stream.read(read_buffer[total_bytes_read..]);
chars = read_buffer[total_bytes_read .. total_bytes_read + bytes_read];
}
total_bytes_read += bytes_read;
const res = parser.consume(chars) catch |err| switch (err) {
error.MethodNotSupported => {
const response = try makeResponseClose(.{ .status_text = status.method_not_allowed });
try conn.stream.writeAll(response);
return;
},
error.HttpVersionNotSupported => {
const response = try makeResponseClose(.{ .status_text = status.http_version_not_supported });
try conn.stream.writeAll(response);
return;
},
error.MissingLineFeed => {
const response = try makeResponseClose(.{ .status_text = status.bad_request });
try conn.stream.writeAll(response);
return;
},
error.InvalidContentLength => {
const response = try makeResponseClose(.{ .status_text = status.bad_request });
try conn.stream.writeAll(response);
return;
},
};
if (total_bytes_read >= read_buffer.len and !res.done) {
if (parser.state == .body) {
const response = try makeResponseClose(.{ .status_text = status.content_too_large });
try conn.stream.writeAll(response);
return;
} else {
const response = try makeResponseClose(.{ .status_text = status.request_header_fields_too_large });
try conn.stream.writeAll(response);
return;
}
}
if (res.done) {
leftover_bytes = bytes_read - res.consumed;
break;
}
}
const response = try makeResponse(.{ .response_body = "PONG\n" });
try conn.stream.writeAll(response);
if (leftover_bytes > 0) {
@memmove(&read_buffer, read_buffer[total_bytes_read - leftover_bytes .. total_bytes_read]);
}
const end = try std.time.Instant.now();
const time_ns = end.since(start);
const time_us = @divFloor(time_ns, std.time.ns_per_us);
log.info("{s} {s} ({} μs)", .{ @tagName(route.method), route.pathname, time_us });
}
}
fn routeCallback(self: ?*anyopaque, route: Parser.Route) void {
@as(*Parser.Route, @alignCast(@ptrCast(self))).* = route;
}

View File

@@ -1,356 +0,0 @@
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 init: Callbacks = .{};
};
const Error = error{
MethodNotSupported,
HttpVersionNotSupported,
MissingLineFeed,
InvalidContentLength,
};
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;
}

6
packages/myid/src/id.zig Normal file
View File

@@ -0,0 +1,6 @@
const std = @import("std");
const web = @import("web");
pub const App = web.Id(.app);
pub const Session = web.Id(.session);
pub const User = web.Id(.user);

View File

@@ -1,235 +1,3 @@
const std = @import("std"); const std = @import("std");
const sqlite = @import("sqlite"); const sqlite = @import("sqlite");
const uuid = @import("uuid.zig"); const web = @import("web");
fn Id(comptime _tag: @Type(.enum_literal)) type {
return struct {
pub const tag = _tag;
bytes: [16]u8,
pub fn new() @This() {
return .{ .bytes = uuid.uuid_v7() };
}
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;
try std.base64.url_safe_no_pad.Decoder.decode(&bytes, encoded);
return .{ .bytes = bytes };
}
pub fn encode(self: @This()) [22]u8 {
var text: [22]u8 = undefined;
std.base64.url_safe_no_pad.Encoder.encode(&text, self.bytes);
return text;
}
};
}
pub const AppId = Id(.app_id);
pub const UserId = Id(.user_id);
pub const CreateAppResult = struct {
aid: AppId,
plain_secret: [32]u8,
};
pub const Database = struct {
db: sqlite.Db,
allocator: std.mem.Allocator,
pub fn init(path: [:0]const u8, allocator: std.mem.Allocator) !Database {
var db = try sqlite.Db.init(.{
.mode = .{ .File = path },
.open_flags = .{
.create = true,
.write = true,
},
.threading_mode = .MultiThread,
});
errdefer db.deinit();
_ = try db.one(void, "PRAGMA journal_mode = WAL", .{}, .{});
_ = try db.one(void, "PRAGMA foreign_keys = ON", .{}, .{});
return .{
.db = db,
.allocator = allocator,
};
}
pub fn deinit(self: *Database) void {
self.db.deinit();
self.* = undefined;
}
// --- MIGRATION -----------------------------------------------------------
fn getUserVersion(self: *Database) !i32 {
const version = try self.db.one(i32, "PRAGMA user_version", .{}, .{});
return version.?;
}
fn setUserVersion(self: *Database, version: i32) !void {
var buf: [100]u8 = undefined;
const query = std.fmt.bufPrint(&buf, "PRAGMA user_version = {d}", .{version}) catch unreachable;
_ = try self.db.oneDynamic(void, query, .{}, .{});
}
pub fn migrate(self: *Database) !void {
var user_version = try self.getUserVersion();
if (user_version == 0) {
_ = try self.db.exec(
\\CREATE TABLE users (
\\ uid BLOB NOT NULL,
\\ name TEXT NOT NULL,
\\ email TEXT NOT NULL UNIQUE,
\\ password TEXT NOT NULL,
\\ PRIMARY KEY (uid)
\\)
, .{}, .{});
_ = try self.db.exec(
\\CREATE TABLE apps (
\\ aid BLOB NOT NULL,
\\ name TEXT NOT NULL,
\\ secret TEXT NOT NULL,
\\ PRIMARY KEY (aid)
\\)
, .{}, .{});
_ = try self.db.exec(
\\CREATE TABLE app_callbacks (
\\ aid BLOB NOT NULL,
\\ callback TEXT NOT NULL,
\\ PRIMARY KEY (aid, callback),
\\ FOREIGN KEY (aid) REFERENCES apps (aid)
\\ ON UPDATE CASCADE
\\ ON DELETE CASCADE
\\)
, .{}, .{});
user_version += 1;
try self.setUserVersion(user_version);
}
}
// --- USERS ---------------------------------------------------------------
pub fn createUser(self: *Database, name: []const u8, email: []const u8, plain_password: []const u8) !UserId {
const uid = UserId.new();
var password_buf: [1000]u8 = undefined;
const password = try std.crypto.pwhash.argon2.strHash(plain_password, .{
.allocator = self.allocator,
.mode = .argon2id,
.params = .owasp_2id,
}, &password_buf);
try self.db.exec("INSERT INTO users (uid, name, email, password) VALUES (?, ?, ?, ?)", .{}, .{
.uid = uid.bytes,
.name = name,
.email = email,
.password = password,
});
return uid;
}
// --- APPS ----------------------------------------------------------------
pub fn createApp(self: *Database, name: []const u8, callbacks: []const []const u8) !CreateAppResult {
_ = try self.db.exec("BEGIN", .{}, .{});
errdefer self.db.exec("ROLLBACK", .{}, .{}) catch unreachable;
const secret_bytes = blk: {
var bytes: [24]u8 = undefined;
std.crypto.random.bytes(&bytes);
break :blk bytes;
};
var plain_secret_buf: [32]u8 = undefined;
const plain_secret = std.base64.url_safe_no_pad.Encoder.encode(&plain_secret_buf, &secret_bytes);
std.debug.assert(plain_secret_buf.len == plain_secret.len);
const aid = AppId.new();
var secret_buf: [1000]u8 = undefined;
const secret = try std.crypto.pwhash.argon2.strHash(plain_secret, .{
.allocator = self.allocator,
.mode = .argon2id,
.params = .owasp_2id,
}, &secret_buf);
try self.db.exec("INSERT INTO apps (aid, name, secret) VALUES (?, ?, ?)", .{}, .{
.aid = aid.bytes,
.name = name,
.secret = secret,
});
var insert_callback = try self.db.prepare("INSERT INTO app_callbacks (aid, callback) VALUES (?, ?)");
defer insert_callback.deinit();
for (callbacks) |callback| {
try insert_callback.exec(.{}, .{ .aid = aid.bytes, .callback = callback });
insert_callback.reset();
}
try self.db.exec("COMMIT", .{}, .{});
return .{
.aid = aid,
.plain_secret = plain_secret_buf,
};
}
};
test "user version" {
var db = try Database.init(":memory:", std.testing.allocator);
defer db.deinit();
try std.testing.expectEqual(0, try db.getUserVersion());
try db.setUserVersion(1);
try std.testing.expectEqual(1, try db.getUserVersion());
}
test "migrate" {
var db = try Database.init(":memory:", std.testing.allocator);
defer db.deinit();
try db.migrate();
}
test "create user" {
var db = try Database.init(":memory:", std.testing.allocator);
defer db.deinit();
try db.migrate();
const uid = try db.createUser("admin", "admin@test.invalid", "admin");
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
const maybe_user = try db.db.oneAlloc(struct {
name: []const u8,
email: []const u8,
password: []const u8,
}, arena.allocator(), "SELECT name, email, password FROM users WHERE uid = ?", .{}, .{
.uid = uid.bytes,
});
defer arena.deinit();
try std.testing.expect(maybe_user != null);
if (maybe_user) |user| {
try std.testing.expectEqualSlices(u8, "admin", user.name);
}
}
test "create app" {
var db = try Database.init(":memory:", std.testing.allocator);
defer db.deinit();
try db.migrate();
_ = try db.createApp("app", &.{
"http://localhost:3000/callback",
"https://example.com/callback",
});
}

View File

@@ -1,41 +0,0 @@
const std = @import("std");
var lock: std.Thread.Mutex = .{};
var last_timestamp: std.atomic.Value(u64) = .{ .raw = 0 };
var counter: std.atomic.Value(u32) = .{ .raw = 0 };
fn getCount(timestamp: u64) u32 {
lock.lock();
defer lock.unlock();
if (last_timestamp.swap(timestamp, .monotonic) != timestamp) {
counter.store(0, .monotonic);
}
return counter.fetchAdd(1, .monotonic) % 4096;
}
pub fn uuid_v7() [16]u8 {
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 res: [16]u8 = undefined;
res[0] = @truncate(timestamp >> 40);
res[1] = @truncate(timestamp >> 32);
res[2] = @truncate(timestamp >> 24);
res[3] = @truncate(timestamp >> 16);
res[4] = @truncate(timestamp >> 8);
res[5] = @truncate(timestamp);
res[6] = (@as(u8, 7) << 4) | @as(u8, @truncate((count >> 8) & 0x0F));
res[7] = @truncate(count);
res[8] = 0x80 | (random[0] & 0x3F);
@memcpy(res[9..16], random[1..8]);
return res;
}

View File

@@ -1,7 +1,19 @@
const std = @import("std"); const std = @import("std");
pub fn build(b: *std.Build) void { pub fn build(b: *std.Build) void {
_ = b.addModule("vecmath", .{ const target = b.standardTargetOptions(.{});
const mod = b.addModule("vecmath", .{
.root_source_file = b.path("src/root.zig"), .root_source_file = b.path("src/root.zig"),
.target = target,
}); });
const mod_tests = b.addTest(.{
.root_module = mod,
});
const run_mod_tests = b.addRunArtifact(mod_tests);
const test_step = b.step("test", "Run tests");
test_step.dependOn(&run_mod_tests.step);
} }

View File

@@ -1,7 +1,7 @@
.{ .{
.name = .vecmath, .name = .vecmath,
.version = "0.0.0", .version = "0.0.0",
.minimum_zig_version = "0.15.2", .minimum_zig_version = "0.16.0",
.paths = .{ .paths = .{
"src", "src",
"build.zig", "build.zig",

View File

@@ -93,7 +93,11 @@ pub const Color = extern struct {
return @bitCast(self); return @bitCast(self);
} }
pub fn format(self: Color, w: *std.io.Writer) !void { pub fn format(self: Color, w: *std.Io.Writer) !void {
try w.print("#{X:0>2}{X:0>2}{X:0>2}{X:0>2}", .{ self.r, self.g, self.b, self.a }); try w.print("#{X:0>2}{X:0>2}{X:0>2}{X:0>2}", .{ self.r, self.g, self.b, self.a });
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -24,7 +24,11 @@ pub const ColorHdr = extern struct {
return @bitCast(self); return @bitCast(self);
} }
pub fn format(self: ColorHdr, w: *std.io.Writer) !void { pub fn format(self: ColorHdr, w: *std.Io.Writer) !void {
try w.print("ColorHdr[{d}, {d}, {d}, {d}]", .{ self.r, self.g, self.b, self.a }); try w.print("ColorHdr[{d}, {d}, {d}, {d}]", .{ self.r, self.g, self.b, self.a });
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -252,4 +252,8 @@ pub const Matrix3x2 = extern struct {
.ty = -inv_det * (self.tx * iy + self.ty * jy), .ty = -inv_det * (self.tx * iy + self.ty * jy),
}; };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -406,4 +406,8 @@ pub const Matrix3x2x8 = struct {
.ty = -inv_det * (self.tx * iy + self.ty * jy), .ty = -inv_det * (self.tx * iy + self.ty * jy),
}; };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -507,4 +507,8 @@ pub const Matrix4x4 = extern struct {
// zig fmt: on // zig fmt: on
}; };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -763,4 +763,8 @@ pub const Matrix4x4x8 = extern struct {
// zig fmt: on // zig fmt: on
}; };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -110,5 +110,5 @@ pub inline fn unlerpInt64(a: i64, b: i64, x: i64) f32 {
} }
test "refAllDecls" { test "refAllDecls" {
std.testing.refAllDeclsRecursive(@This()); std.testing.refAllDecls(@This());
} }

View File

@@ -123,7 +123,11 @@ pub const Complex = extern struct {
}; };
} }
pub fn format(self: Complex, w: *std.io.Writer) !void { pub fn format(self: Complex, w: *std.Io.Writer) !void {
try w.print("Complex[{d:.3}, {d:.3}]", .{ self.re, self.im }); try w.print("Complex[{d:.3}, {d:.3}]", .{ self.re, self.im });
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -193,4 +193,8 @@ pub const Complex_x8 = struct {
.im = @mulAdd(vm.f32x8, vm.ps(t), b.im, @mulAdd(vm.f32x8, -vm.ps(t), a.im, a.im)), .im = @mulAdd(vm.f32x8, vm.ps(t), b.im, @mulAdd(vm.f32x8, -vm.ps(t), a.im, a.im)),
}; };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -179,7 +179,11 @@ pub const Quaternion = extern struct {
}; };
} }
pub fn format(self: Quaternion, w: *std.io.Writer) !void { pub fn format(self: Quaternion, w: *std.Io.Writer) !void {
try w.print("Quaternion[{d:.3}, {d:.3}, {d:.3}, {d:.3}]", .{ self.x, self.y, self.z, self.w }); try w.print("Quaternion[{d:.3}, {d:.3}, {d:.3}, {d:.3}]", .{ self.x, self.y, self.z, self.w });
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -278,4 +278,8 @@ pub const Quaternion_x8 = struct {
.w = @mulAdd(vm.f32x8, vm.ps(t), b.w, @mulAdd(vm.f32x8, -vm.ps(t), a.w, a.w)), .w = @mulAdd(vm.f32x8, vm.ps(t), b.w, @mulAdd(vm.f32x8, -vm.ps(t), a.w, a.w)),
}; };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -31,3 +31,7 @@ pub inline fn epi64(value: i64) i64x4 {
pub inline fn epu64(value: u64) u64x4 { pub inline fn epu64(value: u64) u64x4 {
return @splat(value); return @splat(value);
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}

View File

@@ -235,3 +235,7 @@ test cossin_x8 {
cossin_x8(.{ -1, -0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75 }), cossin_x8(.{ -1, -0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75 }),
); );
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}

View File

@@ -167,7 +167,11 @@ pub const Vector2 = extern struct {
}; };
} }
pub fn format(self: Vector2, w: *std.io.Writer) !void { pub fn format(self: Vector2, w: *std.Io.Writer) !void {
try w.print("[{d}, {d}]", .{ self.x, self.y }); try w.print("[{d}, {d}]", .{ self.x, self.y });
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -110,10 +110,14 @@ pub const Vector2Int = extern struct {
return self.x * other.y - self.y * other.x; return self.x * other.y - self.y * other.x;
} }
pub fn format(self: Vector2Int, w: *std.io.Writer) !void { pub fn format(self: Vector2Int, w: *std.Io.Writer) !void {
try w.print("[{X:0>8}, {X:0>8}]", .{ try w.print("[{X:0>8}, {X:0>8}]", .{
@as(u32, @bitCast(self.x)), @as(u32, @bitCast(self.x)),
@as(u32, @bitCast(self.y)), @as(u32, @bitCast(self.y)),
}); });
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -171,4 +171,8 @@ pub const Vector2Int_x8 = struct {
pub inline fn cross(self: Vector2Int_x8, other: Vector2Int_x8) vm.i32x8 { pub inline fn cross(self: Vector2Int_x8, other: Vector2Int_x8) vm.i32x8 {
return self.x * other.y - self.y * other.x; return self.x * other.y - self.y * other.x;
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -231,4 +231,8 @@ pub const Vector2x8 = struct {
.y = self.x * vm.ps(m.iy) + self.y * vm.ps(m.jy), .y = self.x * vm.ps(m.iy) + self.y * vm.ps(m.jy),
}; };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -192,7 +192,11 @@ pub const Vector3 = extern struct {
}; };
} }
pub fn format(self: Vector3, w: *std.io.Writer) !void { pub fn format(self: Vector3, w: *std.Io.Writer) !void {
try w.print("[{d}, {d}, {d}]", .{ self.x, self.y, self.z }); try w.print("[{d}, {d}, {d}]", .{ self.x, self.y, self.z });
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -117,11 +117,15 @@ pub const Vector3Int = extern struct {
}; };
} }
pub fn format(self: Vector3Int, w: *std.io.Writer) !void { pub fn format(self: Vector3Int, w: *std.Io.Writer) !void {
try w.print("[{X:0>8}, {X:0>8}, {X:0>8}]", .{ try w.print("[{X:0>8}, {X:0>8}, {X:0>8}]", .{
@as(u32, @bitCast(self.x)), @as(u32, @bitCast(self.x)),
@as(u32, @bitCast(self.y)), @as(u32, @bitCast(self.y)),
@as(u32, @bitCast(self.z)), @as(u32, @bitCast(self.z)),
}); });
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -180,4 +180,8 @@ pub const Vector3Int_x8 = struct {
.z = self.x * other.y - self.y * other.x, .z = self.x * other.y - self.y * other.x,
}; };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -258,4 +258,8 @@ pub const Vector3x8 = struct {
.z = v.x * vm.ps(self.iz) + v.y * vm.ps(self.jz) + v.z * vm.ps(self.kz), .z = v.x * vm.ps(self.iz) + v.y * vm.ps(self.jz) + v.z * vm.ps(self.kz),
}; };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -147,7 +147,11 @@ pub const Vector4 = extern struct {
}; };
} }
pub fn format(self: Vector4, w: *std.io.Writer) !void { pub fn format(self: Vector4, w: *std.Io.Writer) !void {
try w.print("[{d}, {d}, {d}, {d}]", .{ self.x, self.y, self.z, self.w }); try w.print("[{d}, {d}, {d}, {d}]", .{ self.x, self.y, self.z, self.w });
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -112,7 +112,7 @@ pub const Vector4Int = extern struct {
return self.x * other.x + self.y * other.y + self.z * other.z + self.w * other.w; return self.x * other.x + self.y * other.y + self.z * other.z + self.w * other.w;
} }
pub fn format(self: Vector4Int, w: *std.io.Writer) !void { pub fn format(self: Vector4Int, w: *std.Io.Writer) !void {
try w.print("[{X:0>8}, {X:0>8}, {X:0>8}, {X:0>8}]", .{ try w.print("[{X:0>8}, {X:0>8}, {X:0>8}, {X:0>8}]", .{
@as(u32, @bitCast(self.x)), @as(u32, @bitCast(self.x)),
@as(u32, @bitCast(self.y)), @as(u32, @bitCast(self.y)),
@@ -120,4 +120,8 @@ pub const Vector4Int = extern struct {
@as(u32, @bitCast(self.w)), @as(u32, @bitCast(self.w)),
}); });
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -177,4 +177,8 @@ pub const Vector4Int_x8 = struct {
pub inline fn dot(self: Vector4Int_x8, other: Vector4Int_x8) vm.i32x8 { pub inline fn dot(self: Vector4Int_x8, other: Vector4Int_x8) vm.i32x8 {
return self.x * other.x + self.y * other.y + self.z * other.z + self.w * other.w; return self.x * other.x + self.y * other.y + self.z * other.z + self.w * other.w;
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -217,4 +217,8 @@ pub const Vector4x8 = struct {
.w = self.x * vm.ps(m.iw) + self.y * vm.ps(m.jw) + self.z * vm.ps(m.kw) + self.w * vm.ps(m.tw), .w = self.x * vm.ps(m.iw) + self.y * vm.ps(m.jw) + self.z * vm.ps(m.kw) + self.w * vm.ps(m.tw),
}; };
} }
test "refAllDecls" {
std.testing.refAllDecls(@This());
}
}; };

View File

@@ -4,7 +4,7 @@ pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{}); const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{}); const optimize = b.standardOptimizeOption(.{});
const module = b.addModule("http", .{ const module = b.addModule("web", .{
.root_source_file = b.path("src/root.zig"), .root_source_file = b.path("src/root.zig"),
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,

View File

@@ -4,13 +4,17 @@ const Connection = @This();
const FileDescriptor = @import("FileDescriptor.zig").FileDescriptor; const FileDescriptor = @import("FileDescriptor.zig").FileDescriptor;
const openssl = @import("openssl.zig"); const openssl = @import("openssl.zig");
const linux = std.os.linux; const iovec = std.posix.iovec;
const iovec_const = std.posix.iovec_const;
address: std.net.Address, address: std.net.Address,
fd: FileDescriptor, fd: FileDescriptor,
ssl: ?*openssl.Ssl, ssl: ?*openssl.Ssl,
node: std.DoublyLinkedList.Node = .{}, node: std.DoublyLinkedList.Node = .{},
// TODO Consider proper usage of `send` syscall with `MSG_MORE` flag and setting
// the `TCP_CORK` option.
pub fn reinit( pub fn reinit(
self: *Connection, self: *Connection,
address: std.net.Address, address: std.net.Address,
@@ -40,12 +44,42 @@ pub fn read(self: *const Connection, buf: []u8) !usize {
pub fn readAll(self: *const Connection, buf: []u8) !void { pub fn readAll(self: *const Connection, buf: []u8) !void {
if (self.ssl) |ssl| { if (self.ssl) |ssl| {
try ssl.readall(buf); try ssl.readAll(buf);
} else { } else {
try self.fd.readAll(buf); try self.fd.readAll(buf);
} }
} }
pub fn readv(self: *const Connection, iov: []const iovec) !usize {
if (self.ssl) |ssl| {
var total_bytes_read: usize = 0;
for (iov) |io| {
const bytes_read = try ssl.read(io.base[0..io.len]);
total_bytes_read += bytes_read;
if (bytes_read < io.len) {
return total_bytes_read;
}
}
return total_bytes_read;
} else {
const bytes_read = try self.fd.readv(iov);
return bytes_read;
}
}
/// Might modify `iov` when `readv` syscall reads partially.
pub fn readvAll(self: *const Connection, iov: []iovec) !void {
if (self.ssl) |ssl| {
for (iov) |io| {
try ssl.readAll(io.base[0..io.len]);
}
} else {
try self.fd.readvAll(iov);
}
}
pub fn write(self: *const Connection, buf: []const u8) !usize { pub fn write(self: *const Connection, buf: []const u8) !usize {
if (self.ssl) |ssl| { if (self.ssl) |ssl| {
const bytes_written = try ssl.write(buf); const bytes_written = try ssl.write(buf);
@@ -63,3 +97,51 @@ pub fn writeAll(self: *const Connection, buf: []const u8) !void {
try self.fd.writeAll(buf); try self.fd.writeAll(buf);
} }
} }
pub fn writev(self: *const Connection, iov: []const iovec_const) !usize {
if (self.ssl) |ssl| {
var total_bytes_written: usize = 0;
for (iov) |io| {
const bytes_written = try ssl.write(io.base[0..io.len]);
total_bytes_written += bytes_written;
if (bytes_written < io.len) {
return total_bytes_written;
}
}
return total_bytes_written;
} else {
const bytes_written = try self.fd.writev(iov);
return bytes_written;
}
}
/// Might modify `iov` when `writev` syscall writes partially.
pub fn writevAll(self: *const Connection, iov: []iovec_const) !void {
if (self.ssl) |ssl| {
for (iov) |io| {
try ssl.writeAll(io.base[0..io.len]);
}
} else {
try self.fd.writevAll(iov);
}
}
pub fn sendfile(self: *const Connection, fd: FileDescriptor, offset: usize, len: usize) !usize {
if (self.ssl) |ssl| {
const bytes_written = try ssl.sendfile(fd, offset, len);
return bytes_written;
} else {
const bytes_written = try self.fd.sendfile(fd, offset, len);
return bytes_written;
}
}
pub fn sendfileAll(self: *const Connection, fd: FileDescriptor, offset: usize, len: usize) !void {
if (self.ssl) |ssl| {
try ssl.sendfileAll(fd, offset, len);
} else {
try self.fd.sendfileAll(fd, offset, len);
}
}

View File

@@ -3,6 +3,9 @@ const std = @import("std");
const linux = std.os.linux; const linux = std.os.linux;
const errno = linux.E.init; const errno = linux.E.init;
const iovec = std.posix.iovec;
const iovec_const = std.posix.iovec_const;
pub const FileDescriptor = enum(i32) { pub const FileDescriptor = enum(i32) {
stdin = 0, stdin = 0,
stdout = 1, stdout = 1,
@@ -30,13 +33,16 @@ pub const FileDescriptor = enum(i32) {
} }
pub fn accept(self: FileDescriptor, noalias addr: ?*linux.sockaddr, noalias len: ?*linux.socklen_t) !FileDescriptor { pub fn accept(self: FileDescriptor, noalias addr: ?*linux.sockaddr, noalias len: ?*linux.socklen_t) !FileDescriptor {
while (true) {
const rc = linux.accept(@intFromEnum(self), addr, len); const rc = linux.accept(@intFromEnum(self), addr, len);
return switch (errno(rc)) { switch (errno(rc)) {
.SUCCESS => @enumFromInt(@as(i32, @intCast(rc))), .SUCCESS => return @enumFromInt(@as(i32, @intCast(rc))),
.INPROGRESS, .AGAIN => continue,
.CONNABORTED => return error.ConnectionAborted, .CONNABORTED => return error.ConnectionAborted,
.PERM => return error.BlockedByFirewall, .PERM => return error.BlockedByFirewall,
else => return error.SystemError, else => return error.SystemError,
}; }
}
} }
pub fn bind(self: FileDescriptor, addr: *const linux.sockaddr, len: linux.socklen_t) !void { pub fn bind(self: FileDescriptor, addr: *const linux.sockaddr, len: linux.socklen_t) !void {
@@ -83,10 +89,11 @@ 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,
.AGAIN => return error.Timeout,
else => return error.SystemError, else => return error.SystemError,
} }
} }
@@ -102,6 +109,40 @@ 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,
.AGAIN, .WOULDBLOCK => error.Timeout,
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 +153,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 +171,68 @@ pub const FileDescriptor = enum(i32) {
total_bytes_written += bytes_written; total_bytes_written += bytes_written;
} }
} }
pub fn writev(self: FileDescriptor, iov: []const iovec_const) !usize {
while (true) {
const rc = linux.writev(@intFromEnum(self), iov.ptr, iov.len);
switch (errno(rc)) {
.SUCCESS => return rc,
.INTR => continue,
else => return error.SystemError,
}
}
}
/// Might modify `iov` when `writev` syscall writes partially.
pub fn writevAll(self: FileDescriptor, iov: []iovec_const) !void {
var total_bytes_written: usize = 0;
var i: usize = 0;
while (i < iov.len) {
var bytes_written = try self.writev(iov[i..]);
total_bytes_written += bytes_written;
// skip whole buffers
while (i < iov.len and bytes_written >= iov[i].len) {
bytes_written -= iov[i].len;
i += 1;
}
// skip part of a buffer
if (bytes_written > 0) {
iov[i].base += bytes_written;
iov[i].len -= bytes_written;
}
}
}
/// Calls `fstat` and returns total size in bytes.
pub fn size(self: FileDescriptor) !usize {
var stat = std.mem.zeroes(linux.Stat);
const rc = linux.fstat(@intFromEnum(self), &stat);
return switch (errno(rc)) {
.SUCCESS => @intCast(stat.size),
else => error.SystemError,
};
}
pub fn sendfile(self: FileDescriptor, fd: FileDescriptor, offset: usize, len: usize) !usize {
var offset_mut = offset;
while (true) {
const rc = linux.sendfile(@intFromEnum(self), @intFromEnum(fd), @ptrCast(&offset_mut), len);
switch (errno(rc)) {
.SUCCESS => return rc,
.INTR => continue,
else => return error.SystemError,
}
}
}
pub fn sendfileAll(self: FileDescriptor, fd: FileDescriptor, offset: usize, len: usize) !void {
var total_bytes_sent: usize = 0;
while (total_bytes_sent < len) {
const bytes_sent = try self.sendfile(fd, offset + total_bytes_sent, len - total_bytes_sent);
total_bytes_sent += bytes_sent;
}
}
}; };

View File

@@ -35,7 +35,7 @@ pub fn Id(comptime _tag: @Type(.enum_literal)) type {
} }
pub fn encodeInto(self: @This(), text: *[22]u8) void { pub fn encodeInto(self: @This(), text: *[22]u8) void {
std.base64.url_safe_no_pad.Encoder.encode(text, self.bytes); encoder.encode(text, self.bytes);
} }
}; };
} }

View File

@@ -0,0 +1,66 @@
const std = @import("std");
const Request = @This();
const http = @import("http.zig");
method: http.Method,
pathname: []const u8,
headers: *HeaderHashMap,
body: []const u8,
pub const HeaderHashMap = std.HashMapUnmanaged(
http.FieldName,
HeaderList,
http.FieldName.HashMapContext,
std.hash_map.default_max_load_percentage,
);
pub const HeaderList = struct {
list: std.SinglyLinkedList,
len: usize,
};
pub const HeaderValue = struct {
value: []const u8,
node: std.SinglyLinkedList.Node,
};
/// Gets a header field value of a given `name`. When there is no such header,
/// `null` is returned. When there is more than one header with the same name,
/// the value of the field that came later will be returned.
pub fn getHeader(self: *const Request, name: http.FieldName) ?[]const u8 {
if (self.headers.get(name)) |list| {
if (list.list.first) |head| {
const header_value: *HeaderValue = @fieldParentPtr("node", head);
return header_value.value;
}
}
}
pub fn getHeaderKnown(self: *const Request, known: http.KnownFieldName) ?[]const u8 {
return self.getHeader(.initKnonw(known));
}
pub fn getHeaders(self: *const Request, name: http.FieldName, values: [][]const u8) []const []const u8 {
var i: usize = 0;
var node: ?*std.SinglyLinkedList.Node = if (self.headers.get(name)) |list| list.list else null;
while (i < values.len) {
if (node) |n| {
const header_value: *HeaderValue = @fieldParentPtr("node", n);
values[i] = header_value.value;
i += 1;
node = n.next;
} else {
break;
}
}
return values[0..i];
}
pub fn getHeaderCount(self: *const Request, name: http.FieldName) usize {
const list = self.headers.get(name) orelse return 0;
return list.len;
}

View File

@@ -1,56 +1,16 @@
const std = @import("std"); const std = @import("std");
const RequestHandler = @This(); const RequestHandler = @This();
const Header = @import("http/Header.zig"); const Request = @import("Request.zig");
const Response = @import("Response.zig"); const Response = @import("Response.zig");
const Route = @import("Route.zig");
const Worker = @import("Worker.zig");
ptr: *anyopaque, ptr: *anyopaque,
vtable: *const VTable, vtable: *const VTable,
pub const VTable = struct { pub const VTable = struct {
/// Called multiple times (could be zero) for each header in the request. handle: *const fn (self: *anyopaque, request: *Request, response: *Response) anyerror!void,
header: *const fn (self: *anyopaque, response: *Response, header: Header) anyerror!void,
/// Called exactly once after the whole request is received. When there is
/// no body, then `body.len == 0`.
body: *const fn (self: *anyopaque, response: *Response, body: []const u8) anyerror!void,
/// Called when the request parsing has halted. Possible reasons are:
///
/// 1. One of the calls to this object returned an error.
/// 2. The request was malformed and the HTTP parser returned an error.
/// 3. The whole request was received.
///
/// When no errors occurs (the third case), this method will be call after
/// `body`. This method should only be used to clean up internal resources,
/// if necessary.
finalize: *const fn (self: *anyopaque) void,
}; };
pub fn noHeader(self: *anyopaque, response: *Response, header: Header) anyerror!void { pub inline fn handle(self: RequestHandler, request: *Request, response: *Response) anyerror!void {
_ = self; try self.vtable.handle(self.ptr, request, response);
_ = response;
_ = header;
}
pub fn noBody(self: *anyopaque, response: *Response, body: []const u8) anyerror!void {
_ = self;
_ = response;
_ = body;
}
pub fn noFinalize(self: *anyopaque) void {
_ = self;
}
pub inline fn rawHeader(rh: RequestHandler, response: *Response, header: Header) anyerror!void {
return rh.vtable.header(rh.ptr, response, header);
}
pub inline fn rawBody(rh: RequestHandler, response: *Response, body: []const u8) anyerror!void {
return rh.vtable.body(rh.ptr, response, body);
}
pub inline fn rawFinalize(rh: RequestHandler) void {
rh.vtable.finalize(rh.ptr);
} }

View File

@@ -1,16 +0,0 @@
const std = @import("std");
const RequestRouter = @This();
const RequestHandler = @import("RequestHandler.zig");
const Route = @import("Route.zig");
ptr: *anyopaque,
vtable: *const VTable,
pub const VTable = struct {
route: *const fn (self: *anyopaque, route: Route) anyerror!RequestHandler,
};
pub inline fn rawRoute(self: RequestRouter, route: Route) anyerror!RequestHandler {
return self.vtable.route(self.ptr, route);
}

View File

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

View File

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

View File

@@ -5,9 +5,11 @@ 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 log = std.log.scoped(.Server);
const linux = std.os.linux; const linux = std.os.linux;
const errno = linux.E.init; const errno = linux.E.init;
@@ -16,7 +18,8 @@ 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,
read_timeout_us: u64,
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 +33,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 +48,29 @@ 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,
/// How much time should a worker wait on an idle connection before closing
/// it. Specifically, how much time can a `read` syscall block for, before
/// the connection is forcefully closed.
read_timeout_us: u64 = 1 * std.time.us_per_s,
}; };
pub fn init(allocator: std.mem.Allocator, options: Options) !Server { pub fn init(allocator: std.mem.Allocator, options: Options) !Server {
@@ -89,7 +109,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 +154,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 +225,8 @@ 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,
.read_timeout_us = options.read_timeout_us,
.connection_queue = .{}, .connection_queue = .{},
.connection_pool = connection_pool, .connection_pool = connection_pool,
@@ -191,11 +239,37 @@ 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 log.debug("Deinitializing Server.", .{});
self.fd.close(); const worker_count = self.workers.len;
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;
} }
@@ -207,14 +281,18 @@ pub fn listen(self: *Server, running: *const std.atomic.Value(bool)) !void {
var spawned: usize = 0; var spawned: usize = 0;
defer { defer {
log.debug("Storing `false` into worker_running.", .{});
worker_running.store(false, .release); worker_running.store(false, .release);
log.debug("Broadcasting connection queued condition variable.", .{});
self.cond_connection_queued.broadcast(); self.cond_connection_queued.broadcast();
for (self.threads[0..spawned]) |*thread| { for (self.threads[0..spawned], 0..) |*thread, i| {
log.debug("Joining the thread of worker #{d}.", .{i});
thread.join(); thread.join();
} }
} }
for (self.workers, 0..) |*worker, i| { for (self.workers, 0..) |*worker, i| {
log.debug("Spawning thread for worker #{d}.", .{i});
self.threads[i] = try std.Thread.spawn(.{}, Worker.worker, .{ worker, self, &worker_running }); self.threads[i] = try std.Thread.spawn(.{}, Worker.worker, .{ worker, self, &worker_running });
spawned += 1; spawned += 1;
} }
@@ -223,34 +301,53 @@ pub fn listen(self: *Server, running: *const std.atomic.Value(bool)) !void {
var address: std.net.Address = undefined; var address: std.net.Address = undefined;
var address_size: u32 = @sizeOf(std.net.Address); var address_size: u32 = @sizeOf(std.net.Address);
log.debug("Accepting connection.", .{});
const fd = self.fd.accept(&address.any, &address_size) catch |e| { const fd = self.fd.accept(&address.any, &address_size) catch |e| {
std.log.err("Error while accepting connection: {}", .{e}); log.err("Error while accepting connection: {}", .{e});
continue; continue;
}; };
log.debug("Accepted connection from {f}", .{address});
const timeout: linux.timeval = .{
.sec = @intCast(self.read_timeout_us / std.time.us_per_s),
.usec = @intCast(self.read_timeout_us % std.time.us_per_s),
};
try fd.setsockopt(linux.SOL.SOCKET, linux.SO.RCVTIMEO, std.mem.asBytes(&timeout));
const ssl: ?*openssl.Ssl = self.maybeInitSsl(fd) catch |e| { const ssl: ?*openssl.Ssl = self.maybeInitSsl(fd) catch |e| {
std.log.err("Error while estabilishing SSL connection: {}", .{e}); log.err("Error while estabilishing SSL connection: {}", .{e});
fd.close(); fd.close();
continue; continue;
}; };
{ {
log.debug("Acquiring mutex.", .{});
self.mutex.lock(); self.mutex.lock();
defer self.mutex.unlock(); log.debug("Acquired mutex.", .{});
defer {
log.debug("Unlocking mutex.", .{});
self.mutex.unlock();
}
while (true) { while (true) {
if (self.connection_pool.pop()) |node| { if (self.connection_pool.pop()) |node| {
const connection: *Connection = @fieldParentPtr("node", node); const connection: *Connection = @fieldParentPtr("node", node);
connection.reinit(address, fd, ssl); connection.reinit(address, fd, ssl);
log.debug("Adding connection to {f} to the connection queue.", .{connection.address});
self.connection_queue.prepend(node); self.connection_queue.prepend(node);
break; break;
} }
log.debug("Waiting on connection freed condition variable.", .{});
self.cond_connection_freed.wait(&self.mutex); self.cond_connection_freed.wait(&self.mutex);
log.debug("Woken up on connection freed condition variable.", .{});
} }
} }
log.debug("Signaling connection queued condition variable.", .{});
self.cond_connection_queued.signal(); self.cond_connection_queued.signal();
} else {
log.debug("Loaded `false` from running, the accept loop exited.", .{});
} }
} }
@@ -258,7 +355,9 @@ fn maybeInitSsl(self: *const Server, fd: FileDescriptor) !?*openssl.Ssl {
if (self.ssl_ctx) |ssl_ctx| { if (self.ssl_ctx) |ssl_ctx| {
const ssl = try openssl.Ssl.new(ssl_ctx); const ssl = try openssl.Ssl.new(ssl_ctx);
try ssl.setFd(fd); try ssl.setFd(fd);
log.debug("Accepting SSL layer.", .{});
try ssl.accept(); try ssl.accept();
log.debug("Accepted SSL layer.", .{});
return ssl; return ssl;
} else { } else {
return null; return null;

View File

@@ -4,76 +4,160 @@ 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");
const log = std.log.scoped(.Worker);
/// 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 {
log.debug("[#{d}] Deinitializing Worker.", .{self.worker_id});
allocator.free(self.header_value_buffer);
self.header_hash_map.deinit(allocator);
self.* = undefined;
}
pub fn worker( pub fn worker(
self: *Worker, self: *Worker,
server: *Server, server: *Server,
running: *const std.atomic.Value(bool), running: *const std.atomic.Value(bool),
) void { ) void {
log.debug("[#{d}] Acquiring mutex.", .{self.worker_id});
server.mutex.lock(); server.mutex.lock();
defer server.mutex.unlock(); log.debug("[#{d}] Acquired mutex.", .{self.worker_id});
defer {
log.debug("[#{d}] Unlocking mutex.", .{self.worker_id});
server.mutex.unlock();
}
while (running.load(.acquire)) { while (running.load(.acquire)) {
if (server.connection_queue.pop()) |node| { if (server.connection_queue.pop()) |node| {
const connection: *Connection = @fieldParentPtr("node", node); const connection: *Connection = @fieldParentPtr("node", node);
log.debug("[#{d}] Popped connection to {f} from the connection queue.", .{ self.worker_id, connection.address });
log.debug("[#{d}] Unlocking mutex.", .{self.worker_id});
server.mutex.unlock(); server.mutex.unlock();
defer { defer {
log.debug("[#{d}] Acquiring mutex.", .{self.worker_id});
server.mutex.lock(); server.mutex.lock();
log.debug("[#{d}] Acquired mutex.", .{self.worker_id});
log.debug("[#{d}] Returning connection to connection pool.", .{self.worker_id});
server.connection_pool.append(&connection.node); server.connection_pool.append(&connection.node);
log.debug("[#{d}] Signaling connection freed condition variable.", .{self.worker_id});
server.cond_connection_freed.signal(); server.cond_connection_freed.signal();
} }
self.handleConnection(server.request_router, connection, running) catch |err| { log.debug("[#{d}] Handling connection to {f}.", .{ self.worker_id, connection.address });
std.log.err("Error while handling connection: {}", .{err}); self.handleConnection(server.request_handler, connection, running) catch |err| {
log.err("[#{d}] Error while handling connection: {}", .{ self.worker_id, err });
}; };
} else { } else {
log.debug("[#{d}] Waiting on connection queued condition variable.", .{self.worker_id});
server.cond_connection_queued.wait(&server.mutex); server.cond_connection_queued.wait(&server.mutex);
log.debug("[#{d}] Woken up on connection queued condition variable.", .{self.worker_id});
} }
} else {
log.debug("[#{d}] Loaded `false` from running, the worker loop exited.", .{self.worker_id});
} }
} }
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}); log.err("[#{d}] Error while handling request: {}", .{ self.worker_id, err });
return err; return err;
}; };
if (!res) break; if (!res) {
log.debug("[#{d}] Request handler indicated to stop handling the connection to {f}.", .{ self.worker_id, connection.address });
break;
}
} else {
log.debug("[#{d}] Loaded `false` from running, the connection handler loop exited.", .{self.worker_id});
} }
} }
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 client_closed: 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,81 +168,154 @@ 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;
bytes_read = try connection.read(self.read_buffer_ptr[read_tail..max_read_tail]); bytes_read = connection.read(self.read_buffer_ptr[read_tail..max_read_tail]) catch |err| switch (err) {
error.Timeout => {
log.debug("[#{d}] Connection to {f} timed out.", .{ self.worker_id, connection.address });
return false;
},
else => return err,
};
if (bytes_read == 0) {
log.debug("[#{d}] Read zero bytes from connection to {f}.", .{ self.worker_id, connection.address });
return false;
}
chunk = self.read_buffer_ptr[read_tail .. read_tail + bytes_read]; chunk = self.read_buffer_ptr[read_tail .. read_tail + bytes_read];
self.read_tail += bytes_read; self.read_tail += bytes_read;
} }
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| {
switch (result) {
.method => |method| request.method = method,
.pathname => |pathname| request.pathname = pathname,
.header => |header| blk: {
if (header.isNamedKnown(.Connection) and std.mem.eql(u8, header.value, "close")) {
client_closed = true;
}
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; leftover_bytes = bytes_read - res.consumed;
self.read_head = (self.read_tail - leftover_bytes) & ~(self.read_buffer_size - 1); self.read_head = (self.read_tail - leftover_bytes) & ~(self.read_buffer_size - 1);
self.read_tail = self.read_head + leftover_bytes; self.read_tail = self.read_head + leftover_bytes;
return true; return !client_closed;
},
} }
} }
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");
} }

View File

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

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

View File

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

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

View File

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

View File

@@ -8,8 +8,24 @@ const UUID = web.UUID;
var running: std.atomic.Value(bool) = .init(true); var running: std.atomic.Value(bool) = .init(true);
fn interruptionHandler(signal: i32) callconv(.c) void { fn interruptionHandler(sig: i32) callconv(.c) void {
switch (signal) { var buf: [32]u8 = undefined;
const signal_name = blk: inline for (@typeInfo(linux.SIG).@"struct".decls) |decl| {
if (comptime std.mem.eql(u8, decl.name, "BLOCK") or
std.mem.eql(u8, decl.name, "UNBLOCK") or
std.mem.eql(u8, decl.name, "SETMASK")) continue;
const decl_value = @field(linux.SIG, decl.name);
if (@TypeOf(decl_value) != comptime_int) continue;
if (decl_value == sig) break :blk "SIG" ++ decl.name;
} else {
break :blk std.fmt.bufPrint(&buf, "#{d}", .{sig}) catch unreachable;
};
std.log.debug("Interrupted with signal {s}.", .{signal_name});
switch (sig) {
linux.SIG.INT, linux.SIG.TERM => { linux.SIG.INT, linux.SIG.TERM => {
running.store(false, .release); running.store(false, .release);
}, },
@@ -17,114 +33,55 @@ 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 {
std.log.info("{f} | {s} {s}", .{
route: web.Route, response.connection.address,
uuid: UUID, @tagName(request.method),
timer: std.time.Timer, request.pathname,
accept: ?[]const u8 = null,
accept_encoding: ?[]const u8 = null,
accept_language: ?[]const u8 = null,
user_agent: ?[]const u8 = null,
fn init(allocator: std.mem.Allocator, route: 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;
}
fn onBody(ctx: *anyopaque, response: *web.Response, body: []const u8) !void {
const self: *Handler = @ptrCast(@alignCast(ctx));
try response.sendResponse(.{
.media_type = "application/json",
.response_body = "{\"ok\":true}\r\n",
}); });
_ = self; if (!std.mem.eql(u8, request.pathname, "/")) {
_ = body; try response.body_writer.writeAll("Not Found\n");
try response.header_writer.writeAll(web.http.status.not_found);
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;
} }
fn onFinalize(ctx: *anyopaque) void { if (request.method != .GET) {
const self: *Handler = @ptrCast(@alignCast(ctx)); try response.body_writer.writeAll("Method Not Allowed\n");
const time_ns = self.timer.read(); try response.header_writer.writeAll(web.http.status.method_not_allowed);
const time_us_ceil = (time_ns + std.time.ns_per_us - 1) / std.time.ns_per_us; 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");
const rps_floor = std.time.ns_per_s / time_ns; response.sendHeadersAndBody();
return;
}
std.log.info("{s} {s} (lat = {} µs, rlat = {} rps)", .{ @tagName(self.route.method), self.route.pathname, time_us_ceil, rps_floor }); try response.body_writer.writeAll("{\"ok\":true}\n");
self.allocator.destroy(self); 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 interface() web.RequestHandler {
return .{
.ptr = undefined,
.vtable = &.{
.handle = handle,
},
};
} }
}; };
@@ -135,8 +92,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 +107,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,
}); });
@@ -163,14 +118,16 @@ pub fn main() !void {
.mask = linux.sigemptyset(), .mask = linux.sigemptyset(),
.flags = linux.SA.RESETHAND, .flags = linux.SA.RESETHAND,
}; };
const rc = linux.sigaction(linux.SIG.INT, &sigaction, null); signal(linux.SIG.INT, &sigaction);
switch (errno(rc)) { signal(linux.SIG.TERM, &sigaction);
.SUCCESS => {},
else => |e| {
std.log.err("Error while estabilishing interruption handler: {s}", .{@tagName(e)});
return error.SystemError;
},
}
try server.listen(&running); try server.listen(&running);
} }
fn signal(sig: u8, action: *const linux.Sigaction) void {
var old_action = std.mem.zeroes(linux.Sigaction);
_ = linux.sigaction(sig, null, &old_action);
if (old_action.handler.handler == linux.SIG.IGN) return;
_ = linux.sigaction(sig, action, null);
}

View File

@@ -19,6 +19,10 @@ pub const Ssl = opaque {
import.SSL_free(self); import.SSL_free(self);
} }
pub inline fn getError(self: *const Ssl, ret_code: i32) i32 {
return import.SSL_get_error(self, ret_code);
}
pub inline fn new(ctx: *SslContext) !*Ssl { pub inline fn new(ctx: *SslContext) !*Ssl {
return import.SSL_new(ctx) orelse error.OpenSslError; return import.SSL_new(ctx) orelse error.OpenSslError;
} }
@@ -26,7 +30,12 @@ pub const Ssl = opaque {
pub inline fn read(self: *Ssl, buf: []u8) !usize { pub inline fn read(self: *Ssl, buf: []u8) !usize {
var bytes_read: usize = undefined; var bytes_read: usize = undefined;
const res = import.SSL_read_ex(self, buf.ptr, buf.len, &bytes_read); const res = import.SSL_read_ex(self, buf.ptr, buf.len, &bytes_read);
return if (res <= 0) error.OpenSslError else bytes_read; if (res <= 0) {
const err = self.getError(res);
return if (err == c_ssl.SSL_ERROR_ZERO_RETURN) 0 else error.OpenSslError;
} else {
return bytes_read;
}
} }
pub fn readAll(self: *Ssl, buf: []u8) !void { pub fn readAll(self: *Ssl, buf: []u8) !void {
@@ -39,6 +48,20 @@ pub const Ssl = opaque {
} }
} }
pub fn sendfile(self: *Ssl, fd: FileDescriptor, offset: usize, size: usize) !usize {
const res = import.SSL_sendfile(self, fd, offset, size, 0);
return if (res <= 0) error.OpenSslError else @intCast(res);
}
pub fn sendfileAll(self: *Ssl, fd: FileDescriptor, offset: usize, size: usize) !void {
var total_bytes_sent: usize = 0;
while (total_bytes_sent < size) {
const bytes_written = try self.sendfile(fd, offset + total_bytes_sent, size - total_bytes_sent);
total_bytes_sent += bytes_written;
}
}
pub inline fn setFd(self: *Ssl, fd: FileDescriptor) !void { pub inline fn setFd(self: *Ssl, fd: FileDescriptor) !void {
const res = import.SSL_set_fd(self, @intFromEnum(fd)); const res = import.SSL_set_fd(self, @intFromEnum(fd));
if (res <= 0) { if (res <= 0) {

View File

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