/* Crafter®.Network Copyright (C) 2026 Catcrafts® Catcrafts.net This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 3.0 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ // HTTP/3 wire-format helpers. This partition is intentionally NOT re-exported // from Crafter.Network — it's an internal building block shared between the // ClientHTTP and ListenerHTTP implementation files. // // Scope: // - QUIC variable-length integers (RFC 9000 §16) // - HTTP/3 frame type/length codec (RFC 9114 §7) // - QPACK field-section encode/decode (RFC 9204) limited to the static // table + literal representations. No dynamic table, no Huffman. This // suffices because both peers in this library are this same library; // interoperability with browsers/curl over h3 would additionally require // a Huffman codec which is out of scope here. export module Crafter.Network:HTTP3; import std; namespace Crafter::HTTP3 { // ---------------- ALPN ---------------- // RFC 9114 §3.1 — final h3 ALPN identifier. export inline constexpr std::string_view kAlpn = "h3"; // ---------------- Frame types (RFC 9114 §7.2) ---------------- export inline constexpr std::uint64_t kFrameData = 0x00; export inline constexpr std::uint64_t kFrameHeaders = 0x01; export inline constexpr std::uint64_t kFrameSettings = 0x04; // WebTransport bidirectional stream frame type (draft-ietf-webtrans-http3). // Distinct from normal HTTP/3 frames — its body is unbounded (runs to FIN) // rather than length-prefixed, and the first bytes of the body are the // session id varint. export inline constexpr std::uint64_t kFrameWtStream = 0x41; // ---------------- Unidirectional stream types (RFC 9114 §6.2) ---------------- export inline constexpr std::uint64_t kStreamControl = 0x00; export inline constexpr std::uint64_t kStreamQpackEnc = 0x02; export inline constexpr std::uint64_t kStreamQpackDec = 0x03; // WebTransport unidirectional stream type (draft-ietf-webtrans-http3). // After this varint comes a session id varint, then opaque payload to FIN. export inline constexpr std::uint64_t kStreamWt = 0x54; // ---------------- SETTINGS parameter identifiers ---------------- // Required to negotiate WebTransport over HTTP/3 + HTTP/3 datagrams. export inline constexpr std::uint64_t kSettingQpackMaxTableCapacity = 0x01; // RFC 9204 export inline constexpr std::uint64_t kSettingQpackBlockedStreams = 0x07; // RFC 9204 export inline constexpr std::uint64_t kSettingEnableConnectProtocol = 0x08; // RFC 9220 export inline constexpr std::uint64_t kSettingH3Datagram = 0x33; // RFC 9297 // Legacy identifiers from older WebTransport / H3-DATAGRAM drafts. Chrome // (as of M120-ish) advertises and looks for the draft-02 / draft-04 ids // alongside the RFC ones; if we only send the modern ids it decides we // don't support WebTransport and aborts with ERR_METHOD_NOT_SUPPORTED. export inline constexpr std::uint64_t kSettingH3DatagramDraft04 = 0xffd277; // draft-ietf-masque-h3-datagram-04 export inline constexpr std::uint64_t kSettingEnableWebTransport = 0x2b603742; // draft-02 boolean export inline constexpr std::uint64_t kSettingWtMaxSessions = 0xc671706a; // draft-ietf-webtrans-http3 (-07+) // ---------------- Errors ---------------- export class HTTP3ProtocolError : public std::runtime_error { public: using std::runtime_error::runtime_error; }; // ---------------- QUIC varint (RFC 9000 §16) ---------------- // Encodes value into the smallest of {1, 2, 4, 8}-byte forms. export inline void EncodeVarint(std::uint64_t value, std::vector& out) { if (value < (1ULL << 6)) { out.push_back(static_cast(value)); } else if (value < (1ULL << 14)) { out.push_back(static_cast(0x40 | (value >> 8))); out.push_back(static_cast(value & 0xFF)); } else if (value < (1ULL << 30)) { out.push_back(static_cast(0x80 | ((value >> 24) & 0x3F))); out.push_back(static_cast((value >> 16) & 0xFF)); out.push_back(static_cast((value >> 8) & 0xFF)); out.push_back(static_cast(value & 0xFF)); } else if (value < (1ULL << 62)) { out.push_back(static_cast(0xC0 | ((value >> 56) & 0x3F))); out.push_back(static_cast((value >> 48) & 0xFF)); out.push_back(static_cast((value >> 40) & 0xFF)); out.push_back(static_cast((value >> 32) & 0xFF)); out.push_back(static_cast((value >> 24) & 0xFF)); out.push_back(static_cast((value >> 16) & 0xFF)); out.push_back(static_cast((value >> 8) & 0xFF)); out.push_back(static_cast(value & 0xFF)); } else { throw HTTP3ProtocolError("varint value exceeds 2^62-1"); } } // Returns true on success. On false, no consumed/value mutation observed. export inline bool DecodeVarint(const std::uint8_t* data, std::size_t available, std::uint64_t& value, std::size_t& consumed) { if (available == 0) return false; std::uint8_t first = data[0]; std::size_t len = std::size_t{1} << (first >> 6); if (available < len) return false; std::uint64_t v = first & 0x3F; for (std::size_t i = 1; i < len; ++i) { v = (v << 8) | data[i]; } value = v; consumed = len; return true; } // ---------------- QPACK / HPACK-style integer (RFC 7541 §5.1) ---------------- // Different beast from QUIC varint. N-bit prefix integer used inside QPACK // representations; the high (8-N) bits of the first byte carry pattern flags. export inline void EncodeQpackInt(std::vector& out, std::uint8_t topBits, int N, std::uint64_t value) { std::uint8_t mask = static_cast((1U << N) - 1); if (value < mask) { out.push_back(static_cast(topBits | value)); return; } out.push_back(static_cast(topBits | mask)); value -= mask; while (value >= 128) { out.push_back(static_cast((value & 0x7F) | 0x80)); value >>= 7; } out.push_back(static_cast(value)); } // Decode N-bit-prefix integer. data[0] holds prefix flags; the low N bits // contribute to the value (continuation bytes follow if low N bits == mask). export inline bool DecodeQpackInt(const std::uint8_t* data, std::size_t available, int N, std::uint64_t& value, std::size_t& consumed) { if (available == 0) return false; std::uint8_t mask = static_cast((1U << N) - 1); std::uint8_t first = data[0] & mask; if (first < mask) { value = first; consumed = 1; return true; } std::uint64_t v = mask; int shift = 0; std::size_t i = 1; while (true) { if (i >= available) return false; std::uint8_t b = data[i++]; v += static_cast(b & 0x7F) << shift; if ((b & 0x80) == 0) break; shift += 7; if (shift > 63) return false; } value = v; consumed = i; return true; } // ---------------- Huffman codec (RFC 7541 Appendix B) ---------------- // Decode-only. Real h3 peers (browsers, curl, cloudflare, etc.) Huffman- // encode header values by default, so without this any external interop // collapses on the first response. We don't emit Huffman ourselves — // peers MUST accept H=0 literal per the spec, so encoding doesn't add // interop value, only wire compactness. The 256-entry table plus a // straightforward bit-walking decoder is the smallest viable form. struct HuffmanCode { std::uint32_t code; std::uint8_t length; }; inline constexpr std::array kHuffmanTable = {{ /* 0 */ {0x1ff8, 13}, /* 1 */ {0x7fffd8, 23}, /* 2 */ {0xfffffe2, 28}, /* 3 */ {0xfffffe3, 28}, /* 4 */ {0xfffffe4, 28}, /* 5 */ {0xfffffe5, 28}, /* 6 */ {0xfffffe6, 28}, /* 7 */ {0xfffffe7, 28}, /* 8 */ {0xfffffe8, 28}, /* 9 */ {0xffffea, 24}, /* 10 */ {0x3ffffffc,30}, /* 11 */ {0xfffffe9, 28}, /* 12 */ {0xfffffea, 28}, /* 13 */ {0x3ffffffd,30}, /* 14 */ {0xfffffeb, 28}, /* 15 */ {0xfffffec, 28}, /* 16 */ {0xfffffed, 28}, /* 17 */ {0xfffffee, 28}, /* 18 */ {0xfffffef, 28}, /* 19 */ {0xffffff0, 28}, /* 20 */ {0xffffff1, 28}, /* 21 */ {0xffffff2, 28}, /* 22 */ {0x3ffffffe,30}, /* 23 */ {0xffffff3, 28}, /* 24 */ {0xffffff4, 28}, /* 25 */ {0xffffff5, 28}, /* 26 */ {0xffffff6, 28}, /* 27 */ {0xffffff7, 28}, /* 28 */ {0xffffff8, 28}, /* 29 */ {0xffffff9, 28}, /* 30 */ {0xffffffa, 28}, /* 31 */ {0xffffffb, 28}, /* 32 */ {0x14, 6}, /* 33 */ {0x3f8, 10}, /* 34 */ {0x3f9, 10}, /* 35 */ {0xffa, 12}, /* 36 */ {0x1ff9, 13}, /* 37 */ {0x15, 6}, /* 38 */ {0xf8, 8}, /* 39 */ {0x7fa, 11}, /* 40 */ {0x3fa, 10}, /* 41 */ {0x3fb, 10}, /* 42 */ {0xf9, 8}, /* 43 */ {0x7fb, 11}, /* 44 */ {0xfa, 8}, /* 45 */ {0x16, 6}, /* 46 */ {0x17, 6}, /* 47 */ {0x18, 6}, /* 48 */ {0x0, 5}, /* 49 */ {0x1, 5}, /* 50 */ {0x2, 5}, /* 51 */ {0x19, 6}, /* 52 */ {0x1a, 6}, /* 53 */ {0x1b, 6}, /* 54 */ {0x1c, 6}, /* 55 */ {0x1d, 6}, /* 56 */ {0x1e, 6}, /* 57 */ {0x1f, 6}, /* 58 */ {0x5c, 7}, /* 59 */ {0xfb, 8}, /* 60 */ {0x7ffc, 15}, /* 61 */ {0x20, 6}, /* 62 */ {0xffb, 12}, /* 63 */ {0x3fc, 10}, /* 64 */ {0x1ffa, 13}, /* 65 */ {0x21, 6}, /* 66 */ {0x5d, 7}, /* 67 */ {0x5e, 7}, /* 68 */ {0x5f, 7}, /* 69 */ {0x60, 7}, /* 70 */ {0x61, 7}, /* 71 */ {0x62, 7}, /* 72 */ {0x63, 7}, /* 73 */ {0x64, 7}, /* 74 */ {0x65, 7}, /* 75 */ {0x66, 7}, /* 76 */ {0x67, 7}, /* 77 */ {0x68, 7}, /* 78 */ {0x69, 7}, /* 79 */ {0x6a, 7}, /* 80 */ {0x6b, 7}, /* 81 */ {0x6c, 7}, /* 82 */ {0x6d, 7}, /* 83 */ {0x6e, 7}, /* 84 */ {0x6f, 7}, /* 85 */ {0x70, 7}, /* 86 */ {0x71, 7}, /* 87 */ {0x72, 7}, /* 88 */ {0xfc, 8}, /* 89 */ {0x73, 7}, /* 90 */ {0xfd, 8}, /* 91 */ {0x1ffb, 13}, /* 92 */ {0x7fff0, 19}, /* 93 */ {0x1ffc, 13}, /* 94 */ {0x3ffc, 14}, /* 95 */ {0x22, 6}, /* 96 */ {0x7ffd, 15}, /* 97 */ {0x3, 5}, /* 98 */ {0x23, 6}, /* 99 */ {0x4, 5}, /*100 */ {0x24, 6}, /*101 */ {0x5, 5}, /*102 */ {0x25, 6}, /*103 */ {0x26, 6}, /*104 */ {0x27, 6}, /*105 */ {0x6, 5}, /*106 */ {0x74, 7}, /*107 */ {0x75, 7}, /*108 */ {0x28, 6}, /*109 */ {0x29, 6}, /*110 */ {0x2a, 6}, /*111 */ {0x7, 5}, /*112 */ {0x2b, 6}, /*113 */ {0x76, 7}, /*114 */ {0x2c, 6}, /*115 */ {0x8, 5}, /*116 */ {0x9, 5}, /*117 */ {0x2d, 6}, /*118 */ {0x77, 7}, /*119 */ {0x78, 7}, /*120 */ {0x79, 7}, /*121 */ {0x7a, 7}, /*122 */ {0x7b, 7}, /*123 */ {0x7ffe, 15}, /*124 */ {0x7fc, 11}, /*125 */ {0x3ffd, 14}, /*126 */ {0x1ffd, 13}, /*127 */ {0xffffffc, 28}, /*128 */ {0xfffe6, 20}, /*129 */ {0x3fffd2, 22}, /*130 */ {0xfffe7, 20}, /*131 */ {0xfffe8, 20}, /*132 */ {0x3fffd3, 22}, /*133 */ {0x3fffd4, 22}, /*134 */ {0x3fffd5, 22}, /*135 */ {0x7fffd9, 23}, /*136 */ {0x3fffd6, 22}, /*137 */ {0x7fffda, 23}, /*138 */ {0x7fffdb, 23}, /*139 */ {0x7fffdc, 23}, /*140 */ {0x7fffdd, 23}, /*141 */ {0x7fffde, 23}, /*142 */ {0xffffeb, 24}, /*143 */ {0x7fffdf, 23}, /*144 */ {0xffffec, 24}, /*145 */ {0xffffed, 24}, /*146 */ {0x3fffd7, 22}, /*147 */ {0x7fffe0, 23}, /*148 */ {0xffffee, 24}, /*149 */ {0x7fffe1, 23}, /*150 */ {0x7fffe2, 23}, /*151 */ {0x7fffe3, 23}, /*152 */ {0x7fffe4, 23}, /*153 */ {0x1fffdc, 21}, /*154 */ {0x3fffd8, 22}, /*155 */ {0x7fffe5, 23}, /*156 */ {0x3fffd9, 22}, /*157 */ {0x7fffe6, 23}, /*158 */ {0x7fffe7, 23}, /*159 */ {0xffffef, 24}, /*160 */ {0x3fffda, 22}, /*161 */ {0x1fffdd, 21}, /*162 */ {0xfffe9, 20}, /*163 */ {0x3fffdb, 22}, /*164 */ {0x3fffdc, 22}, /*165 */ {0x7fffe8, 23}, /*166 */ {0x7fffe9, 23}, /*167 */ {0x1fffde, 21}, /*168 */ {0x7fffea, 23}, /*169 */ {0x3fffdd, 22}, /*170 */ {0x3fffde, 22}, /*171 */ {0xfffff0, 24}, /*172 */ {0x1fffdf, 21}, /*173 */ {0x3fffdf, 22}, /*174 */ {0x7fffeb, 23}, /*175 */ {0x7fffec, 23}, /*176 */ {0x1fffe0, 21}, /*177 */ {0x1fffe1, 21}, /*178 */ {0x3fffe0, 22}, /*179 */ {0x1fffe2, 21}, /*180 */ {0x7fffed, 23}, /*181 */ {0x3fffe1, 22}, /*182 */ {0x7fffee, 23}, /*183 */ {0x7fffef, 23}, /*184 */ {0xfffea, 20}, /*185 */ {0x3fffe2, 22}, /*186 */ {0x3fffe3, 22}, /*187 */ {0x3fffe4, 22}, /*188 */ {0x7ffff0, 23}, /*189 */ {0x3fffe5, 22}, /*190 */ {0x3fffe6, 22}, /*191 */ {0x7ffff1, 23}, /*192 */ {0x3ffffe0,26}, /*193 */ {0x3ffffe1, 26}, /*194 */ {0xfffeb, 20}, /*195 */ {0x7fff1, 19}, /*196 */ {0x3fffe7, 22}, /*197 */ {0x7ffff2, 23}, /*198 */ {0x3fffe8, 22}, /*199 */ {0x1ffffec, 25}, /*200 */ {0x3ffffe2, 26}, /*201 */ {0x3ffffe3,26}, /*202 */ {0x3ffffe4, 26}, /*203 */ {0x7ffffde, 27}, /*204 */ {0x7ffffdf,27}, /*205 */ {0x3ffffe5, 26}, /*206 */ {0xfffff1, 24}, /*207 */ {0x1ffffed,25}, /*208 */ {0x7fff2, 19}, /*209 */ {0x1fffe3, 21}, /*210 */ {0x3ffffe6,26}, /*211 */ {0x7ffffe0, 27}, /*212 */ {0x7ffffe1, 27}, /*213 */ {0x3ffffe7,26}, /*214 */ {0x7ffffe2, 27}, /*215 */ {0xfffff2, 24}, /*216 */ {0x1fffe4, 21}, /*217 */ {0x1fffe5, 21}, /*218 */ {0x3ffffe8, 26}, /*219 */ {0x3ffffe9,26}, /*220 */ {0xffffffd,28}, /*221 */ {0x7ffffe3, 27}, /*222 */ {0x7ffffe4,27}, /*223 */ {0x7ffffe5, 27}, /*224 */ {0xfffec, 20}, /*225 */ {0xfffff3, 24}, /*226 */ {0xfffed, 20}, /*227 */ {0x1fffe6, 21}, /*228 */ {0x3fffe9, 22}, /*229 */ {0x1fffe7, 21}, /*230 */ {0x1fffe8, 21}, /*231 */ {0x7ffff3, 23}, /*232 */ {0x3fffea, 22}, /*233 */ {0x3fffeb, 22}, /*234 */ {0x1ffffee,25}, /*235 */ {0x1ffffef, 25}, /*236 */ {0xfffff4, 24}, /*237 */ {0xfffff5, 24}, /*238 */ {0x3ffffea, 26}, /*239 */ {0x7ffff4, 23}, /*240 */ {0x3ffffeb,26}, /*241 */ {0x7ffffe6, 27}, /*242 */ {0x3ffffec, 26}, /*243 */ {0x3ffffed,26}, /*244 */ {0x7ffffe7, 27}, /*245 */ {0x7ffffe8, 27}, /*246 */ {0x7ffffe9,27}, /*247 */ {0x7ffffea, 27}, /*248 */ {0x7ffffeb, 27}, /*249 */ {0xffffffe,28}, /*250 */ {0x7ffffec, 27}, /*251 */ {0x7ffffed, 27}, /*252 */ {0x7ffffee,27}, /*253 */ {0x7ffffef, 27}, /*254 */ {0x7fffff0, 27}, /*255 */ {0x3ffffee,26}, }}; export inline std::string DecodeHuffman(const std::uint8_t* in, std::size_t inLen) { // Bit-walking decoder. Refill a 64-bit register from the input byte // stream, then for each output symbol scan the 256-entry table for // the unique code that matches the top `bits` of the register at // some length L in [5, 30]. Linear scan is acceptable for the // header-section sizes seen in HTTP/3 traffic; the inner loop is // hot for ~100s of bytes per request. std::string out; std::uint64_t reg = 0; int bits = 0; std::size_t pos = 0; while (true) { while (bits <= 56 && pos < inLen) { reg = (reg << 8) | in[pos++]; bits += 8; } if (bits == 0) return out; bool matched = false; const int maxL = bits < 30 ? bits : 30; for (int L = 5; L <= maxL; ++L) { std::uint32_t want = static_cast( (reg >> (bits - L)) & ((std::uint64_t{1} << L) - 1)); for (std::size_t s = 0; s < 256; ++s) { if (kHuffmanTable[s].length == L && kHuffmanTable[s].code == want) { out.push_back(static_cast(s)); bits -= L; matched = true; break; } } if (matched) break; } if (!matched) { // Tail must be ≤ 7 bits and all 1s — that's the EOS-prefix // padding RFC 7541 §5.2 mandates. Anything else is malformed. if (bits <= 7) { std::uint64_t mask = (std::uint64_t{1} << bits) - 1; if ((reg & mask) == mask) return out; } throw HTTP3ProtocolError("Huffman: invalid encoding"); } } } // ---------------- QPACK static table (RFC 9204 Appendix A, subset) ---------------- // We embed only the entries we either emit (encode) or might need to look // up by index (decode peers using indexed/literal-with-name-ref). The // subset covers all pseudo-headers and a few common content-type/status // values — enough for self-interop. If the peer references an index // outside this table we throw HTTP3ProtocolError. struct StaticEntry { std::string_view name; std::string_view value; // empty if no canonical value (name-only entry) }; inline constexpr std::array kStaticTable = {{ {":authority", ""}, // 0 {":path", "/"}, // 1 {"age", "0"}, // 2 {"content-disposition", ""}, // 3 {"content-length", "0"}, // 4 {"cookie", ""}, // 5 {"date", ""}, // 6 {"etag", ""}, // 7 {"if-modified-since", ""}, // 8 {"if-none-match", ""}, // 9 {"last-modified", ""}, // 10 {"link", ""}, // 11 {"location", ""}, // 12 {"referer", ""}, // 13 {"set-cookie", ""}, // 14 {":method", "CONNECT"}, // 15 {":method", "DELETE"}, // 16 {":method", "GET"}, // 17 {":method", "HEAD"}, // 18 {":method", "OPTIONS"}, // 19 {":method", "POST"}, // 20 {":method", "PUT"}, // 21 {":scheme", "http"}, // 22 {":scheme", "https"}, // 23 {":status", "103"}, // 24 {":status", "200"}, // 25 {":status", "304"}, // 26 {":status", "404"}, // 27 {":status", "503"}, // 28 {"accept", "*/*"}, // 29 {"accept", "application/dns-message"}, // 30 {"accept-encoding", "gzip, deflate, br"}, // 31 {"accept-ranges", "bytes"}, // 32 {"access-control-allow-headers", "cache-control"}, // 33 {"access-control-allow-headers", "content-type"}, // 34 {"access-control-allow-origin", "*"}, // 35 {"cache-control", "max-age=0"}, // 36 {"cache-control", "max-age=2592000"}, // 37 {"cache-control", "max-age=604800"}, // 38 {"cache-control", "no-cache"}, // 39 {"cache-control", "no-store"}, // 40 {"cache-control", "public, max-age=31536000"}, // 41 {"content-encoding", "br"}, // 42 {"content-encoding", "gzip"}, // 43 {"content-type", "application/dns-message"}, // 44 {"content-type", "application/javascript"}, // 45 {"content-type", "application/json"}, // 46 {"content-type", "application/x-www-form-urlencoded"}, // 47 {"content-type", "image/gif"}, // 48 {"content-type", "image/jpeg"}, // 49 {"content-type", "image/png"}, // 50 {"content-type", "text/css"}, // 51 {"content-type", "text/html; charset=utf-8"}, // 52 {"content-type", "text/plain"}, // 53 {"content-type", "text/plain;charset=utf-8"}, // 54 {"range", "bytes=0-"}, // 55 {"strict-transport-security", "max-age=31536000"}, // 56 {"strict-transport-security", "max-age=31536000; includesubdomains"}, // 57 {"strict-transport-security", "max-age=31536000; includesubdomains; preload"}, // 58 {"vary", "accept-encoding"}, // 59 {"vary", "origin"}, // 60 {"x-content-type-options", "nosniff"}, // 61 {"x-xss-protection", "1; mode=block"}, // 62 {":status", "100"}, // 63 {":status", "204"}, // 64 {":status", "206"}, // 65 {":status", "302"}, // 66 {":status", "400"}, // 67 {":status", "403"}, // 68 {":status", "421"}, // 69 {":status", "425"}, // 70 {":status", "500"}, // 71 {"accept-language", ""}, // 72 {"access-control-allow-credentials", "FALSE"}, // 73 {"access-control-allow-credentials", "TRUE"}, // 74 {"access-control-allow-headers", "*"}, // 75 {"access-control-allow-methods", "get"}, // 76 {"access-control-allow-methods", "get, post, options"}, // 77 {"access-control-allow-methods", "options"}, // 78 {"access-control-expose-headers", "content-length"}, // 79 {"access-control-request-headers", "content-type"}, // 80 {"access-control-request-method", "get"}, // 81 {"access-control-request-method", "post"}, // 82 {"alt-svc", "clear"}, // 83 {"authorization", ""}, // 84 {"content-security-policy", "script-src 'none'; object-src 'none'; base-uri 'none'"}, // 85 {"early-data", "1"}, // 86 {"expect-ct", ""}, // 87 {"forwarded", ""}, // 88 {"if-range", ""}, // 89 {"origin", ""}, // 90 {"purpose", "prefetch"}, // 91 {"server", ""}, // 92 {"timing-allow-origin", "*"}, // 93 {"upgrade-insecure-requests", "1"}, // 94 {"user-agent", ""}, // 95 {"x-forwarded-for", ""}, // 96 {"x-frame-options", "deny"}, // 97 {"x-frame-options", "sameorigin"}, // 98 }}; // Lookup a (name, value) pair against the static table; returns -1 if not // present. Linear scan is fine here: ~100 entries, called per header. inline int StaticTableExactLookup(std::string_view name, std::string_view value) { for (std::size_t i = 0; i < kStaticTable.size(); ++i) { if (kStaticTable[i].name == name && kStaticTable[i].value == value) { return static_cast(i); } } return -1; } // Lookup name-only; returns the lowest matching index or -1. inline int StaticTableNameLookup(std::string_view name) { for (std::size_t i = 0; i < kStaticTable.size(); ++i) { if (kStaticTable[i].name == name) return static_cast(i); } return -1; } // ---------------- Field section codec ---------------- // Encodes the QPACK-on-the-wire field section: a 2-byte prefix (Required // Insert Count = 0, Sign+DeltaBase = 0 — i.e. no dynamic table reliance) // followed by per-field representations. We pick the most compact static // representation each header allows; never emit Huffman; never use the // dynamic table. export inline std::vector EncodeFieldSection( const std::vector>& fields) { std::vector out; // Prefix: Required Insert Count (8-bit prefix int = 0) EncodeQpackInt(out, 0x00, 8, 0); // Sign + Delta Base (sign in high bit of 7-bit-prefix int = 0) EncodeQpackInt(out, 0x00, 7, 0); for (const auto& [name, value] : fields) { int exact = StaticTableExactLookup(name, value); if (exact >= 0) { // Indexed Field Line, T=1 (static): pattern 1Tixxxxx, 6-bit prefix. EncodeQpackInt(out, 0xC0, 6, static_cast(exact)); continue; } int nameIdx = StaticTableNameLookup(name); if (nameIdx >= 0) { // Literal Field Line With Name Reference, T=1 (static), // N=0 (allow indexing on intermediaries — moot since no DT): // pattern 01NTxxxx, 4-bit name-index prefix. EncodeQpackInt(out, 0x50, 4, static_cast(nameIdx)); } else { // Literal Field Line With Literal Name, N=0, H=0: pattern // 001NHxxx with a 3-bit name-length prefix. EncodeQpackInt(out, 0x20, 3, name.size()); out.insert(out.end(), name.begin(), name.end()); } // Value: 7-bit length prefix, H=0 (no Huffman). EncodeQpackInt(out, 0x00, 7, value.size()); out.insert(out.end(), value.begin(), value.end()); } return out; } export inline std::vector> DecodeFieldSection( const std::uint8_t* data, std::size_t available) { std::size_t pos = 0; std::uint64_t reqIc = 0, deltaBase = 0; std::size_t cn = 0; if (!DecodeQpackInt(data + pos, available - pos, 8, reqIc, cn)) { throw HTTP3ProtocolError("QPACK: truncated Required Insert Count"); } pos += cn; if (pos >= available) throw HTTP3ProtocolError("QPACK: missing Base"); if (!DecodeQpackInt(data + pos, available - pos, 7, deltaBase, cn)) { throw HTTP3ProtocolError("QPACK: truncated Base"); } pos += cn; if (reqIc != 0) { // Encoder used the dynamic table, which we don't track. Required // Insert Count != 0 means we cannot decode without dynamic-table // state; surface a clean protocol error rather than mis-decoding. throw HTTP3ProtocolError("QPACK: dynamic table reference (Required Insert Count != 0)"); } std::vector> fields; while (pos < available) { std::uint8_t b = data[pos]; if ((b & 0x80) != 0) { // 1Txxxxxx — Indexed Field Line. bool isStatic = (b & 0x40) != 0; std::uint64_t idx = 0; if (!DecodeQpackInt(data + pos, available - pos, 6, idx, cn)) { throw HTTP3ProtocolError("QPACK: truncated indexed field line"); } pos += cn; if (!isStatic) throw HTTP3ProtocolError("QPACK: dynamic-table indexed line unsupported"); if (idx >= kStaticTable.size()) throw HTTP3ProtocolError("QPACK: static index out of range"); const auto& e = kStaticTable[static_cast(idx)]; fields.emplace_back(std::string(e.name), std::string(e.value)); } else if ((b & 0xC0) == 0x40) { // 01NTxxxx — Literal Field Line With Name Reference. bool isStatic = (b & 0x10) != 0; std::uint64_t idx = 0; if (!DecodeQpackInt(data + pos, available - pos, 4, idx, cn)) { throw HTTP3ProtocolError("QPACK: truncated literal-with-nameref index"); } pos += cn; if (!isStatic) throw HTTP3ProtocolError("QPACK: dynamic-table name reference unsupported"); if (idx >= kStaticTable.size()) throw HTTP3ProtocolError("QPACK: static name index out of range"); if (pos >= available) throw HTTP3ProtocolError("QPACK: missing value byte"); bool huffman = (data[pos] & 0x80) != 0; std::uint64_t vlen = 0; if (!DecodeQpackInt(data + pos, available - pos, 7, vlen, cn)) { throw HTTP3ProtocolError("QPACK: truncated value length"); } pos += cn; if (pos + vlen > available) throw HTTP3ProtocolError("QPACK: value runs past buffer"); std::string value = huffman ? DecodeHuffman(data + pos, static_cast(vlen)) : std::string(reinterpret_cast(data + pos), static_cast(vlen)); pos += static_cast(vlen); fields.emplace_back(std::string(kStaticTable[static_cast(idx)].name), std::move(value)); } else if ((b & 0xE0) == 0x20) { // 001NHxxx — Literal Field Line With Literal Name. bool huffmanName = (b & 0x08) != 0; std::uint64_t nlen = 0; if (!DecodeQpackInt(data + pos, available - pos, 3, nlen, cn)) { throw HTTP3ProtocolError("QPACK: truncated literal-name length"); } pos += cn; if (pos + nlen > available) throw HTTP3ProtocolError("QPACK: literal name runs past buffer"); std::string name = huffmanName ? DecodeHuffman(data + pos, static_cast(nlen)) : std::string(reinterpret_cast(data + pos), static_cast(nlen)); pos += static_cast(nlen); if (pos >= available) throw HTTP3ProtocolError("QPACK: missing value byte"); bool huffmanValue = (data[pos] & 0x80) != 0; std::uint64_t vlen = 0; if (!DecodeQpackInt(data + pos, available - pos, 7, vlen, cn)) { throw HTTP3ProtocolError("QPACK: truncated literal-value length"); } pos += cn; if (pos + vlen > available) throw HTTP3ProtocolError("QPACK: literal value runs past buffer"); std::string value = huffmanValue ? DecodeHuffman(data + pos, static_cast(vlen)) : std::string(reinterpret_cast(data + pos), static_cast(vlen)); pos += static_cast(vlen); fields.emplace_back(std::move(name), std::move(value)); } else { // Indexed-with-Post-Base / Literal-with-Post-Base-Name-Reference // both rely on a dynamic-table base offset we don't maintain. throw HTTP3ProtocolError("QPACK: post-base reference unsupported"); } } return fields; } // ---------------- Frame helpers ---------------- // A frame is: type (varint) | length (varint) | payload (length bytes). export inline void WriteFrame(std::vector& out, std::uint64_t type, const std::uint8_t* payload, std::size_t length) { EncodeVarint(type, out); EncodeVarint(static_cast(length), out); out.insert(out.end(), payload, payload + length); } // Empty SETTINGS frame body — we send no settings, accepting all defaults. export inline std::vector BuildControlStreamPrelude() { std::vector out; EncodeVarint(kStreamControl, out); // unidi stream type EncodeVarint(kFrameSettings, out); // SETTINGS frame type EncodeVarint(0, out); // frame length 0 return out; } // Server-side variant that advertises WebTransport-over-HTTP/3 support // to the peer. Without these three SETTINGS the browser silently rejects // the extended CONNECT and the WebTransport.ready promise never resolves. // `maxSessions` becomes the value of SETTINGS_WT_MAX_SESSIONS. export inline std::vector BuildWebTransportControlStreamPrelude( std::uint64_t maxSessions = 1) { // Encode the SETTINGS body first so we can write its length. The two // QPACK settings declare we run with no dynamic table — sent // explicitly because some HTTP/3 stacks (Chrome among them) refuse // to consider the peer ready for extended-CONNECT until they have // seen a baseline QPACK configuration. The draft-02 ENABLE_WEBTRANSPORT // and draft-04 H3_DATAGRAM ids are sent alongside their RFC counterparts // for compatibility with current Chrome (which still negotiates the // draft form even when advertising RFC support). std::vector body; EncodeVarint(kSettingQpackMaxTableCapacity, body); EncodeVarint(0, body); EncodeVarint(kSettingQpackBlockedStreams, body); EncodeVarint(0, body); EncodeVarint(kSettingEnableConnectProtocol, body); EncodeVarint(1, body); EncodeVarint(kSettingH3Datagram, body); EncodeVarint(1, body); EncodeVarint(kSettingH3DatagramDraft04, body); EncodeVarint(1, body); EncodeVarint(kSettingEnableWebTransport, body); EncodeVarint(1, body); EncodeVarint(kSettingWtMaxSessions, body); EncodeVarint(maxSessions, body); std::vector out; EncodeVarint(kStreamControl, out); WriteFrame(out, kFrameSettings, body.data(), body.size()); return out; } // Prefix bytes that go on the front of an outgoing WT bidi stream — the // peer reads these to know which session the stream belongs to. After // this prefix the stream contains opaque WebTransport payload until FIN // (there is no length field — WT_STREAM is the only HTTP/3 frame whose // body runs to end-of-stream). export inline std::vector BuildWtBidiPrefix(std::uint64_t sessionId) { std::vector out; EncodeVarint(kFrameWtStream, out); EncodeVarint(sessionId, out); return out; } // Prefix bytes that go on the front of an outgoing WT unidi stream // (server-initiated → client). Stream-type varint then session id. export inline std::vector BuildWtUnidiPrefix(std::uint64_t sessionId) { std::vector out; EncodeVarint(kStreamWt, out); EncodeVarint(sessionId, out); return out; } }