Skip to content

v0.0.11 - HTTP/2, hardened

Choose a tag to compare

@Kludex Kludex released this 10 Jun 08:22
89866fe

⚡ HTTP/2 has landed - and it came in hardened

This is the big one: zttp speaks HTTP/2 now, end to end, on the same sans-IO pull API you already know. A pure-Zig core does the framing, HPACK, flow control, and stream state; your code just calls receive_data() and next_event(). And before the ink was dry, it went through two security passes - so the new surface ships locked down, not "we'll harden it later."

import zttp

conn = zttp.Connection(zttp.SERVER, protocol=zttp.HTTP2)
conn.receive_data(data)          # same pull API, now multiplexed
event = conn.next_event()        # every event carries its stream_id

🚀 Highlights

  • Full HTTP/2 core, sans-IO and zero-copy. Frame codec, HPACK (Huffman + dynamic table), per-stream state machine, connection + stream flow control, and a bounded event ring - all in pure Zig, no I/O, no syscalls (#5, #18, #19, #32).
  • Read and write. Server reads requests, client reads responses, and the write side serializes responses/requests with outbound flow control and a Stream handle that routes multiplexed sends correctly (#19, #32).
  • Hardened from day one. HTTP/2 Rapid Reset (CVE-2023-44487), CONTINUATION floods (CVE-2024-27316), and H2→H1 header smuggling are all defended in the core - see Security below.
  • Now on Python 3.10 and 3.11 too (#31).

🌐 HTTP/2

  • Server read path with a sans-IO Zig core: preface + SETTINGS handshake, HEADERS/CONTINUATION reassembly, DATA, trailers, and the control frames (RST_STREAM, GOAWAY, SETTINGS, PING, WINDOW_UPDATE). Every event carries a stream_id (0 for HTTP/1.1, so existing code is untouched) (#5).
  • Client responses: read HEADERS/DATA back on a stream you opened, including interim 1xx (#18).
  • Write side, driven from Python: send_response / send_data / end_message through a per-stream Stream handle, with connection- and stream-level send windows parking and draining bytes as WINDOW_UPDATE credit arrives (#19, #32).

🔒 Security

The HTTP/2 surface got a code-level audit and a CVE-driven audit. What shipped:

  • Rapid Reset is bounded (CVE-2023-44487). Streams are evicted the moment they fully close, the concurrency count is derived (no drift), and max_streams / max_stream_resets churn caps trip ENHANCE_YOUR_CALM before a peer can grow the connection without limit (#42).
  • No header-value smuggling. The H2 read path now rejects CR/LF/NUL and edge whitespace in every header and pseudo-header value - the same rule the H1 path and the H2 writer enforce - so a re-serialized request can't be split (#38).
  • Empty :path rejected per RFC 9113 8.3.1, matching the H1 non-empty-target rule (#38).
  • Allocation failures surface honestly. Out-of-memory on the upgrade and H2 flush paths now raise MemoryError instead of being swallowed or returning a half-built event (#43).
  • Fuzzing reaches the H2 engine. The libFuzzer/OSS-Fuzz target builds again and now drives the connection/HPACK/frame/stream machine, with a matching Python oracle and an always-on property test (#44).

🧰 Tooling & platforms

  • Python 3.10 and 3.11 are supported (#31).
  • Smaller, cleaner wheels: debug info is stripped from the extension and stale artifacts are no longer bundled (#35).
  • HTTP/1.1 core regrouped under an h1/ package, making room for h2/ next to it (#29).
  • A libFuzzer + OSS-Fuzz target for the parser (#25), and docs deploy moved to Cloudflare Workers Builds (#27), with a longer wheel-build timeout for the bigger matrix (#33).

📚 Docs

  • HTTP/2 is documented across the site and marked done (#34).
  • README response example updated to the current send_response signature (#28).

Full changelog: v0.0.10...v0.0.11