DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
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 (det_conn_available / det_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 DETWS_ENABLE_WEBSOCKET
23#include "network_drivers/presentation/websocket/websocket.h" // ws_find()/ws_free(): a WS-upgraded slot must never be HTTP-parsed
24#endif
25#if DETWS_ENABLE_SSE
26#include "network_drivers/presentation/sse/sse.h" // sse_free(): release an SSE binding when its HTTP slot closes/reuses
27#endif
28#if DETWS_ENABLE_TLS
30#if DETWS_ENABLE_HTTP2
32#endif
33#include <string.h> // strcmp (ALPN check)
34#endif
35
36#if DETWS_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 return;
44 http_pool[slot_id].slot_id = slot_id; // ensure slot_id is correct before reset reads it
46}
47
48// Release any WebSocket / SSE binding still attached to a slot. WS and SSE upgrades leave the slot
49// as ConnProto::PROTO_HTTP (SSE is just a long-lived HTTP response; WS is pumped separately), so this
50// HTTP proto handler owns their teardown. Both frees are no-ops when the slot has no such binding.
51// Called on close AND on a fresh accept, because a slot can be reaped by the idle sweep or aborted
52// (SSE pool full) without a close event ever firing - so a reused slot must not inherit a stale
53// binding. A stale sse binding is the DoS: http_poll_slot() sees sse_find(slot) and skips HTTP
54// dispatch, wedging every later connection that reuses the slot.
55static inline void http_release_upgrade_bindings(uint8_t slot_id)
56{
57#if DETWS_ENABLE_WEBSOCKET
58 ws_free(slot_id);
59#endif
60#if DETWS_ENABLE_SSE
61 sse_free(slot_id);
62#endif
63}
64
65void http_conn_open(uint8_t slot_id)
66{
67 if (slot_id >= MAX_CONNS)
68 return;
69 http_release_upgrade_bindings(slot_id); // a reused slot must not inherit a prior WS/SSE binding
70#if DETWS_ENABLE_KEEPALIVE
71 http_req_count[slot_id] = 0; // fresh connection: clear the keep-alive request tally
72#endif
73 http_reset(slot_id);
74}
75
76void http_parse(uint8_t slot_id)
77{
78 if (slot_id >= MAX_CONNS)
79 return;
80
81#if DETWS_ENABLE_WEBSOCKET
82 // Once a slot upgrades to WebSocket its rx ring carries WS frames, not HTTP.
83 // The WS frame parser is pumped separately (handle()/the worker loop); feeding
84 // those bytes to the HTTP parser here would consume - and corrupt - the first
85 // WS frame. This guard makes "never HTTP-parse a WS slot" hold for every caller
86 // (the event-queue dispatch raced the WS pump and ate the first frame's header
87 // byte, dropping the first connection after a reboot).
88 if (ws_find(slot_id))
89 return;
90#endif
91
92 HttpReq *req = &http_pool[slot_id];
93
94 // Drain via the transport read API - the parser never touches the ring itself.
95 // Check the terminal state BEFORE consuming so a pipelined next request is left
96 // in the ring; the window is reopened by the worker's det_conn_ack_consumed().
97 while (det_conn_available(slot_id) > 0)
98 {
99 switch (req->parse_state)
100 {
105 return; // terminal state - drain nothing further
106 default:
107 break;
108 }
109
110 uint8_t byte = 0;
111 if (!det_conn_read_byte(slot_id, &byte)) // ring drained between available() and here
112 break;
113 http_parser_feed(req, byte);
114 }
115}
116
117// ---------------------------------------------------------------------------
118// HTTP ProtoHandler - the L5 dispatch seam for an HTTP connection.
119//
120// This is where an HTTP connection is fed: the plaintext path drains the ring
121// through http_parse() (above); the TLS path drives the handshake, then routes
122// decrypted bytes to the HTTP/2 engine (ALPN "h2"), the WebSocket pump (an
123// upgraded slot), or the HTTP/1.1 parser. Keeping it here (Layer 6, with the rest
124// of the HTTP-connection glue) leaves the session layer's dispatcher free of any
125// HTTP / TLS / h2 / ws specifics - it only routes events to registered handlers.
126// ---------------------------------------------------------------------------
127
128#if DETWS_ENABLE_TLS
129// Abort a TLS connection (fatal handshake/read error). det_conn_abort_slot owns
130// the whole teardown: free the TLS context (abrupt), detach the pcb, reset the
131// slot, then RST - so this never reaches into the raw tcp_pcb.
132static void tls_abort(uint8_t slot)
133{
135 http_reset(slot);
136}
137
138// Pump a TLS connection: drive the handshake to completion, then decrypt any
139// application data straight into the HTTP parser (same byte-by-byte feed the
140// plaintext path uses; the rx ring now holds ciphertext, consumed by the BIO).
141static void tls_data(uint8_t slot)
142{
143 if (!det_tls_established(slot))
144 {
145 int h = det_tls_handshake(slot);
146 if (h < 0)
147 {
148 tls_abort(slot);
149 return;
150 }
151 if (h == 0)
152 return; // still handshaking; wait for more ciphertext
153 }
154
155#if DETWS_ENABLE_HTTP2
156 // Just past the handshake: if the client negotiated ALPN "h2", this connection speaks HTTP/2
157 // for its lifetime - hand its decrypted bytes to the h2 engine, not the HTTP/1.1 parser.
158 if (!conn_pool[slot].h2_checked)
159 {
160 conn_pool[slot].h2_checked = 1;
161 const char *alpn = det_tls_alpn(slot);
162 if (alpn && strcmp(alpn, "h2") == 0)
163 {
164 conn_pool[slot].h2 = 1;
165 conn_pool[slot].resp_sink = h2_server_respond; // route responses through the h2 framer
166 h2_server_open(slot);
167 }
168 }
169 if (conn_pool[slot].h2)
170 {
171 h2_server_data(slot);
172 return;
173 }
174#endif
175
176#if DETWS_ENABLE_WEBSOCKET
177 // A TLS slot upgraded to WebSocket is pumped from handle() (it decrypts
178 // records and feeds the WS frame parser, dispatching each frame); leave the
179 // ciphertext in the rx ring for it rather than feeding the HTTP parser here.
180 if (ws_find(slot))
181 return;
182#endif
183
184 uint8_t buf[256];
185 int n;
186 while ((n = det_tls_read(slot, buf, sizeof(buf))) > 0)
187 {
188 HttpReq *req = &http_pool[slot];
189 for (int i = 0; i < n; i++)
190 {
194 break; // terminal state - let handle() dispatch before reading more
195 http_parser_feed(req, buf[i]);
196 }
197 }
198 if (n < 0)
199 tls_abort(slot);
200}
201#endif // DETWS_ENABLE_TLS
202
203// The data/close paths branch on TLS (a TLS slot's rx ring holds ciphertext,
204// decrypted into the parser); accept maps directly.
205static void http_evt_accept(uint8_t slot)
206{
207 http_conn_open(slot); // resets the parser + (keep-alive) the per-conn request tally
208#if DETWS_ENABLE_HTTP2
209 conn_pool[slot].h2 = 0; // a reused slot must re-run the post-handshake ALPN check
210 conn_pool[slot].h2_checked = 0;
211 conn_pool[slot].resp_sink = nullptr; // back to the HTTP/1.1 builder until ALPN says otherwise
212#endif
213}
214static void http_evt_data(uint8_t slot)
215{
216#if DETWS_ENABLE_TLS
217 if (conn_pool[slot].tls)
218 {
219 tls_data(slot);
220 return;
221 }
222#endif
223 http_parse(slot); // a no-op once the slot has upgraded to WebSocket (see http_parse)
224}
225static void http_evt_close(uint8_t slot)
226{
227#if DETWS_ENABLE_TLS
228 if (conn_pool[slot].tls)
229 det_tls_conn_free(slot); // also covers timeouts (EvtType::EVT_ERROR)
230#endif
231 http_release_upgrade_bindings(slot); // FIN/RST/error on an SSE or WS slot must free its binding
232 http_reset(slot);
233}
234// HTTP's poll pump is instance-bound (it dispatches into a DetWebServer's routes), so the routing
235// core installs it here at begin() via http_proto_set_poll(). The trampoline lets the ProtoHandler
236// stay a plain static const while the actual pump lives in the application TU - the on_poll analogue
237// of the resp_sink TX seam. Until installed (e.g. the native harness before begin()) it is a no-op.
238static void (*s_http_poll)(uint8_t slot) = nullptr;
239static void http_evt_poll(uint8_t slot)
240{
241 if (s_http_poll)
242 s_http_poll(slot);
243}
244void http_proto_set_poll(void (*fn)(uint8_t slot))
245{
246 s_http_poll = fn;
247}
248
249static const ProtoHandler s_http_handler = {http_evt_accept, http_evt_data, http_evt_close, http_evt_poll};
250
252{
253 return &s_http_handler;
254}
#define MAX_CONNS
Maximum simultaneous TCP connections (fixed static pool; ~3.95 KB of internal RAM per slot).
Bridge between the HTTP/2 engine (h2_conn) and the server's request pipeline.
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 sse_free(uint8_t slot_id)
Free the SseConn associated with a TCP slot.
Definition sse.cpp:53
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).
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:347
void det_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:496
Deterministic TLS engine: mbedTLS over a static memory pool (DETWS_ENABLE_TLS).
void ws_free(uint8_t slot_id)
Free the WsConn associated with a TCP slot.
Definition websocket.cpp:77
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.