Skip to content

Commit ab6ee35

Browse files
Add Swerver target (Zig HTTP/1.1+2+3 server) (#131)
* Add Swerver target (Zig HTTP/1.1+2+3 server) Swerver is a high-performance HTTP/1.1+2+3 server and API gateway written in Zig 0.16. This adds a probe target that builds swerver from source and serves the root/echo/cookie contract on port 8080. The Dockerfile clones swerver main and builds a small probe app against it (ReleaseFast). config.json points static_root at /app/docroot. * SwerverServer: run as USER nobody, enforce HTTPS on Zig download - Run the runtime container as the unprivileged 'nobody' user (mirrors TrilliumServer). Verified PID 1 runs as uid 65534 and the probe still scores 160/161 (io_uring init is unaffected). - Add --proto '=https' --tlsv1.2 to the Zig download curl so a redirect can't downgrade to plaintext. Base stays Debian trixie (documented inline): swerver's HTTP/3 path links OpenSSL 3.5's QUIC TLS API, absent from bookworm's OpenSSL 3.0.
1 parent feee119 commit ab6ee35

7 files changed

Lines changed: 152 additions & 0 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Http11Probe target for swerver (https://github.com/justinGrosvenor/swerver),
2+
# a high-performance HTTP/1.1+2+3 server and API gateway in Zig.
3+
#
4+
# Base is Debian trixie (not bookworm like the other targets): swerver's
5+
# HTTP/3 path links OpenSSL's QUIC TLS API (SSL_set_quic_tls_transport_params),
6+
# which is only present in OpenSSL 3.5+ — trixie ships it, bookworm (3.0) does not.
7+
FROM debian:trixie AS build
8+
9+
RUN apt-get update && apt-get install -y --no-install-recommends \
10+
ca-certificates curl xz-utils git libssl-dev zlib1g-dev dpkg-dev pkg-config \
11+
&& rm -rf /var/lib/apt/lists/*
12+
13+
# Zig 0.16.0 stable
14+
RUN set -eux; \
15+
ARCH=$(dpkg --print-architecture); \
16+
case "$ARCH" in amd64) ZA=x86_64 ;; arm64) ZA=aarch64 ;; *) echo "unsupported $ARCH" >&2; exit 1 ;; esac; \
17+
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; \
18+
mkdir -p /opt/zig; tar -xJf /tmp/zig.tar.xz -C /opt/zig --strip-components=1; \
19+
ln -s /opt/zig/zig /usr/local/bin/zig; zig version
20+
21+
# Clone swerver, then place the probe app at <swerver>/probeapp so its
22+
# build.zig.zon `.path = ".."` dependency resolves to the swerver tree.
23+
WORKDIR /src
24+
RUN git clone --depth 1 --branch main https://github.com/justinGrosvenor/swerver.git .
25+
RUN MULTIARCH=$(dpkg-architecture -qDEB_HOST_MULTIARCH); \
26+
for lib in libssl.so libssl.a libcrypto.so libcrypto.a; do \
27+
ln -sf "/usr/lib/${MULTIARCH}/${lib}" "/usr/lib/${lib}"; \
28+
done
29+
COPY src/Servers/SwerverServer/main.zig src/Servers/SwerverServer/build.zig src/Servers/SwerverServer/build.zig.zon /src/probeapp/
30+
WORKDIR /src/probeapp
31+
RUN zig build --summary all
32+
33+
# ── runtime ──
34+
FROM debian:trixie-slim
35+
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates libssl3 \
36+
&& rm -rf /var/lib/apt/lists/*
37+
COPY --from=build /src/probeapp/zig-out/bin/swerver-probe /usr/local/bin/swerver-probe
38+
COPY src/Servers/SwerverServer/config.json /app/config.json
39+
COPY src/Servers/SwerverServer/docroot/ /app/docroot/
40+
USER nobody
41+
EXPOSE 8080
42+
CMD ["/usr/local/bin/swerver-probe", "--config", "/app/config.json"]
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const std = @import("std");
2+
pub fn build(b: *std.Build) void {
3+
const target = b.standardTargetOptions(.{});
4+
const optimize: std.builtin.OptimizeMode = .ReleaseFast;
5+
const dep = b.dependency("swerver", .{ .target = target, .optimize = optimize,
6+
.@"enable-tls" = true, .@"enable-http2" = true, .@"enable-http3" = true });
7+
const m = b.createModule(.{ .root_source_file = b.path("main.zig"),
8+
.target = target, .optimize = optimize, .link_libc = true });
9+
m.addImport("swerver", dep.module("swerver"));
10+
const exe = b.addExecutable(.{ .name = "swerver-probe", .root_module = m });
11+
b.installArtifact(exe);
12+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.{
2+
.name = .swerver_probe,
3+
.version = "0.1.0",
4+
.fingerprint = 0x746a6fb0ecdcc9b,
5+
.minimum_zig_version = "0.16.0",
6+
.paths = .{ "build.zig", "build.zig.zon", "main.zig" },
7+
.dependencies = .{ .swerver = .{ .path = ".." } },
8+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{ "server": { "address": "0.0.0.0", "port": 8080, "workers": 1, "static_root": "/app/docroot" } }
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Hello from swerver static probe target. This file exists to exercise conditional and range requests.

src/Servers/SwerverServer/main.zig

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Http11Probe target for swerver. Mirrors the reference servers' endpoint
2+
// contract (see NginxServer/echo.js): GET / -> "OK", POST / -> echo body,
3+
// /echo -> request headers dumped, /cookie -> parsed cookies. A static
4+
// docroot backs the conditional/range probe tests.
5+
const std = @import("std");
6+
const swerver = @import("swerver");
7+
const router = swerver.router;
8+
const response_mod = swerver.response;
9+
10+
fn handleRoot(ctx: *router.HandlerContext) response_mod.Response {
11+
if (ctx.request.method == .POST) {
12+
const body = ctx.request.body.sliceOrNull() orelse "";
13+
return .{ .status = 200, .headers = &[_]response_mod.Header{
14+
.{ .name = "Content-Type", .value = "text/plain" },
15+
}, .body = .{ .bytes = body } };
16+
}
17+
return .{ .status = 200, .headers = &[_]response_mod.Header{
18+
.{ .name = "Content-Type", .value = "text/plain" },
19+
}, .body = .{ .bytes = "OK" } };
20+
}
21+
22+
fn handleEcho(ctx: *router.HandlerContext) response_mod.Response {
23+
var off: usize = 0;
24+
const buf = ctx.response_buf;
25+
for (ctx.request.headers) |h| {
26+
const line = std.fmt.bufPrint(buf[off..], "{s}: {s}\n", .{ h.name, h.value }) catch break;
27+
off += line.len;
28+
}
29+
return .{ .status = 200, .headers = &[_]response_mod.Header{
30+
.{ .name = "Content-Type", .value = "text/plain" },
31+
}, .body = .{ .bytes = buf[0..off] } };
32+
}
33+
34+
fn handleCookie(ctx: *router.HandlerContext) response_mod.Response {
35+
var off: usize = 0;
36+
const buf = ctx.response_buf;
37+
if (ctx.request.getHeader("cookie")) |raw| {
38+
var it = std.mem.splitScalar(u8, raw, ';');
39+
while (it.next()) |pair| {
40+
const trimmed = std.mem.trim(u8, pair, " \t");
41+
if (std.mem.indexOfScalar(u8, trimmed, '=')) |eq| {
42+
if (eq > 0) {
43+
const line = std.fmt.bufPrint(buf[off..], "{s}={s}\n", .{ trimmed[0..eq], trimmed[eq + 1 ..] }) catch break;
44+
off += line.len;
45+
}
46+
}
47+
}
48+
}
49+
return .{ .status = 200, .headers = &[_]response_mod.Header{
50+
.{ .name = "Content-Type", .value = "text/plain" },
51+
}, .body = .{ .bytes = buf[0..off] } };
52+
}
53+
54+
pub fn main(init: std.process.Init) !void {
55+
const allocator = init.gpa;
56+
var loaded: ?swerver.config_file.LoadedConfig = null;
57+
defer if (loaded) |*lc| lc.deinit();
58+
var args = try std.process.Args.Iterator.initAllocator(init.minimal.args, allocator);
59+
defer args.deinit();
60+
_ = args.next();
61+
var config_path: ?[]const u8 = null;
62+
while (args.next()) |a| {
63+
const arg = std.mem.sliceTo(a, 0);
64+
if (std.mem.eql(u8, arg, "--config")) {
65+
if (args.next()) |v| config_path = std.mem.sliceTo(v, 0);
66+
}
67+
}
68+
var cfg: swerver.config.ServerConfig = blk: {
69+
if (config_path) |p| {
70+
loaded = try swerver.config_file.loadConfigFile(allocator, p);
71+
break :blk loaded.?.server_config;
72+
}
73+
break :blk swerver.config.ServerConfig.default();
74+
};
75+
try cfg.validate();
76+
77+
var app = router.Router.init(.{});
78+
try app.get("/", handleRoot);
79+
try app.post("/", handleRoot);
80+
try app.get("/echo", handleEcho);
81+
try app.post("/echo", handleEcho);
82+
try app.get("/cookie", handleCookie);
83+
84+
const srv = try swerver.ServerBuilder.config(cfg).router(app).disablePreencoded().build(allocator);
85+
defer { srv.deinit(); allocator.destroy(srv); }
86+
try srv.run(null);
87+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{ "name": "Swerver", "language": "Zig" }

0 commit comments

Comments
 (0)