DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
websocket.h
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 websocket.h
6 * @brief Layer 6 (Presentation) -- WebSocket frame parser and connection pool.
7 *
8 * Implements RFC 6455 framing with a fixed-size payload buffer per slot.
9 * Connections are tracked in ws_pool[MAX_WS_CONNS]; each entry maps to one
10 * TCP slot in conn_pool[] via slot_id.
11 *
12 * **Frame format (client to server)**
13 * ```
14 * 0 1 2 3
15 * 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
16 * +-+-+-+-+-------+-+-------------+-------------------------------+
17 * |F|R|R|R| opcode|M| payload len | extended payload length |
18 * |I|S|S|S| (4) |A| (7) | (16/64) |
19 * |N|V|V|V| |S| +-------------------------------+
20 * | |1|2|3| |K| | |
21 * +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - -+
22 * | extended payload length continued, if payload len == 127 |
23 * + - - - - - - - - - - - - - - -+-------------------------------+
24 * | | masking key, if MASK set |
25 * +-------------------------------+-------------------------------+
26 * | masking key (continued) | payload data |
27 * +-------------------------------- - - - - - - - - - - - - - - -+
28 * : payload data continued :
29 * +---------------------------------------------------------------+
30 * ```
31 *
32 * **State machine**
33 * ```
34 * WsParseState::WS_HEADER1 -- read FIN + opcode byte
35 * WsParseState::WS_HEADER2 -- read MASK + 7-bit payload length
36 * WsParseState::WS_LEN16_HI -- read extended 16-bit length high byte
37 * WsParseState::WS_LEN16_LO -- read extended 16-bit length low byte
38 * WsParseState::WS_LEN64 -- consume 8-byte 64-bit length (reject; too large)
39 * WsParseState::WS_MASK0..3 -- read 4-byte masking key
40 * WsParseState::WS_PAYLOAD -- accumulate payload bytes (unmasked)
41 * WsParseState::WS_FRAME_READY -- complete frame waiting for dispatch
42 * WsParseState::WS_CLOSED -- connection closed; slot may be recycled
43 * WsParseState::WS_ERROR -- protocol error; close frame sent
44 * ```
45 *
46 * **Limitations**
47 * - A reassembled message must fit in WS_FRAME_SIZE bytes; larger closes 1009.
48 * - RSV bits must be zero (no extensions supported).
49 *
50 * **Fragmentation (RFC 6455 §5.4)**
51 * Fragmented data messages are reassembled into `buf` across continuation
52 * frames; the message is delivered only when the FIN frame arrives. Control
53 * frames (ping/pong/close) may be interleaved between fragments and are
54 * handled immediately without disturbing the partial message.
55 *
56 * @author Douglas Quigg (dstroy0)
57 * @date 2026
58 */
59
60#ifndef DETERMINISTICESPASYNCWEBSERVER_WEBSOCKET_H
61#define DETERMINISTICESPASYNCWEBSERVER_WEBSOCKET_H
62
63#include "ServerConfig.h"
65
66// ---------------------------------------------------------------------------
67// WebSocket opcodes (RFC 6455 §5.2)
68// ---------------------------------------------------------------------------
69
70/** @brief WebSocket frame opcodes. */
71enum class WsOpcode : uint8_t
72{
73 WS_OP_CONTINUATION = 0x0, ///< Continuation frame (data-message fragment; reassembled into buf).
74 WS_OP_TEXT = 0x1, ///< UTF-8 text payload.
75 WS_OP_BINARY = 0x2, ///< Binary payload.
76 WS_OP_CLOSE = 0x8, ///< Connection close.
77 WS_OP_PING = 0x9, ///< Ping (auto-ponged by the library).
78 WS_OP_PONG = 0xA ///< Pong (echoed ping; ignored by library).
79};
80
81/** @brief WebSocket close status codes (RFC 6455 §7.4.1). */
82enum class WsCloseCode : uint16_t
83{
84 WS_CLOSE_NORMAL = 1000, ///< Normal closure.
85 WS_CLOSE_GOING_AWAY = 1001, ///< Endpoint going away.
86 WS_CLOSE_PROTOCOL = 1002, ///< Protocol error.
87 WS_CLOSE_UNSUPPORTED = 1003, ///< Received a data type the endpoint cannot accept (RFC 6455).
88 WS_CLOSE_INVALID_PAYLOAD = 1007, ///< Text message that is not valid UTF-8 (RFC 6455 8.1).
89 WS_CLOSE_TOO_BIG = 1009 ///< Payload too large for WS_FRAME_SIZE.
90};
91
92// ---------------------------------------------------------------------------
93// Frame parser states
94// ---------------------------------------------------------------------------
95
96/** @brief States of the WebSocket frame parser. */
97enum class WsParseState : uint8_t
98{
99 WS_HEADER1, ///< Awaiting first header byte (FIN, RSV, opcode).
100 WS_HEADER2, ///< Awaiting second header byte (MASK, 7-bit length).
101 WS_LEN16_HI, ///< Reading extended 16-bit length, high byte.
102 WS_LEN16_LO, ///< Reading extended 16-bit length, low byte.
103 WS_LEN64, ///< Consuming 8-byte 64-bit length (always rejected).
104 WS_MASK0, ///< Reading masking key byte 0.
105 WS_MASK1, ///< Reading masking key byte 1.
106 WS_MASK2, ///< Reading masking key byte 2.
107 WS_MASK3, ///< Reading masking key byte 3.
108 WS_PAYLOAD, ///< Accumulating payload bytes.
109 WS_FRAME_READY, ///< Complete frame ready for dispatch.
110 WS_CLOSED, ///< Connection closed; slot may be recycled.
111 WS_ERROR ///< Protocol error; close frame has been queued.
112};
113
114// ---------------------------------------------------------------------------
115// Per-connection WebSocket state
116// ---------------------------------------------------------------------------
117
118/**
119 * @brief WebSocket connection state stored in ws_pool[].
120 *
121 * Allocated when an HTTP upgrade handshake succeeds. slot_id ties this
122 * entry back to conn_pool[] and the ring buffer.
123 */
124struct WsConn
125{
126 uint8_t ws_id; ///< Index into ws_pool[] (set at init).
127 uint8_t slot_id; ///< Owning TCP slot in conn_pool[].
128 bool active; ///< True when this entry is in use.
129
130 WsParseState parse_state; ///< Current frame parser state.
131 WsOpcode opcode; ///< Opcode of the frame being parsed.
132 bool fin; ///< FIN bit of the frame being parsed.
133 bool masked; ///< True if client sent a masking key.
134
135 uint8_t mask_key[4]; ///< Client masking key.
136 uint32_t payload_len; ///< Expected payload byte count (current frame).
137 uint32_t payload_idx; ///< Bytes received so far (current frame).
138 uint8_t len64_count; ///< Bytes consumed from 64-bit length.
139 uint8_t buf[WS_FRAME_SIZE + 1]; ///< Reassembled message payload, null-terminated.
140
141 // Fragmentation state (RFC 6455 §5.4). A data message may span multiple
142 // frames (first text/binary with FIN=0, then continuation frames). Control
143 // frames may be interleaved and use a separate buffer so they never clobber
144 // the partially-assembled data message.
145 bool fragmenting; ///< True between a non-FIN data frame and its FIN.
146 WsOpcode msg_opcode; ///< Opcode of the data message being assembled.
147 uint32_t msg_len; ///< Bytes assembled so far across all fragments.
148 uint8_t ctl_buf[125 + 1]; ///< Control-frame payload (ping/pong/close), null-terminated.
149
150#if DETWS_ENABLE_WS_DEFLATE
151 bool pmd; ///< permessage-deflate negotiated on this connection (RFC 7692).
152 bool msg_compressed; ///< Current data message arrived compressed (RSV1 on its first frame).
153#endif
154};
155
156/** @brief Pool of WebSocket connection state, one per MAX_WS_CONNS. */
158
159// ---------------------------------------------------------------------------
160// WebSocket API
161// ---------------------------------------------------------------------------
162
163/**
164 * @brief Initialize all WebSocket pool slots to inactive.
165 *
166 * Called once from DetWebServer::begin().
167 */
168void ws_init();
169
170/// @brief True if @p ws_id is a valid, in-use WebSocket slot. Use this instead of reaching into
171/// ws_pool[ws_id].active from another module.
172bool ws_active(uint8_t ws_id);
173
174/// @brief The NUL-terminated reassembled message payload for @p ws_id, or nullptr if the slot is
175/// out of range / inactive. Use this instead of reaching into ws_pool[ws_id].buf.
176const char *ws_payload(uint8_t ws_id);
177
178/**
179 * @brief Allocate a WsConn slot and bind it to a TCP slot.
180 *
181 * @param slot_id TCP connection slot that just completed an upgrade.
182 * @return Pointer to the allocated WsConn, or nullptr if the pool is full.
183 */
184WsConn *ws_alloc(uint8_t slot_id);
185
186/**
187 * @brief Find the WsConn for a given TCP slot, or nullptr if none.
188 *
189 * @param slot_id TCP connection slot index.
190 */
191WsConn *ws_find(uint8_t slot_id);
192
193/**
194 * @brief Free the WsConn associated with a TCP slot.
195 *
196 * @param slot_id TCP connection slot index.
197 */
198void ws_free(uint8_t slot_id);
199
200/**
201 * @brief Drain the ring buffer for slot_id and feed bytes to the WS parser.
202 *
203 * Stops when the ring buffer is empty or the parser reaches a terminal state
204 * (WsParseState::WS_FRAME_READY, WsParseState::WS_CLOSED, WsParseState::WS_ERROR).
205 *
206 * @param ws WebSocket connection to drain into.
207 */
208void ws_parse(WsConn *ws);
209
210/**
211 * @brief Feed one already-plaintext byte through the WS frame state machine.
212 *
213 * The per-byte core shared by ws_parse() (which reads the plaintext rx ring) and
214 * the TLS receive path (which decrypts ciphertext and feeds the plaintext here).
215 * Callers must stop feeding once parse_state reaches a terminal state
216 * (WsParseState::WS_FRAME_READY / WsParseState::WS_CLOSED / WsParseState::WS_ERROR) and dispatch/reset before
217 * continuing.
218 *
219 * @param ws WebSocket connection.
220 * @param byte Next plaintext byte of the client frame stream.
221 */
222void ws_feed_byte(WsConn *ws, uint8_t byte);
223
224/**
225 * @brief Reset the frame parser back to WsParseState::WS_HEADER1, ready for the next frame.
226 *
227 * Does not change ws->active or ws->slot_id.
228 *
229 * @param ws WebSocket connection to reset.
230 */
231void ws_reset_frame(WsConn *ws);
232
233/**
234 * @brief Send a WebSocket frame to the client.
235 *
236 * Builds the header (no masking -- server-to-client frames are never masked)
237 * and hands both to the transport layer (det_conn_send()). The caller is
238 * responsible for flushing afterwards (det_conn_flush()).
239 *
240 * @param ws WebSocket connection.
241 * @param opcode Frame opcode (WsOpcode::WS_OP_TEXT, WsOpcode::WS_OP_BINARY, WsOpcode::WS_OP_PONG, etc.).
242 * @param payload Payload bytes (may be nullptr for zero-length frames).
243 * @param len Payload length in bytes.
244 * @return true on success, false if the TCP slot is not active.
245 */
246bool ws_send_frame(WsConn *ws, WsOpcode opcode, const uint8_t *payload, uint16_t len);
247
248/**
249 * @brief Set the outbound fragmentation size (RFC 6455 sec 5.4), in payload bytes; 0 = off.
250 *
251 * A runtime override of DETWS_WS_FRAG_SIZE. When >0, a data message longer than @p bytes is split into
252 * that-sized frames by ws_send_frame() (see DETWS_WS_FRAG_SIZE). Applies to all connections.
253 */
254void ws_set_frag_size(uint16_t bytes);
255
256/**
257 * @brief Send a Close frame and mark the slot WsParseState::WS_CLOSED.
258 *
259 * @param ws WebSocket connection.
260 * @param code Close status code (e.g. WsCloseCode::WS_CLOSE_NORMAL).
261 */
262void ws_close(WsConn *ws, WsCloseCode code);
263
264#endif
User-facing configuration for DeterministicESPAsyncWebServer.
#define WS_FRAME_SIZE
Maximum WebSocket frame payload in bytes.
#define MAX_WS_CONNS
Maximum simultaneous WebSocket connections.
WebSocket connection state stored in ws_pool[].
Definition websocket.h:125
bool active
True when this entry is in use.
Definition websocket.h:128
uint8_t len64_count
Bytes consumed from 64-bit length.
Definition websocket.h:138
uint8_t mask_key[4]
Client masking key.
Definition websocket.h:135
uint32_t payload_len
Expected payload byte count (current frame).
Definition websocket.h:136
uint32_t msg_len
Bytes assembled so far across all fragments.
Definition websocket.h:147
uint8_t ws_id
Index into ws_pool[] (set at init).
Definition websocket.h:126
bool fin
FIN bit of the frame being parsed.
Definition websocket.h:132
bool masked
True if client sent a masking key.
Definition websocket.h:133
uint8_t slot_id
Owning TCP slot in conn_pool[].
Definition websocket.h:127
WsOpcode msg_opcode
Opcode of the data message being assembled.
Definition websocket.h:146
uint32_t payload_idx
Bytes received so far (current frame).
Definition websocket.h:137
WsParseState parse_state
Current frame parser state.
Definition websocket.h:130
WsOpcode opcode
Opcode of the frame being parsed.
Definition websocket.h:131
bool fragmenting
True between a non-FIN data frame and its FIN.
Definition websocket.h:145
uint8_t buf[WS_FRAME_SIZE+1]
Reassembled message payload, null-terminated.
Definition websocket.h:139
uint8_t ctl_buf[125+1]
Control-frame payload (ping/pong/close), null-terminated.
Definition websocket.h:148
Layer 4 (Transport) - TCP connection pool, ring buffers, and lwIP integration.
WsCloseCode
WebSocket close status codes (RFC 6455 §7.4.1).
Definition websocket.h:83
@ WS_CLOSE_INVALID_PAYLOAD
Text message that is not valid UTF-8 (RFC 6455 8.1).
@ WS_CLOSE_PROTOCOL
Protocol error.
@ WS_CLOSE_NORMAL
Normal closure.
@ WS_CLOSE_UNSUPPORTED
Received a data type the endpoint cannot accept (RFC 6455).
@ WS_CLOSE_GOING_AWAY
Endpoint going away.
@ WS_CLOSE_TOO_BIG
Payload too large for WS_FRAME_SIZE.
void ws_free(uint8_t slot_id)
Free the WsConn associated with a TCP slot.
Definition websocket.cpp:77
bool ws_send_frame(WsConn *ws, WsOpcode opcode, const uint8_t *payload, uint16_t len)
Send a WebSocket frame to the client.
const char * ws_payload(uint8_t ws_id)
The NUL-terminated reassembled message payload for ws_id, or nullptr if the slot is out of range / in...
Definition websocket.cpp:45
WsParseState
States of the WebSocket frame parser.
Definition websocket.h:98
@ WS_MASK3
Reading masking key byte 3.
@ WS_PAYLOAD
Accumulating payload bytes.
@ WS_FRAME_READY
Complete frame ready for dispatch.
@ WS_LEN16_LO
Reading extended 16-bit length, low byte.
@ WS_ERROR
Protocol error; close frame has been queued.
@ WS_HEADER2
Awaiting second header byte (MASK, 7-bit length).
@ WS_MASK2
Reading masking key byte 2.
@ WS_LEN64
Consuming 8-byte 64-bit length (always rejected).
@ WS_LEN16_HI
Reading extended 16-bit length, high byte.
@ WS_MASK0
Reading masking key byte 0.
@ WS_CLOSED
Connection closed; slot may be recycled.
@ WS_MASK1
Reading masking key byte 1.
@ WS_HEADER1
Awaiting first header byte (FIN, RSV, opcode).
void ws_feed_byte(WsConn *ws, uint8_t byte)
Feed one already-plaintext byte through the WS frame state machine.
void ws_close(WsConn *ws, WsCloseCode code)
Send a Close frame and mark the slot WsParseState::WS_CLOSED.
WsOpcode
WebSocket frame opcodes.
Definition websocket.h:72
@ WS_OP_CLOSE
Connection close.
@ WS_OP_PING
Ping (auto-ponged by the library).
@ WS_OP_CONTINUATION
Continuation frame (data-message fragment; reassembled into buf).
@ WS_OP_PONG
Pong (echoed ping; ignored by library).
@ WS_OP_TEXT
UTF-8 text payload.
@ WS_OP_BINARY
Binary payload.
void ws_parse(WsConn *ws)
Drain the ring buffer for slot_id and feed bytes to the WS parser.
WsConn * ws_find(uint8_t slot_id)
Find the WsConn for a given TCP slot, or nullptr if none.
Definition websocket.cpp:67
void ws_reset_frame(WsConn *ws)
Reset the frame parser back to WsParseState::WS_HEADER1, ready for the next frame.
void ws_set_frag_size(uint16_t bytes)
Set the outbound fragmentation size (RFC 6455 sec 5.4), in payload bytes; 0 = off.
bool ws_active(uint8_t ws_id)
True if ws_id is a valid, in-use WebSocket slot. Use this instead of reaching into ws_pool[ws_id]....
Definition websocket.cpp:40
WsConn * ws_alloc(uint8_t slot_id)
Allocate a WsConn slot and bind it to a TCP slot.
Definition websocket.cpp:50
WsConn ws_pool[MAX_WS_CONNS]
Pool of WebSocket connection state, one per MAX_WS_CONNS.
Definition websocket.cpp:29
void ws_init()
Initialize all WebSocket pool slots to inactive.
Definition websocket.cpp:31