DeterministicESPAsyncWebServer 1.2.0
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
websocket.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 websocket.cpp
6 * @brief WebSocket frame parser and connection pool implementation.
7 *
8 * Handles RFC 6455 framing. Control frames (ping/pong/close) are handled
9 * automatically here; data frames (text/binary) are surfaced to the
10 * application layer via WS_FRAME_READY.
11 *
12 * **Automatic control frame handling**
13 * - Ping -> sends Pong with the same payload immediately.
14 * - Close -> sends echoed Close frame, marks slot WS_CLOSED.
15 * - Pong -> silently discarded (keepalive response, no action needed).
16 */
17
18#include "websocket.h"
19#include "lwip/tcp.h"
20#include <string.h>
21
23
24void ws_init()
25{
26 for (int i = 0; i < MAX_WS_CONNS; i++)
27 {
28 ws_pool[i] = {};
29 ws_pool[i].ws_id = (uint8_t)i;
30 }
31}
32
33WsConn *ws_alloc(uint8_t slot_id)
34{
35 for (int i = 0; i < MAX_WS_CONNS; i++)
36 {
37 if (!ws_pool[i].active)
38 {
39 ws_pool[i] = {};
40 ws_pool[i].ws_id = (uint8_t)i;
41 ws_pool[i].slot_id = slot_id;
42 ws_pool[i].active = true;
44 return &ws_pool[i];
45 }
46 }
47 return nullptr;
48}
49
50WsConn *ws_find(uint8_t slot_id)
51{
52 for (int i = 0; i < MAX_WS_CONNS; i++)
53 {
54 if (ws_pool[i].active && ws_pool[i].slot_id == slot_id)
55 return &ws_pool[i];
56 }
57 return nullptr;
58}
59
60void ws_free(uint8_t slot_id)
61{
62 for (int i = 0; i < MAX_WS_CONNS; i++)
63 {
64 if (ws_pool[i].active && ws_pool[i].slot_id == slot_id)
65 {
66 ws_pool[i] = {};
67 ws_pool[i].ws_id = (uint8_t)i;
68 return;
69 }
70 }
71}
72
74{
76 ws->opcode = WS_OP_TEXT;
77 ws->fin = false;
78 ws->masked = false;
79 ws->payload_len = 0;
80 ws->payload_idx = 0;
81 ws->len64_count = 0;
82 ws->mask_key[0] = ws->mask_key[1] = ws->mask_key[2] = ws->mask_key[3] = 0;
83 ws->buf[0] = '\0';
84}
85
86// ---------------------------------------------------------------------------
87// Frame send helpers
88// ---------------------------------------------------------------------------
89
90bool ws_send_frame(WsConn *ws, WsOpcode opcode,
91 const uint8_t *payload, uint16_t len)
92{
93 TcpConn *conn = &conn_pool[ws->slot_id];
94 if (conn->state != CONN_ACTIVE || !conn->pcb)
95 return false;
96
97 // Server-to-client frames are never masked (RFC 6455 §5.1)
98 uint8_t header[4];
99 uint8_t hlen;
100
101 header[0] = 0x80 | (uint8_t)opcode; // FIN=1
102
103 if (len <= 125)
104 {
105 header[1] = (uint8_t)len;
106 hlen = 2;
107 }
108 else
109 {
110 header[1] = 126;
111 header[2] = (uint8_t)(len >> 8);
112 header[3] = (uint8_t)(len);
113 hlen = 4;
114 }
115
116 tcp_write(conn->pcb, header, hlen, TCP_WRITE_FLAG_COPY);
117 if (len > 0 && payload)
118 tcp_write(conn->pcb, payload, len, TCP_WRITE_FLAG_COPY);
119
120 return true;
121}
122
124{
125 // Send Close frame with 2-byte status code payload
126 uint8_t payload[2] = { (uint8_t)((uint16_t)code >> 8), (uint8_t)code };
127 ws_send_frame(ws, WS_OP_CLOSE, payload, 2);
128
129 TcpConn *conn = &conn_pool[ws->slot_id];
130 if (conn->pcb)
131 tcp_output(conn->pcb);
132
134}
135
136// ---------------------------------------------------------------------------
137// Frame parser
138// ---------------------------------------------------------------------------
139
141{
142 TcpConn *conn = &conn_pool[ws->slot_id];
143 if (conn->state != CONN_ACTIVE)
144 return;
145
146 while (conn->rx_tail != conn->rx_head)
147 {
148 // Stop if we hit a terminal state
149 if (ws->parse_state == WS_FRAME_READY ||
150 ws->parse_state == WS_CLOSED ||
151 ws->parse_state == WS_ERROR)
152 return;
153
154 uint8_t byte = conn->rx_buffer[conn->rx_tail];
155 conn->rx_tail = (conn->rx_tail + 1) % RX_BUF_SIZE;
156
157 switch (ws->parse_state)
158 {
159 case WS_HEADER1:
160 ws->fin = (byte & 0x80) != 0;
161 // RSV1-3 must be zero (no extensions)
162 if (byte & 0x70)
163 {
165 ws->parse_state = WS_ERROR;
166 return;
167 }
168 ws->opcode = (WsOpcode)(byte & 0x0F);
170 break;
171
172 case WS_HEADER2:
173 ws->masked = (byte & 0x80) != 0;
174 {
175 uint8_t len7 = byte & 0x7F;
176 if (len7 <= 125)
177 {
178 ws->payload_len = len7;
180 if (ws->payload_len == 0)
182 }
183 else if (len7 == 126)
184 {
185 ws->payload_len = 0;
187 }
188 else
189 {
190 // 64-bit length -- always too large
191 ws->len64_count = 0;
192 ws->parse_state = WS_LEN64;
193 }
194 }
195 break;
196
197 case WS_LEN16_HI:
198 ws->payload_len = (uint32_t)byte << 8;
200 break;
201
202 case WS_LEN16_LO:
203 ws->payload_len |= byte;
204 if (ws->payload_len > WS_FRAME_SIZE)
205 {
207 ws->parse_state = WS_ERROR;
208 return;
209 }
211 if (ws->payload_len == 0)
213 break;
214
215 case WS_LEN64:
216 // Consume all 8 bytes then reject
217 if (++ws->len64_count == 8)
218 {
220 ws->parse_state = WS_ERROR;
221 return;
222 }
223 break;
224
225 case WS_MASK0: ws->mask_key[0] = byte; ws->parse_state = WS_MASK1; break;
226 case WS_MASK1: ws->mask_key[1] = byte; ws->parse_state = WS_MASK2; break;
227 case WS_MASK2: ws->mask_key[2] = byte; ws->parse_state = WS_MASK3; break;
228 case WS_MASK3:
229 ws->mask_key[3] = byte;
231 break;
232
233 case WS_PAYLOAD:
234 {
235 uint8_t unmasked = byte ^ ws->mask_key[ws->payload_idx % 4];
236 if (ws->payload_idx < WS_FRAME_SIZE)
237 ws->buf[ws->payload_idx] = unmasked;
238 ws->payload_idx++;
239
240 if (ws->payload_idx >= ws->payload_len)
241 {
243 ? ws->payload_idx : WS_FRAME_SIZE] = '\0';
244
245 // Handle control frames automatically
246 if (ws->opcode == WS_OP_PING)
247 {
249 ws->buf, (uint16_t)ws->payload_idx);
250 if (conn->pcb) tcp_output(conn->pcb);
251 ws_reset_frame(ws);
252 }
253 else if (ws->opcode == WS_OP_CLOSE)
254 {
256 }
257 else if (ws->opcode == WS_OP_PONG)
258 {
259 ws_reset_frame(ws);
260 }
261 else if (ws->opcode == WS_OP_CONTINUATION || !ws->fin)
262 {
263 // Fragmentation not supported
265 ws->parse_state = WS_ERROR;
266 }
267 else
268 {
270 }
271 }
272 break;
273 }
274
275 default:
276 break;
277 }
278 }
279}
#define RX_BUF_SIZE
Ring-buffer capacity in bytes per connection slot.
#define WS_FRAME_SIZE
Maximum WebSocket frame payload in bytes.
#define MAX_WS_CONNS
Maximum simultaneous WebSocket connections.
A single TCP connection context.
Definition transport.h:69
volatile ConnState state
Lifecycle state; volatile for inter-task visibility.
Definition transport.h:71
struct tcp_pcb * pcb
lwIP PCB; null when slot is free.
Definition transport.h:72
volatile size_t rx_tail
Consumer read index (main-loop context).
Definition transport.h:77
uint8_t rx_buffer[RX_BUF_SIZE]
Ring buffer storage.
Definition transport.h:75
volatile size_t rx_head
Producer write index (lwIP context).
Definition transport.h:76
WebSocket connection state stored in ws_pool[].
Definition websocket.h:119
bool active
True when this entry is in use.
Definition websocket.h:122
uint8_t len64_count
Bytes consumed from 64-bit length.
Definition websocket.h:132
uint8_t mask_key[4]
Client masking key.
Definition websocket.h:129
uint32_t payload_len
Expected payload byte count.
Definition websocket.h:130
uint8_t ws_id
Index into ws_pool[] (set at init).
Definition websocket.h:120
bool fin
FIN bit of the frame being parsed.
Definition websocket.h:126
bool masked
True if client sent a masking key.
Definition websocket.h:127
uint8_t slot_id
Owning TCP slot in conn_pool[].
Definition websocket.h:121
uint32_t payload_idx
Bytes received so far.
Definition websocket.h:131
WsParseState parse_state
Current frame parser state.
Definition websocket.h:124
WsOpcode opcode
Opcode of the frame being parsed.
Definition websocket.h:125
uint8_t buf[WS_FRAME_SIZE+1]
Unmasked payload, null-terminated.
Definition websocket.h:133
TcpConn conn_pool[MAX_CONNS]
Static pool of connection contexts. Defined in transport.cpp.
Definition transport.cpp:25
@ CONN_ACTIVE
Live connection; PCB is valid.
Definition transport.h:57
void ws_free(uint8_t slot_id)
Free the WsConn associated with a TCP slot.
Definition websocket.cpp:60
bool ws_send_frame(WsConn *ws, WsOpcode opcode, const uint8_t *payload, uint16_t len)
Send a WebSocket frame to the client.
Definition websocket.cpp:90
void ws_close(WsConn *ws, WsCloseCode code)
Send a Close frame and mark the slot WS_CLOSED.
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:50
void ws_reset_frame(WsConn *ws)
Reset the frame parser back to WS_HEADER1, ready for the next frame.
Definition websocket.cpp:73
WsConn * ws_alloc(uint8_t slot_id)
Allocate a WsConn slot and bind it to a TCP slot.
Definition websocket.cpp:33
WsConn ws_pool[MAX_WS_CONNS]
Pool of WebSocket connection state, one per MAX_WS_CONNS.
Definition websocket.cpp:22
void ws_init()
Initialise all WebSocket pool slots to inactive.
Definition websocket.cpp:24
Layer 6 (Presentation) – WebSocket frame parser and connection pool.
@ WS_LEN16_LO
Reading extended 16-bit length, low byte.
Definition websocket.h:96
@ WS_HEADER1
Awaiting first header byte (FIN, RSV, opcode).
Definition websocket.h:93
@ WS_MASK0
Reading masking key byte 0.
Definition websocket.h:98
@ WS_FRAME_READY
Complete frame ready for dispatch.
Definition websocket.h:103
@ WS_HEADER2
Awaiting second header byte (MASK, 7-bit length).
Definition websocket.h:94
@ WS_MASK2
Reading masking key byte 2.
Definition websocket.h:100
@ WS_LEN16_HI
Reading extended 16-bit length, high byte.
Definition websocket.h:95
@ WS_ERROR
Protocol error; close frame has been queued.
Definition websocket.h:105
@ WS_CLOSED
Connection closed; slot may be recycled.
Definition websocket.h:104
@ WS_PAYLOAD
Accumulating payload bytes.
Definition websocket.h:102
@ WS_MASK1
Reading masking key byte 1.
Definition websocket.h:99
@ WS_MASK3
Reading masking key byte 3.
Definition websocket.h:101
@ WS_LEN64
Consuming 8-byte 64-bit length (always rejected).
Definition websocket.h:97
WsOpcode
WebSocket frame opcodes.
Definition websocket.h:67
@ WS_OP_TEXT
UTF-8 text payload.
Definition websocket.h:69
@ WS_OP_CLOSE
Connection close.
Definition websocket.h:71
@ WS_OP_CONTINUATION
Continuation frame (fragmentation – rejected).
Definition websocket.h:68
@ WS_OP_PONG
Pong (echoed ping; ignored by library).
Definition websocket.h:73
@ WS_OP_PING
Ping (auto-ponged by the library).
Definition websocket.h:72
WsCloseCode
WebSocket close status codes (RFC 6455 §7.4.1).
Definition websocket.h:78
@ WS_CLOSE_NORMAL
Normal closure.
Definition websocket.h:79
@ WS_CLOSE_PROTOCOL
Protocol error.
Definition websocket.h:81
@ WS_CLOSE_TOO_BIG
Payload too large for WS_FRAME_SIZE.
Definition websocket.h:83
@ WS_CLOSE_UNSUPPORTED
Unsupported data (e.g. fragmented message).
Definition websocket.h:82