ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
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 pc_h2_conn.cpp
6 * @brief HTTP/2 connection + stream engine - implementation. See pc_h2_conn.h.
7 */
8
10#include "shared_primitives/strbuf.h" // pc_sb frame builder
11
12#if PC_ENABLE_HTTP2
13
14#include <stdio.h>
15#include <string.h>
16
17namespace
18{
19uint32_t rd32(const uint8_t *p)
20{
21 return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3];
22}
23
24void wr(H2Conn *c, const uint8_t *data, size_t len)
25{
26 if (c->cb.write)
27 {
28 c->cb.write(c->cb.io, data, len);
29 }
30}
31
32H2Stream *find_stream(H2Conn *c, uint32_t id)
33{
34 for (int i = 0; i < PC_H2_MAX_STREAMS; i++)
35 {
36 if (c->streams[i].id == id && id != 0)
37 {
38 return &c->streams[i];
39 }
40 }
41 return nullptr;
42}
43
44H2Stream *alloc_stream(H2Conn *c, uint32_t id)
45{
46 for (int i = 0; i < PC_H2_MAX_STREAMS; i++)
47 {
48 if (c->streams[i].id == 0)
49 {
50 c->streams[i].id = id;
51 c->streams[i].state = H2StreamState::H2_ST_OPEN;
52 c->streams[i].send_window = (int32_t)c->peer.initial_window_size;
53 return &c->streams[i];
54 }
55 }
56 return nullptr; // at MAX_CONCURRENT_STREAMS
57}
58
59void send_our_settings(H2Conn *c)
60{
61 static const uint16_t ids[4] = {H2Setting::H2_SETTINGS_ENABLE_PUSH, H2Setting::H2_SETTINGS_MAX_CONCURRENT_STREAMS,
62 H2Setting::H2_SETTINGS_INITIAL_WINDOW_SIZE, H2Setting::H2_SETTINGS_MAX_FRAME_SIZE};
63 static const uint32_t vals[4] = {0, PC_H2_MAX_STREAMS, 65535, PC_H2_MAX_FRAME};
64 uint8_t buf[H2_FRAME_HEADER_LEN + 4 * 6];
65 size_t n = pc_h2_build_settings(buf, sizeof buf, ids, vals, 4);
66 wr(c, buf, n);
67}
68
69void send_control(H2Conn *c, size_t (*build)(uint8_t *, size_t))
70{
71 uint8_t buf[H2_FRAME_HEADER_LEN + 16];
72 size_t n = build(buf, sizeof buf);
73 // Both builders passed here (SETTINGS ACK, 9 bytes; RST_STREAM, 13) fit this 25-byte buffer, so the
74 // zero return is unreachable; kept so a future builder that can fail is not silently written short.
75 if (n) // GCOVR_EXCL_LINE
76 {
77 wr(c, buf, n);
78 }
79}
80
81// HPACK emit context: routes each decoded header to the app callback with the stream id.
82struct EmitCtx
83{
84 H2Conn *c;
85 uint32_t stream_id;
86};
87bool emit_header(void *ctx, const char *name, size_t nl, const char *val, size_t vl)
88{
89 EmitCtx *e = (EmitCtx *)ctx;
90 if (e->c->cb.on_header)
91 {
92 e->c->cb.on_header(e->c->cb.app, e->stream_id, name, nl, val, vl);
93 }
94 return true;
95}
96
97// Decode a complete request header block and deliver it to the application.
98bool decode_block(H2Conn *c, uint32_t stream_id, const uint8_t *block, size_t len, bool end_stream)
99{
100 EmitCtx e = {c, stream_id};
101 if (!pc_hpack_decode(&c->hdec, block, len, c->hscratch, sizeof c->hscratch, emit_header, &e))
102 {
103 return false; // COMPRESSION_ERROR
104 }
105 if (c->cb.on_headers_end)
106 {
107 c->cb.on_headers_end(c->cb.app, stream_id, end_stream);
108 }
109 H2Stream *s = find_stream(c, stream_id);
110 if (s)
111 {
112 s->state = end_stream ? H2StreamState::H2_ST_HALF_CLOSED : H2StreamState::H2_ST_OPEN;
113 }
114 return true;
115}
116
117bool handle_headers(H2Conn *c, const H2FrameHeader *h, const uint8_t *payload)
118{
119 if (h->stream_id == 0 || (h->stream_id & 1) == 0)
120 {
121 return false; // requests are client-initiated odd stream ids (RFC 9113 sec 5.1.1)
122 }
123 const uint8_t *p = payload;
124 size_t plen = h->length;
125 uint8_t pad = 0;
126 if (h->flags & H2_FLAG_PADDED)
127 {
128 if (plen < 1)
129 {
130 return false;
131 }
132 pad = p[0];
133 p++;
134 plen--;
135 }
136 if (h->flags & H2_FLAG_PRIORITY)
137 {
138 if (plen < 5)
139 {
140 return false;
141 }
142 p += 5;
143 plen -= 5; // priority info accepted and ignored
144 }
145 if (pad > plen)
146 {
147 return false;
148 }
149 plen -= pad; // strip trailing padding
150
151 bool end_stream = (h->flags & H2_FLAG_END_STREAM) != 0;
152 if (h->stream_id <= c->last_peer_stream)
153 {
154 return false; // stream ids must increase
155 }
156 c->last_peer_stream = h->stream_id;
157 if (!alloc_stream(c, h->stream_id))
158 {
159 send_control(c, [](uint8_t *b, size_t cap) { return pc_h2_build_rst_stream(b, cap, 0, 0); });
160 return true; // refuse quietly is fine; keep the connection
161 }
162
163 if (h->flags & H2_FLAG_END_HEADERS)
164 {
165 return decode_block(c, h->stream_id, p, plen, end_stream);
166 }
167 // Spans CONTINUATION frames: buffer the fragment.
168 if (plen > sizeof c->hblock)
169 {
170 return false;
171 }
172 memcpy(c->hblock, p, plen);
173 c->hblock_len = plen;
174 c->hblock_stream = h->stream_id;
175 c->hblock_end_stream = end_stream;
176 c->in_header_block = true;
177 return true;
178}
179
180bool handle_continuation(H2Conn *c, const H2FrameHeader *h, const uint8_t *payload)
181{
182 if (!c->in_header_block || h->stream_id != c->hblock_stream)
183 {
184 return false;
185 }
186 if (c->hblock_len + h->length > sizeof c->hblock)
187 {
188 return false;
189 }
190 memcpy(c->hblock + c->hblock_len, payload, h->length);
191 c->hblock_len += h->length;
192 if (h->flags & H2_FLAG_END_HEADERS)
193 {
194 c->in_header_block = false;
195 return decode_block(c, c->hblock_stream, c->hblock, c->hblock_len, c->hblock_end_stream);
196 }
197 return true;
198}
199
200bool handle_data(H2Conn *c, const H2FrameHeader *h, const uint8_t *payload)
201{
202 if (h->stream_id == 0)
203 {
204 return false;
205 }
206 const uint8_t *p = payload;
207 size_t plen = h->length;
208 uint8_t pad = 0;
209 if (h->flags & H2_FLAG_PADDED)
210 {
211 if (plen < 1)
212 {
213 return false;
214 }
215 pad = p[0];
216 p++;
217 plen--;
218 }
219 if (pad > plen)
220 {
221 return false;
222 }
223 plen -= pad;
224
225 bool end_stream = (h->flags & H2_FLAG_END_STREAM) != 0;
226 if (c->cb.on_data)
227 {
228 c->cb.on_data(c->cb.app, h->stream_id, p, plen, end_stream);
229 }
230 H2Stream *s = find_stream(c, h->stream_id);
231 if (s && end_stream)
232 {
233 s->state = H2StreamState::H2_ST_HALF_CLOSED;
234 }
235 // Replenish flow-control windows for the bytes we consumed (whole frame length).
236 if (h->length > 0)
237 {
238 uint8_t wu[H2_FRAME_HEADER_LEN + 4];
239 size_t n = pc_h2_build_window_update(wu, sizeof wu, 0, h->length);
240 wr(c, wu, n);
241 n = pc_h2_build_window_update(wu, sizeof wu, h->stream_id, h->length);
242 wr(c, wu, n);
243 }
244 return true;
245}
246
247bool process_frame(H2Conn *c)
248{
249 H2FrameHeader h;
250 pc_h2_parse_header(c->fbuf, H2_FRAME_HEADER_LEN, &h);
251 const uint8_t *payload = c->fbuf + H2_FRAME_HEADER_LEN;
252
253 // A header block must be continued only by CONTINUATION on the same stream (sec 6.10).
254 if (c->in_header_block && h.type != H2FrameType::H2_CONTINUATION)
255 {
256 return false;
257 }
258
259 switch (h.type)
260 {
261 case H2FrameType::H2_SETTINGS:
262 if (h.flags & H2_FLAG_ACK)
263 {
264 return h.length == 0; // ACK of our settings
265 }
266 if (!pc_h2_parse_settings(payload, h.length, &c->peer))
267 {
268 return false;
269 }
270 send_control(c, pc_h2_build_settings_ack);
271 return true;
272 case H2FrameType::H2_PING:
273 if (h.flags & H2_FLAG_ACK)
274 {
275 return true;
276 }
277 if (h.length != 8)
278 {
279 return false;
280 }
281 {
282 uint8_t pg[H2_FRAME_HEADER_LEN + 8];
283 size_t n = pc_h2_build_ping_ack(pg, sizeof pg, payload);
284 wr(c, pg, n);
285 }
286 return true;
287 case H2FrameType::H2_WINDOW_UPDATE: {
288 if (h.length != 4)
289 {
290 return false;
291 }
292 uint32_t inc = rd32(payload) & 0x7FFFFFFF;
293 if (h.stream_id == 0)
294 {
295 c->conn_send_window += (int32_t)inc;
296 }
297 else
298 {
299 H2Stream *s = find_stream(c, h.stream_id);
300 if (s)
301 {
302 s->send_window += (int32_t)inc;
303 }
304 }
305 return true;
306 }
307 case H2FrameType::H2_HEADERS:
308 return handle_headers(c, &h, payload);
309 case H2FrameType::H2_CONTINUATION:
310 return handle_continuation(c, &h, payload);
311 case H2FrameType::H2_DATA:
312 return handle_data(c, &h, payload);
313 case H2FrameType::H2_RST_STREAM: {
314 H2Stream *s = find_stream(c, h.stream_id);
315 if (s)
316 {
317 s->id = 0; // free the slot
318 }
319 return true;
320 }
321 case H2FrameType::H2_PRIORITY:
322 return true; // accepted, ignored
323 case H2FrameType::H2_GOAWAY:
324 c->phase = 2;
325 return true;
326 case H2FrameType::H2_PUSH_PROMISE:
327 return false; // a server never receives PUSH_PROMISE (sec 8.4)
328 default:
329 return true; // unknown frame types are ignored (sec 4.1)
330 }
331}
332} // namespace
333
334void pc_h2_conn_init(H2Conn *c, const H2Callbacks *cb)
335{
336 memset(c, 0, sizeof(*c));
337 c->cb = *cb;
338 c->phase = 0;
339 pc_h2_settings_defaults(&c->peer);
340 c->conn_send_window = 65535;
341 pc_hpack_dyn_init(&c->hdec, PC_HPACK_TABLE_BYTES);
342 send_our_settings(c);
343}
344
345bool pc_h2_conn_recv(H2Conn *c, const uint8_t *data, size_t len)
346{
347 size_t off = 0;
348 if (c->phase == 0)
349 {
350 while (off < len && c->pre < H2_PREFACE_LEN)
351 {
352 if (data[off] != (uint8_t)H2_PREFACE[c->pre])
353 {
354 return false; // malformed preface
355 }
356 c->pre++;
357 off++;
358 }
359 if (c->pre < H2_PREFACE_LEN)
360 {
361 return true; // preface still incomplete
362 }
363 c->phase = 1;
364 }
365 if (c->phase == 2)
366 {
367 return true; // closing; ignore further input
368 }
369
370 while (off < len)
371 {
372 if (c->fhave < H2_FRAME_HEADER_LEN)
373 {
374 size_t take = H2_FRAME_HEADER_LEN - c->fhave;
375 if (take > len - off)
376 {
377 take = len - off;
378 }
379 memcpy(c->fbuf + c->fhave, data + off, take);
380 c->fhave += take;
381 off += take;
382 if (c->fhave < H2_FRAME_HEADER_LEN)
383 {
384 return true;
385 }
386 }
387 uint32_t plen = ((uint32_t)c->fbuf[0] << 16) | ((uint32_t)c->fbuf[1] << 8) | c->fbuf[2];
388 if (plen > PC_H2_MAX_FRAME)
389 {
390 return false; // FRAME_SIZE_ERROR
391 }
392 size_t total = H2_FRAME_HEADER_LEN + plen;
393 size_t take = total - c->fhave;
394 if (take > len - off)
395 {
396 take = len - off;
397 }
398 memcpy(c->fbuf + c->fhave, data + off, take);
399 c->fhave += take;
400 off += take;
401 if (c->fhave < total)
402 {
403 return true; // frame incomplete
404 }
405 if (!process_frame(c))
406 {
407 return false;
408 }
409 c->fhave = 0;
410 }
411 return true;
412}
413
414bool pc_h2_conn_respond(H2Conn *c, uint32_t stream_id, int status, const char *content_type, const char *body,
415 size_t body_len)
416{
417 H2Stream *s = find_stream(c, stream_id);
418 if (!s)
419 {
420 return false;
421 }
422
423 // Build the HPACK header block: :status, optional content-type, content-length.
424 uint8_t block[256];
425 size_t bo = 0;
426 char num[16];
427 pc_sb sb_num = {num, sizeof num, 0, true};
428 pc_sb_i64(&sb_num, (int64_t)(status));
429 int nl = (int)pc_sb_finish(&sb_num);
430 size_t w = pc_hpack_encode_header(block + bo, sizeof block - bo, ":status", 7, num, (size_t)nl);
431 // GCOVR_EXCL_START :status is a decimal int (<=11 chars) into a fresh 256B block; the encode cannot overflow
432 if (!w)
433 {
434 return false;
435 }
436 // GCOVR_EXCL_STOP
437 bo += w;
438 if (content_type)
439 {
440 // Cap above the largest content-type that can fit this block even at HPACK-Huffman's best
441 // 5-bit/char (~sizeof block * 8/5): a longer value can never fit, so measuring it as `2*block`
442 // still trips the encode's reject below instead of being truncated into a fittable length.
443 w = pc_hpack_encode_header(block + bo, sizeof block - bo, "content-type", 12, content_type,
444 strnlen(content_type, sizeof block * 2));
445 if (!w)
446 {
447 return false;
448 }
449 bo += w;
450 }
451 pc_sb sb_num2 = {num, sizeof num, 0, true};
452 pc_sb_u32(&sb_num2, (uint32_t)((unsigned)body_len));
453 int cl = (int)pc_sb_finish(&sb_num2);
454 w = pc_hpack_encode_header(block + bo, sizeof block - bo, "content-length", 14, num, (size_t)cl);
455 // Reachable: bo has already been advanced by the caller-supplied content-type, which can consume nearly
456 // the whole block, leaving too little room for content-length even though it is only a few octets.
457 if (!w)
458 {
459 return false;
460 }
461 bo += w;
462
463 uint8_t frame[H2_FRAME_HEADER_LEN + sizeof block];
464 size_t n = pc_h2_build_headers(frame, sizeof frame, stream_id, block, bo, body_len == 0);
465 // GCOVR_EXCL_START frame is H2_FRAME_HEADER_LEN + sizeof block; 9 + bo (bo <= 256) always fits
466 if (!n)
467 {
468 return false;
469 }
470 // GCOVR_EXCL_STOP
471 wr(c, frame, n);
472
473 // Body as DATA frames, split to the peer's max frame size, END_STREAM on the last.
474 size_t sent = 0;
475 uint32_t chunk_max = c->peer.max_frame_size ? c->peer.max_frame_size : 16384;
476 while (sent < body_len)
477 {
478 size_t chunk = body_len - sent;
479 if (chunk > chunk_max)
480 {
481 chunk = chunk_max;
482 }
483 bool last = (sent + chunk == body_len);
484 uint8_t dh[H2_FRAME_HEADER_LEN];
485 size_t hn = pc_h2_write_header(dh, sizeof dh, (uint32_t)chunk, H2FrameType::H2_DATA,
486 last ? H2_FLAG_END_STREAM : 0, stream_id);
487 // GCOVR_EXCL_START dh is exactly H2_FRAME_HEADER_LEN; a 9-byte frame header always fits
488 if (!hn)
489 {
490 return false;
491 }
492 // GCOVR_EXCL_STOP
493 wr(c, dh, hn);
494 wr(c, (const uint8_t *)(body + sent), chunk);
495 c->conn_send_window -= (int32_t)chunk;
496 s->send_window -= (int32_t)chunk;
497 sent += chunk;
498 }
499 s->id = 0; // stream complete; free the slot
500 return true;
501}
502
503void pc_h2_conn_goaway(H2Conn *c, uint32_t error)
504{
505 uint8_t buf[H2_FRAME_HEADER_LEN + 8];
506 size_t n = pc_h2_build_goaway(buf, sizeof buf, c->last_peer_stream, error);
507 wr(c, buf, n);
508 c->phase = 2;
509}
510
511#endif // PC_ENABLE_HTTP2
#define PC_H2_MAX_STREAMS
Definition 16mbpsram.h:30
#define PC_H2_MAX_FRAME
Largest HTTP/2 frame we accept, in bytes (advertised as SETTINGS_MAX_FRAME_SIZE). RFC 9113 requires a...
#define PC_HPACK_TABLE_BYTES
Per-connection HPACK dynamic-table size in bytes (our decoder; advertised to the peer as SETTINGS_HEA...
Bounded no-heap string builder that fails closed on overflow (one shared copy).
size_t pc_sb_finish(pc_sb *b)
NUL-terminate and return the built length, or 0 if the build overflowed.
Definition strbuf.h:648
void pc_sb_i64(pc_sb *b, int64_t v)
Append v as signed decimal (64-bit), with a leading '-' when negative.
Definition strbuf.h:315
void pc_sb_u32(pc_sb *b, uint32_t v)
Append v as decimal (no leading zeros; "0" for zero).
Definition strbuf.h:303
Bump-append target; ok latches false once an append would overflow cap.
Definition strbuf.h:30