DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
h2_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 h2_conn.cpp
6 * @brief HTTP/2 connection + stream engine - implementation. See h2_conn.h.
7 */
8
10
11#if DETWS_ENABLE_HTTP2
12
13#include <stdio.h>
14#include <string.h>
15
16namespace
17{
18uint32_t rd32(const uint8_t *p)
19{
20 return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3];
21}
22
23void wr(H2Conn *c, const uint8_t *data, size_t len)
24{
25 if (c->cb.write)
26 c->cb.write(c->cb.io, data, len);
27}
28
29H2Stream *find_stream(H2Conn *c, uint32_t id)
30{
31 for (int i = 0; i < DETWS_H2_MAX_STREAMS; i++)
32 if (c->streams[i].id == id && id != 0)
33 return &c->streams[i];
34 return nullptr;
35}
36
37H2Stream *alloc_stream(H2Conn *c, uint32_t id)
38{
39 for (int i = 0; i < DETWS_H2_MAX_STREAMS; i++)
40 if (c->streams[i].id == 0)
41 {
42 c->streams[i].id = id;
43 c->streams[i].state = H2StreamState::H2_ST_OPEN;
44 c->streams[i].send_window = (int32_t)c->peer.initial_window_size;
45 return &c->streams[i];
46 }
47 return nullptr; // at MAX_CONCURRENT_STREAMS
48}
49
50void send_our_settings(H2Conn *c)
51{
52 const uint16_t ids[4] = {H2Setting::H2_SETTINGS_ENABLE_PUSH, H2Setting::H2_SETTINGS_MAX_CONCURRENT_STREAMS,
53 H2Setting::H2_SETTINGS_INITIAL_WINDOW_SIZE, H2Setting::H2_SETTINGS_MAX_FRAME_SIZE};
54 const uint32_t vals[4] = {0, DETWS_H2_MAX_STREAMS, 65535, DETWS_H2_MAX_FRAME};
55 uint8_t buf[H2_FRAME_HEADER_LEN + 4 * 6];
56 size_t n = h2_build_settings(buf, sizeof buf, ids, vals, 4);
57 wr(c, buf, n);
58}
59
60void send_control(H2Conn *c, size_t (*build)(uint8_t *, size_t))
61{
62 uint8_t buf[H2_FRAME_HEADER_LEN + 16];
63 size_t n = build(buf, sizeof buf);
64 if (n)
65 wr(c, buf, n);
66}
67
68// HPACK emit context: routes each decoded header to the app callback with the stream id.
69struct EmitCtx
70{
71 H2Conn *c;
72 uint32_t stream_id;
73};
74bool emit_header(void *ctx, const char *name, size_t nl, const char *val, size_t vl)
75{
76 EmitCtx *e = (EmitCtx *)ctx;
77 if (e->c->cb.on_header)
78 e->c->cb.on_header(e->c->cb.app, e->stream_id, name, nl, val, vl);
79 return true;
80}
81
82// Decode a complete request header block and deliver it to the application.
83bool decode_block(H2Conn *c, uint32_t stream_id, const uint8_t *block, size_t len, bool end_stream)
84{
85 EmitCtx e = {c, stream_id};
86 if (!hpack_decode(&c->hdec, block, len, c->hscratch, sizeof c->hscratch, emit_header, &e))
87 return false; // COMPRESSION_ERROR
88 if (c->cb.on_headers_end)
89 c->cb.on_headers_end(c->cb.app, stream_id, end_stream);
90 H2Stream *s = find_stream(c, stream_id);
91 if (s)
92 s->state = end_stream ? H2StreamState::H2_ST_HALF_CLOSED : H2StreamState::H2_ST_OPEN;
93 return true;
94}
95
96bool handle_headers(H2Conn *c, const H2FrameHeader *h, const uint8_t *payload)
97{
98 if (h->stream_id == 0 || (h->stream_id & 1) == 0)
99 return false; // requests are client-initiated odd stream ids (RFC 9113 sec 5.1.1)
100 const uint8_t *p = payload;
101 size_t plen = h->length;
102 uint8_t pad = 0;
103 if (h->flags & H2_FLAG_PADDED)
104 {
105 if (plen < 1)
106 return false;
107 pad = p[0];
108 p++;
109 plen--;
110 }
111 if (h->flags & H2_FLAG_PRIORITY)
112 {
113 if (plen < 5)
114 return false;
115 p += 5;
116 plen -= 5; // priority info accepted and ignored
117 }
118 if (pad > plen)
119 return false;
120 plen -= pad; // strip trailing padding
121
122 bool end_stream = (h->flags & H2_FLAG_END_STREAM) != 0;
123 if (h->stream_id <= c->last_peer_stream)
124 return false; // stream ids must increase
125 c->last_peer_stream = h->stream_id;
126 if (!alloc_stream(c, h->stream_id))
127 {
128 send_control(c, [](uint8_t *b, size_t cap) { return h2_build_rst_stream(b, cap, 0, 0); });
129 return true; // refuse quietly is fine; keep the connection
130 }
131
132 if (h->flags & H2_FLAG_END_HEADERS)
133 return decode_block(c, h->stream_id, p, plen, end_stream);
134 // Spans CONTINUATION frames: buffer the fragment.
135 if (plen > sizeof c->hblock)
136 return false;
137 memcpy(c->hblock, p, plen);
138 c->hblock_len = plen;
139 c->hblock_stream = h->stream_id;
140 c->hblock_end_stream = end_stream;
141 c->in_header_block = true;
142 return true;
143}
144
145bool handle_continuation(H2Conn *c, const H2FrameHeader *h, const uint8_t *payload)
146{
147 if (!c->in_header_block || h->stream_id != c->hblock_stream)
148 return false;
149 if (c->hblock_len + h->length > sizeof c->hblock)
150 return false;
151 memcpy(c->hblock + c->hblock_len, payload, h->length);
152 c->hblock_len += h->length;
153 if (h->flags & H2_FLAG_END_HEADERS)
154 {
155 c->in_header_block = false;
156 return decode_block(c, c->hblock_stream, c->hblock, c->hblock_len, c->hblock_end_stream);
157 }
158 return true;
159}
160
161bool handle_data(H2Conn *c, const H2FrameHeader *h, const uint8_t *payload)
162{
163 if (h->stream_id == 0)
164 return false;
165 const uint8_t *p = payload;
166 size_t plen = h->length;
167 uint8_t pad = 0;
168 if (h->flags & H2_FLAG_PADDED)
169 {
170 if (plen < 1)
171 return false;
172 pad = p[0];
173 p++;
174 plen--;
175 }
176 if (pad > plen)
177 return false;
178 plen -= pad;
179
180 bool end_stream = (h->flags & H2_FLAG_END_STREAM) != 0;
181 if (c->cb.on_data)
182 c->cb.on_data(c->cb.app, h->stream_id, p, plen, end_stream);
183 H2Stream *s = find_stream(c, h->stream_id);
184 if (s && end_stream)
185 s->state = H2StreamState::H2_ST_HALF_CLOSED;
186 // Replenish flow-control windows for the bytes we consumed (whole frame length).
187 if (h->length > 0)
188 {
189 uint8_t wu[H2_FRAME_HEADER_LEN + 4];
190 size_t n = h2_build_window_update(wu, sizeof wu, 0, h->length);
191 wr(c, wu, n);
192 n = h2_build_window_update(wu, sizeof wu, h->stream_id, h->length);
193 wr(c, wu, n);
194 }
195 return true;
196}
197
198bool process_frame(H2Conn *c)
199{
200 H2FrameHeader h;
201 h2_parse_header(c->fbuf, H2_FRAME_HEADER_LEN, &h);
202 const uint8_t *payload = c->fbuf + H2_FRAME_HEADER_LEN;
203
204 // A header block must be continued only by CONTINUATION on the same stream (sec 6.10).
205 if (c->in_header_block && h.type != H2FrameType::H2_CONTINUATION)
206 return false;
207
208 switch (h.type)
209 {
210 case H2FrameType::H2_SETTINGS:
211 if (h.flags & H2_FLAG_ACK)
212 return h.length == 0; // ACK of our settings
213 if (!h2_parse_settings(payload, h.length, &c->peer))
214 return false;
215 send_control(c, h2_build_settings_ack);
216 return true;
217 case H2FrameType::H2_PING:
218 if (h.flags & H2_FLAG_ACK)
219 return true;
220 if (h.length != 8)
221 return false;
222 {
223 uint8_t pg[H2_FRAME_HEADER_LEN + 8];
224 size_t n = h2_build_ping_ack(pg, sizeof pg, payload);
225 wr(c, pg, n);
226 }
227 return true;
228 case H2FrameType::H2_WINDOW_UPDATE: {
229 if (h.length != 4)
230 return false;
231 uint32_t inc = rd32(payload) & 0x7FFFFFFF;
232 if (h.stream_id == 0)
233 c->conn_send_window += (int32_t)inc;
234 else
235 {
236 H2Stream *s = find_stream(c, h.stream_id);
237 if (s)
238 s->send_window += (int32_t)inc;
239 }
240 return true;
241 }
242 case H2FrameType::H2_HEADERS:
243 return handle_headers(c, &h, payload);
244 case H2FrameType::H2_CONTINUATION:
245 return handle_continuation(c, &h, payload);
246 case H2FrameType::H2_DATA:
247 return handle_data(c, &h, payload);
248 case H2FrameType::H2_RST_STREAM: {
249 H2Stream *s = find_stream(c, h.stream_id);
250 if (s)
251 s->id = 0; // free the slot
252 return true;
253 }
254 case H2FrameType::H2_PRIORITY:
255 return true; // accepted, ignored
256 case H2FrameType::H2_GOAWAY:
257 c->phase = 2;
258 return true;
259 case H2FrameType::H2_PUSH_PROMISE:
260 return false; // a server never receives PUSH_PROMISE (sec 8.4)
261 default:
262 return true; // unknown frame types are ignored (sec 4.1)
263 }
264}
265} // namespace
266
267void h2_conn_init(H2Conn *c, const H2Callbacks *cb)
268{
269 memset(c, 0, sizeof(*c));
270 c->cb = *cb;
271 c->phase = 0;
272 h2_settings_defaults(&c->peer);
273 c->conn_send_window = 65535;
274 hpack_dyn_init(&c->hdec, DETWS_HPACK_TABLE_BYTES);
275 send_our_settings(c);
276}
277
278bool h2_conn_recv(H2Conn *c, const uint8_t *data, size_t len)
279{
280 size_t off = 0;
281 if (c->phase == 0)
282 {
283 while (off < len && c->pre < H2_PREFACE_LEN)
284 {
285 if (data[off] != (uint8_t)H2_PREFACE[c->pre])
286 return false; // malformed preface
287 c->pre++;
288 off++;
289 }
290 if (c->pre < H2_PREFACE_LEN)
291 return true; // preface still incomplete
292 c->phase = 1;
293 }
294 if (c->phase == 2)
295 return true; // closing; ignore further input
296
297 while (off < len)
298 {
299 if (c->fhave < H2_FRAME_HEADER_LEN)
300 {
301 size_t take = H2_FRAME_HEADER_LEN - c->fhave;
302 if (take > len - off)
303 take = len - off;
304 memcpy(c->fbuf + c->fhave, data + off, take);
305 c->fhave += take;
306 off += take;
307 if (c->fhave < H2_FRAME_HEADER_LEN)
308 return true;
309 }
310 uint32_t plen = ((uint32_t)c->fbuf[0] << 16) | ((uint32_t)c->fbuf[1] << 8) | c->fbuf[2];
311 if (plen > DETWS_H2_MAX_FRAME)
312 return false; // FRAME_SIZE_ERROR
313 size_t total = H2_FRAME_HEADER_LEN + plen;
314 size_t take = total - c->fhave;
315 if (take > len - off)
316 take = len - off;
317 memcpy(c->fbuf + c->fhave, data + off, take);
318 c->fhave += take;
319 off += take;
320 if (c->fhave < total)
321 return true; // frame incomplete
322 if (!process_frame(c))
323 return false;
324 c->fhave = 0;
325 }
326 return true;
327}
328
329bool h2_conn_respond(H2Conn *c, uint32_t stream_id, int status, const char *content_type, const char *body,
330 size_t body_len)
331{
332 H2Stream *s = find_stream(c, stream_id);
333 if (!s)
334 return false;
335
336 // Build the HPACK header block: :status, optional content-type, content-length.
337 uint8_t block[256];
338 size_t bo = 0;
339 char num[16];
340 int nl = snprintf(num, sizeof num, "%d", status);
341 size_t w = hpack_encode_header(block + bo, sizeof block - bo, ":status", 7, num, (size_t)nl);
342 // GCOVR_EXCL_START :status is a decimal int (<=11 chars) into a fresh 256B block; the encode cannot overflow
343 if (!w)
344 return false;
345 // GCOVR_EXCL_STOP
346 bo += w;
347 if (content_type)
348 {
349 // Cap above the largest content-type that can fit this block even at HPACK-Huffman's best
350 // 5-bit/char (~sizeof block * 8/5): a longer value can never fit, so measuring it as `2*block`
351 // still trips the encode's reject below instead of being truncated into a fittable length.
352 w = hpack_encode_header(block + bo, sizeof block - bo, "content-type", 12, content_type,
353 strnlen(content_type, sizeof block * 2));
354 if (!w)
355 return false;
356 bo += w;
357 }
358 int cl = snprintf(num, sizeof num, "%u", (unsigned)body_len);
359 w = hpack_encode_header(block + bo, sizeof block - bo, "content-length", 14, num, (size_t)cl);
360 // GCOVR_EXCL_START content-length is a decimal number; it cannot overflow the 256B block
361 if (!w)
362 return false;
363 // GCOVR_EXCL_STOP
364 bo += w;
365
366 uint8_t frame[H2_FRAME_HEADER_LEN + sizeof block];
367 size_t n = h2_build_headers(frame, sizeof frame, stream_id, block, bo, body_len == 0);
368 // GCOVR_EXCL_START frame is H2_FRAME_HEADER_LEN + sizeof block; 9 + bo (bo <= 256) always fits
369 if (!n)
370 return false;
371 // GCOVR_EXCL_STOP
372 wr(c, frame, n);
373
374 // Body as DATA frames, split to the peer's max frame size, END_STREAM on the last.
375 size_t sent = 0;
376 uint32_t chunk_max = c->peer.max_frame_size ? c->peer.max_frame_size : 16384;
377 while (sent < body_len)
378 {
379 size_t chunk = body_len - sent;
380 if (chunk > chunk_max)
381 chunk = chunk_max;
382 bool last = (sent + chunk == body_len);
383 uint8_t dh[H2_FRAME_HEADER_LEN];
384 size_t hn = h2_write_header(dh, sizeof dh, (uint32_t)chunk, H2FrameType::H2_DATA, last ? H2_FLAG_END_STREAM : 0,
385 stream_id);
386 // GCOVR_EXCL_START dh is exactly H2_FRAME_HEADER_LEN; a 9-byte frame header always fits
387 if (!hn)
388 return false;
389 // GCOVR_EXCL_STOP
390 wr(c, dh, hn);
391 wr(c, (const uint8_t *)(body + sent), chunk);
392 c->conn_send_window -= (int32_t)chunk;
393 s->send_window -= (int32_t)chunk;
394 sent += chunk;
395 }
396 s->id = 0; // stream complete; free the slot
397 return true;
398}
399
400void h2_conn_goaway(H2Conn *c, uint32_t error)
401{
402 uint8_t buf[H2_FRAME_HEADER_LEN + 8];
403 size_t n = h2_build_goaway(buf, sizeof buf, c->last_peer_stream, error);
404 wr(c, buf, n);
405 c->phase = 2;
406}
407
408#endif // DETWS_ENABLE_HTTP2
#define DETWS_HPACK_TABLE_BYTES
Per-connection HPACK dynamic-table size in bytes (our decoder; advertised to the peer as SETTINGS_HEA...
#define DETWS_H2_MAX_FRAME
Largest HTTP/2 frame we accept, in bytes (advertised as SETTINGS_MAX_FRAME_SIZE). RFC 9113 requires a...
#define DETWS_H2_MAX_STREAMS
Max concurrent HTTP/2 streams per connection (advertised as MAX_CONCURRENT_STREAMS).
HTTP/2 connection + stream engine (RFC 9113) over the HPACK + frame layers.