Skip to content

OrlovEvgeny/libhttp.zig

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

libhttp.zig

CI Release Zig

Zero-allocation HTTP/1.1, HTTP/2, and HTTP/3 protocol library for Zig. All parsing and formatting operates on caller-provided buffers

Zig 0.15.0+.

Install

zig fetch --save git+https://github.com/OrlovEvgeny/libhttp.zig
// build.zig
const libhttp = b.dependency("libhttp", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("http", libhttp.module("libhttp"));

Architecture

The library is organized in four layers:

┌─────────────────────────────────────────────┐
│  Public API (http.Server, http.Client)      │
├──────────┬──────────┬───────────────────────┤
│  H1      │  H2      │  H3                   │
│  proto/  │  proto/  │  proto/               │
│  h1/     │  h2/     │  h3/ + QUIC vtable    │
├──────────┴──────────┴───────────────────────┤
│  Common (headers, status, URI, cookies,     │
│  redirects, encoding, cache, ranges, ETags) │
├─────────────────────────────────────────────┤
│  TLS (config types, backend contract,       │
│  passthrough backend for testing)           │
└─────────────────────────────────────────────┘

Zero allocation. All functions write into caller-provided []u8 buffers and return slices. No std.mem.Allocator is ever required

Zero-copy parsing. Parsed struct fields are []const u8 slices pointing directly into the input buffer

Format-agnostic I/O. H1/H2 take *std.Io.Reader / *std.Io.Writer. H3 takes a Quic vtable. TLS backends produce these same interfaces

Module structure

http.Method, http.Status, http.Header, http.Uri, http.Version, http.DateTime
http.Server, http.Client
http.Request, http.Response, http.Body
http.h1.*                -- H1 connection, parser, scanner, chunked codec, I/O
http.h2.*                -- H2 connection, HPACK, flow control, streams, priorities
http.h3.*                -- H3 connection, QPACK, frames, QUIC interface
http.common.*            -- headers, URI, media types, auth, cache-control, ranges, ETags,
                            quality values, cookies, redirects, content-encoding, timeouts
http.tls.*               -- TLS config, backend contract, passthrough backend
http.proxy.*             -- intermediary header rewriting, Via, request/response classification
http.cache.*             -- canStore, evaluateFreshness, merge304Headers
http.alpn.*              -- ALPN token helpers for TLS negotiation
http.ConnectionPool(T)   -- fixed-capacity client connection pool
http.BufferPool(N, Size) -- fixed-capacity buffer pool

Server (H1)

const http = @import("http");

var server = http.Server.init(&reader, &writer, .{});
var head_buf: [4096]u8 = undefined;
var header_buf: [32]http.Header = undefined;

const req = try server.recvRequest(&head_buf);

if (req.expect_continue)
    try server.send100Continue(&head_buf);

try server.sendResponse(&head_buf, &header_buf, .ok, null, req,
    &.{.{ .name = "Content-Type", .value = "text/plain" }},
    "hello", .{});

if (server.shouldClose()) break;

Request body

var body_buf: [65536]u8 = undefined;
const n = try server.recvRequestBody(&body_buf, req);
const body = body_buf[0..n];

Chunked body with trailers

var body_buf: [65536]u8 = undefined;
var trailer_block: [1024]u8 = undefined;
var trailer_hdrs: [8]http.Header = undefined;

const result = try server.recvRequestBodyWithTrailers(
    &body_buf, &trailer_block, &trailer_hdrs, req);

const body = body_buf[0..result.body_len];
const trailers = result.trailers;

Pipelining

while (true) {
    const req = try server.recvRequest(&head_buf);
    // ... process ...
    try server.sendResponse(&head_buf, &header_buf, .ok, null, req, &.{}, body, .{});
    if (server.shouldClose()) break;
}

Graceful shutdown

server.conn.initiateGracefulClose();
// shouldClose() now returns true, next recvRequest returns ConnectionClosing

Upgrades (WebSocket, etc.)

const req = try server.recvRequest(&head_buf);
if (req.upgrade_requested) {
    try server.sendResponse(&head_buf, &header_buf, .switching_protocols, null, req,
        &.{.{ .name = "Upgrade", .value = "websocket" },
            .{ .name = "Connection", .value = "Upgrade" }}, null, .{});

    var adapter = server.takeRequestUpgradeAdapter(req).?;
    // adapter.read() / adapter.write() for raw transport
}

CONNECT tunnels

if (req.connect_requested) {
    try server.sendResponse(&head_buf, &header_buf, .ok, null, req, &.{}, null, .{});
    var adapter = server.takeRequestTunnelAdapter(req).?;
    // bidirectional tunnel
}

Client (H1)

var client = http.Client.init(&reader, &writer, .{});
var head_buf: [4096]u8 = undefined;
var header_buf: [32]http.Header = undefined;

const sent = try client.sendRequest(&head_buf, &header_buf,
    "GET",
    try http.RequestTarget.parse("http://example.com/resource"),
    .@"HTTP/1.1",
    &.{}, null, .{});

const resp = try client.recvResponse(&head_buf, sent);

var body_buf: [65536]u8 = undefined;
const n = try client.recvResponseBody(&body_buf, resp);

Sending a request body

const sent = try client.sendRequest(&head_buf, &header_buf,
    "POST",
    try http.RequestTarget.parse("/api/data"),
    .@"HTTP/1.1",
    &.{.{ .name = "Content-Type", .value = "application/json" }},
    "{\"key\":\"val\"}",
    .{});

HTTP/2

Server

const h2 = http.h2;

var conn = h2.ServerConnection.init(&reader, &writer, options, &streams, &decoder, &encoder);
try conn.recvPreface(&frame_buf);

while (true) {
    const event = try conn.recvFrame(&frame_buf, &hdr_block_buf, &headers_out, &name_buf, &value_buf);
    switch (event) {
        .headers => |h| {
            // h.stream_id, h.headers, h.end_stream
            try conn.sendHeaders(&frame_buf, h.stream_id, &response_headers, true);
        },
        .data => |d| {
            // d.stream_id, d.data, d.end_stream
        },
        .goaway => break,
        else => {},
    }
}

Graceful GOAWAY

try conn.initiateGracefulShutdown(&frame_buf);
// Continue processing existing streams...
if (conn.isFullyDrained()) break;

RFC 9218 priorities

const p = try http.h2.Priority.parse("u=0, i");
// p.urgency = 0, p.incremental = true

var buf: [16]u8 = undefined;
const formatted = try p.format(&buf); // "u=0, i"

HTTP/3

H3 uses a QUIC transport interface instead of Reader/Writer. You provide a QUIC implementation via the Quic vtable - the library does not include a QUIC stack.

Server

const h3 = http.h3;

// Provide your QUIC implementation through the vtable.
const quic = h3.Quic{ .ptr = @ptrCast(my_quic_conn), .vtable = &MyQuic.vtable };

var conn = h3.ServerConnection.init(quic, .{}, &streams, &decoder, &encoder);
try conn.handshake(&buf);

while (true) {
    const event = try conn.recvEvent(&buf, &hdr_block_buf, &headers_out, &name_buf, &value_buf);
    switch (event) {
        .headers => |h| {
            try conn.sendHeaders(&buf, h.stream_id, &response_headers, true);
        },
        .data => |d| {
            // d.stream_id, d.data, d.end_stream
        },
        .goaway => break,
        else => {},
    }
}

Client

var conn = h3.ClientConnection.init(quic, .{}, &streams, &decoder, &encoder);
try conn.handshake(&buf);

const stream_id = try conn.sendHeaders(&buf, &request_headers, false);
try conn.sendData(&buf, stream_id, request_body, true);

const event = try conn.recvEvent(&buf, &hdr_block_buf, &headers_out, &name_buf, &value_buf);

QUIC vtable

const MyQuic = struct {
    // ... your QUIC state ...

    const vtable = h3.Quic.VTable{
        .read = readFn,
        .write = writeFn,
        .openBidiStream = openBidiFn,
        .openUniStream = openUniFn,
        .acceptBidiStream = acceptBidiFn,
        .acceptUniStream = acceptUniFn,
        .resetStream = resetFn,
        .stopStream = stopFn,
        .closeConnection = closeFn,
    };
};

TLS

The library provides TLS configuration types and a pluggable backend interface. Actual TLS implementations (OpenSSL, BoringSSL, std.crypto.tls) are external - the library defines the contract.

Configuration

// Client TLS config
const client_cfg = http.tls.ClientConfig{
    .server_name = "example.com",
    .alpn_protocols = http.alpn.default_alpn_list,
    .verify_server_cert = true,
};

// Server TLS config
const server_cfg = http.tls.ServerConfig{
    .cert_key = .{ .cert_pem = cert_bytes, .key_pem = key_bytes },
    .alpn_protocols = http.alpn.default_alpn_list,
};

Passthrough backend (testing)

var session: http.tls.none.Session = .{};
const result = try http.tls.none.initClient(&session, &reader, &writer, client_cfg);
// result.reader, result.writer - same as transport (no encryption)
// result.negotiated_protocol - first ALPN token

Bringing your own TLS backend

Implement three functions and a Session type:

const MyTls = struct {
    const Session = struct {
        // Your TLS session state (OpenSSL SSL*, std.crypto.tls.Client, etc.)
        // Embed Io.Reader/Writer to produce them from TLS reads/writes.
    };

    fn initClient(session: *Session, reader: *Io.Reader, writer: *Io.Writer,
                  cfg: http.tls.ClientConfig) http.tls.HandshakeError!http.tls.HandshakeResult {
        // Perform TLS handshake, return wrapped reader/writer
    }

    fn initServer(session: *Session, reader: *Io.Reader, writer: *Io.Writer,
                  cfg: http.tls.ServerConfig) http.tls.HandshakeError!http.tls.HandshakeResult {
        // Perform TLS handshake for server side
    }

    fn deinit(session: *Session) void {
        // Clean up TLS resources
    }

    // Validate at compile time:
    comptime { http.tls.validateBackend(@This()); }
};

Protocol negotiation flow

// 1. Configure ALPN
const cfg = http.tls.ClientConfig{
    .server_name = "example.com",
    .alpn_protocols = http.alpn.default_alpn_list, // "h2", "http/1.1"
};

// 2. Perform TLS handshake (with your backend)
const result = try MyTls.initClient(&session, &reader, &writer, cfg);

// 3. Select protocol based on negotiation
switch (result.negotiated_protocol) {
    .h2 => {
        var server = http.Server.initH2(result.reader, result.writer, ...);
    },
    .http_1_1 => {
        var server = http.Server.init(result.reader, result.writer, .{});
    },
    .h3 => unreachable, // H3 uses QUIC, not TCP+TLS
    .unknown => return error.UnsupportedProtocol,
}

Low-level parsing (no I/O)

Request head

const input = "GET /index.html HTTP/1.1\r\nHost: example.com\r\n\r\n";
const req = try http.h1.RequestHead.parse(input);

req.method      // .GET
req.uri         // "/index.html"
req.version     // .@"HTTP/1.1"
req.host        // "example.com"
req.content_length // ?u64

Response head

const input = "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\n";
const resp = try http.h1.ResponseHead.parse(input);

resp.status         // .ok
resp.content_length // 5

Iterating raw headers

var it = req.iterateHeaders(head_bytes);
while (it.next()) |hdr| {
    // hdr.name, hdr.value -- slices into head_bytes
}

Well-known header lookup

if (http.WellKnown.match("Content-Type")) |wk| {
    const canonical = wk.toCanonical(); // "Content-Type"
}

// Includes cookie headers:
if (http.WellKnown.match("Set-Cookie")) |wk| {
    // wk == .set_cookie
}

Low-level I/O

Receiving a head from the wire

var head_buf: [8192]u8 = undefined;
const result = try http.h1.recvHead(&reader, &head_buf);

const head = try http.h1.RequestHead.parse(head_buf[0..result.head_end]);
const overflow = head_buf[result.head_end..result.data_end];

Sending a head

var head_buf: [1024]u8 = undefined;
try http.h1.sendRequestHead(&writer, &head_buf, "POST", "/api", .@"HTTP/1.1", &.{
    .{ .name = "Host", .value = "example.com" },
    .{ .name = "Content-Length", .value = "13" },
});
try http.h1.sendBody(&writer, "{\"key\":\"val\"}");

Chunked encoding

// Sending
var chunk_buf: [256]u8 = undefined;
try http.h1.sendChunk(&writer, &chunk_buf, "first chunk");
try http.h1.sendChunk(&writer, &chunk_buf, "second chunk");
try http.h1.sendLastChunk(&writer, &chunk_buf);

// Or all at once:
try http.h1.sendBodyChunked(&writer, &chunk_buf, "entire body");

// With trailers:
try http.h1.sendLastChunkWithTrailers(&writer, &chunk_buf, &.{
    .{ .name = "Checksum", .value = "abc123" },
});

Body framing dispatch

const body_len = http.h1.responseBodyLength(method, status, te, content_length);

var body_buf: [65536]u8 = undefined;
const n = try http.h1.recvBody(&reader, &body_buf, body_len, overflow);

body_len is a tagged union: .length(n), .chunked, or .read_until_close.

Protocol-agnostic types

For code that handles both H1 and H2:

// From H1
const req = http.Request.fromH1(received_request, head_bytes);

// From H2
const req = http.Request.fromH2(received_headers_event);

// Uniform access regardless of protocol
req.method        // []const u8
req.method_enum   // ?Method
req.target        // path (H1) or :path (H2)
req.authority     // Host header (H1) or :authority (H2)
req.protocol      // .h1 or .h2
req.body          // .none, .content_length, .chunked, .stream

Response builder

var resp = http.Response.init(.ok);
_ = resp.setContentType("application/json");

var len_buf: [20]u8 = undefined;
_ = resp.setContentLength(1234, &len_buf);

_ = resp.setHeader("Cache-Control", "no-cache");
// resp.headers() returns []const Header

Cookies (RFC 6265)

Parsing Set-Cookie

const sc = try http.common.SetCookie.parse(
    "sid=abc123; Domain=example.com; Path=/; Secure; HttpOnly; SameSite=Lax"
);
// sc.name = "sid", sc.value = "abc123"
// sc.domain = "example.com", sc.secure = true, sc.http_only = true
// sc.same_site = .lax

Iterating Cookie request headers

var it = http.common.CookieIterator.init("id=abc; theme=dark; lang=en");
while (it.next()) |pair| {
    // pair.name, pair.value
}

Finding a cookie by name

if (http.common.findCookie(cookie_header, "session_id")) |value| {
    // use value
}

Formatting Set-Cookie

var buf: [256]u8 = undefined;
if (sc.format(&buf)) |header_value| {
    _ = resp.setHeader("Set-Cookie", header_value);
}

Redirect handling (RFC 9110)

const redirect = @import("http").common;

// Classify a status code
if (redirect.classifyRedirect(.moved_permanently)) |rt| {
    rt.isPermanent()    // true
    rt.preservesMethod() // false
}

// Determine method after redirect
const new_method = redirect.redirectMethod(.POST, .see_other);
// new_method == .GET (303 always changes to GET)

// Check if safe to auto-follow
redirect.isSafeToAutoFollow(.GET, .temporary_preserves_method) // true
redirect.isSafeToAutoFollow(.POST, .permanent_allows_method_change) // false

// Extract Location header
if (redirect.extractLocation(resp.headers())) |location| {
    // follow redirect to location
}

Content-Encoding (RFC 9110)

Header-level negotiation only - actual compression/decompression is external.

// Parse Content-Encoding
var it = http.common.ContentEncodingIterator.init("gzip, br");
while (it.next()) |enc| {
    // enc == .gzip, then .br
}

// Identify an encoding token
http.common.Encoding.fromString("br")    // .br
http.common.Encoding.fromString("zstd")  // .zstd

// Select best encoding from Accept-Encoding
const offered = [_][]const u8{ "gzip", "br", "zstd" };
if (http.common.selectEncoding(accept_encoding_header, &offered)) |idx| {
    // offered[idx] is the best match
}

// Check if a specific encoding is acceptable
http.common.isAcceptable("gzip", "gzip, deflate, br") // true

Timeouts

// Use preset defaults
const cfg = http.common.TimeoutConfig.client_default;
// connect: 30s, read: 30s, write: 30s, idle: 90s, request: 300s

const srv_cfg = http.common.TimeoutConfig.server_default;
// read: 60s, write: 60s, idle: 120s, request: 600s

// Custom configuration
const custom = http.common.TimeoutConfig{
    .connect_ms = 5_000,
    .read_ms = 10_000,
};

// Check expiration in your event loop
if (http.common.TimeoutConfig.isExpired(cfg.read_ms, elapsed_ms)) {
    return error.Timeout;
}

ALPN negotiation

const proto = http.alpn.NegotiatedProtocol.fromAlpnToken(alpn_bytes);
switch (proto) {
    .h2 => // use HTTP/2
    .http_1_1 => // use HTTP/1.1
    .h3 => // use HTTP/3
    .unknown => // fallback
}

// Standard token lists for TLS configuration:
http.alpn.default_alpn_list          // &.{ "h2", "http/1.1" }
http.alpn.default_alpn_list_with_h3  // &.{ "h3", "h2", "http/1.1" }
http.alpn.h2_only_alpn_list          // &.{ "h2" }
http.alpn.h1_only_alpn_list          // &.{ "http/1.1" }
http.alpn.h3_only_alpn_list          // &.{ "h3" }

Content negotiation

// Accept header
var it = http.common.AcceptIterator.init("text/html;q=0.9, application/json");
while (it.next()) |item| {
    // item.media_type, item.quality
}

// Server-driven selection
const offered = [_]http.common.MediaType{
    .{ .type_ = "text", .subtype = "html" },
    .{ .type_ = "application", .subtype = "json" },
};
if (http.common.selectMediaType(accept_header, &offered)) |idx| {
    // offered[idx] is best match
}

// Accept-Encoding / Accept-Language
const encodings = [_][]const u8{ "gzip", "br", "deflate" };
if (http.common.selectToken(accept_encoding, &encodings)) |idx| {
    // encodings[idx]
}

Cache-Control

http.common.hasCacheDirective("no-cache, max-age=3600", "no-cache") // true
http.common.getMaxAge("max-age=3600") // 3600

var it = http.common.CacheDirectiveIterator.init("private, max-age=3600");
while (it.next()) |dir| {
    switch (dir) {
        .max_age => |seconds| { ... },
        .private => |field_names| { ... },
        ...
    }
}

Byte ranges

var it = try http.common.RangeIterator.init("bytes=0-499, -500");
while (it.next()) |range| {
    switch (range) {
        .slice => |s| { ... },
        .suffix => |n| { ... },
        .from => |f| { ... },
    }
}

// Resolve against resource size
const resolved = http.common.resolveRange(.{ .suffix = 500 }, 10000);
// resolved.?.first = 9500, resolved.?.last = 9999

ETags

const etag = try http.common.ETag.parse("\"abc\"");
http.common.matchesStrong("\"xyz\", \"abc\"", etag) // true
http.common.matchesWeak("W/\"abc\"", etag) // true

Security limits

var server = http.Server.init(&reader, &writer, .{
    .max_header_count = 128,
    .max_header_name_len = 256,
    .max_header_value_len = 8192,
    .max_request_line_len = 8192,
    .max_head_size = 65536,
});

Exceeding any limit returns error.HeaderLimitExceeded. Additionally:

  • NUL and control characters in header values are rejected
  • Duplicate Transfer-Encoding headers are rejected (request smuggling vector)
  • Transfer-Encoding on HTTP/1.0 messages is rejected
  • Absolute-form URI authority must match the Host header

Connection pool

const Pool = http.ConnectionPool(MyConn);
var entries: [64]Pool.Entry = undefined;
var pool = Pool.init(&entries);

// Reuse existing connection
if (pool.acquire("example.com", 443)) |entry| {
    // use entry.conn
    defer pool.release(entry);
}

// Or claim a new slot
if (pool.claim("example.com", 443)) |entry| {
    entry.conn = openConnection(...);
    defer pool.release(entry);
}

// Evict stale entries
_ = pool.evictIdle(now_ms, 30_000);

Buffer pool

var pool: http.BufferPool(8, 4096) = .{};

const buf = pool.acquire() orelse return error.NoBuffers;
defer pool.release(buf);
// use buf[0..4096]

Buffer size guidance

Buffer Recommended Purpose
Head buffer 8192 HTTP/1.1 request/response head parsing
Body buffer 65536 Request/response body reads
H2 frame buffer 16384 + 9 H2 frame payload (default SETTINGS_MAX_FRAME_SIZE)
Chunk buffer 256–8192 Chunked transfer encoding overhead
Header array 32–128 entries Parsed header name/value pairs

Connection stats

var conn = http.h1.ServerConnection.init(&reader, &writer, .{});
// ... process requests ...

conn.stats.messages_in  // requests received
conn.stats.messages_out // responses sent

Other utilities

// URI parsing
const uri = try http.Uri.parse("http://example.com:8080/path?q=1");

// Percent encoding
var buf: [128]u8 = undefined;
const encoded = try http.common.PercentEncoding.encode(&buf, "hello world");

// Media types
const mt = try http.common.MediaType.parse("text/html; charset=utf-8");

// Basic auth
var dec_buf: [128]u8 = undefined;
const auth = try http.common.BasicAuth.decode("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==", &dec_buf);

// HTTP dates
const dt = try http.DateTime.parse("Sun, 06 Nov 1994 08:49:37 GMT");

// Status codes
const s = try http.Status.parse("404");
s.phrase() // "Not Found"
s.class()  // .client_error

// Methods
const m = http.Method.fromString("POST").?;
m.safe()       // false
m.idempotent() // false

Integration with libxev

Use libxev for the event loop and TCP accept. Each accepted connection spawns a thread that bridges std.net.Stream to *Io.Reader / *Io.Writer for libhttp.

Add both dependencies to build.zig.zon:

zig fetch --save git+https://github.com/lithdew/libxev
zig fetch --save git+https://github.com/OrlovEvgeny/libhttp.zig
// build.zig
const xev_dep = b.dependency("libxev", .{ .target = target, .optimize = optimize });
const http_dep = b.dependency("libhttp", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("xev", xev_dep.module("xev"));
exe.root_module.addImport("http", http_dep.module("libhttp"));
const std = @import("std");
const xev = @import("xev");
const http = @import("http");

pub fn main() !void {
    var loop = try xev.Loop.init(.{});
    defer loop.deinit();

    const addr = std.net.Address.initIp4(.{ 0, 0, 0, 0 }, 8080);
    var server = try xev.TCP.init(addr);
    try server.bind(addr);
    try server.listen(128);

    var accept_completion: xev.Completion = undefined;
    server.accept(&loop, &accept_completion, {}, onAccept);

    try loop.run(.until_done);
}

fn onAccept(_: void, loop: *xev.Loop, _: *xev.Completion, result: xev.AcceptError!xev.TCP) xev.CallbackAction {
    const client = result catch return .rearm;
    const stream = std.net.Stream{ .handle = client.fd };
    _ = std.Thread.spawn(.{}, handleConnection, .{stream}) catch return .rearm;
    _ = loop;
    return .rearm;
}

fn handleConnection(stream: std.net.Stream) void {
    defer stream.close();

    var read_buf: [8192]u8 = undefined;
    var write_buf: [8192]u8 = undefined;
    var net_reader = std.net.Stream.Reader.init(stream, &read_buf);
    var net_writer = std.net.Stream.Writer.init(stream, &write_buf);

    var server = http.Server.init(net_reader.interface(), &net_writer.interface, .{});
    var head_buf: [4096]u8 = undefined;
    var header_buf: [32]http.Header = undefined;

    while (true) {
        const req = server.recvRequest(&head_buf) catch return;
        server.sendResponse(&head_buf, &header_buf, .ok, null, req,
            &.{.{ .name = "Content-Type", .value = "text/plain" }},
            "hello from libxev", .{}) catch return;
        if (server.shouldClose()) return;
    }
}

Integration with tls.zig

Implement libhttp's TLS backend contract (http.tls.validateBackend) wrapping tls.zig to serve HTTPS. tls.zig does not currently support ALPN, so the example defaults to HTTP/1.1.

zig fetch --save git+https://github.com/tls-zig/tls.zig
zig fetch --save git+https://github.com/OrlovEvgeny/libhttp.zig
// build.zig
const tls_dep = b.dependency("tls", .{ .target = target, .optimize = optimize });
const http_dep = b.dependency("libhttp", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("tls", tls_dep.module("tls"));
exe.root_module.addImport("http", http_dep.module("libhttp"));
const std = @import("std");
const Io = std.Io;
const tls = @import("tls");
const http = @import("http");

const TlsBackend = struct {
    const Session = struct {
        conn: tls.Connection,
        read_buf: [16384]u8,
        write_buf: [16384]u8,

        fn reader(self: *Session) *Io.Reader {
            return self.conn.reader(&self.read_buf);
        }

        fn writer(self: *Session) *Io.Writer {
            return self.conn.writer(&self.write_buf);
        }
    };

    fn initClient(
        session: *Session,
        reader: *Io.Reader,
        writer: *Io.Writer,
        cfg: http.tls.ClientConfig,
    ) http.tls.HandshakeError!http.tls.HandshakeResult {
        session.conn = tls.clientFromStream(reader, writer, .{
            .server_name = cfg.server_name,
        }) catch return error.TlsHandshakeFailed;

        // tls.zig does not support ALPN; default to HTTP/1.1.
        return .{
            .reader = session.reader(),
            .writer = session.writer(),
            .negotiated_protocol = .http_1_1,
        };
    }

    fn initServer(
        session: *Session,
        reader: *Io.Reader,
        writer: *Io.Writer,
        cfg: http.tls.ServerConfig,
    ) http.tls.HandshakeError!http.tls.HandshakeResult {
        session.conn = tls.serverFromStream(reader, writer, .{
            .cert_pem = cfg.cert_key.cert_pem,
            .key_pem = cfg.cert_key.key_pem,
        }) catch return error.TlsHandshakeFailed;

        return .{
            .reader = session.reader(),
            .writer = session.writer(),
            .negotiated_protocol = .http_1_1,
        };
    }

    fn deinit(session: *Session) void {
        session.conn.close();
    }

    comptime {
        http.tls.validateBackend(@This());
    }
};

pub fn main() !void {
    const addr = std.net.Address.initIp4(.{ 0, 0, 0, 0 }, 8443);
    var tcp = try std.net.Address.listen(addr, .{ .reuse_address = true });
    defer tcp.deinit();

    while (true) {
        const accepted = try tcp.accept();
        _ = try std.Thread.spawn(.{}, handleTlsConn, .{accepted.stream});
    }
}

fn handleTlsConn(stream: std.net.Stream) void {
    defer stream.close();

    var read_buf: [8192]u8 = undefined;
    var write_buf: [8192]u8 = undefined;
    var net_reader = std.net.Stream.Reader.init(stream, &read_buf);
    var net_writer = std.net.Stream.Writer.init(stream, &write_buf);

    var session: TlsBackend.Session = undefined;
    const result = TlsBackend.initServer(&session, net_reader.interface(), &net_writer.interface, .{
        .cert_key = .{ .cert_pem = @embedFile("cert.pem"), .key_pem = @embedFile("key.pem") },
    }) catch return;
    defer TlsBackend.deinit(&session);

    var server = http.Server.init(result.reader, result.writer, .{});
    var head_buf: [4096]u8 = undefined;
    var header_buf: [32]http.Header = undefined;

    while (true) {
        const req = server.recvRequest(&head_buf) catch return;
        server.sendResponse(&head_buf, &header_buf, .ok, null, req,
            &.{.{ .name = "Content-Type", .value = "text/plain" }},
            "hello over TLS", .{}) catch return;
        if (server.shouldClose()) return;
    }
}

Integration with http.zig (httpz)

libhttp's parsing utilities work standalone — you can use them inside any HTTP framework for header inspection, content negotiation, and caching logic without replacing the framework's transport.

Using libhttp utilities in httpz handlers

zig fetch --save git+https://github.com/karlseguin/http.zig
zig fetch --save git+https://github.com/OrlovEvgeny/libhttp.zig
// build.zig
const httpz_dep = b.dependency("httpz", .{ .target = target, .optimize = optimize });
const libhttp_dep = b.dependency("libhttp", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("httpz", httpz_dep.module("httpz"));
exe.root_module.addImport("http", libhttp_dep.module("libhttp"));
const std = @import("std");
const httpz = @import("httpz");
const http = @import("http");

pub fn main() !void {
    var server = try httpz.Server().init(std.heap.page_allocator, .{ .port = 8080 });
    defer server.deinit();

    server.router().get("/resource", handleResource);
    server.router().get("/file", handleRangedFile);

    try server.listen();
}

fn handleResource(_: *httpz.Request, resp: *httpz.Response) !void {
    // Content negotiation via libhttp.
    const accept = resp.req.header("accept") orelse "*/*";
    const offered = [_]http.common.MediaType{
        .{ .type_ = "application", .subtype = "json" },
        .{ .type_ = "text", .subtype = "html" },
    };

    if (http.common.selectMediaType(accept, &offered)) |idx| {
        if (idx == 0) {
            resp.content_type = .JSON;
            resp.body = "{\"status\":\"ok\"}";
        } else {
            resp.content_type = .HTML;
            resp.body = "<h1>OK</h1>";
        }
    } else {
        resp.status = 406;
    }

    // Cookie inspection.
    if (resp.req.header("cookie")) |cookies| {
        if (http.common.findCookie(cookies, "session_id")) |sid| {
            std.log.info("session: {s}", .{sid});
        }
    }

    // Cache-control parsing.
    if (resp.req.header("cache-control")) |cc| {
        if (http.common.getMaxAge(cc)) |age| {
            std.log.info("client max-age: {d}", .{age});
        }
    }
}

fn handleRangedFile(req: *httpz.Request, resp: *httpz.Response) !void {
    const file_size: u64 = 102400;

    if (req.header("range")) |range_hdr| {
        var it = http.common.RangeIterator.init(range_hdr) catch {
            resp.status = 416;
            return;
        };
        if (it.next()) |range| {
            const resolved = http.common.resolveRange(range, file_size) orelse {
                resp.status = 416;
                return;
            };
            resp.status = 206;
            var buf: [64]u8 = undefined;
            const cr = std.fmt.bufPrint(&buf, "bytes {d}-{d}/{d}", .{
                resolved.first, resolved.last, file_size,
            }) catch return;
            resp.header("Content-Range", cr);
            // ... serve resolved.first..resolved.last bytes ...
        }
    }
}

Protocol-level coexistence

httpz handles HTTP/1.1 routing. For connections negotiated as HTTP/2 (via ALPN during TLS), route them to libhttp's H2 module instead.

const std = @import("std");
const Io = std.Io;
const http = @import("http");

fn handleConnection(stream: std.net.Stream) void {
    defer stream.close();

    var read_buf: [8192]u8 = undefined;
    var write_buf: [8192]u8 = undefined;
    var net_reader = std.net.Stream.Reader.init(stream, &read_buf);
    var net_writer = std.net.Stream.Writer.init(stream, &write_buf);

    // Perform TLS handshake with your backend.
    var session: MyTlsBackend.Session = undefined;
    const tls_result = MyTlsBackend.initServer(
        &session, net_reader.interface(), &net_writer.interface, server_tls_config,
    ) catch return;
    defer MyTlsBackend.deinit(&session);

    switch (tls_result.negotiated_protocol) {
        .h2 => handleH2(tls_result.reader, tls_result.writer),
        .http_1_1, .unknown => handleH1(tls_result.reader, tls_result.writer),
        .h3 => unreachable, // H3 uses QUIC, not TCP+TLS
    }
}

fn handleH1(reader: *Io.Reader, writer: *Io.Writer) void {
    // Use httpz for HTTP/1.1 routing, or libhttp's H1 server directly.
    var server = http.Server.init(reader, writer, .{});
    var head_buf: [4096]u8 = undefined;
    var header_buf: [32]http.Header = undefined;

    while (true) {
        const req = server.recvRequest(&head_buf) catch return;
        server.sendResponse(&head_buf, &header_buf, .ok, null, req,
            &.{.{ .name = "Content-Type", .value = "text/plain" }},
            "HTTP/1.1 response", .{}) catch return;
        if (server.shouldClose()) return;
    }
}

fn handleH2(reader: *Io.Reader, writer: *Io.Writer) void {
    const h2 = http.h2;
    var stream_slots: [64]h2.Stream = undefined;
    var dec_entries: [128]h2.hpack.EntryMeta = undefined;
    var dec_data: [8192]u8 = undefined;
    var enc_entries: [128]h2.hpack.EntryMeta = undefined;
    var enc_data: [8192]u8 = undefined;

    var decoder = h2.hpack.Decoder.init(&dec_entries, &dec_data, 4096);
    var encoder = h2.hpack.Encoder.init(&enc_entries, &enc_data, 4096);
    var streams = h2.StreamTable.init(&stream_slots, true);

    var server = http.Server.initH2(reader, writer, .{}, &streams, &decoder, &encoder);
    var frame_buf: [16393]u8 = undefined;
    server.handshake(&frame_buf) catch return;

    var hdr_block_buf: [8192]u8 = undefined;
    var headers_out: [64]http.Header = undefined;
    var name_buf: [4096]u8 = undefined;
    var value_buf: [8192]u8 = undefined;

    while (true) {
        const event = server.recvH2Frame(
            &frame_buf, &hdr_block_buf, &headers_out, &name_buf, &value_buf,
        ) catch return;
        switch (event) {
            .headers => |h| {
                server.sendH2Headers(&frame_buf, h.stream_id, &.{
                    .{ .name = ":status", .value = "200" },
                    .{ .name = "content-type", .value = "text/plain" },
                }, true) catch return;
            },
            .goaway => return,
            else => {},
        }
    }
}

License

MIT

About

HTTP/1.1, HTTP/2, and HTTP/3 protocol library for Zig

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages