diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index df34d85..8be365f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,11 +19,11 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 - - uses: goto-bus-stop/setup-zig@v2 + - uses: actions/checkout@v5 + - uses: mlugg/setup-zig@v2 with: - version: 0.14.0 - - name: Build all examples + version: 0.15.2 + - name: Build all examples run: zig build run-tests: @@ -35,9 +35,9 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 - - uses: goto-bus-stop/setup-zig@v2 + - uses: actions/checkout@v5 + - uses: mlugg/setup-zig@v2 with: - version: 0.14.0 - - name: Build all examples + version: 0.15.2 + - name: Build all examples run: zig build test --summary all diff --git a/README.md b/README.md index 0f6bb36..a8bea64 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ## Installing -Compatible Zig Version: `0.14.0` +Compatible Zig Version: `0.15.2` Compatible [tardy](https://github.com/tardy-org/tardy) Version: `v0.3.0` @@ -62,8 +62,8 @@ zzz can be configured to utilize minimal memory while remaining performant. The - `poll` for Linux, Mac and Windows. - Layered Router, including Middleware - Single and Multithreaded Support -- TLS using [secsock](https://github.com/tardy-org/secsock) -- Memory Pooling for minimal allocations +- TLS using [secsock](https://github.com/tardy-org/secsock) +- Memory Pooling for minimal allocations ## Contribution Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in zzz by you, shall be licensed as MPL2.0, without any additional terms or conditions. diff --git a/build.zig b/build.zig index 037a9eb..a0af588 100644 --- a/build.zig +++ b/build.zig @@ -38,7 +38,11 @@ pub fn build(b: *std.Build) void { const tests = b.addTest(.{ .name = "tests", - .root_source_file = b.path("./src/tests.zig"), + .root_module = b.addModule("tests", .{ + .root_source_file = b.path("./src/tests.zig"), + .target = target, + .optimize = optimize, + }), }); tests.root_module.addImport("tardy", tardy); tests.root_module.addImport("secsock", secsock); @@ -55,22 +59,24 @@ fn add_example( name: []const u8, link_libc: bool, target: std.Build.ResolvedTarget, - optimize: std.builtin.Mode, + optimize: std.builtin.OptimizeMode, zzz_module: *std.Build.Module, ) void { - const example = b.addExecutable(.{ - .name = name, + const mod = b.createModule(.{ .root_source_file = b.path(b.fmt("./examples/{s}/main.zig", .{name})), - .target = target, .optimize = optimize, + .target = target, .strip = false, + .link_libc = link_libc, }); + mod.addImport("zzz", zzz_module); - if (link_libc) { - example.linkLibC(); - } - - example.root_module.addImport("zzz", zzz_module); + const example = b.addExecutable(.{ + .name = name, + .root_module = mod, + // without llvm leads to error: undefined symbol: tardy_swap_frame + .use_llvm = true, + }); const install_artifact = b.addInstallArtifact(example, .{}); b.getInstallStep().dependOn(&install_artifact.step); diff --git a/build.zig.zon b/build.zig.zon index 2200eb4..6971249 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,18 +2,17 @@ .name = .zzz, .fingerprint = 0xc3273dca261a7ae0, .version = "0.3.0", - .minimum_zig_version = "0.14.0", + .minimum_zig_version = "0.15.2", .dependencies = .{ .tardy = .{ - .url = "git+https://github.com/tardy-org/tardy?ref=v0.3.0#cd454060f3b6006368d53c05ab96cd16c73c34de", - .hash = "tardy-0.3.0-69wrgi7PAwDFhO7m0aXae6N15s2b28VIOrnRrSHHake6", + .url = "git+https://github.com/bernardassan/tardy?ref=zig-0.15.2#edd54c2dbdb745760848b083b7f844b05d531148", + .hash = "tardy-0.3.0-69wrgn77AwDLFqiryrDCuNl-q7xF9VEUdONc7ytrNvsM", }, .secsock = .{ - .url = "git+https://github.com/tardy-org/secsock?ref=v0.1.0#263dcd630e32c7a5c7a0522a8d1fd04e39b75c24", - .hash = "secsock-0.0.0-p0qurf09AQD95s1NQF2MGpBqMmFz7cKZWibsgv_SQBAr", + .url = "git+https://github.com/bernardassan/secsock?ref=zig-0.15.1#25cec3e1b68dac92c17f3071caacff6b71c00b68", + .hash = "secsock-0.0.0-p0qurQhGAQBqG1dPh8s4htvzl1w8qiGBCxUV9uTOrt9h", }, }, - .paths = .{ "README.md", "LICENSE", diff --git a/docs/getting_started.md b/docs/getting_started.md index 35673cb..8fc3469 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -39,19 +39,22 @@ pub fn main() !void { const host: []const u8 = "0.0.0.0"; const port: u16 = 9862; - var gpa = std.heap.GeneralPurposeAllocator(.{ .thread_safe = true }){}; + var gpa: std.heap.DebugAllocator(.{ .thread_safe = true }) = .init; const allocator = gpa.allocator(); defer _ = gpa.deinit(); - var t = try Tardy.init(allocator, .{ .threading = .auto }); + var t: Tardy = try .init(allocator, .{ .threading = .auto }); defer t.deinit(); - var router = try Router.init(allocator, &.{ + var router: Router = try .init(allocator, &.{ Route.init("/").get({}, base_handler).layer(), }, .{}); defer router.deinit(allocator); - var socket = try Socket.init(.{ .tcp = .{ .host = host, .port = port } }); + // create socket for tardy + var socket: Socket = try .init(.{ + .tcp = .{ .host = host, .port = port }, + }); defer socket.close_blocking(); try socket.bind(); try socket.listen(4096); @@ -65,7 +68,7 @@ pub fn main() !void { EntryParams{ .router = &router, .socket = socket }, struct { fn entry(rt: *Runtime, p: EntryParams) !void { - var server = Server.init(.{ + var server: Server = .init(.{ .stack_size = 1024 * 1024 * 4, .socket_buffer_bytes = 1024 * 2, .keepalive_count_max = null, @@ -78,4 +81,4 @@ pub fn main() !void { } ``` -The snippet above handles all of the basic tasks involved with serving a plaintext route using zzz's HTTP implementation. +The snippet above handles all of the basic tasks involved with serving a plaintext route using zzz's HTTP implementation. diff --git a/docs/https.md b/docs/https.md index 6dca483..4812060 100644 --- a/docs/https.md +++ b/docs/https.md @@ -25,6 +25,7 @@ const Respond = http.Respond; const secsock = zzz.secsock; const SecureSocket = secsock.SecureSocket; +const Compression = http.Middlewares.Compression; fn root_handler(ctx: *const Context, _: void) !Respond { const body = @@ -50,25 +51,32 @@ pub fn main() !void { const host: []const u8 = "0.0.0.0"; const port: u16 = 9862; - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa: std.heap.DebugAllocator(.{ .thread_safe = true }) = .{ + .backing_allocator = std.heap.smp_allocator, + }; const allocator = gpa.allocator(); defer _ = gpa.deinit(); - var t = try Tardy.init(allocator, .{ .threading = .auto }); + var t: Tardy = try .init(allocator, .{ .threading = .auto }); defer t.deinit(); - var router = try Router.init(allocator, &.{ + var router: Router = try .init(allocator, &.{ Route.init("/").get({}, root_handler).layer(), + Compression(.{ .gzip = .{} }), + Route.init("/embed/pico.min.css").embed_file( + .{ .mime = .CSS }, + @embedFile("embed/pico.min.css"), + ).layer(), }, .{}); defer router.deinit(allocator); // create socket for tardy - var socket = try Socket.init(.{ .tcp = .{ .host = host, .port = port } }); + var socket: Socket = try .init(.{ .tcp = .{ .host = host, .port = port } }); defer socket.close_blocking(); try socket.bind(); try socket.listen(1024); - var bearssl = secsock.BearSSL.init(allocator); + var bearssl: secsock.BearSSL = .init(allocator); defer bearssl.deinit(); try bearssl.add_cert_chain( "CERTIFICATE", @@ -87,7 +95,7 @@ pub fn main() !void { EntryParams{ .router = &router, .socket = secure }, struct { fn entry(rt: *Runtime, p: EntryParams) !void { - var server = Server.init(.{ .stack_size = 1024 * 1024 * 8 }); + var server: Server = .init(.{ .stack_size = 1024 * 1024 * 8 }); try server.serve(rt, p.router, .{ .secure = p.socket }); } }.entry, diff --git a/examples/basic/main.zig b/examples/basic/main.zig index ed7274c..9a2b008 100644 --- a/examples/basic/main.zig +++ b/examples/basic/main.zig @@ -1,20 +1,20 @@ const std = @import("std"); -const log = std.log.scoped(.@"examples/basic"); const zzz = @import("zzz"); const http = zzz.HTTP; - const tardy = zzz.tardy; -const Tardy = tardy.Tardy(.auto); const Runtime = tardy.Runtime; const Socket = tardy.Socket; - const Server = http.Server; const Router = http.Router; const Context = http.Context; const Route = http.Route; const Respond = http.Respond; +const log = std.log.scoped(.@"examples/basic"); + +const Tardy = tardy.Tardy(.auto); + fn base_handler(ctx: *const Context, _: void) !Respond { return ctx.response.apply(.{ .status = .OK, @@ -27,20 +27,22 @@ pub fn main() !void { const host: []const u8 = "0.0.0.0"; const port: u16 = 9862; - var gpa = std.heap.GeneralPurposeAllocator(.{ .thread_safe = true }){}; + var gpa: std.heap.DebugAllocator(.{ .thread_safe = true }) = .init; const allocator = gpa.allocator(); defer _ = gpa.deinit(); - var t = try Tardy.init(allocator, .{ .threading = .auto }); + var t: Tardy = try .init(allocator, .{ .threading = .auto }); defer t.deinit(); - var router = try Router.init(allocator, &.{ + var router: Router = try .init(allocator, &.{ Route.init("/").get({}, base_handler).layer(), }, .{}); defer router.deinit(allocator); // create socket for tardy - var socket = try Socket.init(.{ .tcp = .{ .host = host, .port = port } }); + var socket: Socket = try .init(.{ + .tcp = .{ .host = host, .port = port }, + }); defer socket.close_blocking(); try socket.bind(); try socket.listen(4096); @@ -54,7 +56,7 @@ pub fn main() !void { EntryParams{ .router = &router, .socket = socket }, struct { fn entry(rt: *Runtime, p: EntryParams) !void { - var server = Server.init(.{ + var server: Server = .init(.{ .stack_size = 1024 * 1024 * 4, .socket_buffer_bytes = 1024 * 2, .keepalive_count_max = null, diff --git a/examples/cookies/main.zig b/examples/cookies/main.zig index 27d633b..7bd6b95 100644 --- a/examples/cookies/main.zig +++ b/examples/cookies/main.zig @@ -1,14 +1,10 @@ const std = @import("std"); -const log = std.log.scoped(.@"examples/cookies"); const zzz = @import("zzz"); const http = zzz.HTTP; - const tardy = zzz.tardy; -const Tardy = tardy.Tardy(.auto); const Runtime = tardy.Runtime; const Socket = tardy.Socket; - const Server = http.Server; const Router = http.Router; const Context = http.Context; @@ -17,14 +13,18 @@ const Middleware = http.Middleware; const Respond = http.Respond; const Cookie = http.Cookie; +const log = std.log.scoped(.@"examples/cookies"); + +const Tardy = tardy.Tardy(.auto); + fn base_handler(ctx: *const Context, _: void) !Respond { var iter = ctx.request.cookies.iterator(); while (iter.next()) |kv| log.debug("cookie: k={s} v={s}", .{ kv.key_ptr.*, kv.value_ptr.* }); - const cookie = Cookie.init("example_cookie", "abcdef123"); + const cookie: Cookie = .init("example_cookie", "abcdef123"); return ctx.response.apply(.{ .status = .OK, - .mime = http.Mime.HTML, + .mime = .HTML, .body = "Hello, world!", .headers = &.{ .{ "Set-Cookie", try cookie.to_string_alloc(ctx.allocator) }, @@ -36,20 +36,20 @@ pub fn main() !void { const host: []const u8 = "0.0.0.0"; const port: u16 = 9862; - var gpa = std.heap.GeneralPurposeAllocator(.{ .thread_safe = true }){}; + var gpa: std.heap.DebugAllocator(.{ .thread_safe = true }) = .init; const allocator = gpa.allocator(); defer _ = gpa.deinit(); - var t = try Tardy.init(allocator, .{ .threading = .single }); + var t: Tardy = try .init(allocator, .{ .threading = .single }); defer t.deinit(); - var router = try Router.init(allocator, &.{ + var router: Router = try .init(allocator, &.{ Route.init("/").get({}, base_handler).layer(), }, .{}); defer router.deinit(allocator); // create socket for tardy - var socket = try Socket.init(.{ .tcp = .{ .host = host, .port = port } }); + var socket: Socket = try .init(.{ .tcp = .{ .host = host, .port = port } }); defer socket.close_blocking(); try socket.bind(); try socket.listen(4096); @@ -63,7 +63,7 @@ pub fn main() !void { EntryParams{ .router = &router, .socket = socket }, struct { fn entry(rt: *Runtime, p: EntryParams) !void { - var server = Server.init(.{ + var server: Server = .init(.{ .stack_size = 1024 * 1024 * 4, .socket_buffer_bytes = 1024 * 2, .keepalive_count_max = null, diff --git a/examples/form/main.zig b/examples/form/main.zig index 2cb122f..eeb9f9d 100644 --- a/examples/form/main.zig +++ b/examples/form/main.zig @@ -1,14 +1,10 @@ const std = @import("std"); -const log = std.log.scoped(.@"examples/form"); const zzz = @import("zzz"); const http = zzz.HTTP; - const tardy = zzz.tardy; -const Tardy = tardy.Tardy(.auto); const Runtime = tardy.Runtime; const Socket = tardy.Socket; - const Server = http.Server; const Router = http.Router; const Context = http.Context; @@ -17,6 +13,10 @@ const Form = http.Form; const Query = http.Query; const Respond = http.Respond; +const log = std.log.scoped(.@"examples/form"); + +const Tardy = tardy.Tardy(.auto); + fn base_handler(ctx: *const Context, _: void) !Respond { const body = \\
@@ -30,7 +30,7 @@ fn base_handler(ctx: *const Context, _: void) !Respond { \\

\\ \\ - \\
+ \\ ; return ctx.response.apply(.{ @@ -80,20 +80,20 @@ pub fn main() !void { const host: []const u8 = "0.0.0.0"; const port: u16 = 9862; - var gpa = std.heap.GeneralPurposeAllocator(.{ .thread_safe = true }){}; + var gpa: std.heap.DebugAllocator(.{ .thread_safe = true }) = .init; const allocator = gpa.allocator(); defer _ = gpa.deinit(); - var t = try Tardy.init(allocator, .{ .threading = .auto }); + var t: Tardy = try .init(allocator, .{ .threading = .auto }); defer t.deinit(); - var router = try Router.init(allocator, &.{ + var router: Router = try .init(allocator, &.{ Route.init("/").get({}, base_handler).layer(), Route.init("/generate").get({}, generate_handler).post({}, generate_handler).layer(), }, .{}); defer router.deinit(allocator); - var socket = try Socket.init(.{ .tcp = .{ .host = host, .port = port } }); + var socket: Socket = try .init(.{ .tcp = .{ .host = host, .port = port } }); defer socket.close_blocking(); try socket.bind(); try socket.listen(4096); @@ -107,7 +107,7 @@ pub fn main() !void { EntryParams{ .router = &router, .socket = socket }, struct { fn entry(rt: *Runtime, p: EntryParams) !void { - var server = Server.init(.{ + var server: Server = .init(.{ .stack_size = 1024 * 1024 * 4, .socket_buffer_bytes = 1024 * 2, }); diff --git a/examples/fs/main.zig b/examples/fs/main.zig index d566ebb..80f4b58 100644 --- a/examples/fs/main.zig +++ b/examples/fs/main.zig @@ -1,24 +1,23 @@ const std = @import("std"); -const log = std.log.scoped(.@"examples/fs"); const zzz = @import("zzz"); const http = zzz.HTTP; - const tardy = zzz.tardy; -const Tardy = tardy.Tardy(.auto); const Runtime = tardy.Runtime; const Socket = tardy.Socket; const Dir = tardy.Dir; - const Server = http.Server; const Router = http.Router; const Context = http.Context; const Route = http.Route; const Respond = http.Respond; const FsDir = http.FsDir; - const Compression = http.Middlewares.Compression; +const log = std.log.scoped(.@"examples/fs"); + +const Tardy = tardy.Tardy(.auto); + fn base_handler(ctx: *const Context, _: void) !Respond { const body = \\ @@ -36,22 +35,23 @@ fn base_handler(ctx: *const Context, _: void) !Respond { }); } +// Test With: http://localhost:9862/index.html pub fn main() !void { const host: []const u8 = "0.0.0.0"; const port: u16 = 9862; - var gpa = std.heap.GeneralPurposeAllocator( - .{ .thread_safe = true }, - ){ .backing_allocator = std.heap.c_allocator }; + var gpa: std.heap.DebugAllocator(.{ .thread_safe = true }) = .{ + .backing_allocator = std.heap.smp_allocator, + }; const allocator = gpa.allocator(); defer _ = gpa.deinit(); - var t = try Tardy.init(allocator, .{ .threading = .auto }); + var t: Tardy = try .init(allocator, .{ .threading = .auto }); defer t.deinit(); - const static_dir = Dir.from_std(try std.fs.cwd().openDir("examples/fs/static", .{})); + const static_dir: Dir = .from_std(try std.fs.cwd().openDir("examples/fs/static", .{})); - var router = try Router.init(allocator, &.{ + var router: Router = try .init(allocator, &.{ Compression(.{ .gzip = .{} }), Route.init("/").get({}, base_handler).layer(), FsDir.serve("/", static_dir), @@ -63,7 +63,7 @@ pub fn main() !void { socket: Socket, }; - var socket = try Socket.init(.{ .tcp = .{ .host = host, .port = port } }); + var socket: Socket = try .init(.{ .tcp = .{ .host = host, .port = port } }); defer socket.close_blocking(); try socket.bind(); try socket.listen(256); @@ -72,7 +72,7 @@ pub fn main() !void { EntryParams{ .router = &router, .socket = socket }, struct { fn entry(rt: *Runtime, p: EntryParams) !void { - var server = Server.init(.{ + var server: Server = .init(.{ .stack_size = 1024 * 1024 * 4, .socket_buffer_bytes = 1024 * 4, }); diff --git a/examples/middleware/main.zig b/examples/middleware/main.zig index 1efa7e1..a84cca2 100644 --- a/examples/middleware/main.zig +++ b/examples/middleware/main.zig @@ -1,14 +1,10 @@ const std = @import("std"); -const log = std.log.scoped(.@"examples/middleware"); const zzz = @import("zzz"); const http = zzz.HTTP; - const tardy = zzz.tardy; -const Tardy = tardy.Tardy(.auto); const Runtime = tardy.Runtime; const Socket = tardy.Socket; - const Server = http.Server; const Router = http.Router; const Context = http.Context; @@ -17,6 +13,10 @@ const Next = http.Next; const Respond = http.Respond; const Middleware = http.Middleware; +const log = std.log.scoped(.@"examples/middleware"); + +const Tardy = tardy.Tardy(.auto); + fn root_handler(ctx: *const Context, id: i8) !Respond { const body_fmt = \\ @@ -39,7 +39,7 @@ fn root_handler(ctx: *const Context, id: i8) !Respond { // client and then continue to await more requests. return ctx.response.apply(.{ .status = .OK, - .mime = http.Mime.HTML, + .mime = .HTML, .body = body[0..], }); } @@ -59,16 +59,16 @@ pub fn main() !void { const host: []const u8 = "0.0.0.0"; const port: u16 = 9862; - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa: std.heap.DebugAllocator(.{}) = .init; const allocator = gpa.allocator(); defer _ = gpa.deinit(); - var t = try Tardy.init(allocator, .{ .threading = .single }); + var t: Tardy = try .init(allocator, .{ .threading = .single }); defer t.deinit(); const num: i8 = 12; - var router = try Router.init(allocator, &.{ + var router: Router = try .init(allocator, &.{ Middleware.init({}, passing_middleware).layer(), Route.init("/").get(num, root_handler).layer(), Middleware.init({}, failing_middleware).layer(), @@ -82,7 +82,7 @@ pub fn main() !void { socket: Socket, }; - var socket = try Socket.init(.{ .tcp = .{ .host = host, .port = port } }); + var socket: Socket = try .init(.{ .tcp = .{ .host = host, .port = port } }); defer socket.close_blocking(); try socket.bind(); try socket.listen(256); @@ -91,7 +91,7 @@ pub fn main() !void { EntryParams{ .router = &router, .socket = socket }, struct { fn entry(rt: *Runtime, p: EntryParams) !void { - var server = Server.init(.{}); + var server: Server = .init(.{}); try server.serve(rt, p.router, .{ .normal = p.socket }); } }.entry, diff --git a/examples/sse/main.zig b/examples/sse/main.zig index 715dfab..1236ec9 100644 --- a/examples/sse/main.zig +++ b/examples/sse/main.zig @@ -1,15 +1,11 @@ const std = @import("std"); -const log = std.log.scoped(.@"examples/sse"); const zzz = @import("zzz"); const http = zzz.HTTP; - const tardy = zzz.tardy; -const Tardy = tardy.Tardy(.auto); const Runtime = tardy.Runtime; const Socket = tardy.Socket; const Timer = tardy.Timer; - const Server = http.Server; const Router = http.Router; const Context = http.Context; @@ -17,8 +13,12 @@ const Route = http.Route; const Respond = http.Respond; const SSE = http.SSE; +const log = std.log.scoped(.@"examples/sse"); + +const Tardy = tardy.Tardy(.auto); + fn sse_handler(ctx: *const Context, _: void) !Respond { - var sse = try SSE.init(ctx); + var sse: SSE = try .init(ctx); while (true) { sse.send(.{ .data = "hello from handler!" }) catch break; @@ -32,20 +32,20 @@ pub fn main() !void { const host: []const u8 = "0.0.0.0"; const port: u16 = 9862; - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa: std.heap.DebugAllocator(.{}) = .init; const allocator = gpa.allocator(); defer _ = gpa.deinit(); - var t = try Tardy.init(allocator, .{ .threading = .single }); + var t: Tardy = try .init(allocator, .{ .threading = .single }); defer t.deinit(); - const router = try Router.init(allocator, &.{ - Route.init("/").embed_file(.{ .mime = http.Mime.HTML }, @embedFile("./index.html")).layer(), + const router: Router = try .init(allocator, &.{ + Route.init("/").embed_file(.{ .mime = .HTML }, @embedFile("./index.html")).layer(), Route.init("/stream").get({}, sse_handler).layer(), }, .{}); // create socket for tardy - var socket = try Socket.init(.{ .tcp = .{ .host = host, .port = port } }); + var socket: Socket = try .init(.{ .tcp = .{ .host = host, .port = port } }); defer socket.close_blocking(); try socket.bind(); try socket.listen(256); @@ -59,7 +59,7 @@ pub fn main() !void { EntryParams{ .router = &router, .socket = socket }, struct { fn entry(rt: *Runtime, p: EntryParams) !void { - var server = Server.init(.{ + var server: Server = .init(.{ .stack_size = 1024 * 1024 * 4, .socket_buffer_bytes = 1024 * 2, }); diff --git a/examples/tls/main.zig b/examples/tls/main.zig index d8a07e4..e0e969a 100644 --- a/examples/tls/main.zig +++ b/examples/tls/main.zig @@ -1,24 +1,23 @@ const std = @import("std"); -const log = std.log.scoped(.@"examples/tls"); const zzz = @import("zzz"); const http = zzz.HTTP; - const tardy = zzz.tardy; -const Tardy = tardy.Tardy(.auto); const Runtime = tardy.Runtime; const Socket = tardy.Socket; - const Server = http.Server; const Context = http.Context; const Route = http.Route; const Router = http.Router; const Respond = http.Respond; - const secsock = zzz.secsock; const SecureSocket = secsock.SecureSocket; const Compression = http.Middlewares.Compression; +const log = std.log.scoped(.@"examples/tls"); + +const Tardy = tardy.Tardy(.auto); + fn root_handler(ctx: *const Context, _: void) !Respond { const body = \\ @@ -43,32 +42,32 @@ pub fn main() !void { const host: []const u8 = "0.0.0.0"; const port: u16 = 9862; - var gpa = std.heap.GeneralPurposeAllocator( - .{ .thread_safe = true }, - ){ .backing_allocator = std.heap.c_allocator }; + var gpa: std.heap.DebugAllocator(.{ .thread_safe = true }) = .{ + .backing_allocator = std.heap.smp_allocator, + }; const allocator = gpa.allocator(); defer _ = gpa.deinit(); - var t = try Tardy.init(allocator, .{ .threading = .auto }); + var t: Tardy = try .init(allocator, .{ .threading = .auto }); defer t.deinit(); - var router = try Router.init(allocator, &.{ + var router: Router = try .init(allocator, &.{ Route.init("/").get({}, root_handler).layer(), Compression(.{ .gzip = .{} }), Route.init("/embed/pico.min.css").embed_file( - .{ .mime = http.Mime.CSS }, + .{ .mime = .CSS }, @embedFile("embed/pico.min.css"), ).layer(), }, .{}); defer router.deinit(allocator); // create socket for tardy - var socket = try Socket.init(.{ .tcp = .{ .host = host, .port = port } }); + var socket: Socket = try .init(.{ .tcp = .{ .host = host, .port = port } }); defer socket.close_blocking(); try socket.bind(); try socket.listen(1024); - var bearssl = secsock.BearSSL.init(allocator); + var bearssl: secsock.BearSSL = .init(allocator); defer bearssl.deinit(); try bearssl.add_cert_chain( "CERTIFICATE", @@ -87,7 +86,7 @@ pub fn main() !void { EntryParams{ .router = &router, .socket = secure }, struct { fn entry(rt: *Runtime, p: EntryParams) !void { - var server = Server.init(.{ .stack_size = 1024 * 1024 * 8 }); + var server: Server = .init(.{ .stack_size = 1024 * 1024 * 8 }); try server.serve(rt, p.router, .{ .secure = p.socket }); } }.entry, diff --git a/examples/unix/main.zig b/examples/unix/main.zig index 92c9b88..06300ff 100644 --- a/examples/unix/main.zig +++ b/examples/unix/main.zig @@ -1,39 +1,39 @@ const std = @import("std"); -const log = std.log.scoped(.@"examples/benchmark"); const zzz = @import("zzz"); const http = zzz.HTTP; - const tardy = zzz.tardy; -const Tardy = tardy.Tardy(.auto); const Runtime = tardy.Runtime; const Socket = tardy.Socket; - const Server = http.Server; const Context = http.Context; const Route = http.Route; const Router = http.Router; const Respond = http.Respond; +const log = std.log.scoped(.@"examples/benchmark"); + +const Tardy = tardy.Tardy(.auto); pub const std_options: std.Options = .{ .log_level = .err }; pub fn root_handler(ctx: *const Context, _: void) !Respond { return ctx.response.apply(.{ .status = .OK, - .mime = http.Mime.HTML, - .body = "This is an HTTP benchmark", + .mime = .HTML, + .body = "This is an HTTP benchmark\n", }); } +// Test With: curl --unix-socket /tmp/zzz.sock http://localhost/ pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{ .thread_safe = true }){}; + var gpa: std.heap.DebugAllocator(.{ .thread_safe = true }) = .init; const allocator = gpa.allocator(); defer _ = gpa.deinit(); - var t = try Tardy.init(allocator, .{ .threading = .auto }); + var t: Tardy = try .init(allocator, .{ .threading = .auto }); defer t.deinit(); - var router = try Router.init(allocator, &.{ + var router: Router = try .init(allocator, &.{ Route.init("/").get({}, root_handler).layer(), }, .{}); defer router.deinit(allocator); @@ -43,7 +43,7 @@ pub fn main() !void { socket: Socket, }; - var socket = try Socket.init(.{ .unix = "/tmp/zzz.sock" }); + var socket: Socket = try .init(.{ .unix = "/tmp/zzz.sock" }); defer std.fs.deleteFileAbsolute("/tmp/zzz.sock") catch unreachable; defer socket.close_blocking(); try socket.bind(); @@ -53,7 +53,7 @@ pub fn main() !void { EntryParams{ .router = &router, .socket = socket }, struct { fn entry(rt: *Runtime, p: EntryParams) !void { - var server = Server.init(.{}); + var server: Server = .init(.{}); try server.serve(rt, p.router, .{ .normal = p.socket }); } }.entry, diff --git a/src/core/any_case_string_map.zig b/src/core/any_case_string_map.zig index e138bad..08ef8b2 100644 --- a/src/core/any_case_string_map.zig +++ b/src/core/any_case_string_map.zig @@ -1,5 +1,6 @@ const std = @import("std"); const assert = std.debug.assert; +const testing = std.testing; const Pool = @import("tardy").Pool; @@ -7,7 +8,7 @@ const AnyCaseStringContext = struct { const Self = @This(); pub fn hash(_: Self, key: []const u8) u64 { - var wyhash = std.hash.Wyhash.init(0); + var wyhash: std.hash.Wyhash = .init(0); for (key) |b| wyhash.update(&.{std.ascii.toLower(b)}); return wyhash.final(); } @@ -21,10 +22,8 @@ const AnyCaseStringContext = struct { pub const AnyCaseStringMap = std.HashMap([]const u8, []const u8, AnyCaseStringContext, 80); -const testing = std.testing; - test "AnyCaseStringMap: Add Stuff" { - var map = AnyCaseStringMap.init(testing.allocator); + var map: AnyCaseStringMap = .init(testing.allocator); defer map.deinit(); try map.put("Content-Length", "100"); diff --git a/src/core/pseudoslice.zig b/src/core/pseudoslice.zig index c4d519b..fd0d66d 100644 --- a/src/core/pseudoslice.zig +++ b/src/core/pseudoslice.zig @@ -1,5 +1,7 @@ const std = @import("std"); const assert = std.debug.assert; +const testing = std.testing; + const log = std.log.scoped(.@"zzz/core/pseudoslice"); // The Pseudoslice will basically stitch together two different buffers, using @@ -11,7 +13,7 @@ pub const Pseudoslice = struct { len: usize, pub fn init(first: []const u8, second: []const u8, shared: []u8) Pseudoslice { - return Pseudoslice{ + return .{ .first = first, .second = second, .shared = shared, @@ -56,12 +58,10 @@ pub const Pseudoslice = struct { } }; -const testing = std.testing; - test "Pseudoslice General" { - var buffer = [_]u8{0} ** 1024; + var buffer: [1024]u8 = @splat(0); const value = "hello, my name is muki"; - var pseudo = Pseudoslice.init(value[0..6], value[6..], buffer[0..]); + var pseudo: Pseudoslice = .init(value[0..6], value[6..], buffer[0..]); for (0..pseudo.len) |i| { for (0..i) |j| { @@ -71,9 +71,9 @@ test "Pseudoslice General" { } test "Pseudoslice Empty Second" { - var buffer = [_]u8{0} ** 1024; + var buffer: [1024]u8 = @splat(0); const value = "hello, my name is muki"; - var pseudo = Pseudoslice.init(value[0..], &.{}, buffer[0..]); + var pseudo: Pseudoslice = .init(value[0..], &.{}, buffer[0..]); for (0..pseudo.len) |i| { try testing.expectEqualStrings(value[0..i], pseudo.get(0, i)); @@ -87,7 +87,7 @@ test "Pseudoslice First and Shared Same" { const value = "hello, my name is muki"; std.mem.copyForwards(u8, buffer, value[0..6]); - var pseudo = Pseudoslice.init(buffer[0..6], value[6..], buffer); + var pseudo: Pseudoslice = .init(buffer[0..6], value[6..], buffer); for (0..pseudo.len) |i| { for (0..i) |j| { diff --git a/src/core/typed_storage.zig b/src/core/typed_storage.zig index 5abe3f2..06866fb 100644 --- a/src/core/typed_storage.zig +++ b/src/core/typed_storage.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const testing = std.testing; pub const TypedStorage = struct { arena: std.heap.ArenaAllocator, @@ -6,8 +7,8 @@ pub const TypedStorage = struct { pub fn init(allocator: std.mem.Allocator) TypedStorage { return .{ - .arena = std.heap.ArenaAllocator.init(allocator), - .storage = std.AutoHashMapUnmanaged(u64, *anyopaque){}, + .arena = .init(allocator), + .storage = .empty, }; } @@ -40,10 +41,8 @@ pub const TypedStorage = struct { } }; -const testing = std.testing; - test "TypedStorage: Basic" { - var storage = TypedStorage.init(testing.allocator); + var storage: TypedStorage = .init(testing.allocator); defer storage.deinit(); // Test inserting and getting different types diff --git a/src/core/wrapping.zig b/src/core/wrapping.zig index 78c0607..5464260 100644 --- a/src/core/wrapping.zig +++ b/src/core/wrapping.zig @@ -1,5 +1,6 @@ const std = @import("std"); const assert = std.debug.assert; +const testing = std.testing; /// Special values for Wrapped types. const Wrapped = enum(usize) { null = 0, true = 1, false = 2, void = 3 }; @@ -114,8 +115,6 @@ pub fn unwrap(comptime T: type, value: anytype) T { }; } -const testing = std.testing; - test "wrap/unwrap - integers" { try testing.expectEqual(@as(usize, 42), wrap(usize, @as(u8, 42))); try testing.expectEqual(@as(usize, 42), wrap(usize, @as(u16, 42))); diff --git a/src/http/context.zig b/src/http/context.zig index 60dee37..923e64d 100644 --- a/src/http/context.zig +++ b/src/http/context.zig @@ -1,23 +1,21 @@ const std = @import("std"); -const Request = @import("request.zig").Request; -const Response = @import("response.zig").Response; -const Runtime = @import("tardy").Runtime; +const Io = std.Io; +const Runtime = @import("tardy").Runtime; const secsock = @import("secsock"); const SecureSocket = secsock.SecureSocket; -const Capture = @import("router/routing_trie.zig").Capture; - -const TypedStorage = @import("../core/typed_storage.zig").TypedStorage; const AnyCaseStringMap = @import("../core/any_case_string_map.zig").AnyCaseStringMap; +const TypedStorage = @import("../core/typed_storage.zig").TypedStorage; +const Request = @import("request.zig").Request; +const Response = @import("response.zig").Response; +const Capture = @import("router/routing_trie.zig").Capture; /// HTTP Context. Contains all of the various information /// that will persist throughout the lifetime of this Request/Response. pub const Context = struct { allocator: std.mem.Allocator, - /// Not safe to access unless you are manually sending the headers - /// and returning the .responded variant of Respond. - header_buffer: *std.ArrayList(u8), + header_writer: *Io.Writer, runtime: *Runtime, /// The Request that triggered this handler. request: *const Request, diff --git a/src/http/cookie.zig b/src/http/cookie.zig index 352da8f..f4c513f 100644 --- a/src/http/cookie.zig +++ b/src/http/cookie.zig @@ -1,4 +1,7 @@ const std = @import("std"); +const testing = std.testing; +const Io = std.Io; + const Date = @import("date.zig").Date; pub const Cookie = struct { @@ -34,15 +37,14 @@ pub const Cookie = struct { }; pub fn to_string_buf(self: Cookie, buf: []u8) ![]const u8 { - var list = std.ArrayListUnmanaged(u8).initBuffer(buf); - const writer = list.fixedWriter(); + const writer: std.Io.Writer = .fixed(buf); try writer.print("{s}={s}", .{ self.name, self.value }); if (self.domain) |domain| try writer.print("; Domain={s}", .{domain}); if (self.path) |path| try writer.print("; Path={s}", .{path}); if (self.expires) |exp| { try writer.writeAll("; Expires="); - try exp.to_http_date().into_writer(writer); + try exp.to_http_date().into_writer(&writer); } if (self.max_age) |age| try writer.print("; Max-Age={d}", .{age}); if (self.same_site) |same_site| try writer.print( @@ -52,13 +54,13 @@ pub const Cookie = struct { if (self.secure) try writer.writeAll("; Secure"); if (self.http_only) try writer.writeAll("; HttpOnly"); - return list.items; + return writer.buffered(); } pub fn to_string_alloc(self: Cookie, allocator: std.mem.Allocator) ![]const u8 { - var list = try std.ArrayListUnmanaged(u8).initCapacity(allocator, 128); - errdefer list.deinit(allocator); - const writer = list.writer(allocator); + var aw: Io.Writer.Allocating = try .initCapacity(allocator, 128); + errdefer aw.deinit(); + const writer = &aw.writer; try writer.print("{s}={s}", .{ self.name, self.value }); if (self.domain) |domain| try writer.print("; Domain={s}", .{domain}); @@ -75,7 +77,7 @@ pub const Cookie = struct { if (self.secure) try writer.writeAll("; Secure"); if (self.http_only) try writer.writeAll("; HttpOnly"); - return list.toOwnedSlice(allocator); + return try aw.toOwnedSlice(); } }; @@ -86,7 +88,7 @@ pub const CookieMap = struct { pub fn init(allocator: std.mem.Allocator) CookieMap { return .{ .allocator = allocator, - .map = std.StringHashMap([]const u8).init(allocator), + .map = .init(allocator), }; } @@ -144,10 +146,8 @@ pub const CookieMap = struct { } }; -const testing = std.testing; - test "Cookie: Header Parsing" { - var cookie_map = CookieMap.init(testing.allocator); + var cookie_map: CookieMap = .init(testing.allocator); defer cookie_map.deinit(); try cookie_map.parse_from_header("sessionId=abc123; java=slop"); @@ -156,7 +156,7 @@ test "Cookie: Header Parsing" { } test "Cookie: Response Formatting" { - const cookie = Cookie{ + const cookie: Cookie = .{ .name = "session", .value = "abc123", .path = "/", diff --git a/src/http/date.zig b/src/http/date.zig index cf0f0ef..1a1a26e 100644 --- a/src/http/date.zig +++ b/src/http/date.zig @@ -1,5 +1,7 @@ const std = @import("std"); const assert = std.debug.assert; +const testing = std.testing; +const Io = std.Io; const day_names: []const []const u8 = &.{ "Mon", @@ -17,18 +19,18 @@ const Month = struct { }; const months: []const Month = &.{ - Month{ .name = "Jan", .days = 31 }, - Month{ .name = "Feb", .days = 28 }, - Month{ .name = "Mar", .days = 31 }, - Month{ .name = "Apr", .days = 30 }, - Month{ .name = "May", .days = 31 }, - Month{ .name = "Jun", .days = 30 }, - Month{ .name = "Jul", .days = 31 }, - Month{ .name = "Aug", .days = 31 }, - Month{ .name = "Sep", .days = 30 }, - Month{ .name = "Oct", .days = 31 }, - Month{ .name = "Nov", .days = 30 }, - Month{ .name = "Dec", .days = 31 }, + .{ .name = "Jan", .days = 31 }, + .{ .name = "Feb", .days = 28 }, + .{ .name = "Mar", .days = 31 }, + .{ .name = "Apr", .days = 30 }, + .{ .name = "May", .days = 31 }, + .{ .name = "Jun", .days = 30 }, + .{ .name = "Jul", .days = 31 }, + .{ .name = "Aug", .days = 31 }, + .{ .name = "Sep", .days = 30 }, + .{ .name = "Oct", .days = 31 }, + .{ .name = "Nov", .days = 30 }, + .{ .name = "Dec", .days = 31 }, }; pub const Date = struct { @@ -63,8 +65,7 @@ pub const Date = struct { return try std.fmt.allocPrint(allocator, format, date); } - pub fn into_writer(date: HTTPDate, writer: anytype) !void { - assert(std.meta.hasMethod(@TypeOf(writer), "print")); + pub fn into_writer(date: HTTPDate, writer: *Io.Writer) !void { try writer.print(format, date); } }; @@ -72,7 +73,7 @@ pub const Date = struct { ts: i64, pub fn init(ts: i64) Date { - return Date{ .ts = ts }; + return .{ .ts = ts }; } fn is_leap_year(year: i64) bool { @@ -108,7 +109,7 @@ pub const Date = struct { const minute: u8 = @intCast(@mod(@divFloor(remsecs, 60), 60)); const second: u8 = @intCast(@mod(remsecs, 60)); - return HTTPDate{ + return .{ .day_name = day_names[@intCast(week_day)], .day = @intCast(day), .month = months[month].name, @@ -120,19 +121,17 @@ pub const Date = struct { } }; -const testing = std.testing; - test "Parse Basic Date (Buffer)" { const ts = 1727411110; - var date: Date = Date.init(ts); - var buffer = [_]u8{0} ** 29; + var date: Date = .init(ts); + var buffer: [29]u8 = @splat(0); const http_date = date.to_http_date(); try testing.expectEqualStrings("Fri, 27 Sep 2024 04:25:10 GMT", try http_date.into_buf(buffer[0..])); } test "Parse Basic Date (Alloc)" { const ts = 1727464105; - var date: Date = Date.init(ts); + var date: Date = .init(ts); const http_date = date.to_http_date(); const http_string = try http_date.into_alloc(testing.allocator); defer testing.allocator.free(http_string); @@ -141,11 +140,11 @@ test "Parse Basic Date (Alloc)" { test "Parse Basic Date (Writer)" { const ts = 672452112; - var date: Date = Date.init(ts); + var date: Date = .init(ts); const http_date = date.to_http_date(); - var buffer = [_]u8{0} ** 29; - var stream = std.io.fixedBufferStream(buffer[0..]); - try http_date.into_writer(stream.writer()); - const http_string = stream.getWritten(); + var buffer: [29]u8 = @splat(0); + var stream_w: Io.Writer = .fixed(&buffer); + try http_date.into_writer(&stream_w); + const http_string = stream_w.buffered(); try testing.expectEqualStrings("Wed, 24 Apr 1991 00:15:12 GMT", http_string); } diff --git a/src/http/form.zig b/src/http/form.zig index 6cd887e..dc641c3 100644 --- a/src/http/form.zig +++ b/src/http/form.zig @@ -1,11 +1,12 @@ const std = @import("std"); const assert = std.debug.assert; +const testing = std.testing; const AnyCaseStringMap = @import("../core/any_case_string_map.zig").AnyCaseStringMap; const Context = @import("context.zig").Context; pub fn decode_alloc(allocator: std.mem.Allocator, input: []const u8) ![]const u8 { - var list = try std.ArrayListUnmanaged(u8).initCapacity(allocator, input.len); + var list: std.ArrayList(u8) = try .initCapacity(allocator, input.len); defer list.deinit(allocator); var input_index: usize = 0; @@ -98,7 +99,7 @@ fn construct_map_from_body(allocator: std.mem.Allocator, m: *AnyCaseStringMap, b pub fn Form(comptime T: type) type { return struct { pub fn parse(allocator: std.mem.Allocator, ctx: *const Context) !T { - var m = AnyCaseStringMap.init(ctx.allocator); + var m: AnyCaseStringMap = .init(ctx.allocator); defer { var it = m.iterator(); while (it.next()) |entry| { @@ -127,14 +128,12 @@ pub fn Query(comptime T: type) type { }; } -const testing = std.testing; - test "FormData: Parsing from Body" { const UserRole = enum { admin, visitor }; const User = struct { id: u32, name: []const u8, age: u8, role: UserRole }; const body: []const u8 = "id=10&name=John&age=12&role=visitor"; - var m = AnyCaseStringMap.init(testing.allocator); + var m: AnyCaseStringMap = .init(testing.allocator); defer { var it = m.iterator(); while (it.next()) |entry| { @@ -158,7 +157,7 @@ test "FormData: Parsing Missing Fields" { const User = struct { id: u32, name: []const u8, age: u8 }; const body: []const u8 = "id=10"; - var m = AnyCaseStringMap.init(testing.allocator); + var m: AnyCaseStringMap = .init(testing.allocator); defer { var it = m.iterator(); while (it.next()) |entry| { @@ -177,7 +176,7 @@ test "FormData: Parsing Missing Fields" { test "FormData: Parsing Missing Value" { const body: []const u8 = "abc=abc&id="; - var m = AnyCaseStringMap.init(testing.allocator); + var m: AnyCaseStringMap = .init(testing.allocator); defer { var it = m.iterator(); while (it.next()) |entry| { diff --git a/src/http/lib.zig b/src/http/lib.zig index 5b2b3f3..148534e 100644 --- a/src/http/lib.zig +++ b/src/http/lib.zig @@ -1,32 +1,26 @@ -pub const Status = @import("status.zig").Status; -pub const Method = @import("method.zig").Method; -pub const Request = @import("request.zig").Request; -pub const Response = @import("response.zig").Response; -pub const Respond = @import("response.zig").Respond; -pub const Mime = @import("mime.zig").Mime; -pub const Encoding = @import("encoding.zig").Encoding; -pub const Date = @import("date.zig").Date; +pub const Context = @import("context.zig").Context; pub const Cookie = @import("cookie.zig").Cookie; - +pub const Date = @import("date.zig").Date; +pub const Encoding = @import("encoding.zig").Encoding; pub const Form = @import("form.zig").Form; +pub const Method = @import("method.zig").Method; +pub const Middlewares = @import("middlewares/lib.zig"); +pub const Mime = @import("mime.zig").Mime; pub const Query = @import("form.zig").Query; - -pub const Context = @import("context.zig").Context; - +pub const Request = @import("request.zig").Request; +pub const Respond = @import("response.zig").Respond; +pub const Response = @import("response.zig").Response; pub const Router = @import("router.zig").Router; -pub const Route = @import("router/route.zig").Route; -pub const SSE = @import("sse.zig").SSE; - +pub const FsDir = @import("router/fs_dir.zig").FsDir; pub const Layer = @import("router/middleware.zig").Layer; pub const Middleware = @import("router/middleware.zig").Middleware; pub const MiddlewareFn = @import("router/middleware.zig").MiddlewareFn; pub const Next = @import("router/middleware.zig").Next; -pub const Middlewares = @import("middlewares/lib.zig"); - -pub const FsDir = @import("router/fs_dir.zig").FsDir; - +pub const Route = @import("router/route.zig").Route; pub const Server = @import("server.zig").Server; pub const ServerConfig = @import("server.zig").ServerConfig; +pub const SSE = @import("sse.zig").SSE; +pub const Status = @import("status.zig").Status; pub const HTTPError = error{ TooManyHeaders, diff --git a/src/http/method.zig b/src/http/method.zig index 8302a03..2743c6e 100644 --- a/src/http/method.zig +++ b/src/http/method.zig @@ -1,4 +1,6 @@ const std = @import("std"); +const testing = std.testing; + const log = std.log.scoped(.@"zzz/http/method"); pub const Method = enum(u8) { @@ -45,8 +47,6 @@ pub const Method = enum(u8) { } }; -const testing = std.testing; - test "Parsing Strings" { for (std.meta.tags(Method)) |method| { const method_string = @tagName(method); diff --git a/src/http/middlewares/compression.zig b/src/http/middlewares/compression.zig index a2e7935..49ec776 100644 --- a/src/http/middlewares/compression.zig +++ b/src/http/middlewares/compression.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const flate = std.compress.flate; const Respond = @import("../response.zig").Respond; const Middleware = @import("../router/middleware.zig").Middleware; @@ -6,9 +7,9 @@ const Next = @import("../router/middleware.zig").Next; const Layer = @import("../router/middleware.zig").Layer; const TypedMiddlewareFn = @import("../router/middleware.zig").TypedMiddlewareFn; -const Kind = union(enum) { - gzip: std.compress.gzip.Options, -}; +const Kind = union(enum) { gzip: struct { + level: flate.Compress.Level = .default, +} }; /// Compression Middleware. /// @@ -16,23 +17,23 @@ const Kind = union(enum) { /// will properly compress the body and add the proper `Content-Encoding` header. pub fn Compression(comptime compression: Kind) Layer { const func: TypedMiddlewareFn(void) = switch (compression) { - .gzip => |inner| struct { + .gzip => |_| struct { fn gzip_mw(next: *Next, _: void) !Respond { const respond = try next.run(); const response = next.context.response; if (response.body) |body| if (respond == .standard) { - var compressed = try std.ArrayListUnmanaged(u8).initCapacity(next.context.allocator, body.len); - errdefer compressed.deinit(next.context.allocator); + var compressed: std.Io.Writer.Allocating = try .initCapacity(next.context.allocator, body.len); + errdefer compressed.deinit(); - var body_stream = std.io.fixedBufferStream(body); - try std.compress.gzip.compress( - body_stream.reader(), - compressed.writer(next.context.allocator), - inner, - ); + var body_stream: flate.Compress = .init(&compressed.writer, &.{}, .{ + .level = compression.gzip.level, + .container = .gzip, + }); + try body_stream.writer.writeAll(body); + try body_stream.writer.flush(); try response.headers.put("Content-Encoding", "gzip"); - response.body = try compressed.toOwnedSlice(next.context.allocator); + response.body = try compressed.toOwnedSlice(); return .standard; }; diff --git a/src/http/middlewares/lib.zig b/src/http/middlewares/lib.zig index c6077f3..8b52164 100644 --- a/src/http/middlewares/lib.zig +++ b/src/http/middlewares/lib.zig @@ -1,4 +1,3 @@ pub const Compression = @import("compression.zig").Compression; - pub const RateLimitConfig = @import("rate_limit.zig").RateLimitConfig; pub const RateLimiting = @import("rate_limit.zig").RateLimiting; diff --git a/src/http/middlewares/rate_limit.zig b/src/http/middlewares/rate_limit.zig index bea12f8..c0286e6 100644 --- a/src/http/middlewares/rate_limit.zig +++ b/src/http/middlewares/rate_limit.zig @@ -53,10 +53,10 @@ pub const RateLimitConfig = struct { max_tokens: u16, response_on_limited: ?Respond, ) RateLimitConfig { - const map = std.AutoHashMap(u128, Bucket).init(allocator); - const respond = response_on_limited orelse Response.Fields{ + const map: std.AutoHashMap(u128, Bucket) = .init(allocator); + const respond = response_on_limited orelse .{ .status = .@"Too Many Requests", - .mime = Mime.TEXT, + .mime = .TEXT, .body = "", }; diff --git a/src/http/mime.zig b/src/http/mime.zig index 34adb56..3dcc85c 100644 --- a/src/http/mime.zig +++ b/src/http/mime.zig @@ -1,5 +1,6 @@ const std = @import("std"); const assert = std.debug.assert; +const testing = std.testing; const Pair = @import("../core/lib.zig").Pair; @@ -144,7 +145,7 @@ const all_mime_types = blk: { break :blk return_mimes; }; -const mime_extension_map = blk: { +const mime_extension_map: std.StaticStringMap(Mime) = blk: { const num_pairs = num: { var count: usize = 0; for (all_mime_types) |mime| { @@ -177,10 +178,10 @@ const mime_extension_map = blk: { } } - break :blk std.StaticStringMap(Mime).initComptime(pairs); + break :blk .initComptime(pairs); }; -const mime_content_map = blk: { +const mime_content_map: std.StaticStringMap(Mime) = blk: { const num_pairs = num: { var count: usize = 0; for (all_mime_types) |mime| { @@ -213,11 +214,9 @@ const mime_content_map = blk: { } } - break :blk std.StaticStringMap(Mime).initComptime(pairs); + break :blk .initComptime(pairs); }; -const testing = std.testing; - test "MIME from extensions" { for (all_mime_types) |mime| { switch (mime.extension) { diff --git a/src/http/request.zig b/src/http/request.zig index c74e1f0..9fa1ba4 100644 --- a/src/http/request.zig +++ b/src/http/request.zig @@ -1,12 +1,14 @@ const std = @import("std"); -const log = std.log.scoped(.@"zzz/http/request"); const assert = std.debug.assert; +const testing = std.testing; const AnyCaseStringMap = @import("../core/any_case_string_map.zig").AnyCaseStringMap; const CookieMap = @import("cookie.zig").CookieMap; const HTTPError = @import("lib.zig").HTTPError; const Method = @import("lib.zig").Method; +const log = std.log.scoped(.@"zzz/http/request"); + pub const Request = struct { allocator: std.mem.Allocator, method: ?Method = null, @@ -18,10 +20,10 @@ pub const Request = struct { /// This is for constructing a Request. pub fn init(allocator: std.mem.Allocator) Request { - const headers = AnyCaseStringMap.init(allocator); - const cookies = CookieMap.init(allocator); + const headers: AnyCaseStringMap = .init(allocator); + const cookies: CookieMap = .init(allocator); - return Request{ + return .{ .allocator = allocator, .headers = headers, .cookies = cookies, @@ -124,8 +126,6 @@ pub const Request = struct { } }; -const testing = std.testing; - test "Parse Request" { const request_text = \\GET / HTTP/1.1 @@ -159,8 +159,9 @@ test "Expect ContentTooLong Error" { \\Accept: text/html ; - const request_text = std.fmt.comptimePrint(request_text_format, .{[_]u8{'a'} ** 4096}); - var request = Request.init(testing.allocator); + const large_content: [4096]u8 = @splat('a'); + const request_text = std.fmt.comptimePrint(request_text_format, .{large_content}); + var request: Request = .init(testing.allocator); defer request.deinit(); const err = request.parse_headers(request_text[0..], .{ @@ -178,8 +179,9 @@ test "Expect URITooLong Error" { \\Accept: text/html ; - const request_text = std.fmt.comptimePrint(request_text_format, .{[_]u8{'a'} ** 4096}); - var request = Request.init(testing.allocator); + const large_content: [4096]u8 = @splat('a'); + const request_text = std.fmt.comptimePrint(request_text_format, .{large_content[0..]}); + var request: Request = .init(testing.allocator); defer request.deinit(); const err = request.parse_headers(request_text[0..], .{ @@ -196,9 +198,9 @@ test "Expect Malformed when URI missing /" { \\Connection: keep-alive \\Accept: text/html ; - - const request_text = std.fmt.comptimePrint(request_text_format, .{[_]u8{'a'} ** 256}); - var request = Request.init(testing.allocator); + const content: [256]u8 = @splat('a'); + const request_text = std.fmt.comptimePrint(request_text_format, .{content[0..]}); + var request: Request = .init(testing.allocator); defer request.deinit(); const err = request.parse_headers(request_text[0..], .{ @@ -216,7 +218,7 @@ test "Expect Incorrect HTTP Version" { \\Accept: text/html ; - var request = Request.init(testing.allocator); + var request: Request = .init(testing.allocator); defer request.deinit(); const err = request.parse_headers(request_text[0..], .{ @@ -234,7 +236,7 @@ test "Malformed AnyCaseStringMap" { \\Accept: text/html ; - var request = Request.init(testing.allocator); + var request: Request = .init(testing.allocator); defer request.deinit(); const err = request.parse_headers(request_text[0..], .{ diff --git a/src/http/response.zig b/src/http/response.zig index 5f42a2a..a7a606f 100644 --- a/src/http/response.zig +++ b/src/http/response.zig @@ -1,12 +1,13 @@ const std = @import("std"); const assert = std.debug.assert; +const Io = std.Io; + +const Stream = @import("tardy").Stream; const AnyCaseStringMap = @import("../core/any_case_string_map.zig").AnyCaseStringMap; -const Status = @import("lib.zig").Status; -const Mime = @import("lib.zig").Mime; const Date = @import("lib.zig").Date; - -const Stream = @import("tardy").Stream; +const Mime = @import("lib.zig").Mime; +const Status = @import("lib.zig").Status; pub const Respond = enum { // When we are returning a real HTTP request, we use this. @@ -31,8 +32,8 @@ pub const Response = struct { }; pub fn init(allocator: std.mem.Allocator) Response { - const headers = AnyCaseStringMap.init(allocator); - return Response{ .headers = headers }; + const headers: AnyCaseStringMap = .init(allocator); + return .{ .headers = headers }; } pub fn deinit(self: *Response) void { @@ -54,7 +55,7 @@ pub const Response = struct { self.headers.clearRetainingCapacity(); } - pub fn headers_into_writer(self: *Response, writer: anytype, content_length: ?usize) !void { + pub fn headers_into_writer(self: *Response, writer: *Io.Writer, content_length: ?usize) !void { // Status Line const status = self.status.?; try writer.print("HTTP/1.1 {d} {s}\r\n", .{ @intFromEnum(status), @tagName(status) }); diff --git a/src/http/router.zig b/src/http/router.zig index 91b79c9..e68caea 100644 --- a/src/http/router.zig +++ b/src/http/router.zig @@ -1,22 +1,20 @@ const std = @import("std"); -const log = std.log.scoped(.@"zzz/http/router"); const assert = std.debug.assert; +const AnyCaseStringMap = @import("../core/any_case_string_map.zig").AnyCaseStringMap; +const Context = @import("context.zig").Context; +const Mime = @import("mime.zig").Mime; +const Request = @import("request.zig").Request; +const Respond = @import("response.zig").Respond; +const Response = @import("response.zig").Response; const Layer = @import("router/middleware.zig").Layer; const Route = @import("router/route.zig").Route; const TypedHandlerFn = @import("router/route.zig").TypedHandlerFn; - const Bundle = @import("router/routing_trie.zig").Bundle; - const Capture = @import("router/routing_trie.zig").Capture; -const Request = @import("request.zig").Request; -const Response = @import("response.zig").Response; -const Respond = @import("response.zig").Respond; -const Mime = @import("mime.zig").Mime; -const Context = @import("context.zig").Context; - const RoutingTrie = @import("router/routing_trie.zig").RoutingTrie; -const AnyCaseStringMap = @import("../core/any_case_string_map.zig").AnyCaseStringMap; + +const log = std.log.scoped(.@"zzz/http/router"); /// Default not found handler: send a plain text response. pub const default_not_found_handler = struct { @@ -45,12 +43,10 @@ pub const Router = struct { layers: []const Layer, configuration: Configuration, ) !Router { - const self = Router{ - .routes = try RoutingTrie.init(allocator, layers), + return .{ + .routes = try .init(allocator, layers), .configuration = configuration, }; - - return self; } pub fn deinit(self: *Router, allocator: std.mem.Allocator) void { @@ -66,7 +62,7 @@ pub const Router = struct { ) !Bundle { queries.clearRetainingCapacity(); - return try self.routes.get_bundle(allocator, path, captures, queries) orelse Bundle{ + return try self.routes.get_bundle(allocator, path, captures, queries) orelse .{ .route = Route.init("").all({}, self.configuration.not_found), .captures = captures[0..], .queries = queries, diff --git a/src/http/router/fs_dir.zig b/src/http/router/fs_dir.zig index b9b8ee5..a6d76b5 100644 --- a/src/http/router/fs_dir.zig +++ b/src/http/router/fs_dir.zig @@ -1,20 +1,20 @@ const std = @import("std"); -const log = std.log.scoped(.@"zzz/http/router"); const assert = std.debug.assert; -const Route = @import("route.zig").Route; -const Layer = @import("middleware.zig").Layer; -const Request = @import("../request.zig").Request; -const Respond = @import("../response.zig").Respond; -const Mime = @import("../mime.zig").Mime; -const Context = @import("../context.zig").Context; - -const Runtime = @import("tardy").Runtime; -const ZeroCopy = @import("tardy").ZeroCopy; const Dir = @import("tardy").Dir; +const Runtime = @import("tardy").Runtime; const Stat = @import("tardy").Stat; - const Stream = @import("tardy").Stream; +const ZeroCopy = @import("tardy").ZeroCopy; + +const Context = @import("../context.zig").Context; +const Mime = @import("../mime.zig").Mime; +const Request = @import("../request.zig").Request; +const Respond = @import("../response.zig").Respond; +const Layer = @import("middleware.zig").Layer; +const Route = @import("route.zig").Route; + +const log = std.log.scoped(.@"zzz/http/router"); pub const FsDir = struct { fn fs_dir_handler(ctx: *const Context, dir: Dir) !Respond { @@ -45,14 +45,14 @@ pub const FsDir = struct { error.NotFound => { return ctx.response.apply(.{ .status = .@"Not Found", - .mime = Mime.HTML, + .mime = .HTML, }); }, else => return e, }; const stat = try file.stat(ctx.runtime); - var hash = std.hash.Wyhash.init(0); + var hash: std.hash.Wyhash = .init(0); hash.update(std.mem.asBytes(&stat.size)); if (stat.modified) |modified| { hash.update(std.mem.asBytes(&modified.seconds)); @@ -69,7 +69,7 @@ pub const FsDir = struct { // If the ETag matches. return ctx.response.apply(.{ .status = .@"Not Modified", - .mime = Mime.HTML, + .mime = .HTML, }); } } @@ -78,12 +78,16 @@ pub const FsDir = struct { response.status = .OK; response.mime = mime; - try response.headers_into_writer(ctx.header_buffer.writer(), stat.size); - const headers = ctx.header_buffer.items; + response.headers_into_writer(ctx.header_writer, stat.size) catch |err| switch (err) { + error.WriteFailed => return error.ExceededMaxHttpHeaderSize, + else => unreachable, + }; + const headers = ctx.header_writer.buffered(); const length = try ctx.socket.send_all(ctx.runtime, headers); if (headers.len != length) return error.SendingHeadersFailed; - var buffer = ctx.header_buffer.allocatedSlice(); + // Reuse the header_buffer for file read/send + var buffer = ctx.header_writer.buffer[0..]; while (true) { const read_count = file.read(ctx.runtime, buffer, null) catch |e| switch (e) { error.EndOfFile => break, @@ -105,6 +109,7 @@ pub const FsDir = struct { "{s}/%r", .{std.mem.trimRight(u8, url_path, "/")}, ); + log.debug("url with match {s}", .{url_with_match_all}); return Route.init(url_with_match_all).get(dir, fs_dir_handler).layer(); } diff --git a/src/http/router/middleware.zig b/src/http/router/middleware.zig index 191d10c..79e21f0 100644 --- a/src/http/router/middleware.zig +++ b/src/http/router/middleware.zig @@ -1,18 +1,18 @@ const std = @import("std"); -const log = std.log.scoped(.@"zzz/router/middleware"); const assert = std.debug.assert; const Runtime = @import("tardy").Runtime; -const wrap = @import("../../core/wrapping.zig").wrap; const Pseudoslice = @import("../../core/pseudoslice.zig").Pseudoslice; -const Server = @import("../server.zig").Server; - -const Mime = @import("../mime.zig").Mime; -const Route = @import("route.zig").Route; -const HandlerWithData = @import("route.zig").HandlerWithData; +const wrap = @import("../../core/wrapping.zig").wrap; const Context = @import("../context.zig").Context; +const Mime = @import("../mime.zig").Mime; const Respond = @import("../response.zig").Respond; +const Server = @import("../server.zig").Server; +const HandlerWithData = @import("route.zig").HandlerWithData; +const Route = @import("route.zig").Route; + +const log = std.log.scoped(.@"zzz/router/middleware"); pub const Layer = union(enum) { /// Route diff --git a/src/http/router/route.zig b/src/http/router/route.zig index f29d2c0..5004d08 100644 --- a/src/http/router/route.zig +++ b/src/http/router/route.zig @@ -1,24 +1,23 @@ const std = @import("std"); -const builtin = @import("builtin"); -const log = std.log.scoped(.@"zzz/http/route"); const assert = std.debug.assert; +const builtin = @import("builtin"); const wrap = @import("../../core/wrapping.zig").wrap; - +const Context = @import("../context.zig").Context; +const Encoding = @import("../encoding.zig").Encoding; const Method = @import("../method.zig").Method; +const Mime = @import("../mime.zig").Mime; const Request = @import("../request.zig").Request; const Response = @import("../response.zig").Response; const Respond = @import("../response.zig").Respond; -const Mime = @import("../mime.zig").Mime; -const Encoding = @import("../encoding.zig").Encoding; - const FsDir = @import("fs_dir.zig").FsDir; -const Context = @import("../context.zig").Context; const Layer = @import("middleware.zig").Layer; - const MiddlewareWithData = @import("middleware.zig").MiddlewareWithData; +const log = std.log.scoped(.@"zzz/http/route"); + pub const HandlerFn = *const fn (*const Context, usize) anyerror!Respond; + pub fn TypedHandlerFn(comptime T: type) type { return *const fn (*const Context, T) anyerror!Respond; } diff --git a/src/http/router/routing_trie.zig b/src/http/router/routing_trie.zig index c59ec33..d54495d 100644 --- a/src/http/router/routing_trie.zig +++ b/src/http/router/routing_trie.zig @@ -1,18 +1,17 @@ const std = @import("std"); const assert = std.debug.assert; -const log = std.log.scoped(.@"zzz/http/routing_trie"); - -const Layer = @import("middleware.zig").Layer; -const Route = @import("route.zig").Route; +const testing = std.testing; -const Respond = @import("../response.zig").Respond; +const AnyCaseStringMap = @import("../../core/any_case_string_map.zig").AnyCaseStringMap; +const decode_alloc = @import("../form.zig").decode_alloc; const Context = @import("../lib.zig").Context; - +const Respond = @import("../response.zig").Respond; const HandlerWithData = @import("route.zig").HandlerWithData; +const Layer = @import("middleware.zig").Layer; const MiddlewareWithData = @import("middleware.zig").MiddlewareWithData; -const AnyCaseStringMap = @import("../../core/any_case_string_map.zig").AnyCaseStringMap; +const Route = @import("route.zig").Route; -const decode_alloc = @import("../form.zig").decode_alloc; +const log = std.log.scoped(.@"zzz/http/routing_trie"); fn TokenHashMap(comptime V: type) type { return std.HashMap(Token, V, struct { @@ -136,7 +135,7 @@ pub const RoutingTrie = struct { return .{ .token = token, .route = route, - .children = TokenHashMap(Node).init(allocator), + .children = .init(allocator), }; } @@ -152,13 +151,13 @@ pub const RoutingTrie = struct { }; root: Node, - middlewares: std.ArrayListUnmanaged(MiddlewareWithData), + middlewares: std.ArrayList(MiddlewareWithData), /// Initialize the routing tree with the given routes. pub fn init(allocator: std.mem.Allocator, layers: []const Layer) !Self { var self: Self = .{ - .root = Node.init(allocator, .{ .fragment = "" }, null), - .middlewares = try std.ArrayListUnmanaged(MiddlewareWithData).initCapacity(allocator, 0), + .root = .init(allocator, .{ .fragment = "" }, null), + .middlewares = .empty, }; for (layers) |layer| { @@ -168,7 +167,7 @@ pub const RoutingTrie = struct { var iter = std.mem.tokenizeScalar(u8, route.path, '/'); while (iter.next()) |chunk| { - const token: Token = Token.parse_chunk(chunk); + const token: Token = .parse_chunk(chunk); if (current.children.getPtr(token)) |child| { current = child; } else { @@ -183,7 +182,7 @@ pub const RoutingTrie = struct { }; for (route.handlers, 0..) |handler, i| if (handler) |h| { - r.handlers[i] = HandlerWithData{ + r.handlers[i] = .{ .handler = h.handler, .middlewares = self.middlewares.items, .data = h.data, @@ -229,21 +228,21 @@ pub const RoutingTrie = struct { .match => |kind| { switch (kind) { .signed => if (std.fmt.parseInt(i64, chunk, 10)) |value| { - captures[capture_idx] = Capture{ .signed = value }; + captures[capture_idx] = .{ .signed = value }; } else |_| continue :child_loop, .unsigned => if (std.fmt.parseInt(u64, chunk, 10)) |value| { - captures[capture_idx] = Capture{ .unsigned = value }; + captures[capture_idx] = .{ .unsigned = value }; } else |_| continue :child_loop, // Float types MUST have a '.' to differentiate them. .float => if (std.mem.indexOfScalar(u8, chunk, '.')) |_| { if (std.fmt.parseFloat(f64, chunk)) |value| { - captures[capture_idx] = Capture{ .float = value }; + captures[capture_idx] = .{ .float = value }; } else |_| continue :child_loop; } else continue :child_loop, - .string => captures[capture_idx] = Capture{ .string = chunk }, + .string => captures[capture_idx] = .{ .string = chunk }, .remaining => { const rest = iter.buffer[(iter.index - chunk.len)..]; - captures[capture_idx] = Capture{ .remaining = rest }; + captures[capture_idx] = .{ .remaining = rest }; current = child; capture_idx += 1; @@ -264,7 +263,7 @@ pub const RoutingTrie = struct { return null; } - var duped = try std.ArrayListUnmanaged([]const u8).initCapacity(allocator, 0); + var duped: std.ArrayList([]const u8) = .empty; defer duped.deinit(allocator); errdefer for (duped.items) |d| allocator.free(d); @@ -293,7 +292,7 @@ pub const RoutingTrie = struct { } } - return Bundle{ + return .{ .route = current.route orelse return null, .captures = captures[0..capture_idx], .queries = queries, @@ -302,11 +301,9 @@ pub const RoutingTrie = struct { } }; -const testing = std.testing; - test "Chunk Parsing (Fragment)" { const chunk = "thisIsAFragment"; - const token: Token = Token.parse_chunk(chunk); + const token: Token = .parse_chunk(chunk); switch (token) { .fragment => |inner| try testing.expectEqualStrings(chunk, inner), @@ -323,7 +320,7 @@ test "Chunk Parsing (Match)" { "%s", }; - const matches = [_]TokenMatch{ + const matches: [5]TokenMatch = .{ TokenMatch.signed, TokenMatch.signed, TokenMatch.unsigned, @@ -332,7 +329,7 @@ test "Chunk Parsing (Match)" { }; for (chunks, matches) |chunk, match| { - const token: Token = Token.parse_chunk(chunk); + const token: Token = .parse_chunk(chunk); switch (token) { .fragment => return error.IncorrectTokenParsing, @@ -353,7 +350,7 @@ test "Path Parsing (Mixed)" { var iter = std.mem.tokenizeScalar(u8, path, '/'); for (parsed) |expected| { - const token = Token.parse_chunk(iter.next().?); + const token: Token = .parse_chunk(iter.next().?); switch (token) { .fragment => |inner| try testing.expectEqualStrings(expected.fragment, inner), .match => |inner| try testing.expectEqual(expected.match, inner), @@ -362,7 +359,7 @@ test "Path Parsing (Mixed)" { } test "Constructing Routing from Path" { - var s = try RoutingTrie.init(testing.allocator, &.{ + var s: RoutingTrie = try .init(testing.allocator, &.{ Route.init("/item").layer(), Route.init("/item/%i/description").layer(), Route.init("/item/%i/hello").layer(), @@ -376,7 +373,7 @@ test "Constructing Routing from Path" { } test "Routing with Paths" { - var s = try RoutingTrie.init(testing.allocator, &.{ + var s: RoutingTrie = try .init(testing.allocator, &.{ Route.init("/item").layer(), Route.init("/item/%i/description").layer(), Route.init("/item/%i/hello").layer(), @@ -386,10 +383,10 @@ test "Routing with Paths" { }); defer s.deinit(testing.allocator); - var q = AnyCaseStringMap.init(testing.allocator); + var q: AnyCaseStringMap = .init(testing.allocator); defer q.deinit(); - var captures: [8]Capture = [_]Capture{undefined} ** 8; + var captures: [8]Capture = @splat(undefined); try testing.expectEqual(null, try s.get_bundle(testing.allocator, "/item/name", captures[0..], &q)); @@ -409,7 +406,7 @@ test "Routing with Paths" { } test "Routing with Remaining" { - var s = try RoutingTrie.init(testing.allocator, &.{ + var s: RoutingTrie = try .init(testing.allocator, &.{ Route.init("/item").layer(), Route.init("/item/%f/price_float").layer(), Route.init("/item/name/%r").layer(), @@ -417,10 +414,10 @@ test "Routing with Remaining" { }); defer s.deinit(testing.allocator); - var q = AnyCaseStringMap.init(testing.allocator); + var q: AnyCaseStringMap = .init(testing.allocator); defer q.deinit(); - var captures: [8]Capture = [_]Capture{undefined} ** 8; + var captures: [8]Capture = @splat(undefined); try testing.expectEqual(null, try s.get_bundle(testing.allocator, "/item/name", captures[0..], &q)); @@ -465,7 +462,7 @@ test "Routing with Remaining" { } test "Routing with Queries" { - var s = try RoutingTrie.init(testing.allocator, &.{ + var s: RoutingTrie = try .init(testing.allocator, &.{ Route.init("/item").layer(), Route.init("/item/%f/price_float").layer(), Route.init("/item/name/%r").layer(), @@ -473,10 +470,10 @@ test "Routing with Queries" { }); defer s.deinit(testing.allocator); - var q = AnyCaseStringMap.init(testing.allocator); + var q: AnyCaseStringMap = .init(testing.allocator); defer q.deinit(); - var captures: [8]Capture = [_]Capture{undefined} ** 8; + var captures: [8]Capture = @splat(undefined); try testing.expectEqual(null, try s.get_bundle( testing.allocator, diff --git a/src/http/server.zig b/src/http/server.zig index d3432db..329f190 100644 --- a/src/http/server.zig +++ b/src/http/server.zig @@ -1,47 +1,42 @@ const std = @import("std"); +const assert = std.debug.assert; +const Io = std.Io; const builtin = @import("builtin"); const tag = builtin.os.tag; -const assert = std.debug.assert; -const log = std.log.scoped(.@"zzz/http/server"); -const TypedStorage = @import("../core/typed_storage.zig").TypedStorage; -const Pseudoslice = @import("../core/pseudoslice.zig").Pseudoslice; -const AnyCaseStringMap = @import("../core/any_case_string_map.zig").AnyCaseStringMap; +const AcceptResult = @import("tardy").AcceptResult; +const Cross = @import("tardy").Cross; +const Pool = @import("tardy").Pool; +const PoolKind = @import("tardy").PoolKind; +const RecvResult = @import("tardy").RecvResult; +pub const Runtime = @import("tardy").Runtime; +const secsock = @import("secsock"); +const SecureSocket = secsock.SecureSocket; +const SendResult = @import("tardy").SendResult; +const Socket = @import("tardy").Socket; +const TardyCreator = @import("tardy").Tardy; +pub const Task = @import("tardy").Task; +const ZeroCopy = @import("tardy").ZeroCopy; +const AnyCaseStringMap = @import("../core/any_case_string_map.zig").AnyCaseStringMap; +const Pseudoslice = @import("../core/pseudoslice.zig").Pseudoslice; +const TypedStorage = @import("../core/typed_storage.zig").TypedStorage; const Context = @import("context.zig").Context; +const HTTPError = @import("lib.zig").HTTPError; +const Mime = @import("mime.zig").Mime; const Request = @import("request.zig").Request; -const Response = @import("response.zig").Response; const Respond = @import("response.zig").Respond; -const Capture = @import("router/routing_trie.zig").Capture; -const SSE = @import("sse.zig").SSE; - -const Mime = @import("mime.zig").Mime; +const Response = @import("response.zig").Response; const Router = @import("router.zig").Router; -const Route = @import("router/route.zig").Route; const Layer = @import("router/middleware.zig").Layer; const Middleware = @import("router/middleware.zig").Middleware; -const HTTPError = @import("lib.zig").HTTPError; - -const HandlerWithData = @import("router/route.zig").HandlerWithData; - const Next = @import("router/middleware.zig").Next; +const Route = @import("router/route.zig").Route; +const HandlerWithData = @import("router/route.zig").HandlerWithData; +const Capture = @import("router/routing_trie.zig").Capture; +const SSE = @import("sse.zig").SSE; -pub const Runtime = @import("tardy").Runtime; -pub const Task = @import("tardy").Task; -const TardyCreator = @import("tardy").Tardy; - -const Cross = @import("tardy").Cross; -const Pool = @import("tardy").Pool; -const PoolKind = @import("tardy").PoolKind; -const Socket = @import("tardy").Socket; -const ZeroCopy = @import("tardy").ZeroCopy; - -const AcceptResult = @import("tardy").AcceptResult; -const RecvResult = @import("tardy").RecvResult; -const SendResult = @import("tardy").SendResult; - -const secsock = @import("secsock"); -const SecureSocket = secsock.SecureSocket; +const log = std.log.scoped(.@"zzz/http/server"); pub const TLSFileOptions = union(enum) { buffer: []const u8, @@ -128,7 +123,7 @@ pub const Provision = struct { initalized: bool = false, recv_slice: []u8, zc_recv_buffer: ZeroCopy(u8), - header_buffer: std.ArrayList(u8), + header_writer: Io.Writer, arena: std.heap.ArenaAllocator, storage: TypedStorage, captures: []Capture, @@ -173,7 +168,7 @@ pub const Server = struct { provision.response.clear(); provision.storage.clear(); provision.zc_recv_buffer.clear_retaining_capacity(); - provision.header_buffer.clearRetainingCapacity(); + _ = provision.header_writer.consumeAll(); _ = provision.arena.reset(.{ .retain_with_limit = config.connection_arena_bytes_retain }); provision.recv_slice = try provision.zc_recv_buffer.get_write_area(config.socket_buffer_bytes); @@ -235,15 +230,16 @@ pub const Server = struct { provision.zc_recv_buffer = ZeroCopy(u8).init(rt.allocator, config.socket_buffer_bytes) catch { @panic("attempting to allocate more memory than available. (ZeroCopyBuffer)"); }; - provision.header_buffer = std.ArrayList(u8).init(rt.allocator); - provision.arena = std.heap.ArenaAllocator.init(rt.allocator); + provision.arena = .init(rt.allocator); + // TODO: use a server config option + provision.header_writer = .fixed(try provision.arena.allocator().alloc(u8, 8 * 1024)); provision.captures = rt.allocator.alloc(Capture, config.capture_count_max) catch { @panic("attempting to allocate more memory than available. (Captures)"); }; - provision.queries = AnyCaseStringMap.init(rt.allocator); - provision.storage = TypedStorage.init(rt.allocator); - provision.request = Request.init(rt.allocator); - provision.response = Response.init(rt.allocator); + provision.queries = .init(rt.allocator); + provision.storage = .init(rt.allocator); + provision.request = .init(rt.allocator); + provision.response = .init(rt.allocator); provision.initalized = true; } defer prepare_new_request(null, provision, config) catch unreachable; @@ -287,7 +283,7 @@ pub const Server = struct { }, ); - log.info("rt{d} - \"{s} {s}\" {s} ({})", .{ + log.info("rt{d} - \"{s} {s}\" {s} ({f})", .{ rt.id, @tagName(provision.request.method.?), provision.request.uri.?, @@ -361,7 +357,7 @@ pub const Server = struct { const context: Context = .{ .runtime = rt, .allocator = provision.arena.allocator(), - .header_buffer = &provision.header_buffer, + .header_writer = &provision.header_writer, .request = &provision.request, .response = &provision.response, .storage = &provision.storage, @@ -377,7 +373,7 @@ pub const Server = struct { }; const next_respond: Respond = next.run() catch |e| blk: { - log.warn("rt{d} - \"{s} {s}\" {} ({})", .{ + log.warn("rt{d} - \"{s} {s}\" {} ({f})", .{ rt.id, @tagName(provision.request.method.?), provision.request.uri.?, @@ -391,7 +387,7 @@ pub const Server = struct { break :blk try provision.response.apply(.{ .status = .@"Internal Server Error", - .mime = Mime.TEXT, + .mime = .TEXT, .body = body, }); }; @@ -423,11 +419,11 @@ pub const Server = struct { const body = provision.response.body orelse ""; const content_length = body.len; - try provision.response.headers_into_writer(provision.header_buffer.writer(), content_length); - const headers = provision.header_buffer.items; + try provision.response.headers_into_writer(&provision.header_writer, content_length); + const headers = provision.header_writer.buffered(); var sent: usize = 0; - const pseudo = Pseudoslice.init(headers, body, provision.recv_slice); + const pseudo: Pseudoslice = .init(headers, body, provision.recv_slice); while (sent < pseudo.len) { const send_slice = pseudo.get(sent, sent + provision.recv_slice.len); @@ -455,7 +451,7 @@ pub const Server = struct { }, }; - log.info("connection ({}) closed", .{secure.socket.addr}); + log.info("connection ({f}) closed", .{secure.socket.addr}); if (!accept_queued.*) { try rt.spawn( @@ -477,7 +473,7 @@ pub const Server = struct { log.info("security mode: {s}", .{@tagName(sock)}); const secure: SecureSocket = switch (sock) { - .normal => |s| SecureSocket.unsecured(s), + .normal => |s| .unsecured(s), .secure => |sec| sec, }; @@ -485,7 +481,7 @@ pub const Server = struct { const pooling: PoolKind = if (self.config.connection_count_max == null) .grow else .static; const provision_pool = try rt.allocator.create(Pool(Provision)); - provision_pool.* = try Pool(Provision).init(rt.allocator, count, pooling); + provision_pool.* = try .init(rt.allocator, count, pooling); errdefer rt.allocator.destroy(provision_pool); const connection_count = try rt.allocator.create(usize); @@ -496,6 +492,14 @@ pub const Server = struct { errdefer rt.allocator.destroy(accept_queued); accept_queued.* = true; + // Use a Max Header Size of 8KiB same as Nginx, Tomcat and Httpd but + // consider making this configurable + // https://stackoverflow.com/questions/686217/maximum-on-http-header-values + const max_http_header_size = 1024 * 8; + const pool_header_buffer: []u8 = try rt.allocator.alloc(u8, count * max_http_header_size); + errdefer rt.allocator.free(pool_header_buffer); + var next_header_buffer_index: usize = 0; + // initialize first batch of provisions :) for (provision_pool.items) |*provision| { provision.initalized = true; @@ -505,15 +509,17 @@ pub const Server = struct { ) catch { @panic("attempting to allocate more memory than available. (ZeroCopy)"); }; - provision.header_buffer = std.ArrayList(u8).init(rt.allocator); - provision.arena = std.heap.ArenaAllocator.init(rt.allocator); + provision.header_writer = .fixed(pool_header_buffer[next_header_buffer_index..][0..max_http_header_size]); + next_header_buffer_index += max_http_header_size; + + provision.arena = .init(rt.allocator); provision.captures = rt.allocator.alloc(Capture, self.config.capture_count_max) catch { @panic("attempting to allocate more memory than available. (Captures)"); }; - provision.queries = AnyCaseStringMap.init(rt.allocator); - provision.storage = TypedStorage.init(rt.allocator); - provision.request = Request.init(rt.allocator); - provision.response = Response.init(rt.allocator); + provision.queries = .init(rt.allocator); + provision.storage = .init(rt.allocator); + provision.request = .init(rt.allocator); + provision.response = .init(rt.allocator); } try rt.spawn( diff --git a/src/http/sse.zig b/src/http/sse.zig index d0a6b5d..44c54e1 100644 --- a/src/http/sse.zig +++ b/src/http/sse.zig @@ -1,15 +1,16 @@ const std = @import("std"); +const Writer = std.Io.Writer; -const Pseudoslice = @import("../core/pseudoslice.zig").Pseudoslice; +const Runtime = @import("tardy").Runtime; +const secsock = @import("secsock"); +const SecureSocket = secsock.SecureSocket; -const Provision = @import("server.zig").Provision; +const Pseudoslice = @import("../core/pseudoslice.zig").Pseudoslice; const Context = @import("context.zig").Context; const Mime = @import("mime.zig").Mime; +const Provision = @import("server.zig").Provision; -const Runtime = @import("tardy").Runtime; - -const secsock = @import("secsock"); -const SecureSocket = secsock.SecureSocket; +const log = std.log.scoped(.@"zzz/http/sse"); const SSEMessage = struct { id: ?[]const u8 = null, @@ -20,40 +21,38 @@ const SSEMessage = struct { pub const SSE = struct { socket: SecureSocket, - allocator: std.mem.Allocator, - list: std.ArrayListUnmanaged(u8), + writer: Writer.Allocating, runtime: *Runtime, pub fn init(ctx: *const Context) !SSE { const response = ctx.response; response.status = .OK; - response.mime = Mime{ + response.mime = .{ .content_type = .{ .single = "text/event-stream" }, .extension = .{ .single = "" }, .description = "SSE", }; - var list = try std.ArrayListUnmanaged(u8).initCapacity(ctx.allocator, 0); - errdefer list.deinit(ctx.allocator); + var writer: Writer.Allocating = .init(ctx.allocator); + errdefer writer.deinit(); - try ctx.response.headers_into_writer(ctx.header_buffer.writer(), null); - const headers = ctx.header_buffer.items; + try ctx.response.headers_into_writer(ctx.header_writer, null); + const headers = ctx.header_writer.buffered(); const sent = try ctx.socket.send_all(ctx.runtime, headers); if (sent != headers.len) return error.Closed; return .{ .socket = ctx.socket, - .allocator = ctx.allocator, - .list = list, + .writer = writer, .runtime = ctx.runtime, }; } pub fn send(self: *SSE, message: SSEMessage) !void { - // just reuse the list - defer self.list.clearRetainingCapacity(); - const writer = self.list.writer(self.allocator); + var aw = &self.writer; + defer aw.clearRetainingCapacity(); // reuse the writer + const writer = &aw.writer; if (message.id) |id| try writer.print("id: {s}\n", .{id}); if (message.event) |event| try writer.print("event: {s}\n", .{event}); @@ -64,7 +63,8 @@ pub const SSE = struct { if (message.retry) |retry| try writer.print("retry: {d}\n", .{retry}); try writer.writeByte('\n'); - const sent = try self.socket.send_all(self.runtime, self.list.items); - if (sent != self.list.items.len) return error.Closed; + const written = aw.written(); + const sent = try self.socket.send_all(self.runtime, written); + if (sent != written.len) return error.Closed; } }; diff --git a/src/lib.zig b/src/lib.zig index 96d9dd7..ca7256c 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -1,10 +1,9 @@ const std = @import("std"); -/// Internally exposed Tardy. -pub const tardy = @import("tardy"); - /// Internally exposed secsock. pub const secsock = @import("secsock"); +/// Internally exposed Tardy. +pub const tardy = @import("tardy"); /// HyperText Transfer Protocol. /// Supports: HTTP/1.1