DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
qpack.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file qpack.cpp
6 * @brief QPACK (RFC 9204) - implementation. See qpack.h.
7 *
8 * The static table is generated verbatim from RFC 9204 Appendix A (0-indexed). The prefix-integer
9 * and Huffman primitives are shared with HPACK via hpack_prim.h. No dynamic table is maintained:
10 * we encode only against the static table and reject any dynamic-table reference on decode.
11 */
12
14
15#if DETWS_ENABLE_HTTP3
16
17#include "network_drivers/presentation/hpack_prim/hpack_prim.h" // shared prefix-int + Huffman
18#include <string.h>
19
20namespace
21{
22
23// QPACK static table (RFC 9204 Appendix A, 0-indexed). {name, value}. Generated from the RFC.
24const char *const QPACK_STATIC[99][2] = {
25 {":authority", ""},
26 {":path", "/"},
27 {"age", "0"},
28 {"content-disposition", ""},
29 {"content-length", "0"},
30 {"cookie", ""},
31 {"date", ""},
32 {"etag", ""},
33 {"if-modified-since", ""},
34 {"if-none-match", ""},
35 {"last-modified", ""},
36 {"link", ""},
37 {"location", ""},
38 {"referer", ""},
39 {"set-cookie", ""},
40 {":method", "CONNECT"},
41 {":method", "DELETE"},
42 {":method", "GET"},
43 {":method", "HEAD"},
44 {":method", "OPTIONS"},
45 {":method", "POST"},
46 {":method", "PUT"},
47 {":scheme", "http"},
48 {":scheme", "https"},
49 {":status", "103"},
50 {":status", "200"},
51 {":status", "304"},
52 {":status", "404"},
53 {":status", "503"},
54 {"accept", "*/*"},
55 {"accept", "application/dns-message"},
56 {"accept-encoding", "gzip, deflate, br"},
57 {"accept-ranges", "bytes"},
58 {"access-control-allow-headers", "cache-control"},
59 {"access-control-allow-headers", "content-type"},
60 {"access-control-allow-origin", "*"},
61 {"cache-control", "max-age=0"},
62 {"cache-control", "max-age=2592000"},
63 {"cache-control", "max-age=604800"},
64 {"cache-control", "no-cache"},
65 {"cache-control", "no-store"},
66 {"cache-control", "public, max-age=31536000"},
67 {"content-encoding", "br"},
68 {"content-encoding", "gzip"},
69 {"content-type", "application/dns-message"},
70 {"content-type", "application/javascript"},
71 {"content-type", "application/json"},
72 {"content-type", "application/x-www-form-urlencoded"},
73 {"content-type", "image/gif"},
74 {"content-type", "image/jpeg"},
75 {"content-type", "image/png"},
76 {"content-type", "text/css"},
77 {"content-type", "text/html;charset=utf-8"},
78 {"content-type", "text/plain"},
79 {"content-type", "text/plain;charset=utf-8"},
80 {"range", "bytes=0-"},
81 {"strict-transport-security", "max-age=31536000"},
82 {"strict-transport-security", "max-age=31536000;includesubdomains"},
83 {"strict-transport-security", "max-age=31536000;includesubdomains;preload"},
84 {"vary", "accept-encoding"},
85 {"vary", "origin"},
86 {"x-content-type-options", "nosniff"},
87 {"x-xss-protection", "1; mode=block"},
88 {":status", "100"},
89 {":status", "204"},
90 {":status", "206"},
91 {":status", "302"},
92 {":status", "400"},
93 {":status", "403"},
94 {":status", "421"},
95 {":status", "425"},
96 {":status", "500"},
97 {"accept-language", ""},
98 {"access-control-allow-credentials", "FALSE"},
99 {"access-control-allow-credentials", "TRUE"},
100 {"access-control-allow-headers", "*"},
101 {"access-control-allow-methods", "get"},
102 {"access-control-allow-methods", "get, post, options"},
103 {"access-control-allow-methods", "options"},
104 {"access-control-expose-headers", "content-length"},
105 {"access-control-request-headers", "content-type"},
106 {"access-control-request-method", "get"},
107 {"access-control-request-method", "post"},
108 {"alt-svc", "clear"},
109 {"authorization", ""},
110 {"content-security-policy", "script-src 'none';object-src 'none';base-uri 'none'"},
111 {"early-data", "1"},
112 {"expect-ct", ""},
113 {"forwarded", ""},
114 {"if-range", ""},
115 {"origin", ""},
116 {"purpose", "prefetch"},
117 {"server", ""},
118 {"timing-allow-origin", "*"},
119 {"upgrade-insecure-requests", "1"},
120 {"user-agent", ""},
121 {"x-forwarded-for", ""},
122 {"x-frame-options", "deny"},
123 {"x-frame-options", "sameorigin"},
124};
125
126// Read a length-prefixed string (H bit at 0x80, 7-bit length prefix) at block[*pos] into out.
127bool decode_str7(const uint8_t *block, size_t len, size_t *pos, char *out, size_t cap, size_t *out_len)
128{
129 if (*pos >= len)
130 return false;
131 bool huff = (block[*pos] & 0x80) != 0;
132 size_t c = 0;
133 uint32_t slen = 0;
134 if (!hpack_decode_int(block + *pos, len - *pos, 7, &c, &slen))
135 return false;
136 *pos += c;
137 if (*pos + slen > len)
138 return false;
139 if (huff)
140 {
141 if (!hpack_huff_decode(block + *pos, slen, out, cap, out_len))
142 return false;
143 }
144 else
145 {
146 if (slen > cap)
147 return false;
148 memcpy(out, block + *pos, slen);
149 *out_len = slen;
150 }
151 *pos += slen;
152 return true;
153}
154
155// Encode a length-prefixed string (H bit at 0x80, 7-bit length prefix); Huffman when shorter.
156size_t encode_str7(uint8_t *out, size_t cap, const char *s, size_t n)
157{
158 size_t hl = hpack_huff_len(s, n);
159 if (hl < n)
160 {
161 size_t hdr = hpack_encode_int(out, cap, 7, 0x80, (uint32_t)hl);
162 if (!hdr)
163 return 0;
164 size_t body = hpack_huff_encode(out + hdr, cap - hdr, s, n);
165 if (body != hl)
166 return 0;
167 return hdr + body;
168 }
169 size_t hdr = hpack_encode_int(out, cap, 7, 0x00, (uint32_t)n);
170 if (!hdr || hdr + n > cap)
171 return 0;
172 memcpy(out + hdr, s, n);
173 return hdr + n;
174}
175
176} // namespace
177
178size_t qpack_encode_prefix(uint8_t *out, size_t cap)
179{
180 if (cap < 2)
181 return 0;
182 out[0] = 0x00; // Required Insert Count = 0
183 out[1] = 0x00; // S = 0, Delta Base = 0
184 return 2;
185}
186
187size_t qpack_encode_header(uint8_t *out, size_t cap, const char *name, size_t name_len, const char *value,
188 size_t value_len)
189{
190 int name_idx = -1, full_idx = -1;
191 for (int i = 0; i < 99; i++)
192 {
193 if (strnlen(QPACK_STATIC[i][0], name_len + 1) == name_len && memcmp(QPACK_STATIC[i][0], name, name_len) == 0)
194 {
195 if (name_idx < 0)
196 name_idx = i;
197 if (strnlen(QPACK_STATIC[i][1], value_len + 1) == value_len &&
198 memcmp(QPACK_STATIC[i][1], value, value_len) == 0)
199 {
200 full_idx = i;
201 break;
202 }
203 }
204 }
205 if (full_idx >= 0) // Indexed Field Line, static: 1 T=1 i(6)
206 return hpack_encode_int(out, cap, 6, 0xC0, (uint32_t)full_idx);
207
208 if (name_idx >= 0)
209 { // Literal Field Line with Name Reference, static: 01 N=0 T=1 i(4)
210 size_t o = hpack_encode_int(out, cap, 4, 0x50, (uint32_t)name_idx);
211 if (!o)
212 return 0;
213 size_t vs = encode_str7(out + o, cap - o, value, value_len);
214 if (!vs)
215 return 0;
216 return o + vs;
217 }
218
219 // Literal Field Line with Literal Name: 001 N=0 H NameLen(3), name string, value string.
220 size_t hl = hpack_huff_len(name, name_len);
221 bool huff = hl < name_len;
222 size_t nbytes = huff ? hl : name_len;
223 size_t o = hpack_encode_int(out, cap, 3, (uint8_t)(0x20 | (huff ? 0x08 : 0x00)), (uint32_t)nbytes);
224 if (!o)
225 return 0;
226 if (huff)
227 {
228 size_t body = hpack_huff_encode(out + o, cap - o, name, name_len);
229 if (body != hl)
230 return 0;
231 o += body;
232 }
233 else
234 {
235 if (o + name_len > cap)
236 return 0;
237 memcpy(out + o, name, name_len);
238 o += name_len;
239 }
240 size_t vs = encode_str7(out + o, cap - o, value, value_len);
241 if (!vs)
242 return 0;
243 return o + vs;
244}
245
246bool qpack_decode(const uint8_t *block, size_t len, char *scratch, size_t scratch_cap, QpackEmitFn emit, void *ctx)
247{
248 size_t pos = 0;
249 // Encoded Field Section Prefix (RFC 9204 sec 4.5.1): Required Insert Count, then S + Delta Base.
250 size_t c = 0;
251 uint32_t ric = 0;
252 if (!hpack_decode_int(block + pos, len - pos, 8, &c, &ric))
253 return false;
254 pos += c;
255 if (ric != 0) // a non-zero Required Insert Count references the dynamic table (capacity 0)
256 return false;
257 uint32_t base = 0;
258 if (!hpack_decode_int(block + pos, len - pos, 7, &c, &base)) // S bit + Delta Base; ignored when RIC = 0
259 return false;
260 pos += c;
261
262 while (pos < len)
263 {
264 uint8_t b = block[pos];
265 if (b & 0x80)
266 { // Indexed Field Line (sec 4.5.2): 1 T i(6)
267 if (!(b & 0x40)) // T = 0 -> dynamic table
268 return false;
269 uint32_t idx = 0;
270 if (!hpack_decode_int(block + pos, len - pos, 6, &c, &idx) || idx >= 99)
271 return false;
272 pos += c;
273 const char *nm = QPACK_STATIC[idx][0];
274 const char *vl = QPACK_STATIC[idx][1];
275 if (!emit(ctx, nm, strnlen(nm, scratch_cap + 1), vl, strnlen(vl, scratch_cap + 1)))
276 return false;
277 }
278 else if ((b & 0xC0) == 0x40)
279 { // Literal Field Line with Name Reference (sec 4.5.4): 01 N T i(4)
280 bool is_static = (b & 0x10) != 0;
281 uint32_t idx = 0;
282 if (!hpack_decode_int(block + pos, len - pos, 4, &c, &idx))
283 return false;
284 pos += c;
285 if (!is_static || idx >= 99) // dynamic name reference
286 return false;
287 const char *nm = QPACK_STATIC[idx][0];
288 size_t nlen = strnlen(nm, scratch_cap + 1);
289 if (nlen > scratch_cap)
290 return false;
291 memcpy(scratch, nm, nlen);
292 size_t vlen = 0;
293 if (!decode_str7(block, len, &pos, scratch + nlen, scratch_cap - nlen, &vlen))
294 return false;
295 if (!emit(ctx, scratch, nlen, scratch + nlen, vlen))
296 return false;
297 }
298 else if ((b & 0xE0) == 0x20)
299 { // Literal Field Line with Literal Name (sec 4.5.6): 001 N H NameLen(3)
300 bool huff = (b & 0x08) != 0;
301 uint32_t nlen32 = 0;
302 if (!hpack_decode_int(block + pos, len - pos, 3, &c, &nlen32))
303 return false;
304 pos += c;
305 if (pos + nlen32 > len)
306 return false;
307 size_t nlen = 0;
308 if (huff)
309 {
310 if (!hpack_huff_decode(block + pos, nlen32, scratch, scratch_cap, &nlen))
311 return false;
312 }
313 else
314 {
315 if (nlen32 > scratch_cap)
316 return false;
317 memcpy(scratch, block + pos, nlen32);
318 nlen = nlen32;
319 }
320 pos += nlen32;
321 size_t vlen = 0;
322 if (!decode_str7(block, len, &pos, scratch + nlen, scratch_cap - nlen, &vlen))
323 return false;
324 if (!emit(ctx, scratch, nlen, scratch + nlen, vlen))
325 return false;
326 }
327 else
328 { // 0001 xxxx Indexed Post-Base / 0000 xxxx Literal Post-Base Name Ref: both dynamic
329 return false;
330 }
331 }
332 return true;
333}
334
335#endif // DETWS_ENABLE_HTTP3
Low-level field-coding primitives shared by HPACK and QPACK.
QPACK field-section compression for HTTP/3 (RFC 9204).