DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
quic_server.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 quic_server.cpp
6 * @brief HTTP/3 server glue - implementation. See quic_server.h.
7 */
8
10
11#if DETWS_ENABLE_HTTP3
12
15#include "shared_primitives/ring.h" // DetAtomic
16#include <string.h>
17
18#if defined(ARDUINO)
20#endif
21
22// The pool (QuicConn + H3Conn per slot) and the ingest ring are large, so on a PSRAM board they can
23// be moved to external RAM (like the HTTP/2 pool). Default is internal DRAM; a build that overflows
24// sets DETWS_QUIC_SERVER_IN_PSRAM=1 on a core built with CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY.
25#ifndef DETWS_QUIC_SERVER_IN_PSRAM
26#define DETWS_QUIC_SERVER_IN_PSRAM 0
27#endif
28#ifndef DETWS_QUIC_SERVER_ACK_DRAM
29#define DETWS_QUIC_SERVER_ACK_DRAM 0 ///< consciously accept the pool in internal DRAM (roomy S3 / P4)
30#endif
31// The QuicConn + H3Conn pool plus the ingest ring are tens of KB; on a device that is a deliberate
32// footprint choice. Fail fast (like DETWS_ENABLE_SSH_ZLIB / DETWS_ENABLE_HTTP2) so it is not an
33// accidental DRAM overflow: move the pool to PSRAM, or acknowledge the internal-DRAM cost.
34#if defined(ARDUINO) && !DETWS_QUIC_SERVER_IN_PSRAM && !DETWS_QUIC_SERVER_ACK_DRAM
35#error \
36 "DeterministicESPAsyncWebServer: DETWS_ENABLE_HTTP3 - the quic_server QuicConn+H3Conn pool + ingest ring are tens of KB. Set DETWS_QUIC_SERVER_IN_PSRAM=1 on a PSRAM board (S3 / P4 / WROVER built with CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y, tools/psram/README.md), OR set DETWS_QUIC_SERVER_ACK_DRAM=1 to accept the internal-DRAM cost (fits a small pool on a roomy chip)."
37#endif
38#if DETWS_QUIC_SERVER_IN_PSRAM && defined(ARDUINO)
39#include <esp_attr.h>
40#if defined(EXT_RAM_BSS_ATTR)
41#define DETWS_QUIC_POOL_ATTR EXT_RAM_BSS_ATTR
42#elif defined(EXT_RAM_ATTR)
43#define DETWS_QUIC_POOL_ATTR EXT_RAM_ATTR
44#else
45#define DETWS_QUIC_POOL_ATTR
46#endif
47#else
48#define DETWS_QUIC_POOL_ATTR
49#endif
50
51namespace
52{
53// One buffered inbound datagram (payload + the peer it arrived from).
54struct QuicIngest
55{
56 uint8_t data[DETWS_QUIC_MAX_DATAGRAM];
57 uint16_t len;
58 char ip[16];
59 uint16_t port;
60};
61
62// One pool slot: a QUIC connection, its HTTP/3 engine, and the peer to reply to.
63struct QuicSlot
64{
65 bool used;
66 uint32_t id; ///< stable handle for quic_server_respond()
67 QuicConn qc;
68 H3Conn h3;
69 char peer_ip[16];
70 uint16_t peer_port;
71 uint32_t last_ms; ///< detws_millis() of the last datagram received (idle-reaping clock)
72};
73
74// HTTP/3 QUIC connection + ingest buffers, owned by one instance (internal linkage). Placed in
75// PSRAM (DETWS_QUIC_POOL_ATTR) when configured; kept separate from the DRAM control state below
76// so only the large buffers move off internal RAM. One named owner, unreachable cross-TU.
77struct QuicServerPoolCtx
78{
79 QuicSlot pool[DETWS_QUIC_MAX_CONNS];
80 QuicIngest ring[DETWS_QUIC_INGEST_RING];
81};
82DETWS_QUIC_POOL_ATTR QuicServerPoolCtx s_qpool;
83
84// All HTTP/3 QUIC server control state, owned by one instance (internal linkage): the ingest
85// ring cursors, the server config + request callback + app pointer, the bound port / running
86// flag / next connection id, and (host) the outbound sink. One named owner, unreachable cross-TU.
87struct QuicServerCtx
88{
89 DetAtomic<size_t> ring_head; ///< producer (udp / ingest) advances
90 DetAtomic<size_t> ring_tail; ///< consumer (poll) advances
91 QuicServerConfig cfg;
92 QuicServerRequestFn on_request = nullptr;
93 void *app = nullptr;
94 uint16_t port = 0;
95 bool running = false;
96 uint32_t next_id = 1;
97#if !defined(ARDUINO)
98 QuicServerOutFn out_sink = nullptr;
99 void *out_ctx = nullptr;
100#endif
101};
102QuicServerCtx s_quic;
103
104void copy_str(char *dst, size_t cap, const char *src)
105{
106 size_t n = 0;
107 if (src)
108 while (src[n] && n + 1 < cap)
109 {
110 dst[n] = src[n];
111 n++;
112 }
113 dst[n] = 0;
114}
115
116bool cid_eq(const uint8_t *a, uint8_t alen, const uint8_t *b, uint8_t blen)
117{
118 return alen == blen && memcmp(a, b, alen) == 0;
119}
120
121void server_send(const char *ip, uint16_t port, const uint8_t *data, size_t len)
122{
123#if defined(ARDUINO)
124 det_udp_listener_sendto(s_quic.port, ip, port, data, len);
125#else
126 if (s_quic.out_sink)
127 s_quic.out_sink(s_quic.out_ctx, data, len, ip, port);
128#endif
129}
130
131// --- ingest ring (SPSC: one producer fills, quic_server_poll consumes) ------------------------
132bool ring_push(const uint8_t *dg, size_t len, const char *ip, uint16_t port)
133{
134 if (len == 0 || len > DETWS_QUIC_MAX_DATAGRAM)
135 return false;
136 size_t head = s_quic.ring_head;
137 size_t next = (head + 1) % DETWS_QUIC_INGEST_RING;
138 if (next == (size_t)s_quic.ring_tail)
139 return false; // ring full: drop (QUIC recovers via retransmission)
140 QuicIngest *e = &s_qpool.ring[head];
141 memcpy(e->data, dg, len);
142 e->len = (uint16_t)len;
143 copy_str(e->ip, sizeof e->ip, ip);
144 e->port = port;
145 s_quic.ring_head = next; // publish after the record is fully written
146 return true;
147}
148
149bool ring_pop(QuicIngest *out)
150{
151 size_t tail = s_quic.ring_tail;
152 if (tail == (size_t)s_quic.ring_head)
153 return false;
154 *out = s_qpool.ring[tail];
155 s_quic.ring_tail = (tail + 1) % DETWS_QUIC_INGEST_RING;
156 return true;
157}
158
159// --- slot pool --------------------------------------------------------------------------------
160QuicSlot *slot_by_id(uint32_t id)
161{
162 for (uint8_t i = 0; i < DETWS_QUIC_MAX_CONNS; i++)
163 if (s_qpool.pool[i].used && s_qpool.pool[i].id == id)
164 return &s_qpool.pool[i];
165 return nullptr;
166}
167
168QuicSlot *alloc_slot()
169{
170 for (uint8_t i = 0; i < DETWS_QUIC_MAX_CONNS; i++)
171 if (!s_qpool.pool[i].used)
172 {
173 QuicSlot *s = &s_qpool.pool[i];
174 memset(s, 0, sizeof *s);
175 s->used = true;
176 s->id = s_quic.next_id++;
177 if (s_quic.next_id == 0)
178 s_quic.next_id = 1; // GCOVR_EXCL_LINE never hand out 0; next_id wraps only after 2^32 allocations
179 return s;
180 }
181 return nullptr;
182}
183
184// The HTTP/3 engine surfaces a completed request here; forward it to the application by conn id.
185void h3_on_request(void *app, H3Conn * /*h3*/, uint64_t stream_id, const char *method, const char *path,
186 const char *authority, const uint8_t *body, size_t body_len)
187{
188 QuicSlot *s = (QuicSlot *)app;
189 if (s_quic.on_request)
190 s_quic.on_request(s_quic.app, s->id, stream_id, method, path, authority, body, body_len);
191}
192
193// Open a connection for a client's first Initial packet.
194QuicSlot *open_conn(const QuicLongHeader *lh, const char *ip, uint16_t port)
195{
196 QuicSlot *s = alloc_slot();
197 if (!s)
198 return nullptr;
199
200 QuicTlsConfig tc;
201 memset(&tc, 0, sizeof tc);
202 tc.cert_der = s_quic.cfg.cert_der;
203 tc.cert_len = s_quic.cfg.cert_len;
204 memcpy(tc.ed25519_seed, s_quic.cfg.ed25519_seed, sizeof tc.ed25519_seed);
205 quic_tp_defaults(&tc.params);
206 // A real HTTP/3 endpoint must advertise flow-control room, or every request stream (and the
207 // client's control / QPACK streams) is blocked - the RFC 9000 sec 18.2 defaults are all zero.
208 tc.params.initial_max_data = 1048576;
209 tc.params.initial_max_sd_bidi_remote = 262144; // client-initiated request streams
210 tc.params.initial_max_sd_uni = 262144; // client control / QPACK encoder+decoder streams
211 tc.params.initial_max_streams_bidi = DETWS_H3_MAX_STREAMS;
212 tc.params.initial_max_streams_uni = DETWS_H3_MAX_STREAMS;
213 tc.params.max_idle_timeout = DETWS_QUIC_IDLE_MS; // both ends reclaim the connection after this idle
214 s_quic.cfg.rng(tc.ephemeral_priv, sizeof tc.ephemeral_priv);
215 s_quic.cfg.rng(tc.random, sizeof tc.random);
216#if DETWS_ENABLE_PQC_KEX
217 s_quic.cfg.rng(tc.mlkem_m, sizeof tc.mlkem_m); // fresh ML-KEM Encaps randomness per handshake
218#endif
219
220 uint8_t our_scid[DETWS_QUIC_SCID_LEN];
221 s_quic.cfg.rng(our_scid, sizeof our_scid);
222
223 QuicConnCallbacks cb;
224 memset(&cb, 0, sizeof cb); // h3_conn_init installs the real callbacks
225 quic_conn_init(&s->qc, &tc, lh->dcid, lh->dcid_len, lh->scid, lh->scid_len, our_scid, DETWS_QUIC_SCID_LEN, &cb);
226 h3_conn_init(&s->h3, &s->qc, h3_on_request, s);
227
228 copy_str(s->peer_ip, sizeof s->peer_ip, ip);
229 s->peer_port = port;
230 return s; // last_ms is set by the poll that received this datagram
231}
232
233// Route a datagram to its connection by Destination Connection ID. Sets *is_initial when it is an
234// unmatched Initial (the caller opens a new connection) and copies the parsed long header out.
235QuicSlot *route(const uint8_t *dg, size_t len, bool *is_initial, QuicLongHeader *lh_out)
236{
237 *is_initial = false;
238 if (len < 1)
239 return nullptr; // GCOVR_EXCL_LINE poll only routes ring entries; ring_push rejects len==0, so len>=1 here
240 if (quic_is_long_header(dg[0]))
241 {
242 if (!quic_parse_long_header(dg, len, lh_out))
243 return nullptr;
244 for (uint8_t i = 0; i < DETWS_QUIC_MAX_CONNS; i++)
245 {
246 QuicSlot *s = &s_qpool.pool[i];
247 if (!s->used)
248 continue;
249 if (cid_eq(lh_out->dcid, lh_out->dcid_len, s->qc.scid, s->qc.scid_len) ||
250 cid_eq(lh_out->dcid, lh_out->dcid_len, s->qc.odcid, s->qc.odcid_len))
251 return s;
252 }
253 if (lh_out->version == QUIC_VERSION_1 && lh_out->type == QuicLongPacket::QUIC_LP_INITIAL)
254 *is_initial = true;
255 return nullptr;
256 }
257 // Short header (1-RTT): the DCID is our chosen SCID, whose length only we know.
258 if (len < (size_t)1 + DETWS_QUIC_SCID_LEN)
259 return nullptr;
260 for (uint8_t i = 0; i < DETWS_QUIC_MAX_CONNS; i++)
261 {
262 QuicSlot *s = &s_qpool.pool[i];
263 if (s->used && s->qc.scid_len == DETWS_QUIC_SCID_LEN && memcmp(dg + 1, s->qc.scid, DETWS_QUIC_SCID_LEN) == 0)
264 return s;
265 }
266 return nullptr;
267}
268
269void flush_and_reap(uint32_t now_ms)
270{
271 uint8_t out[DETWS_QUIC_MAX_DATAGRAM];
272 for (uint8_t i = 0; i < DETWS_QUIC_MAX_CONNS; i++)
273 {
274 QuicSlot *s = &s_qpool.pool[i];
275 if (!s->used)
276 continue;
277 quic_conn_on_timeout(&s->qc, now_ms); // retransmit a lost handshake flight (PTO)
278 size_t n;
279 while ((n = quic_conn_send(&s->qc, out, sizeof out)) > 0)
280 server_send(s->peer_ip, s->peer_port, out, n);
281 // Reap a closed connection, or one idle past the timeout (wrap-safe delta) so a client that
282 // never closes cannot leak the fixed pool.
283 if (quic_conn_is_closed(&s->qc) || (uint32_t)(now_ms - s->last_ms) >= DETWS_QUIC_IDLE_MS)
284 s->used = false;
285 }
286}
287
288#if defined(ARDUINO)
289void udp_ingest_cb(const uint8_t *data, size_t len, DetUdpPeer *peer, void * /*ctx*/)
290{
291 char ip[16];
292 uint16_t port = 0;
293 if (!det_udp_peer_addr(peer, ip, sizeof ip, &port))
294 return;
295 ring_push(data, len, ip, port);
296}
297#endif
298} // namespace
299
300bool quic_server_begin(uint16_t port, const QuicServerConfig *cfg, QuicServerRequestFn on_request, void *app)
301{
302 if (!cfg || !cfg->rng)
303 return false;
304 s_quic.cfg = *cfg;
305 s_quic.on_request = on_request;
306 s_quic.app = app;
307 s_quic.port = port ? port : DETWS_HTTP3_PORT;
308 for (uint8_t i = 0; i < DETWS_QUIC_MAX_CONNS; i++)
309 s_qpool.pool[i].used = false;
310 s_quic.ring_head = 0;
311 s_quic.ring_tail = 0;
312 s_quic.next_id = 1;
313 s_quic.running = true;
314#if defined(ARDUINO)
315 return det_udp_listen(s_quic.port, udp_ingest_cb, nullptr);
316#else
317 return true; // host: fed through quic_server_ingest()
318#endif
319}
320
321void quic_server_poll(uint32_t now_ms)
322{
323 if (!s_quic.running)
324 return;
325 QuicIngest ig;
326 while (ring_pop(&ig))
327 {
328 bool is_initial = false;
329 QuicLongHeader lh;
330 QuicSlot *s = route(ig.data, ig.len, &is_initial, &lh);
331 if (!s && is_initial)
332 s = open_conn(&lh, ig.ip, ig.port);
333 if (!s)
334 continue;
335 s->last_ms = now_ms; // liveness for idle reaping
336 quic_conn_recv(&s->qc, ig.data, ig.len);
337 }
338 flush_and_reap(now_ms);
339}
340
341bool quic_server_respond(uint32_t conn_id, uint64_t stream_id, int status, const char *content_type,
342 const uint8_t *body, size_t body_len)
343{
344 QuicSlot *s = slot_by_id(conn_id);
345 if (!s)
346 return false;
347 return h3_conn_respond(&s->h3, stream_id, status, content_type, body, body_len);
348}
349
350uint8_t quic_server_active_conns(void)
351{
352 uint8_t n = 0;
353 for (uint8_t i = 0; i < DETWS_QUIC_MAX_CONNS; i++)
354 if (s_qpool.pool[i].used)
355 n++;
356 return n;
357}
358
359void quic_server_stop(void)
360{
361 s_quic.running = false;
362 for (uint8_t i = 0; i < DETWS_QUIC_MAX_CONNS; i++)
363 s_qpool.pool[i].used = false;
364 s_quic.ring_head = 0;
365 s_quic.ring_tail = 0;
366}
367
368#if !defined(ARDUINO)
369void quic_server_set_out_sink(QuicServerOutFn fn, void *ctx)
370{
371 s_quic.out_sink = fn;
372 s_quic.out_ctx = ctx;
373}
374
375bool quic_server_ingest(const uint8_t *datagram, size_t len, const char *ip, uint16_t port)
376{
377 return ring_push(datagram, len, ip, port);
378}
379#endif
380
381#endif // DETWS_ENABLE_HTTP3
#define DETWS_HTTP3_PORT
UDP port the HTTP/3 (QUIC) server binds by default (used by DetWebServer::h3_cert).
#define DETWS_H3_MAX_STREAMS
Maximum concurrent request streams per HTTP/3 connection.
QUIC packet headers and packet-number coding (RFC 9000 sec 17).
HTTP/3 server glue - binds UDP to a pool of QUIC + HTTP/3 connections (RFC 9000/9114).
QUIC transport parameters (RFC 9000 sec 18) carried in the TLS quic_transport_parameters extension (R...
Shared single-producer / single-consumer byte-ring primitive.
bool det_udp_peer_addr(const struct DetUdpPeer *peer, char *ip_out, size_t ip_cap, uint16_t *port_out)
Copy a received peer's source IPv4 address (dotted-quad) and port out.
Definition udp.cpp:224
bool det_udp_listen(uint16_t port, DetUdpHandler handler, void *ctx)
Bind a UDP port and route incoming datagrams to handler.
Definition udp.cpp:151
bool det_udp_listener_sendto(uint16_t listen_port, const char *dst_ip, uint16_t dst_port, const uint8_t *data, size_t len)
Send a datagram from the listener bound on listen_port to an address.
Definition udp.cpp:234
Layer 4 (Transport) - connectionless UDP datagram service.