DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
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 h3_conn.cpp
6 * @brief HTTP/3 application engine over QUIC streams (see h3_conn.h).
7 */
8
10
11#if DETWS_ENABLE_HTTP3
12
15#include <string.h>
16
17namespace
18{
19H3Stream *h3_stream_get(H3Conn *h3, uint64_t id, bool create)
20{
21 H3Stream *free_slot = nullptr;
22 for (size_t i = 0; i < DETWS_H3_MAX_STREAMS; i++)
23 {
24 if (h3->streams[i].role != H3StreamRole::H3_ROLE_FREE && h3->streams[i].id == id)
25 return &h3->streams[i];
26 if (!free_slot && h3->streams[i].role == H3StreamRole::H3_ROLE_FREE)
27 free_slot = &h3->streams[i];
28 }
29 if (!create || !free_slot)
30 return nullptr;
31 memset(free_slot, 0, sizeof(*free_slot));
32 free_slot->id = id;
33 return free_slot;
34}
35
36// Copy a bounded, NUL-terminated field.
37void set_field(char *dst, size_t cap, const char *src, size_t len)
38{
39 if (len >= cap)
40 len = cap - 1;
41 memcpy(dst, src, len);
42 dst[len] = '\0';
43}
44
45// QPACK emit target: capture the request pseudo-headers.
46struct ReqEmit
47{
48 H3Stream *st;
49};
50bool req_emit(void *ctx, const char *name, size_t nlen, const char *value, size_t vlen)
51{
52 H3Stream *st = ((ReqEmit *)ctx)->st;
53 if (nlen == 7 && memcmp(name, ":method", 7) == 0)
54 set_field(st->method, sizeof(st->method), value, vlen);
55 else if (nlen == 5 && memcmp(name, ":path", 5) == 0)
56 set_field(st->path, sizeof(st->path), value, vlen);
57 else if (nlen == 10 && memcmp(name, ":authority", 10) == 0)
58 set_field(st->authority, sizeof(st->authority), value, vlen);
59 return true; // ignore regular headers for now (routing is by method + path)
60}
61
62// Parse the accumulated request stream: decode HEADERS, coalesce DATA into a body, and dispatch.
63void dispatch_request(H3Conn *h3, H3Stream *st)
64{
65 static uint8_t body[DETWS_H3_STREAM_BUF];
66 static char scratch[DETWS_H3_PATH_LEN + DETWS_H3_AUTHORITY_LEN + 64];
67 size_t body_len = 0;
68
69 size_t off = 0;
70 while (off < st->buf_len)
71 {
72 H3Frame fr;
73 if (!h3_frame_parse(st->buf + off, st->buf_len - off, &fr))
74 break;
75 size_t payload = off + fr.header_len;
76 if (payload + fr.length > st->buf_len)
77 break; // incomplete frame
78 const uint8_t *fp = st->buf + payload;
79 if (fr.type == H3FrameType::H3_HEADERS)
80 {
81 ReqEmit e = {st};
82 qpack_decode(fp, (size_t)fr.length, scratch, sizeof(scratch), req_emit, &e);
83 st->have_headers = true;
84 }
85 else if (fr.type == H3FrameType::H3_DATA)
86 {
87 // Copy only while there is room left in body. room is 0 once body is full (no underflow),
88 // and take is clamped to it, so body_len + take <= sizeof(body).
89 size_t room = (body_len < sizeof(body)) ? sizeof(body) - body_len : 0;
90 size_t take = (size_t)fr.length;
91 if (take > room)
92 take = room;
93 if (take)
94 memcpy(body + body_len, fp, take);
95 body_len += take;
96 }
97 off = payload + (size_t)fr.length;
98 }
99
100 if (st->have_headers && h3->on_request)
101 h3->on_request(h3->app, h3, st->id, st->method, st->path, st->authority, body, body_len);
102}
103
104void append(H3Stream *st, const uint8_t *data, size_t len)
105{
106 if (len > sizeof(st->buf) - st->buf_len)
107 len = sizeof(st->buf) - st->buf_len;
108 memcpy(st->buf + st->buf_len, data, len);
109 st->buf_len += len;
110}
111
112// Read the leading stream-type varint of a uni stream and set st->role; consumes it from the buffer.
113// Returns false if more bytes are needed (nothing consumed).
114bool h3_classify_uni_stream(H3Stream *st)
115{
116 uint64_t type = 0;
117 size_t c = 0;
118 if (!quic_varint_decode(st->buf, st->buf_len, &type, &c))
119 return false; // need more bytes for the varint
120 st->type_read = true;
121 if (type == 0x00)
122 st->role = H3StreamRole::H3_ROLE_CONTROL;
123 else if (type == 0x02)
124 st->role = H3StreamRole::H3_ROLE_QPACK_ENC;
125 else if (type == 0x03)
126 st->role = H3StreamRole::H3_ROLE_QPACK_DEC;
127 else
128 st->role = H3StreamRole::H3_ROLE_OTHER_UNI;
129 memmove(st->buf, st->buf + c, st->buf_len - c);
130 st->buf_len -= c;
131 return true;
132}
133
134// Parse whatever complete frames the control stream holds (SETTINGS first), consuming them.
135void h3_consume_control(H3Conn *h3, H3Stream *st)
136{
137 size_t off = 0;
138 while (off < st->buf_len)
139 {
140 H3Frame fr;
141 if (!h3_frame_parse(st->buf + off, st->buf_len - off, &fr))
142 break;
143 if (off + fr.header_len + fr.length > st->buf_len)
144 break;
145 if (fr.type == H3FrameType::H3_SETTINGS)
146 {
147 h3_settings_defaults(&h3->peer_settings);
148 h3_parse_settings(st->buf + off + fr.header_len, (size_t)fr.length, &h3->peer_settings);
149 }
150 off += fr.header_len + (size_t)fr.length;
151 }
152 memmove(st->buf, st->buf + off, st->buf_len - off);
153 st->buf_len -= off;
154}
155
156void on_stream_data(void *app, QuicConn *, uint64_t stream_id, const uint8_t *data, size_t len, bool fin)
157{
158 H3Conn *h3 = (H3Conn *)app;
159 H3Stream *st = h3_stream_get(h3, stream_id, true);
160 if (!st)
161 return;
162
163 if (st->role == H3StreamRole::H3_ROLE_FREE)
164 st->role = (stream_id & 0x03) == 0x00 ? H3StreamRole::H3_ROLE_REQUEST : H3StreamRole::H3_ROLE_OTHER_UNI;
165
166 append(st, data, len);
167
168 // A unidirectional stream begins with a stream-type varint; classify it once.
169 if (st->role != H3StreamRole::H3_ROLE_REQUEST && !st->type_read && st->buf_len >= 1 && !h3_classify_uni_stream(st))
170 return; // need more bytes for the varint
171
172 if (st->role == H3StreamRole::H3_ROLE_CONTROL)
173 {
174 h3_consume_control(h3, st);
175 return;
176 }
177 if (st->role != H3StreamRole::H3_ROLE_REQUEST)
178 {
179 st->buf_len = 0; // QPACK/other uni streams: nothing to do (static-table only)
180 return;
181 }
182
183 if (fin)
184 dispatch_request(h3, st);
185}
186
187void on_handshake_done(void *app, QuicConn *qc)
188{
189 H3Conn *h3 = (H3Conn *)app;
190 if (h3->control_opened)
191 return;
192 h3->control_opened = true;
193
194 // Server control stream (id 3): stream type 0x00 + SETTINGS.
195 uint8_t buf[64];
196 size_t p = quic_varint_encode(buf, sizeof(buf), 0x00);
197 const uint64_t ids[] = {H3Setting::H3_SETTINGS_QPACK_MAX_TABLE_CAPACITY,
198 H3Setting::H3_SETTINGS_QPACK_BLOCKED_STREAMS};
199 const uint64_t vals[] = {0, 0};
200 p += h3_build_settings(buf + p, sizeof(buf) - p, ids, vals, 2);
201 quic_conn_stream_send(qc, 3, buf, p, false);
202
203 // QPACK encoder (id 7, type 0x02) and decoder (id 11, type 0x03) streams: type byte only.
204 uint8_t t;
205 size_t n = quic_varint_encode(&t, 1, 0x02);
206 quic_conn_stream_send(qc, 7, &t, n, false);
207 n = quic_varint_encode(&t, 1, 0x03);
208 quic_conn_stream_send(qc, 11, &t, n, false);
209 h3->next_uni_id = 15;
210}
211} // namespace
212
213void h3_conn_init(H3Conn *h3, QuicConn *qc, H3RequestFn on_request, void *app)
214{
215 memset(h3, 0, sizeof(*h3));
216 h3->qc = qc;
217 h3->on_request = on_request;
218 h3->app = app;
219 h3->next_uni_id = 3;
220 for (size_t i = 0; i < DETWS_H3_MAX_STREAMS; i++)
221 h3->streams[i].id = UINT64_MAX;
222 h3_settings_defaults(&h3->peer_settings);
223
224 QuicConnCallbacks cb = {on_stream_data, on_handshake_done, h3};
225 qc->cb = cb;
226}
227
228bool h3_conn_respond(H3Conn *h3, uint64_t stream_id, int status, const char *content_type, const uint8_t *body,
229 size_t body_len)
230{
231 H3Stream *st = h3_stream_get(h3, stream_id, false);
232 if (st)
233 st->responded = true;
234
235 // QPACK field section: prefix + :status + optional content-type + content-length.
236 uint8_t block[256];
237 size_t bp = qpack_encode_prefix(block, sizeof(block));
238 char st3[4];
239 st3[0] = (char)('0' + (status / 100) % 10);
240 st3[1] = (char)('0' + (status / 10) % 10);
241 st3[2] = (char)('0' + status % 10);
242 st3[3] = '\0';
243 bp += qpack_encode_header(block + bp, sizeof(block) - bp, ":status", 7, st3, 3);
244 if (content_type)
245 // Cap above the largest content-type that can fit this block even at QPACK-Huffman's best
246 // 5-bit/char (~sizeof block * 8/5), so an over-long value trips the encode's reject below
247 // instead of being truncated into a fittable length (see the matching h2_conn note).
248 bp += qpack_encode_header(block + bp, sizeof(block) - bp, "content-type", 12, content_type,
249 strnlen(content_type, sizeof(block) * 2));
250 char clen[16];
251 size_t cl = 0;
252 {
253 // decimal content-length without stdlib
254 char tmp[16];
255 size_t n = 0;
256 size_t v = body_len;
257 do
258 {
259 tmp[n++] = (char)('0' + v % 10);
260 v /= 10;
261 } while (v);
262 while (n)
263 clen[cl++] = tmp[--n];
264 }
265 bp += qpack_encode_header(block + bp, sizeof(block) - bp, "content-length", 14, clen, cl);
266
267 // HEADERS frame + DATA frame, sent on the request stream with FIN.
268 uint8_t out[DETWS_H3_STREAM_BUF];
269 size_t op = h3_build_headers(out, sizeof(out), block, bp);
270 if (!op)
271 return false;
272 if (body_len)
273 {
274 size_t dn = h3_build_data(out + op, sizeof(out) - op, body, body_len);
275 if (!dn)
276 return false;
277 op += dn;
278 }
279 return quic_conn_stream_send(h3->qc, stream_id, out, op, true) == op;
280}
281
282#endif // DETWS_ENABLE_HTTP3
#define DETWS_H3_MAX_STREAMS
Maximum concurrent request streams per HTTP/3 connection.
HTTP/3 application engine over QUIC streams (RFC 9114).
QPACK field-section compression for HTTP/3 (RFC 9204).
QUIC variable-length integer coding (RFC 9000 sec 16).