diff --git a/src/Servers/SwerverServer/Dockerfile b/src/Servers/SwerverServer/Dockerfile new file mode 100644 index 0000000..f681460 --- /dev/null +++ b/src/Servers/SwerverServer/Dockerfile @@ -0,0 +1,42 @@ +# Http11Probe target for swerver (https://github.com/justinGrosvenor/swerver), +# a high-performance HTTP/1.1+2+3 server and API gateway in Zig. +# +# Base is Debian trixie (not bookworm like the other targets): swerver's +# HTTP/3 path links OpenSSL's QUIC TLS API (SSL_set_quic_tls_transport_params), +# which is only present in OpenSSL 3.5+ — trixie ships it, bookworm (3.0) does not. +FROM debian:trixie AS build + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates curl xz-utils git libssl-dev zlib1g-dev dpkg-dev pkg-config \ + && rm -rf /var/lib/apt/lists/* + +# Zig 0.16.0 stable +RUN set -eux; \ + ARCH=$(dpkg --print-architecture); \ + case "$ARCH" in amd64) ZA=x86_64 ;; arm64) ZA=aarch64 ;; *) echo "unsupported $ARCH" >&2; exit 1 ;; esac; \ + curl -fsSL --proto '=https' --tlsv1.2 --retry 5 --retry-all-errors --retry-delay 3 --connect-timeout 30 "https://ziglang.org/download/0.16.0/zig-${ZA}-linux-0.16.0.tar.xz" -o /tmp/zig.tar.xz; \ + mkdir -p /opt/zig; tar -xJf /tmp/zig.tar.xz -C /opt/zig --strip-components=1; \ + ln -s /opt/zig/zig /usr/local/bin/zig; zig version + +# Clone swerver, then place the probe app at /probeapp so its +# build.zig.zon `.path = ".."` dependency resolves to the swerver tree. +WORKDIR /src +RUN git clone --depth 1 --branch main https://github.com/justinGrosvenor/swerver.git . +RUN MULTIARCH=$(dpkg-architecture -qDEB_HOST_MULTIARCH); \ + for lib in libssl.so libssl.a libcrypto.so libcrypto.a; do \ + ln -sf "/usr/lib/${MULTIARCH}/${lib}" "/usr/lib/${lib}"; \ + done +COPY src/Servers/SwerverServer/main.zig src/Servers/SwerverServer/build.zig src/Servers/SwerverServer/build.zig.zon /src/probeapp/ +WORKDIR /src/probeapp +RUN zig build --summary all + +# ── runtime ── +FROM debian:trixie-slim +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates libssl3 \ + && rm -rf /var/lib/apt/lists/* +COPY --from=build /src/probeapp/zig-out/bin/swerver-probe /usr/local/bin/swerver-probe +COPY src/Servers/SwerverServer/config.json /app/config.json +COPY src/Servers/SwerverServer/docroot/ /app/docroot/ +USER nobody +EXPOSE 8080 +CMD ["/usr/local/bin/swerver-probe", "--config", "/app/config.json"] diff --git a/src/Servers/SwerverServer/build.zig b/src/Servers/SwerverServer/build.zig new file mode 100644 index 0000000..1a434ad --- /dev/null +++ b/src/Servers/SwerverServer/build.zig @@ -0,0 +1,12 @@ +const std = @import("std"); +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize: std.builtin.OptimizeMode = .ReleaseFast; + const dep = b.dependency("swerver", .{ .target = target, .optimize = optimize, + .@"enable-tls" = true, .@"enable-http2" = true, .@"enable-http3" = true }); + const m = b.createModule(.{ .root_source_file = b.path("main.zig"), + .target = target, .optimize = optimize, .link_libc = true }); + m.addImport("swerver", dep.module("swerver")); + const exe = b.addExecutable(.{ .name = "swerver-probe", .root_module = m }); + b.installArtifact(exe); +} diff --git a/src/Servers/SwerverServer/build.zig.zon b/src/Servers/SwerverServer/build.zig.zon new file mode 100644 index 0000000..08efdd9 --- /dev/null +++ b/src/Servers/SwerverServer/build.zig.zon @@ -0,0 +1,8 @@ +.{ + .name = .swerver_probe, + .version = "0.1.0", + .fingerprint = 0x746a6fb0ecdcc9b, + .minimum_zig_version = "0.16.0", + .paths = .{ "build.zig", "build.zig.zon", "main.zig" }, + .dependencies = .{ .swerver = .{ .path = ".." } }, +} diff --git a/src/Servers/SwerverServer/config.json b/src/Servers/SwerverServer/config.json new file mode 100644 index 0000000..8eb942c --- /dev/null +++ b/src/Servers/SwerverServer/config.json @@ -0,0 +1 @@ +{ "server": { "address": "0.0.0.0", "port": 8080, "workers": 1, "static_root": "/app/docroot" } } diff --git a/src/Servers/SwerverServer/docroot/index.txt b/src/Servers/SwerverServer/docroot/index.txt new file mode 100644 index 0000000..3d54691 --- /dev/null +++ b/src/Servers/SwerverServer/docroot/index.txt @@ -0,0 +1 @@ +Hello from swerver static probe target. This file exists to exercise conditional and range requests. diff --git a/src/Servers/SwerverServer/main.zig b/src/Servers/SwerverServer/main.zig new file mode 100644 index 0000000..ec2cb62 --- /dev/null +++ b/src/Servers/SwerverServer/main.zig @@ -0,0 +1,87 @@ +// Http11Probe target for swerver. Mirrors the reference servers' endpoint +// contract (see NginxServer/echo.js): GET / -> "OK", POST / -> echo body, +// /echo -> request headers dumped, /cookie -> parsed cookies. A static +// docroot backs the conditional/range probe tests. +const std = @import("std"); +const swerver = @import("swerver"); +const router = swerver.router; +const response_mod = swerver.response; + +fn handleRoot(ctx: *router.HandlerContext) response_mod.Response { + if (ctx.request.method == .POST) { + const body = ctx.request.body.sliceOrNull() orelse ""; + return .{ .status = 200, .headers = &[_]response_mod.Header{ + .{ .name = "Content-Type", .value = "text/plain" }, + }, .body = .{ .bytes = body } }; + } + return .{ .status = 200, .headers = &[_]response_mod.Header{ + .{ .name = "Content-Type", .value = "text/plain" }, + }, .body = .{ .bytes = "OK" } }; +} + +fn handleEcho(ctx: *router.HandlerContext) response_mod.Response { + var off: usize = 0; + const buf = ctx.response_buf; + for (ctx.request.headers) |h| { + const line = std.fmt.bufPrint(buf[off..], "{s}: {s}\n", .{ h.name, h.value }) catch break; + off += line.len; + } + return .{ .status = 200, .headers = &[_]response_mod.Header{ + .{ .name = "Content-Type", .value = "text/plain" }, + }, .body = .{ .bytes = buf[0..off] } }; +} + +fn handleCookie(ctx: *router.HandlerContext) response_mod.Response { + var off: usize = 0; + const buf = ctx.response_buf; + if (ctx.request.getHeader("cookie")) |raw| { + var it = std.mem.splitScalar(u8, raw, ';'); + while (it.next()) |pair| { + const trimmed = std.mem.trim(u8, pair, " \t"); + if (std.mem.indexOfScalar(u8, trimmed, '=')) |eq| { + if (eq > 0) { + const line = std.fmt.bufPrint(buf[off..], "{s}={s}\n", .{ trimmed[0..eq], trimmed[eq + 1 ..] }) catch break; + off += line.len; + } + } + } + } + return .{ .status = 200, .headers = &[_]response_mod.Header{ + .{ .name = "Content-Type", .value = "text/plain" }, + }, .body = .{ .bytes = buf[0..off] } }; +} + +pub fn main(init: std.process.Init) !void { + const allocator = init.gpa; + var loaded: ?swerver.config_file.LoadedConfig = null; + defer if (loaded) |*lc| lc.deinit(); + var args = try std.process.Args.Iterator.initAllocator(init.minimal.args, allocator); + defer args.deinit(); + _ = args.next(); + var config_path: ?[]const u8 = null; + while (args.next()) |a| { + const arg = std.mem.sliceTo(a, 0); + if (std.mem.eql(u8, arg, "--config")) { + if (args.next()) |v| config_path = std.mem.sliceTo(v, 0); + } + } + var cfg: swerver.config.ServerConfig = blk: { + if (config_path) |p| { + loaded = try swerver.config_file.loadConfigFile(allocator, p); + break :blk loaded.?.server_config; + } + break :blk swerver.config.ServerConfig.default(); + }; + try cfg.validate(); + + var app = router.Router.init(.{}); + try app.get("/", handleRoot); + try app.post("/", handleRoot); + try app.get("/echo", handleEcho); + try app.post("/echo", handleEcho); + try app.get("/cookie", handleCookie); + + const srv = try swerver.ServerBuilder.config(cfg).router(app).disablePreencoded().build(allocator); + defer { srv.deinit(); allocator.destroy(srv); } + try srv.run(null); +} diff --git a/src/Servers/SwerverServer/probe.json b/src/Servers/SwerverServer/probe.json new file mode 100644 index 0000000..dac8835 --- /dev/null +++ b/src/Servers/SwerverServer/probe.json @@ -0,0 +1 @@ +{ "name": "Swerver", "language": "Zig" }