ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
presentation.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 presentation.cpp
6 * @brief Layer 6 (Presentation) - wires the transport ring buffer to the HTTP parser.
7 *
8 * This file is now a thin adapter. The HTTP parsing logic lives in
9 * http_parser.cpp. This layer only knows about:
10 * - The transport RX read API (pc_conn_available / pc_conn_read_byte) - it
11 * never indexes the ring itself; transport owns rx_buffer / rx_head / rx_tail
12 * - Slot-indexed helpers that the session and application layers expect
13 *
14 * The slot-indexed `http_reset()` pre-stamps slot_id then delegates to
15 * `http_parser_reset()`. The slot-indexed `http_parse()` drains all
16 * available bytes from the ring buffer through `http_parser_feed()` and
17 * stops as soon as the parser reaches any terminal state.
18 */
19
20#include "presentation.h"
21#include "network_drivers/session/proto_handler.h" // ProtoHandler (the L5 dispatch seam this registers into)
22#if PC_ENABLE_WEBSOCKET
23#include "network_drivers/presentation/http/websocket/websocket.h" // ws_find()/ws_free(): a WS-upgraded slot must never be HTTP-parsed
24#endif
25#if PC_ENABLE_SSE
26#include "network_drivers/presentation/http/sse/sse.h" // pc_sse_free(): release an SSE binding when its HTTP slot closes/reuses
27#endif
28#if PC_ENABLE_TLS
30#if PC_ENABLE_HTTP2
32#endif
33#include <string.h> // strcmp (ALPN check)
34#endif
35
36#if PC_ENABLE_KEEPALIVE
37uint16_t http_req_count[MAX_CONNS];
38#endif
39
40void http_reset(uint8_t slot_id)
41{
42 if (slot_id >= MAX_CONNS)
43 {
44 return;
45 }
46 http_pool[slot_id].slot_id = slot_id; // ensure slot_id is correct before reset reads it
48}
49
50// Release any WebSocket / SSE binding still attached to a slot. WS and SSE upgrades leave the slot
51// as ConnProto::PROTO_HTTP (SSE is just a long-lived HTTP response; WS is pumped separately), so this
52// HTTP proto handler owns their teardown. Both frees are no-ops when the slot has no such binding.
53// Called on close AND on a fresh accept, because a slot can be reaped by the idle sweep or aborted
54// (SSE pool full) without a close event ever firing - so a reused slot must not inherit a stale
55// binding. A stale sse binding is the DoS: http_poll_slot() sees pc_sse_find(slot) and skips HTTP
56// dispatch, wedging every later connection that reuses the slot.
57static inline void http_release_upgrade_bindings(uint8_t slot_id)
58{
59#if PC_ENABLE_WEBSOCKET
60 ws_free(slot_id);
61#endif
62#if PC_ENABLE_SSE
63 pc_sse_free(slot_id);
64#endif
65}
66
67void http_conn_open(uint8_t slot_id)
68{
69 if (slot_id >= MAX_CONNS)
70 {
71 return;
72 }
73 http_release_upgrade_bindings(slot_id); // a reused slot must not inherit a prior WS/SSE binding
74#if PC_ENABLE_KEEPALIVE
75 http_req_count[slot_id] = 0; // fresh connection: clear the keep-alive request tally
76#endif
77 http_reset(slot_id);
78}
79
80void http_parse(uint8_t slot_id)
81{
82 if (slot_id >= MAX_CONNS)
83 {
84 return;
85 }
86
87#if PC_ENABLE_WEBSOCKET
88 // Once a slot upgrades to WebSocket its rx ring carries WS frames, not HTTP.
89 // The WS frame parser is pumped separately (handle()/the worker loop); feeding
90 // those bytes to the HTTP parser here would consume - and corrupt - the first
91 // WS frame. This guard makes "never HTTP-parse a WS slot" hold for every caller
92 // (the event-queue dispatch raced the WS pump and ate the first frame's header
93 // byte, dropping the first connection after a reboot).
94 if (ws_find(slot_id))
95 {
96 return;
97 }
98#endif
99
100 HttpReq *req = &http_pool[slot_id];
101
102 // Drain via the transport read API - the parser never touches the ring itself.
103 // Check the terminal state BEFORE consuming so a pipelined next request is left
104 // in the ring; the window is reopened by the worker's pc_conn_ack_consumed().
105 while (pc_conn_available(slot_id) > 0)
106 {
107 switch (req->parse_state)
108 {
113 return; // terminal state - drain nothing further
114 default:
115 break;
116 }
117
118 uint8_t byte = 0;
119 // The false arm cannot fire on any thread count, not merely on this single-
120 // threaded host harness: pc_conn_available() and pc_conn_read_byte() both
121 // compare the same slot's rx_head vs rx_tail, and nothing between the two
122 // calls here mutates either (the switch above only reads req->parse_state).
123 // rx_tail is single-consumer-owned - this loop is the only writer for this
124 // slot - and the producer (tcp.cpp's recv callback) always checks
125 // pc_ring_free() before writing, so rx_head can never be advanced to equal
126 // the rx_tail snapshot available() just found nonzero ("the free-space check
127 // ... guarantees it fits, so head can never overrun tail" - tcp.cpp). A
128 // "drain" of the ring between the two calls would require a second consumer,
129 // which the single-consumer-per-slot design does not have.
130 if (!pc_conn_read_byte(slot_id, &byte)) // GCOVR_EXCL_BR_LINE
131 {
132 break; // GCOVR_EXCL_LINE
133 }
134 http_parser_feed(req, byte);
135 }
136}
137
138// ---------------------------------------------------------------------------
139// HTTP ProtoHandler - the L5 dispatch seam for an HTTP connection.
140//
141// This is where an HTTP connection is fed: the plaintext path drains the ring
142// through http_parse() (above); the TLS path drives the handshake, then routes
143// decrypted bytes to the HTTP/2 engine (ALPN "h2"), the WebSocket pump (an
144// upgraded slot), or the HTTP/1.1 parser. Keeping it here (Layer 6, with the rest
145// of the HTTP-connection glue) leaves the session layer's dispatcher free of any
146// HTTP / TLS / h2 / ws specifics - it only routes events to registered handlers.
147// ---------------------------------------------------------------------------
148
149#if PC_ENABLE_TLS
150// Abort a TLS connection (fatal handshake/read error). pc_conn_abort_slot owns
151// the whole teardown: free the TLS context (abrupt), detach the pcb, reset the
152// slot, then RST - so this never reaches into the raw tcp_pcb.
153static void tls_abort(uint8_t slot)
154{
155 pc_conn_abort_slot(slot);
156 http_reset(slot);
157}
158
159// Pump a TLS connection: drive the handshake to completion, then decrypt any
160// application data straight into the HTTP parser (same byte-by-byte feed the
161// plaintext path uses; the rx ring now holds ciphertext, consumed by the BIO).
162static void tls_data(uint8_t slot)
163{
164 if (!pc_tls_established(slot))
165 {
166 int h = pc_tls_handshake(slot);
167 if (h < 0)
168 {
169 tls_abort(slot);
170 return;
171 }
172 if (h == 0)
173 {
174 return; // still handshaking; wait for more ciphertext
175 }
176 }
177
178#if PC_ENABLE_HTTP2
179 // Just past the handshake: if the client negotiated ALPN "h2", this connection speaks HTTP/2
180 // for its lifetime - hand its decrypted bytes to the h2 engine, not the HTTP/1.1 parser.
181 if (!conn_pool[slot].pc_h2_checked)
182 {
183 conn_pool[slot].pc_h2_checked = 1;
184 const char *alpn = pc_tls_alpn(slot);
185 if (alpn && strcmp(alpn, "h2") == 0)
186 {
187 conn_pool[slot].h2 = 1;
188 conn_pool[slot].pc_resp_sink = pc_h2_server_respond; // route responses through the h2 framer
189 pc_h2_server_open(slot);
190 }
191 }
192 if (conn_pool[slot].h2)
193 {
194 pc_h2_server_data(slot);
195 return;
196 }
197#endif
198
199#if PC_ENABLE_WEBSOCKET
200 // A TLS slot upgraded to WebSocket is pumped from handle() (it decrypts
201 // records and feeds the WS frame parser, dispatching each frame); leave the
202 // ciphertext in the rx ring for it rather than feeding the HTTP parser here.
203 if (ws_find(slot))
204 {
205 return;
206 }
207#endif
208
209 uint8_t buf[256];
210 int n;
211 while ((n = pc_tls_read(slot, buf, sizeof(buf))) > 0)
212 {
213 HttpReq *req = &http_pool[slot];
214 for (int i = 0; i < n; i++)
215 {
219 {
220 break; // terminal state - let handle() dispatch before reading more
221 }
222 http_parser_feed(req, buf[i]);
223 }
224 }
225 if (n < 0)
226 {
227 tls_abort(slot);
228 }
229}
230#endif // PC_ENABLE_TLS
231
232// The data/close paths branch on TLS (a TLS slot's rx ring holds ciphertext,
233// decrypted into the parser); accept maps directly.
234static void http_evt_accept(uint8_t slot)
235{
236 http_conn_open(slot); // resets the parser + (keep-alive) the per-conn request tally
237#if PC_ENABLE_HTTP2
238 conn_pool[slot].h2 = 0; // a reused slot must re-run the post-handshake ALPN check
239 conn_pool[slot].pc_h2_checked = 0;
240 conn_pool[slot].pc_resp_sink = nullptr; // back to the HTTP/1.1 builder until ALPN says otherwise
241#endif
242}
243static void http_evt_data(uint8_t slot)
244{
245#if PC_ENABLE_TLS
246 if (conn_pool[slot].tls)
247 {
248 tls_data(slot);
249 return;
250 }
251#endif
252 http_parse(slot); // a no-op once the slot has upgraded to WebSocket (see http_parse)
253}
254static void http_evt_close(uint8_t slot)
255{
256#if PC_ENABLE_TLS
257 if (conn_pool[slot].tls)
258 {
259 pc_tls_conn_free(slot); // also covers timeouts (EvtType::EVT_ERROR)
260 }
261#endif
262 http_release_upgrade_bindings(slot); // FIN/RST/error on an SSE or WS slot must free its binding
263 http_reset(slot);
264}
265// HTTP's poll pump is instance-bound (it dispatches into a PC's routes), so the routing
266// core installs it here at begin() via http_proto_set_poll(). The trampoline lets the ProtoHandler
267// stay a plain static const while the actual pump lives in the application TU - the on_poll analogue
268// of the pc_resp_sink TX seam. Until installed (e.g. the native harness before begin()) it is a no-op.
269static void (*s_http_poll)(uint8_t slot) = nullptr;
270static void http_evt_poll(uint8_t slot)
271{
272 if (s_http_poll)
273 {
274 s_http_poll(slot);
275 }
276}
277void http_proto_set_poll(void (*fn)(uint8_t slot))
278{
279 s_http_poll = fn;
280}
281
282static const ProtoHandler s_http_handler = {http_evt_accept, http_evt_data, http_evt_close, http_evt_poll};
283
285{
286 return &s_http_handler;
287}
#define MAX_CONNS
Definition c2_defaults.h:47
void http_parser_reset(HttpReq *req)
Reset a parser context to the initial (ParseState::PARSE_METHOD) state.
HttpReq http_pool[CONN_POOL_SLOTS]
Pool of parser contexts, one per connection-pool slot (incl. reserved dispatch slots).
void http_parser_feed(HttpReq *p, uint8_t byte)
Feed one byte to the parser state machine.
@ PARSE_ERROR
Unrecoverable parse failure → 400.
@ PARSE_ENTITY_TOO_LARGE
Content-Length > BODY_BUF_SIZE → 413.
@ PARSE_COMPLETE
Full request parsed; ready for dispatch.
@ PARSE_URI_TOO_LONG
Path exceeds MAX_PATH_LEN → 414.
void http_reset(uint8_t slot_id)
Reset the HTTP parser for a connection slot.
const ProtoHandler * http_proto_handler(void)
void http_proto_set_poll(void(*fn)(uint8_t slot))
Install the HTTP per-slot poll pump (the routing core's instance-bound on_poll).
void http_parse(uint8_t slot_id)
Drain the transport ring buffer and advance the HTTP parser.
void http_conn_open(uint8_t slot_id)
Initialize a slot for a freshly-accepted HTTP connection.
Layer 6 (Presentation) - wires the transport ring buffer to the HTTP parser.
Layer 5 (Session) - per-protocol connection handler dispatch table.
void pc_sse_free(uint8_t slot_id)
Free the SseConn associated with a TCP slot.
Definition sse.cpp:55
Layer 6 (Presentation) – Server-Sent Events connection pool.
Fully-parsed HTTP/1.1 request.
ParseState parse_state
Current parser state.
uint8_t slot_id
Transport slot index (set by presentation layer).
Per-protocol connection event/poll callbacks (Layer 5 dispatch vtable).
void pc_conn_abort_slot(uint8_t slot)
Hard-abort connection slot (RST) for a fatal condition. The transport owns the teardown order: free t...
Definition tcp.cpp:682
TcpConn conn_pool[CONN_POOL_SLOTS]
Static pool of connection contexts. Defined in tcp.cpp. Sized CONN_POOL_SLOTS: MAX_CONNS TCP slots pl...
Definition tcp.cpp:425
Deterministic TLS engine: mbedTLS over a static memory pool (PC_ENABLE_TLS).
void ws_free(uint8_t slot_id)
Free the WsConn associated with a TCP slot.
Definition websocket.cpp:79
WsConn * ws_find(uint8_t slot_id)
Find the WsConn for a given TCP slot, or nullptr if none.
Definition websocket.cpp:67
Layer 6 (Presentation) – WebSocket frame parser and connection pool.