DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
websocket_sse.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_sse.cpp
6 * @brief WebSocket (RFC 6455) and Server-Sent Events upgrade + public API for DetWebServer.
7 *
8 * Split out of dwserver.cpp (single-purpose server files). The WS handshake (Sec-WebSocket-Accept
9 * over SHA-1+base64, optional permessage-deflate), the SSE 200 upgrade, and the send/broadcast
10 * public API for both. The frame codecs live in the presentation layer (websocket/, sse/); this
11 * is the DetWebServer glue. The upgrade entry points are called by the route dispatcher in
12 * dwserver.cpp (declared in server/dwserver_internal.h). Behaviour is identical to the pre-split code.
13 */
14
15#include "dwserver.h"
16#include "network_drivers/transport/tcp.h" // conn_pool, det_conn_*, TcpConn/ConnState
18#if DETWS_ENABLE_WEBSOCKET
19#include "network_drivers/presentation/base64/base64.h" // base64_decode/encode
20#include "network_drivers/presentation/sha1/sha1.h" // sha1, SHA1_DIGEST_LEN
21#include "network_drivers/presentation/websocket/websocket.h" // ws_pool, WsConn, ws_alloc/send_frame/close
22#endif
23#if DETWS_ENABLE_SSE
24#include "network_drivers/presentation/sse/sse.h" // sse_pool, SseConn, sse_alloc/write
25#endif
26#include <stdio.h>
27#include <string.h>
28
29#if DETWS_ENABLE_WEBSOCKET
30// Magic GUID concatenated to the client key for the WS accept hash (RFC 6455 4.2.2).
31static const char WS_MAGIC[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
32#endif
33
34// ---------------------------------------------------------------------------
35// WebSocket handshake helpers
36// ---------------------------------------------------------------------------
37
38#if DETWS_ENABLE_WEBSOCKET
39/**
40 * @brief Compute the Sec-WebSocket-Accept value for the HTTP 101 response.
41 *
42 * Concatenates the client key with the RFC 6455 magic GUID, SHA-1 hashes
43 * the result, and base64-encodes the 20-byte digest into @p out.
44 * @p out must be at least 29 bytes (28 base64 chars + null terminator).
45 */
46static const size_t WS_MAX_KEY_LEN = 64;
47
48static bool ws_accept_key(const char *client_key, char *out)
49{
50 size_t key_len = strnlen(client_key, WS_MAX_KEY_LEN + 1);
51 if (key_len > WS_MAX_KEY_LEN)
52 {
53 out[0] = '\0';
54 return false;
55 }
56 // RFC 6455 4.2.1: the Sec-WebSocket-Key must base64-decode to exactly 16 bytes.
57 uint8_t raw[24];
58 if (base64_decode(client_key, raw, sizeof(raw)) != 16)
59 {
60 out[0] = '\0';
61 return false;
62 }
63 size_t magic_len = sizeof(WS_MAGIC) - 1;
64 char concat[WS_MAX_KEY_LEN + sizeof(WS_MAGIC)];
65 memcpy(concat, client_key, key_len);
66 memcpy(concat + key_len, WS_MAGIC, magic_len);
67
68 uint8_t digest[SHA1_DIGEST_LEN];
69 sha1((const uint8_t *)concat, key_len + magic_len, digest);
70 base64_encode(digest, SHA1_DIGEST_LEN, out);
71 return true;
72}
73
74/**
75 * @brief Send the HTTP 101 Switching Protocols handshake and upgrade the slot.
76 *
77 * Does NOT close the TCP connection -- that is intentional. The slot moves
78 * from HTTP parse ownership to WS frame parse ownership.
79 */
80/**
81 * @brief Send a 426 Upgrade Required for an unsupported Sec-WebSocket-Version.
82 *
83 * RFC 6455 §4.2.1: if the version is not 13 the server MUST respond with a
84 * 426 and include a Sec-WebSocket-Version header listing the versions it
85 * supports. Closes the connection afterward.
86 */
87void ws_send_version_required(uint8_t slot_id)
88{
89 if (!det_conn_active(slot_id))
90 {
91 http_reset(slot_id);
92 return;
93 }
94
95 static const char resp[] = "HTTP/1.1 426 Upgrade Required\r\n"
96 "Sec-WebSocket-Version: 13\r\n"
97 "Content-Length: 0\r\n"
98 "Connection: close\r\n\r\n";
99
100 det_conn_send(slot_id, resp, (u16_t)(sizeof(resp) - 1));
101 det_conn_flush(slot_id);
102 det_conn_begin_close(slot_id); // dwell in ConnState::CONN_CLOSING until the response drains
103
104 http_reset(slot_id);
105}
106
107bool ws_do_upgrade(uint8_t slot_id, HttpReq *req, WsConnectHandler on_connect)
108{
109 const char *client_key = http_get_header(req, "Sec-WebSocket-Key");
110 if (!client_key)
111 return false;
112
113 char accept[32];
114 if (!ws_accept_key(client_key, accept))
115 return false;
116
117 if (!det_conn_active(slot_id))
118 return false;
119
120 char hdr[WS_HDR_BUF_SIZE];
121 int hlen;
122#if DETWS_ENABLE_WS_DEFLATE
123 // Negotiate permessage-deflate (RFC 7692) if the client offered it. We force
124 // no_context_takeover in both directions so each message decompresses
125 // independently (the INFLATE window is the message buffer, not a kept window).
126 const char *ws_ext = http_get_header(req, "Sec-WebSocket-Extensions");
127 bool pmd = ws_ext && strstr(ws_ext, "permessage-deflate");
128 hlen = snprintf(hdr, sizeof(hdr),
129 "HTTP/1.1 101 Switching Protocols\r\n"
130 "Upgrade: websocket\r\n"
131 "Connection: Upgrade\r\n"
132 "Sec-WebSocket-Accept: %s\r\n"
133 "%s"
134 "\r\n",
135 accept,
136 pmd ? "Sec-WebSocket-Extensions: permessage-deflate; client_no_context_takeover; "
137 "server_no_context_takeover\r\n"
138 : "");
139#else
140 hlen = snprintf(hdr, sizeof(hdr),
141 "HTTP/1.1 101 Switching Protocols\r\n"
142 "Upgrade: websocket\r\n"
143 "Connection: Upgrade\r\n"
144 "Sec-WebSocket-Accept: %s\r\n\r\n",
145 accept);
146#endif
147
148 det_conn_send(slot_id, hdr, (u16_t)hlen);
149 det_conn_flush(slot_id);
150
151 // Reset HTTP parser but keep the TCP slot -- WS owns it now
152 http_reset(slot_id);
153
154 WsConn *ws = ws_alloc(slot_id);
155 if (!ws)
156 {
157 // No WS slot available -- abort the connection (transport owns the teardown)
158 det_conn_abort_slot(slot_id);
159 return false;
160 }
161
162#if DETWS_ENABLE_WS_DEFLATE
163 ws->pmd = pmd;
164#endif
165 if (on_connect)
166 on_connect(ws->ws_id);
167
168 return true;
169}
170#endif // DETWS_ENABLE_WEBSOCKET
171
172// ---------------------------------------------------------------------------
173// SSE upgrade helper
174// ---------------------------------------------------------------------------
175
176#if DETWS_ENABLE_SSE
177/**
178 * @brief Send the HTTP 200 + SSE headers and promote the slot to SSE mode.
179 */
180bool sse_do_upgrade(uint8_t slot_id, HttpReq *req, SseConnectHandler on_connect)
181{
182 if (!det_conn_active(slot_id))
183 return false;
184
185 static const char SSE_HDR[] = "HTTP/1.1 200 OK\r\n"
186 "Content-Type: text/event-stream\r\n"
187 "Cache-Control: no-cache\r\n"
188 "Connection: keep-alive\r\n\r\n";
189
190 det_conn_send(slot_id, SSE_HDR, (u16_t)(sizeof(SSE_HDR) - 1));
191 det_conn_flush(slot_id);
192
193 // Copy the path BEFORE resetting the parser: http_reset() zeroes the whole
194 // HttpReq (including req->path), so a pointer into it would dangle. The saved
195 // path is what sse_broadcast() matches against.
196 char path[MAX_PATH_LEN];
197 strncpy(path, req->path, sizeof(path) - 1);
198 path[sizeof(path) - 1] = '\0';
199 http_reset(slot_id);
200
201 SseConn *sse = sse_alloc(slot_id, path);
202 if (!sse)
203 {
204 det_conn_abort_slot(slot_id); // transport owns detach + reset + RST
205 return false;
206 }
207
208 if (on_connect)
209 on_connect(sse->sse_id);
210
211 return true;
212}
213#endif // DETWS_ENABLE_SSE
214
215// ---------------------------------------------------------------------------
216// WebSocket public API
217// ---------------------------------------------------------------------------
218
219#if DETWS_ENABLE_WEBSOCKET
220void DetWebServer::ws_send_text(uint8_t ws_id, const char *text)
221{
222 if (ws_id >= MAX_WS_CONNS || !ws_pool[ws_id].active)
223 return;
224 WsConn *ws = &ws_pool[ws_id];
226 return;
227 uint16_t len = (uint16_t)strnlen(text, 0xFFFF);
228 if (ws_send_frame(ws, WsOpcode::WS_OP_TEXT, (const uint8_t *)text, len))
229 {
230 if (det_conn_active(ws->slot_id))
232 }
233}
234
235void DetWebServer::ws_send_binary(uint8_t ws_id, const uint8_t *data, uint16_t len)
236{
237 if (ws_id >= MAX_WS_CONNS || !ws_pool[ws_id].active)
238 return;
239 WsConn *ws = &ws_pool[ws_id];
241 return;
242 if (ws_send_frame(ws, WsOpcode::WS_OP_BINARY, data, len))
243 {
244 if (det_conn_active(ws->slot_id))
246 }
247}
248
249void DetWebServer::ws_disconnect(uint8_t ws_id)
250{
251 if (ws_id >= MAX_WS_CONNS || !ws_pool[ws_id].active)
252 return;
253 WsConn *ws = &ws_pool[ws_id];
255 if (det_conn_active(ws->slot_id))
257 // handle() detects WsParseState::WS_CLOSED next tick and fires ws_close callback
258}
259#endif // DETWS_ENABLE_WEBSOCKET
260
261// ---------------------------------------------------------------------------
262// Server-Sent Events public API
263// ---------------------------------------------------------------------------
264
265#if DETWS_ENABLE_SSE
266void DetWebServer::sse_send(uint8_t sse_id, const char *data, const char *event, const char *id)
267{
268 if (sse_id >= MAX_SSE_CONNS || !sse_pool[sse_id].active)
269 return;
270 SseConn *sse = &sse_pool[sse_id];
271 if (sse_write(sse, data, event, id))
272 {
273 if (det_conn_active(sse->slot_id))
275 }
276}
277
278void DetWebServer::sse_broadcast(const char *path, const char *data, const char *event, const char *id)
279{
280 for (int i = 0; i < MAX_SSE_CONNS; i++)
281 {
282 if (!sse_pool[i].active)
283 continue;
284 if (strcmp(sse_pool[i].path, path) != 0)
285 continue;
286 SseConn *sse = &sse_pool[i];
287 if (sse_write(sse, data, event, id))
288 {
289 if (det_conn_active(sse->slot_id))
291 }
292 }
293}
294#endif // DETWS_ENABLE_SSE
#define WS_HDR_BUF_SIZE
Stack buffer for the HTTP 101 Switching Protocols response sent during the WebSocket handshake.
#define MAX_PATH_LEN
Maximum URL path length (including leading /).
#define MAX_SSE_CONNS
Maximum simultaneous SSE connections.
#define MAX_WS_CONNS
Maximum simultaneous WebSocket connections.
void base64_encode(const uint8_t *src, size_t src_len, char *dst)
Encode src_len bytes of src as Base64.
Definition base64.cpp:26
size_t base64_decode(const char *src, uint8_t *dst, size_t dst_cap)
Decode a null-terminated Base64 string.
Definition base64.cpp:65
Base64 encoder/decoder.
Layer 7 (Application) - public HTTP routing API.
Library-private declarations shared between dwserver.cpp and the src/server/*.cpp request-handler tra...
const char * http_get_header(const HttpReq *req, const char *key)
Look up a header value by name (case-insensitive).
void http_reset(uint8_t slot_id)
Reset the HTTP parser for a connection slot.
void sha1(const uint8_t *data, size_t len, uint8_t digest[SHA1_DIGEST_LEN])
Compute a SHA-1 digest over an arbitrary byte buffer.
Definition sha1.cpp:24
Software SHA-1 implementation - no platform dependencies.
#define SHA1_DIGEST_LEN
SHA-1 digest length in bytes.
Definition sha1.h:22
bool sse_write(SseConn *sse, const char *data, const char *event, const char *id)
Write one SSE event record to a client.
Definition sse.cpp:88
SseConn sse_pool[MAX_SSE_CONNS]
Pool of SSE connection state, one per MAX_SSE_CONNS.
Definition sse.cpp:14
SseConn * sse_alloc(uint8_t slot_id, const char *path)
Allocate an SseConn and bind it to a TCP slot.
Definition sse.cpp:25
Layer 6 (Presentation) – Server-Sent Events connection pool.
Fully-parsed HTTP/1.1 request.
char path[MAX_PATH_LEN]
URL path, null-terminated; no query string.
SSE connection state stored in sse_pool[].
Definition sse.h:46
uint8_t sse_id
Index into sse_pool[] (set at init).
Definition sse.h:47
uint8_t slot_id
Owning TCP slot in conn_pool[].
Definition sse.h:48
WebSocket connection state stored in ws_pool[].
Definition websocket.h:125
uint8_t ws_id
Index into ws_pool[] (set at init).
Definition websocket.h:126
uint8_t slot_id
Owning TCP slot in conn_pool[].
Definition websocket.h:127
WsParseState parse_state
Current frame parser state.
Definition websocket.h:130
bool det_conn_send(uint8_t slot, const void *data, u16_t len)
Send len bytes on connection slot (copies data; TLS-aware).
Definition tcp.cpp:362
void det_conn_flush(uint8_t slot)
Flush queued bytes / finish the send on slot (TLS-aware).
Definition tcp.cpp:413
void det_conn_begin_close(uint8_t slot_id)
Begin a graceful close that dwells in ConnState::CONN_CLOSING until the peer ACKs.
Definition tcp.cpp:572
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
Layer 4 (Transport) - TCP connection pool, ring buffers, and lwIP integration.
bool ws_send_frame(WsConn *ws, WsOpcode opcode, const uint8_t *payload, uint16_t len)
Send a WebSocket frame to the client.
void ws_close(WsConn *ws, WsCloseCode code)
Send a Close frame and mark the slot WsParseState::WS_CLOSED.
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
Layer 6 (Presentation) – WebSocket frame parser and connection pool.
@ WS_CLOSE_NORMAL
Normal closure.
@ WS_ERROR
Protocol error; close frame has been queued.
@ WS_CLOSED
Connection closed; slot may be recycled.
@ WS_OP_TEXT
UTF-8 text payload.
@ WS_OP_BINARY
Binary payload.