578 lines
33 KiB
Text
578 lines
33 KiB
Text
|
|
/*
|
||
|
|
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;
|
||
|
|
|
||
|
|
// ---------------- 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;
|
||
|
|
|
||
|
|
// ---------------- 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<std::uint8_t>& out) {
|
||
|
|
if (value < (1ULL << 6)) {
|
||
|
|
out.push_back(static_cast<std::uint8_t>(value));
|
||
|
|
} else if (value < (1ULL << 14)) {
|
||
|
|
out.push_back(static_cast<std::uint8_t>(0x40 | (value >> 8)));
|
||
|
|
out.push_back(static_cast<std::uint8_t>(value & 0xFF));
|
||
|
|
} else if (value < (1ULL << 30)) {
|
||
|
|
out.push_back(static_cast<std::uint8_t>(0x80 | ((value >> 24) & 0x3F)));
|
||
|
|
out.push_back(static_cast<std::uint8_t>((value >> 16) & 0xFF));
|
||
|
|
out.push_back(static_cast<std::uint8_t>((value >> 8) & 0xFF));
|
||
|
|
out.push_back(static_cast<std::uint8_t>(value & 0xFF));
|
||
|
|
} else if (value < (1ULL << 62)) {
|
||
|
|
out.push_back(static_cast<std::uint8_t>(0xC0 | ((value >> 56) & 0x3F)));
|
||
|
|
out.push_back(static_cast<std::uint8_t>((value >> 48) & 0xFF));
|
||
|
|
out.push_back(static_cast<std::uint8_t>((value >> 40) & 0xFF));
|
||
|
|
out.push_back(static_cast<std::uint8_t>((value >> 32) & 0xFF));
|
||
|
|
out.push_back(static_cast<std::uint8_t>((value >> 24) & 0xFF));
|
||
|
|
out.push_back(static_cast<std::uint8_t>((value >> 16) & 0xFF));
|
||
|
|
out.push_back(static_cast<std::uint8_t>((value >> 8) & 0xFF));
|
||
|
|
out.push_back(static_cast<std::uint8_t>(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<std::uint8_t>& out, std::uint8_t topBits,
|
||
|
|
int N, std::uint64_t value) {
|
||
|
|
std::uint8_t mask = static_cast<std::uint8_t>((1U << N) - 1);
|
||
|
|
if (value < mask) {
|
||
|
|
out.push_back(static_cast<std::uint8_t>(topBits | value));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
out.push_back(static_cast<std::uint8_t>(topBits | mask));
|
||
|
|
value -= mask;
|
||
|
|
while (value >= 128) {
|
||
|
|
out.push_back(static_cast<std::uint8_t>((value & 0x7F) | 0x80));
|
||
|
|
value >>= 7;
|
||
|
|
}
|
||
|
|
out.push_back(static_cast<std::uint8_t>(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<std::uint8_t>((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<std::uint64_t>(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<HuffmanCode, 256> 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<std::uint32_t>(
|
||
|
|
(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<char>(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<StaticEntry, 99> 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<int>(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<int>(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<std::uint8_t> EncodeFieldSection(
|
||
|
|
const std::vector<std::pair<std::string, std::string>>& fields) {
|
||
|
|
std::vector<std::uint8_t> 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<std::uint64_t>(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<std::uint64_t>(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<std::pair<std::string, std::string>> 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<std::pair<std::string, std::string>> 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<std::size_t>(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<std::size_t>(vlen))
|
||
|
|
: std::string(reinterpret_cast<const char*>(data + pos),
|
||
|
|
static_cast<std::size_t>(vlen));
|
||
|
|
pos += static_cast<std::size_t>(vlen);
|
||
|
|
fields.emplace_back(std::string(kStaticTable[static_cast<std::size_t>(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<std::size_t>(nlen))
|
||
|
|
: std::string(reinterpret_cast<const char*>(data + pos),
|
||
|
|
static_cast<std::size_t>(nlen));
|
||
|
|
pos += static_cast<std::size_t>(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<std::size_t>(vlen))
|
||
|
|
: std::string(reinterpret_cast<const char*>(data + pos),
|
||
|
|
static_cast<std::size_t>(vlen));
|
||
|
|
pos += static_cast<std::size_t>(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<std::uint8_t>& out, std::uint64_t type,
|
||
|
|
const std::uint8_t* payload, std::size_t length) {
|
||
|
|
EncodeVarint(type, out);
|
||
|
|
EncodeVarint(static_cast<std::uint64_t>(length), out);
|
||
|
|
out.insert(out.end(), payload, payload + length);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Empty SETTINGS frame body — we send no settings, accepting all defaults.
|
||
|
|
export inline std::vector<std::uint8_t> BuildControlStreamPrelude() {
|
||
|
|
std::vector<std::uint8_t> out;
|
||
|
|
EncodeVarint(kStreamControl, out); // unidi stream type
|
||
|
|
EncodeVarint(kFrameSettings, out); // SETTINGS frame type
|
||
|
|
EncodeVarint(0, out); // frame length 0
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
}
|