ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
h3_conn.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 pc_h3_conn.cpp
6 * @brief HTTP/3 application engine over QUIC streams (see pc_h3_conn.h).
7 */
8
10
11#if PC_ENABLE_HTTP3
12
15#include <string.h>
16
17// pc_h3_conn_respond builds its response HEADERS frame from a fixed 256-byte QPACK block into a
18// PC_H3_STREAM_BUF output buffer, which is why that builder's failure guard carries a coverage
19// exclusion. PC_H3_STREAM_BUF is an overridable macro (h3_conn.h), so pin the relationship here
20// rather than let a shrunken buffer silently make the excluded path reachable: 256 bytes of QPACK
21// plus the frame's type + length varints (at most 8 each).
22static_assert(PC_H3_STREAM_BUF >= 256 + 16,
23 "PC_H3_STREAM_BUF must hold a whole response HEADERS frame: the 256-byte QPACK field section "
24 "pc_h3_conn_respond builds plus the H3 frame type and length varints");
25
26namespace
27{
28H3Stream *pc_h3_stream_get(H3Conn *h3, uint64_t id, bool create)
29{
30 H3Stream *free_slot = nullptr;
31 for (size_t i = 0; i < PC_H3_MAX_STREAMS; i++)
32 {
33 if (h3->streams[i].role != H3StreamRole::H3_ROLE_FREE && h3->streams[i].id == id)
34 {
35 return &h3->streams[i];
36 }
37 if (!free_slot && h3->streams[i].role == H3StreamRole::H3_ROLE_FREE)
38 {
39 free_slot = &h3->streams[i];
40 }
41 }
42 if (!create || !free_slot)
43 {
44 return nullptr;
45 }
46 memset(free_slot, 0, sizeof(*free_slot));
47 free_slot->id = id;
48 return free_slot;
49}
50
51// Copy a bounded, NUL-terminated field.
52void set_field(char *dst, size_t cap, const char *src, size_t len)
53{
54 if (len >= cap)
55 {
56 len = cap - 1;
57 }
58 memcpy(dst, src, len);
59 dst[len] = '\0';
60}
61
62// QPACK emit target: capture the request pseudo-headers.
63struct ReqEmit
64{
65 H3Stream *st;
66};
67bool req_emit(void *ctx, const char *name, size_t nlen, const char *value, size_t vlen)
68{
69 H3Stream *st = ((ReqEmit *)ctx)->st;
70 if (nlen == 7 && memcmp(name, ":method", 7) == 0)
71 {
72 set_field(st->method, sizeof(st->method), value, vlen);
73 }
74 else if (nlen == 5 && memcmp(name, ":path", 5) == 0)
75 {
76 set_field(st->path, sizeof(st->path), value, vlen);
77 }
78 else if (nlen == 10 && memcmp(name, ":authority", 10) == 0)
79 {
80 set_field(st->authority, sizeof(st->authority), value, vlen);
81 }
82 return true; // ignore regular headers for now (routing is by method + path)
83}
84
85// Parse the accumulated request stream: decode HEADERS, coalesce DATA into a body, and dispatch.
86void dispatch_request(H3Conn *h3, H3Stream *st)
87{
88 static uint8_t body[PC_H3_STREAM_BUF];
89 static char scratch[PC_H3_PATH_LEN + PC_H3_AUTHORITY_LEN + 64];
90 size_t body_len = 0;
91
92 size_t off = 0;
93 while (off < st->buf_len)
94 {
95 H3Frame fr;
96 if (!pc_h3_frame_parse(st->buf + off, st->buf_len - off, &fr))
97 {
98 break;
99 }
100 size_t payload = off + fr.header_len;
101 if (payload + fr.length > st->buf_len)
102 {
103 break; // incomplete frame
104 }
105 const uint8_t *fp = st->buf + payload;
106 if (fr.type == H3FrameType::H3_HEADERS)
107 {
108 ReqEmit e = {st};
109 pc_qpack_decode(fp, (size_t)fr.length, scratch, sizeof(scratch), req_emit, &e);
110 st->have_headers = true;
111 }
112 else if (fr.type == H3FrameType::H3_DATA)
113 {
114 // Copy only while there is room left in body. room is 0 once body is full (no underflow),
115 // and take is clamped to it, so body_len + take <= sizeof(body). Both arms of both guards
116 // are defensive: body and st->buf are the same PC_H3_STREAM_BUF size, and every DATA
117 // payload counted into body_len sits behind a frame header inside st->buf, so the running
118 // total is always strictly below sizeof(body) and take never exceeds room.
119 size_t room = (body_len < sizeof(body)) ? sizeof(body) - body_len : 0; // GCOVR_EXCL_LINE
120 size_t take = (size_t)fr.length;
121 if (take > room) // GCOVR_EXCL_LINE
122 {
123 take = room; // GCOVR_EXCL_LINE
124 }
125 if (take)
126 {
127 memcpy(body + body_len, fp, take);
128 }
129 body_len += take;
130 }
131 off = payload + (size_t)fr.length;
132 }
133
134 if (st->have_headers && h3->on_request)
135 {
136 h3->on_request(h3->app, h3, st->id, st->method, st->path, st->authority, body, body_len);
137 }
138}
139
140void append(H3Stream *st, const uint8_t *data, size_t len)
141{
142 if (len > sizeof(st->buf) - st->buf_len)
143 {
144 len = sizeof(st->buf) - st->buf_len;
145 }
146 memcpy(st->buf + st->buf_len, data, len);
147 st->buf_len += len;
148}
149
150// Read the leading stream-type varint of a uni stream and set st->role; consumes it from the buffer.
151// Returns false if more bytes are needed (nothing consumed).
152bool pc_h3_classify_uni_stream(H3Stream *st)
153{
154 uint64_t type = 0;
155 size_t c = 0;
156 if (!pc_quic_varint_decode(st->buf, st->buf_len, &type, &c))
157 {
158 return false; // need more bytes for the varint
159 }
160 st->type_read = true;
161 if (type == 0x00)
162 {
163 st->role = H3StreamRole::H3_ROLE_CONTROL;
164 }
165 else if (type == 0x02)
166 {
167 st->role = H3StreamRole::H3_ROLE_QPACK_ENC;
168 }
169 else if (type == 0x03)
170 {
171 st->role = H3StreamRole::H3_ROLE_QPACK_DEC;
172 }
173 else
174 {
175 st->role = H3StreamRole::H3_ROLE_OTHER_UNI;
176 }
177 memmove(st->buf, st->buf + c, st->buf_len - c);
178 st->buf_len -= c;
179 return true;
180}
181
182// Parse whatever complete frames the control stream holds (SETTINGS first), consuming them.
183void pc_h3_consume_control(H3Conn *h3, H3Stream *st)
184{
185 size_t off = 0;
186 while (off < st->buf_len)
187 {
188 H3Frame fr;
189 if (!pc_h3_frame_parse(st->buf + off, st->buf_len - off, &fr))
190 {
191 break;
192 }
193 if (off + fr.header_len + fr.length > st->buf_len)
194 {
195 break;
196 }
197 if (fr.type == H3FrameType::H3_SETTINGS)
198 {
199 pc_h3_settings_defaults(&h3->peer_settings);
200 pc_h3_parse_settings(st->buf + off + fr.header_len, (size_t)fr.length, &h3->peer_settings);
201 }
202 off += fr.header_len + (size_t)fr.length;
203 }
204 memmove(st->buf, st->buf + off, st->buf_len - off);
205 st->buf_len -= off;
206}
207
208void on_stream_data(void *app, QuicConn *, uint64_t stream_id, const uint8_t *data, size_t len, bool fin)
209{
210 H3Conn *h3 = (H3Conn *)app;
211 H3Stream *st = pc_h3_stream_get(h3, stream_id, true);
212 if (!st)
213 {
214 return;
215 }
216
217 if (st->role == H3StreamRole::H3_ROLE_FREE)
218 {
219 st->role = (stream_id & 0x03) == 0x00 ? H3StreamRole::H3_ROLE_REQUEST : H3StreamRole::H3_ROLE_OTHER_UNI;
220 }
221
222 append(st, data, len);
223
224 // A unidirectional stream begins with a stream-type varint; classify it once.
225 if (st->role != H3StreamRole::H3_ROLE_REQUEST && !st->type_read && st->buf_len >= 1 &&
226 !pc_h3_classify_uni_stream(st))
227 {
228 return; // need more bytes for the varint
229 }
230
231 if (st->role == H3StreamRole::H3_ROLE_CONTROL)
232 {
233 pc_h3_consume_control(h3, st);
234 return;
235 }
236 if (st->role != H3StreamRole::H3_ROLE_REQUEST)
237 {
238 st->buf_len = 0; // QPACK/other uni streams: nothing to do (static-table only)
239 return;
240 }
241
242 if (fin)
243 {
244 dispatch_request(h3, st);
245 }
246}
247
248void on_handshake_done(void *app, QuicConn *qc)
249{
250 H3Conn *h3 = (H3Conn *)app;
251 if (h3->control_opened)
252 {
253 return;
254 }
255 h3->control_opened = true;
256
257 // Server control stream (id 3): stream type 0x00 + SETTINGS.
258 uint8_t buf[64];
259 size_t p = pc_quic_varint_encode(buf, sizeof(buf), 0x00);
260 static const uint64_t ids[] = {H3Setting::H3_SETTINGS_QPACK_MAX_TABLE_CAPACITY,
261 H3Setting::H3_SETTINGS_QPACK_BLOCKED_STREAMS};
262 static const uint64_t vals[] = {0, 0};
263 p += pc_h3_build_settings(buf + p, sizeof(buf) - p, ids, vals, 2);
264 pc_quic_conn_stream_send(qc, 3, buf, p, false);
265
266 // QPACK encoder (id 7, type 0x02) and decoder (id 11, type 0x03) streams: type byte only.
267 uint8_t t;
268 size_t n = pc_quic_varint_encode(&t, 1, 0x02);
269 pc_quic_conn_stream_send(qc, 7, &t, n, false);
270 n = pc_quic_varint_encode(&t, 1, 0x03);
271 pc_quic_conn_stream_send(qc, 11, &t, n, false);
272 h3->next_uni_id = 15;
273}
274} // namespace
275
276void pc_h3_conn_init(H3Conn *h3, QuicConn *qc, H3RequestFn on_request, void *app)
277{
278 memset(h3, 0, sizeof(*h3));
279 h3->qc = qc;
280 h3->on_request = on_request;
281 h3->app = app;
282 h3->next_uni_id = 3;
283 for (size_t i = 0; i < PC_H3_MAX_STREAMS; i++)
284 {
285 h3->streams[i].id = UINT64_MAX;
286 }
287 pc_h3_settings_defaults(&h3->peer_settings);
288
289 QuicConnCallbacks cb = {on_stream_data, on_handshake_done, h3};
290 qc->cb = cb;
291}
292
293bool pc_h3_conn_respond(H3Conn *h3, uint64_t stream_id, int status, const char *content_type, const uint8_t *body,
294 size_t body_len)
295{
296 H3Stream *st = pc_h3_stream_get(h3, stream_id, false);
297 if (st)
298 {
299 st->responded = true;
300 }
301
302 // QPACK field section: prefix + :status + optional content-type + content-length.
303 uint8_t block[256];
304 size_t bp = pc_qpack_encode_prefix(block, sizeof(block));
305 char st3[4];
306 st3[0] = (char)('0' + (status / 100) % 10);
307 st3[1] = (char)('0' + (status / 10) % 10);
308 st3[2] = (char)('0' + status % 10);
309 st3[3] = '\0';
310 bp += pc_qpack_encode_header(block + bp, sizeof(block) - bp, ":status", 7, st3, 3);
311 if (content_type)
312 {
313 // Cap above the largest content-type that can fit this block even at QPACK-Huffman's best
314 // 5-bit/char (~sizeof block * 8/5), so an over-long value trips the encode's reject below
315 // instead of being truncated into a fittable length (see the matching pc_h2_conn note).
316 bp += pc_qpack_encode_header(block + bp, sizeof(block) - bp, "content-type", 12, content_type,
317 strnlen(content_type, sizeof(block) * 2));
318 }
319 char clen[16];
320 size_t cl = 0;
321 {
322 // decimal content-length without stdlib
323 char tmp[16];
324 size_t n = 0;
325 size_t v = body_len;
326 do
327 {
328 tmp[n++] = (char)('0' + v % 10);
329 v /= 10;
330 } while (v);
331 while (n)
332 {
333 clen[cl++] = tmp[--n];
334 }
335 }
336 bp += pc_qpack_encode_header(block + bp, sizeof(block) - bp, "content-length", 14, clen, cl);
337
338 // HEADERS frame + DATA frame, sent on the request stream with FIN.
339 uint8_t out[PC_H3_STREAM_BUF];
340 size_t op = pc_h3_build_headers(out, sizeof(out), block, bp);
341 if (!op) // GCOVR_EXCL_LINE block is a fixed 256 bytes and out is PC_H3_STREAM_BUF, whose floor the
342 {
343 return false; // GCOVR_EXCL_LINE static_assert at the top of this file pins: the HEADERS frame always fits
344 }
345 if (body_len)
346 {
347 size_t dn = pc_h3_build_data(out + op, sizeof(out) - op, body, body_len);
348 if (!dn)
349 {
350 return false;
351 }
352 op += dn;
353 }
354 return pc_quic_conn_stream_send(h3->qc, stream_id, out, op, true) == op;
355}
356
357#endif // PC_ENABLE_HTTP3
#define PC_H3_MAX_STREAMS
Definition 16mbpsram.h:33
QPACK field-section compression for HTTP/3 (RFC 9204).