cwist
Modern C web framework with libttak runtime
Loading...
Searching...
No Matches
websocket.c
Go to the documentation of this file.
1#ifndef _GNU_SOURCE
2#define _GNU_SOURCE
3#endif
8#include "ws_utils.h"
9
10#include <ctype.h>
11#include <stdio.h>
12#include <stdlib.h>
13#include <string.h>
14#include <strings.h>
15#include <unistd.h>
16#include <arpa/inet.h>
17#include <errno.h>
18
24#define WS_GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
25
26/* Per-frame payload size cap (16 MiB). RFC 6455 allows up to 2^63 bytes per
27 * frame, but allocating that blindly gives any connected client a trivial OOM
28 * DoS vector. Applications that need larger transfers should use WebSocket
29 * message fragmentation (RFC 6455 section 5.4) with frames within this cap,
30 * bounded in total by CWIST_WS_MAX_MESSAGE_BYTES below. */
31#define CWIST_WS_MAX_PAYLOAD_BYTES ((uint64_t)(16u * 1024u * 1024u))
32
33/* Reassembled-message size cap (64 MiB). Each individual fragment is bounded
34 * by CWIST_WS_MAX_PAYLOAD_BYTES, but a client can send many frames to grow the
35 * reassembly buffer without limit. 64 MiB is a generous ceiling that stops
36 * multi-fragment OOM attacks while accommodating large real-world messages. */
37#define CWIST_WS_MAX_MESSAGE_BYTES ((size_t)(64u * 1024u * 1024u))
38
45static char *ws_strcasestr(const char *haystack, const char *needle) {
46 if (!*needle) return (char *)haystack;
47 for (; *haystack; haystack++) {
48 const char *h = haystack;
49 const char *n = needle;
50 while (*h && *n && tolower((unsigned char)*h) == tolower((unsigned char)*n)) {
51 h++;
52 n++;
53 }
54 if (!*n) return (char *)haystack;
55 }
56 return NULL;
57}
58
72 if (!req || !res) return false;
73
74 // Validate Headers
75 char *connection = cwist_http_header_get(req->headers, "Connection");
76 char *upgrade = cwist_http_header_get(req->headers, "Upgrade");
77 char *key = cwist_http_header_get(req->headers, "Sec-WebSocket-Key");
78
79 if (!connection || !upgrade || !key) return false;
80 if (ws_strcasestr(connection, "Upgrade") == NULL) return false;
81 if (strcasecmp(upgrade, "websocket") != 0) return false;
82
83 /* RFC 6455 section 4.2.1: the client MUST include Sec-WebSocket-Version: 13. */
84 char *ws_version = cwist_http_header_get(req->headers, "Sec-WebSocket-Version");
85 if (!ws_version || strcmp(ws_version, "13") != 0) return false;
86
87 // Handshake Key Generation
88 char combined_key[512];
89 snprintf(combined_key, sizeof(combined_key), "%s%s", key, WS_GUID);
90
91 uint8_t hash[20];
93
94 size_t accept_len;
96 if (!accept_key) return false;
97
98 res->status_code = 101;
99 cwist_sstring_assign(res->status_text, "Switching Protocols");
100 cwist_http_header_add(&res->headers, "Upgrade", "websocket");
101 cwist_http_header_add(&res->headers, "Connection", "Upgrade");
102 cwist_http_header_add(&res->headers, "Sec-WebSocket-Accept", accept_key);
103
105 return true;
106}
107
120 if (!req || client_fd < 0) return NULL;
121
123 if (!res) return NULL;
124
125 if (!cwist_websocket_upgrade_response(req, res)) {
127 return NULL;
128 }
129
131
133
134 if (err.error.err_i16 != 0) return NULL;
135
136 req->upgraded = true;
137
139 if (!ws) return NULL;
140 ws->fd = client_fd;
141 ws->is_closed = false;
142 ws->frag_buf = NULL;
143 ws->frag_len = 0;
144 ws->frag_cap = 0;
146 return ws;
147}
148
156static ssize_t read_exact(int fd, void *buf, size_t len) {
157 size_t total = 0;
158 while (total < len) {
159 ssize_t n = recv(fd, (uint8_t *)buf + total, len - total, 0);
160 if (n <= 0) return -1;
161 total += n;
162 }
163 return total;
164}
165
181static bool ws_frag_append(cwist_websocket *ws, const uint8_t *src, size_t len) {
182 if (len == 0) return true;
183 /* Reject a reassembled message that exceeds the total-message cap, even
184 * though each individual frame already passed the per-frame check. */
185 if (ws->frag_len + len > CWIST_WS_MAX_MESSAGE_BYTES) return false;
186 size_t need = ws->frag_len + len + 1; /* +1 for null terminator */
187 if (need > ws->frag_cap) {
188 size_t new_cap = ws->frag_cap ? ws->frag_cap * 2 : 4096;
189 if (new_cap < need) new_cap = need;
191 if (!nb) return false;
192 if (ws->frag_len) memcpy(nb, ws->frag_buf, ws->frag_len);
193 cwist_free(ws->frag_buf);
194 ws->frag_buf = nb;
195 ws->frag_cap = new_cap;
196 }
197 memcpy(ws->frag_buf + ws->frag_len, src, len);
198 ws->frag_len += len;
199 return true;
200}
201
218 if (!ws || ws->is_closed) return NULL;
219
220 while (1) {
221 uint8_t head[2];
222 if (read_exact(ws->fd, head, 2) < 0) return NULL;
223
224 bool fin = (head[0] & 0x80) != 0;
225 cwist_ws_opcode_t opcode = head[0] & 0x0F;
226 bool masked = (head[1] & 0x80) != 0;
227 uint64_t payload_len = head[1] & 0x7F;
228
229 /* RFC 6455 section 5.2: RSV1/RSV2/RSV3 MUST be 0 unless an extension
230 * defines their meaning. CWIST has no such extension. */
231 if (head[0] & 0x70) return NULL;
232
233 /* Client-to-server frames must be masked per RFC 6455 section 5.3. */
234 if (!masked) return NULL;
235
236 if (payload_len == 126) {
238 if (read_exact(ws->fd, &len16, 2) < 0) return NULL;
239 payload_len = ntohs(len16);
240 } else if (payload_len == 127) {
242 if (read_exact(ws->fd, &len64, 8) < 0) return NULL;
243 uint8_t *p = (uint8_t *)&len64;
244 payload_len = ((uint64_t)p[0] << 56) | ((uint64_t)p[1] << 48) | ((uint64_t)p[2] << 40) |
245 ((uint64_t)p[3] << 32) | ((uint64_t)p[4] << 24) | ((uint64_t)p[5] << 16) |
246 ((uint64_t)p[6] << 8) | ((uint64_t)p[7]);
247 }
248
249 /* Reject frames that would force a huge allocation before any payload
250 * is read. A single malicious frame with length=4 GiB costs only
251 * ~14 bytes on the wire but would OOM-kill the server otherwise. */
252 if (payload_len > CWIST_WS_MAX_PAYLOAD_BYTES) return NULL;
253
254 /* RFC 6455 section 5.5: control frames MUST NOT carry more than 125 bytes. */
255 bool is_control_early = (opcode >= 0x8);
256 if (is_control_early && payload_len > 125) return NULL;
257
259 if (read_exact(ws->fd, masking_key, 4) < 0) return NULL;
260
261 uint8_t *payload = NULL;
262 if (payload_len > 0) {
263 payload = (uint8_t *)cwist_alloc(payload_len + 1);
264 if (!payload) return NULL;
265 if (read_exact(ws->fd, payload, payload_len) < 0) {
266 cwist_free(payload);
267 return NULL;
268 }
269 for (uint64_t i = 0; i < payload_len; i++) payload[i] ^= masking_key[i % 4];
270 payload[payload_len] = '\0';
271 }
272
273 /* --- Fragmented-message reassembly (RFC 6455 section 5.4) -------------- */
274
275 /* Control frames (CLOSE/PING/PONG) are never fragmented: deliver
276 * immediately, even when a data message fragmentation is underway. */
277 bool is_control = (opcode >= 0x8);
278
279 if (!is_control && !fin) {
280 /* Fragment with FIN=0: start or continue reassembly. */
281 if (opcode == CWIST_WS_FRAME_CONTINUATION) {
282 /* RFC 6455 section 5.4: CONTINUATION is only valid when a fragmented
283 * message is already in progress. */
284 if (ws->frag_len == 0) {
285 cwist_free(payload);
286 return NULL;
287 }
288 } else {
289 /* First fragment: save opcode (text vs binary). */
290 ws->frag_opcode = opcode;
291 }
292 if (!ws_frag_append(ws, payload, (size_t)payload_len)) {
293 cwist_free(payload);
294 return NULL;
295 }
296 cwist_free(payload);
297 continue; /* read the next frame */
298 }
299
300 /* RFC 6455 section 5.4: a CONTINUATION frame is only valid inside an
301 * active fragmented-message sequence. The FIN=0 case is rejected in
302 * the reassembly branch above; without this check an orphan
303 * CONTINUATION with FIN=1 would fall through and be delivered as a
304 * complete message. */
305 if (!is_control && opcode == CWIST_WS_FRAME_CONTINUATION && ws->frag_len == 0) {
306 cwist_free(payload);
307 return NULL;
308 }
309
310 /* FIN=1 data frame: may be the last fragment of a multi-frame message. */
311 if (!is_control && ws->frag_len > 0) {
312 /* Final CONTINUATION frame: flush the reassembly buffer. */
313 if (!ws_frag_append(ws, payload, (size_t)payload_len)) {
314 cwist_free(payload);
315 cwist_free(ws->frag_buf);
316 ws->frag_buf = NULL;
317 ws->frag_len = ws->frag_cap = 0;
318 return NULL;
319 }
320 cwist_free(payload);
321
322 /* Hand ownership of frag_buf to the returned frame. */
323 payload = ws->frag_buf;
324 payload_len = ws->frag_len;
325 opcode = ws->frag_opcode;
326
327 ws->frag_buf = NULL;
328 ws->frag_len = ws->frag_cap = 0;
329 }
330
331 /* --- Deliver the complete frame ---------------------------------- */
332
333 if (opcode == CWIST_WS_FRAME_CLOSE) {
334 /* RFC 6455 section 5.5.1: a CLOSE body, if present, MUST be >= 2 bytes. */
335 if (payload_len == 1) {
336 cwist_free(payload);
337 return NULL;
338 }
339 /* Echo the status code (first 2 bytes) back before closing. */
340 cwist_websocket_send(ws, CWIST_WS_FRAME_CLOSE, payload, (payload_len >= 2) ? 2 : 0);
341 ws->is_closed = true;
342 } else if (opcode == CWIST_WS_FRAME_PING) {
343 /* RFC 6455 section 5.5.3: respond to every PING with a PONG carrying
344 * the same payload (up to 125 bytes per section 5.5). */
345 cwist_websocket_send(ws, CWIST_WS_FRAME_PONG, payload, payload_len);
346 }
347
349 if (!frame) {
350 cwist_free(payload);
351 return NULL;
352 }
353 frame->fin = fin;
354 frame->opcode = opcode;
355 frame->payload = payload;
356 frame->payload_len = (size_t)payload_len;
357 return frame;
358 }
359}
360
372static int ws_send_all(int fd, const uint8_t *buf, size_t len) {
373 size_t off = 0;
374 while (off < len) {
375#ifdef MSG_NOSIGNAL
376 ssize_t n = send(fd, buf + off, len - off, MSG_NOSIGNAL);
377#else
378 ssize_t n = send(fd, buf + off, len - off, 0);
379#endif
380 if (n < 0) {
381 if (errno == EINTR) continue;
382 return -1;
383 }
384 if (n == 0) return -1;
385 off += (size_t)n;
386 }
387 return 0;
388}
389
399 size_t len) {
400 if (!ws || ws->is_closed || (!data && len > 0)) return -1;
401
402 uint8_t head[10]; // Max header size (2 + 8)
403 size_t head_len = 2;
404
405 head[0] = 0x80 | (opcode & 0x0F); // FIN=1
406
407 if (len < 126) {
408 head[1] = len;
409 } else if (len < 65536) {
410 head[1] = 126;
411 head[2] = (len >> 8) & 0xFF;
412 head[3] = len & 0xFF;
413 head_len += 2;
414 } else {
415 head[1] = 127;
416 for (int i = 0; i < 8; i++) {
417 head[2 + i] = (len >> ((7 - i) * 8)) & 0xFF;
418 }
419 head_len += 8;
420 }
421
422 // Server does not mask frames
423
424 if (ws_send_all(ws->fd, head, head_len) != 0) return -1;
425 if (len > 0) {
426 if (ws_send_all(ws->fd, data, len) != 0) return -1;
427 }
428
429 return 0;
430}
431
437 if (frame) {
438 if (frame->payload) cwist_free(frame->payload);
440 }
441}
442
448 if (!ws || ws->is_closed || !data || len == 0 || chunk_payload_size == 0) return -1;
449
451 if (!cwist_seq_split(data, len, chunk_payload_size, &msg)) return -1;
452
453 int rc = 0;
454 for (size_t i = 0; i < msg.count && rc == 0; i++) {
455 if (cwist_websocket_send(ws, CWIST_WS_FRAME_BINARY, msg.chunks[i], msg.chunk_lens[i]) !=
456 0) {
457 rc = -1;
458 }
459 }
460
462 return rc;
463}
464
469 if (!out_len) return NULL;
470 *out_len = 0;
471 if (!ws || ws->is_closed) return NULL;
472
474 if (!a) return NULL;
475
476 uint8_t *result = NULL;
479 if (!frame) break;
480
481 if (frame->opcode == CWIST_WS_FRAME_BINARY && frame->payload && frame->payload_len > 0) {
483 if (cwist_seq_chunk_parse(frame->payload, frame->payload_len, &chunk)) {
485 }
486 }
487
488 bool is_close = (frame->opcode == CWIST_WS_FRAME_CLOSE);
490 if (is_close) break;
491 }
492
493 const uint8_t *assembled = NULL;
494 size_t assembled_len = 0;
496 result = (uint8_t *)cwist_alloc(assembled_len + 1);
497 if (result) {
499 result[assembled_len] = '\0';
500 *out_len = assembled_len;
501 }
502 }
503
505 return result;
506}
507
513 if (ws && !ws->is_closed) {
515 ws->is_closed = true;
516 }
517}
518
524 if (ws) {
525 cwist_free(ws->frag_buf);
526 cwist_free(ws);
527 }
528}
Memory allocation wrappers backed by libttak.
void cwist_free(void *ptr)
Free memory obtained via cwist_alloc/cwist_strdup/etc.
Definition alloc.c:52
void * cwist_alloc(size_t size)
Allocate zeroed memory tracked by libttak.
Definition alloc.c:17
cwist_error_t cwist_sstring_assign(cwist_sstring *str, const char *data)
Assign a C string.
Definition sstring.c:504
cwist_error_t cwist_http_send_response(int client_fd, cwist_http_response *res)
Send a response over a socket.
Definition http.c:2878
char * cwist_http_header_get(cwist_http_header_node *head, const char *key)
Find a header value by key.
Definition http.c:1880
cwist_error_t cwist_http_header_add(cwist_http_header_node **head, const char *key, const char *value)
Add a header to the list.
Definition http.c:1861
void cwist_http_response_destroy(cwist_http_response *res)
Destroy an HTTP response object.
Definition http.c:2315
cwist_http_response * cwist_http_response_create(void)
Create a new HTTP response object.
Definition http.c:2291
HTTP Protocol Definitions and Helpers.
WebSocket upgrade and frame handling.
cwist_ws_opcode_t
Definition websocket.h:13
@ CWIST_WS_FRAME_PING
Definition websocket.h:18
@ CWIST_WS_FRAME_BINARY
Definition websocket.h:16
@ CWIST_WS_FRAME_CONTINUATION
Definition websocket.h:14
@ CWIST_WS_FRAME_CLOSE
Definition websocket.h:17
@ CWIST_WS_FRAME_PONG
Definition websocket.h:19
size_t head
Definition otel.c:57
size_t cap
Definition otel.c:56
TCP-like sequenced message fragmentation and reassembly.
bool cwist_seq_assembler_is_complete(const cwist_seq_assembler_t *a)
Return true when every expected chunk has been received.
Definition seq.c:343
bool cwist_seq_chunk_parse(const uint8_t *data, size_t len, cwist_seq_chunk_t *out)
Parse a sequence chunk from raw bytes.
Definition seq.c:63
cwist_seq_assembler_t * cwist_seq_assembler_create(void)
Create an empty assembler.
Definition seq.c:196
void cwist_seq_assembler_destroy(cwist_seq_assembler_t *a)
Destroy an assembler and its internal buffer.
Definition seq.c:223
void cwist_seq_message_free(cwist_seq_message_t *msg)
Free all buffers allocated by cwist_seq_split().
Definition seq.c:178
bool cwist_seq_assembler_get_data(cwist_seq_assembler_t *a, const uint8_t **out_data, size_t *out_len)
Borrow the assembled message when complete.
Definition seq.c:390
bool cwist_seq_assembler_feed(cwist_seq_assembler_t *a, const cwist_seq_chunk_t *chunk)
Feed one parsed chunk into the assembler.
Definition seq.c:268
struct cwist_seq_assembler cwist_seq_assembler_t
Opaque stateful assembler that reorders chunks and detects duplicates.
Definition seq.h:69
bool cwist_seq_split(const uint8_t *data, size_t len, uint16_t chunk_payload_size, cwist_seq_message_t *out)
Split data into sequenced chunks.
Definition seq.c:125
Integrated error structure used throughout the system.
Definition cwist_err.h:72
cwist_http_header_node * headers
Definition http.h:72
HTTP Response Object. Supports standard string body or Zero-Copy pointer body.
Definition http.h:82
cwist_sstring * status_text
e.g., "OK"
Definition http.h:85
cwist_http_status_t status_code
Definition http.h:84
cwist_http_header_node * headers
Definition http.h:86
Parsed view of one sequenced chunk.
Definition seq.h:45
Split a message into sequenced chunks.
Definition seq.h:60
cwist_ws_opcode_t frag_opcode
Definition websocket.h:32
size_t frag_cap
Definition websocket.h:31
size_t frag_len
Definition websocket.h:30
uint8_t * frag_buf
Definition websocket.h:29
#define WS_GUID
Definition websocket.c:24
cwist_websocket * cwist_websocket_upgrade(cwist_http_request *req, int client_fd)
Upgrade an HTTP request to a WebSocket connection.
Definition websocket.c:119
int cwist_websocket_send_sequenced(cwist_websocket *ws, const uint8_t *data, size_t len, uint16_t chunk_payload_size)
Send a large payload as sequenced binary frames.
Definition websocket.c:446
cwist_ws_frame * cwist_websocket_receive(cwist_websocket *ws)
Receive the next complete WebSocket message from a connected client.
Definition websocket.c:217
void cwist_websocket_close(cwist_websocket *ws)
Initiate a close handshake for the WebSocket wrapper.
Definition websocket.c:512
int cwist_websocket_send(cwist_websocket *ws, cwist_ws_opcode_t opcode, const uint8_t *data, size_t len)
Send a single FIN-terminated WebSocket frame to the peer.
Definition websocket.c:398
uint8_t * cwist_websocket_receive_sequenced(cwist_websocket *ws, size_t *out_len)
Receive and reassemble a sequenced binary message.
Definition websocket.c:468
#define CWIST_WS_MAX_MESSAGE_BYTES
Definition websocket.c:37
bool cwist_websocket_upgrade_response(cwist_http_request *req, cwist_http_response *res)
Validate the upgrade request and fill a 101 Switching Protocols response.
Definition websocket.c:71
void cwist_websocket_frame_destroy(cwist_ws_frame *frame)
Release a frame and its owned payload buffer.
Definition websocket.c:436
#define CWIST_WS_MAX_PAYLOAD_BYTES
Definition websocket.c:31
void cwist_websocket_destroy(cwist_websocket *ws)
Destroy the WebSocket wrapper without closing the underlying socket.
Definition websocket.c:523
char * base64_encode(const uint8_t *data, size_t input_length, size_t *output_length)
Base64-encode binary data for WebSocket handshake header generation.
Definition ws_utils.c:258
void sha1(const uint8_t *data, size_t len, uint8_t *hash)
Compute a SHA-1 digest for the provided input bytes.
Definition ws_utils.c:235
Internal cryptographic helpers used during the WebSocket handshake.