cwist
Modern C web framework with libttak runtime
Loading...
Searching...
No Matches
websocket_async.c
Go to the documentation of this file.
1#ifndef _GNU_SOURCE
2#define _GNU_SOURCE
3#endif
24
25#include <errno.h>
26#include <fcntl.h>
27#include <limits.h>
28#include <pthread.h>
29#include <stdatomic.h>
30#include <stdbool.h>
31#include <stdlib.h>
32#include <string.h>
33#include <time.h>
34#include <unistd.h>
35
36/* Same caps as websocket.c: per-frame 16 MiB, reassembled message 64 MiB. */
37#define WS_ASYNC_MAX_PAYLOAD_BYTES ((uint64_t)(16u * 1024u * 1024u))
38#define WS_ASYNC_MAX_MESSAGE_BYTES ((size_t)(64u * 1024u * 1024u))
39
40/* Absolute parked-write deadline budget (seconds), mirroring the HTTP
41 * parked-response writer's progress-reset + absolute-deadline scheme. */
42#define WS_ASYNC_PARK_DEADLINE_SEC 30u
43
44#define WS_ASYNC_READ_CHUNK 16384u
45
46/* Reactor slot payload: just the connection pointer. */
47typedef struct {
49} ws_async_slot_t;
50
51_Static_assert(sizeof(ws_async_slot_t) <= CWIST_REACTOR_PAYLOAD_SIZE,
52 "ws async slot state must fit a reactor slot payload");
53
54typedef struct ws_async_sweep ws_async_sweep_t;
55
57 int fd;
58 cwist_reactor_t *reactor;
59 cwist_ws_on_message_t on_message;
60 cwist_ws_on_close_t on_close;
61 void *user_data;
62
63 /* Read stash: raw bytes not yet consumed by the incremental parser. */
64 uint8_t *stash;
65 size_t stash_len;
66 size_t stash_cap;
67
68 /* Fragmented-message reassembly state (RFC 6455 section 5.4), same
69 * shape as cwist_websocket in websocket.h. */
70 uint8_t *frag_buf;
71 size_t frag_len;
72 size_t frag_cap;
73 cwist_ws_opcode_t frag_opcode;
74
75 /* Parked write: owned buffer + offset, resumed by a one-shot POLLOUT slot
76 * (model: http_parked_write_t in src/net/http/http.c). */
77 uint8_t *park_buf;
78 size_t park_off;
79 size_t park_len;
80 uint32_t park_deadline_sec;
81
82 uint32_t last_active_sec; /* Monotonic timestamp of last frame activity (idle reaping). */
83 ws_async_sweep_t *sweep; /* Owning idle-sweep context (NULL when reaping is disabled). */
84 struct cwist_websocket_async *sweep_next; /* Intrusive sweep list link. */
85 bool closed; /* CLOSE handshake initiated (send side). */
86 bool closing; /* Termination deferred to the woken slot callback. */
87 bool terminate_pending; /* Connection must be torn down this turn. */
88 bool read_armed; /* A one-shot read slot is registered. */
89};
90
91static void ws_async_read_ready(int fd, void *ctx);
92static void ws_async_write_ready(int fd, void *ctx);
93
94/* One watchdog thread per reactor drives the idle sweep; see the comment
95 * above ws_async_sweep_get. Connections link into their reactor's sweep
96 * list intrusively. */
97struct ws_async_sweep {
98 cwist_reactor_t *reactor;
99 uint32_t interval_sec;
100 cwist_websocket_async *head; /* Guarded by lock. */
101 bool dead; /* Destruction decided; no new connections. */
102 unsigned int posts; /* Posted sweeps not yet consumed. */
103 cwist_reactor_post_t node; /* Embedded post node (watchdog -> reactor). */
104 pthread_mutex_t lock;
105 ws_async_sweep_t *next; /* Global registry. */
106};
107
108static ws_async_sweep_t *g_ws_sweeps;
109static pthread_mutex_t g_ws_sweeps_lock = PTHREAD_MUTEX_INITIALIZER;
110
111static void ws_async_sweep_watchdog_free(ws_async_sweep_t *sweep);
112static void ws_async_sweep_post_cb(void *ctx);
113static void *ws_async_sweep_watchdog(void *arg);
114
125static void ws_async_sweep_mark_dead(ws_async_sweep_t *sweep) {
126 pthread_mutex_lock(&g_ws_sweeps_lock);
127 pthread_mutex_lock(&sweep->lock);
128 sweep->dead = true;
129 pthread_mutex_unlock(&sweep->lock);
130 pthread_mutex_unlock(&g_ws_sweeps_lock);
131}
132
133/* ------------------------------------------------------------------ */
134/* Lifecycle */
135/* ------------------------------------------------------------------ */
136
148static void ws_async_teardown(cwist_websocket_async *ws) {
149 /* Protocol layers (e.g. graphql-ws) release per-connection state here;
150 * runs before any memory is freed and exactly once per connection. */
151 if (ws->on_close) ws->on_close(ws, ws->user_data);
152 if (ws->fd >= 0) {
153 close(ws->fd);
154 ws->fd = -1;
155 }
156 cwist_free(ws->stash);
157 cwist_free(ws->frag_buf);
158 cwist_free(ws->park_buf);
159 cwist_free(ws);
160}
161
168static void ws_async_sweep_unlink(cwist_websocket_async *ws) {
169 ws_async_sweep_t *sweep = ws->sweep;
170 if (!sweep) return;
171 pthread_mutex_lock(&sweep->lock);
172 cwist_websocket_async **pp = &sweep->head;
173 while (*pp && *pp != ws) pp = &(*pp)->sweep_next;
174 if (*pp) *pp = ws->sweep_next;
175 bool empty = (sweep->head == NULL);
176 pthread_mutex_unlock(&sweep->lock);
177 ws->sweep = NULL;
178 if (empty) ws_async_sweep_mark_dead(sweep);
179}
180
200static void ws_async_terminate(cwist_websocket_async *ws) {
201 if (!ws) return;
202 ws_async_sweep_unlink(ws);
203 bool parked = ws->park_len > ws->park_off;
204 if (ws->read_armed || parked) {
205 ws->closing = true;
206 /* With a read slot armed, the read callback performs the teardown;
207 * cancel the parked-write slot so it cannot dispatch as well. With
208 * only the write slot pending, shutdown() wakes it and its callback
209 * performs the teardown. */
210 if (ws->read_armed && parked) cwist_reactor_del(ws->reactor, ws->fd);
211 /* FIN now; the peer sees EOF immediately even though the final close
212 * waits for the woken slot. */
213 shutdown(ws->fd, SHUT_RDWR);
214 return;
215 }
216 ws_async_teardown(ws);
217}
218
223static uint32_t ws_async_park_deadline(void) {
225}
226
227/* ------------------------------------------------------------------ */
228/* Idle reaping */
229/* ------------------------------------------------------------------ */
230
240static uint32_t ws_async_idle_timeout_sec(void) {
241 static _Atomic int cached = -1;
243 if (v < 0) {
244 const char *env = getenv("CWIST_WS_ASYNC_IDLE_TIMEOUT_SEC");
245 v = 300; /* default: 5 minutes */
246 if (env && *env) {
247 char *end = NULL;
248 long parsed = strtol(env, &end, 10);
249 if (end != env && *end == '\0' && parsed >= 0 && parsed <= INT_MAX) v = (int)parsed;
250 }
252 }
253 return (uint32_t)v;
254}
255
265static void ws_async_touch(cwist_websocket_async *ws) {
266 ws->last_active_sec = cwist_fast_monotonic_sec();
267}
268
282static bool ws_async_idle_reap(cwist_websocket_async *ws) {
283 uint32_t timeout_sec = ws_async_idle_timeout_sec();
284 if (timeout_sec == 0) return false;
286 if (ws->last_active_sec > 0 && (now - ws->last_active_sec) > timeout_sec) {
287 ws_async_terminate(ws);
288 return true;
289 }
290 return false;
291}
292
293/* ------------------------------------------------------------------ */
294/* Periodic idle sweep (one watchdog thread per reactor) */
295/* ------------------------------------------------------------------ */
296
297/* Lazy per-event reaping (the HTTP keep-alive precedent) cannot reap a
298 * totally silent connection: the one-shot slot model gives each fd at most
299 * one read + one POLLOUT slot, so an idle fd fires no event. Instead each
300 * reactor that owns WS async connections gets ONE watchdog thread (plain
301 * pthreads + nanosleep — portable across io_uring, epoll, and kqueue, with
302 * no Linux-only timerfd). The thread sleeps the sweep interval, then hands
303 * the sweep to the reactor thread via cwist_reactor_post(); the posted
304 * callback runs the same conn-list walk + terminate as the per-event reaper,
305 * so all list mutation stays on the reactor thread. The per-event reaper
306 * still runs at every read/write event, so the sweep cadence only affects
307 * how late a silent connection is reaped, not whether.
308 *
309 * Lifetime: the context is marked dead when its connection list empties
310 * (terminate) or the walk finds it empty. The watchdog notices the dead
311 * flag within one 100 ms sleep slice, stops posting, and exits; the context
312 * is freed by the watchdog thread alone after it exits its posting loop and
313 * observes posts == 0: the callback only ever decrements the posts counter
314 * (as its last touch of the context), so the watchdog can never reference
315 * freed memory. The posts counter covers the window where a post is in
316 * flight while the context is being torn down; cwist_reactor_destroy()
317 * drains pending posts, so the wait always terminates. */
318
328static uint32_t ws_async_sweep_interval(uint32_t timeout_sec) {
329 uint32_t interval = timeout_sec / 2;
330 if (interval < 1) interval = 1;
331 if (interval > 60) interval = 60;
332 return interval;
333}
334
347static void ws_async_sweep_watchdog_free(ws_async_sweep_t *sweep) {
348 const struct timespec slice = {.tv_sec = 0, .tv_nsec = 100000000L};
349 for (;;) {
350 pthread_mutex_lock(&sweep->lock);
351 bool drained = (sweep->posts == 0);
352 pthread_mutex_unlock(&sweep->lock);
353 if (drained) break;
355 }
356
357 pthread_mutex_lock(&g_ws_sweeps_lock);
358 ws_async_sweep_t **pp = &g_ws_sweeps;
359 while (*pp && *pp != sweep) pp = &(*pp)->next;
360 if (*pp) *pp = sweep->next;
361 pthread_mutex_unlock(&g_ws_sweeps_lock);
362 pthread_mutex_destroy(&sweep->lock);
363 cwist_free(sweep);
364}
365
377static void ws_async_sweep_post_cb(void *ctx) {
378 ws_async_sweep_t *sweep = (ws_async_sweep_t *)ctx;
379
380 uint32_t timeout_sec = ws_async_idle_timeout_sec();
381 if (timeout_sec > 0) {
383 /* Reap at most one victim per tick: ws_async_terminate unlinks the
384 * connection (and marks the sweep dead when the list empties), so
385 * the next posted sweep restarts from the head. Sweeps are small
386 * and ticks are ~interval apart, so the pace is fine. */
387 pthread_mutex_lock(&sweep->lock);
389 for (cwist_websocket_async *ws = sweep->head; ws; ws = ws->sweep_next) {
390 if (ws->last_active_sec > 0 && (now - ws->last_active_sec) > timeout_sec) {
391 victim = ws;
392 break;
393 }
394 }
395 pthread_mutex_unlock(&sweep->lock);
396 if (victim) ws_async_terminate(victim);
397 }
398
399 /* posts-- is the last touch of the context: the watchdog frees it only
400 * after observing posts == 0. */
401 pthread_mutex_lock(&sweep->lock);
402 bool empty = (sweep->head == NULL);
403 if (sweep->posts > 0) sweep->posts--;
404 pthread_mutex_unlock(&sweep->lock);
405 if (empty) ws_async_sweep_mark_dead(sweep);
406}
407
408/* Sleep the interval in 100 ms slices so dead-flag shutdown latency stays
409 * low without a condvar; nanosleep is EINTR-tolerant. */
410#define WS_ASYNC_SWEEP_SLICE_MS 100u
411
423static void *ws_async_sweep_watchdog(void *arg) {
424 ws_async_sweep_t *sweep = (ws_async_sweep_t *)arg;
425 const struct timespec slice = {.tv_sec = 0, .tv_nsec = 100000000L};
426 uint32_t slices = sweep->interval_sec * (1000u / WS_ASYNC_SWEEP_SLICE_MS);
427
428 for (;;) {
429 for (uint32_t i = 0; i < slices; i++) {
431 pthread_mutex_lock(&sweep->lock);
432 bool dead = sweep->dead;
433 pthread_mutex_unlock(&sweep->lock);
434 if (dead) goto out;
435 }
436 pthread_mutex_lock(&sweep->lock);
437 bool dead = sweep->dead;
438 if (!dead) sweep->posts++;
439 pthread_mutex_unlock(&sweep->lock);
440 if (dead) goto out;
441 if (!cwist_reactor_post(sweep->reactor, &sweep->node)) {
442 pthread_mutex_lock(&sweep->lock);
443 sweep->posts--;
444 pthread_mutex_unlock(&sweep->lock);
445 }
446 }
447
448out:
449 /* Sole freer of the context: the watchdog is done with it, and any
450 * posted sweep still queued must drain before the memory is released. */
451 ws_async_sweep_watchdog_free(sweep);
452 return NULL;
453}
454
467static void ws_async_sweep_link(cwist_websocket_async *ws, cwist_reactor_t *reactor) {
468 uint32_t timeout_sec = ws_async_idle_timeout_sec();
469 if (timeout_sec == 0) return;
470
471 pthread_mutex_lock(&g_ws_sweeps_lock);
472 for (ws_async_sweep_t *s = g_ws_sweeps; s; s = s->next) {
473 /* A dead context awaiting teardown is unowned: a fresh one is
474 * created so the new connection is swept. dead is only written
475 * under g_ws_sweeps_lock (mark_dead), so this read is safe. */
476 if (s->reactor == reactor && !s->dead) {
477 pthread_mutex_lock(&s->lock);
478 ws->sweep = s;
479 ws->sweep_next = s->head;
480 s->head = ws;
481 pthread_mutex_unlock(&s->lock);
482 pthread_mutex_unlock(&g_ws_sweeps_lock);
483 return;
484 }
485 }
486 pthread_mutex_unlock(&g_ws_sweeps_lock);
487
488 ws_async_sweep_t *sweep = (ws_async_sweep_t *)cwist_alloc(sizeof(*sweep));
489 if (!sweep) return;
490 sweep->reactor = reactor;
491 sweep->interval_sec = ws_async_sweep_interval(timeout_sec);
492 sweep->head = ws;
493 sweep->dead = false;
494 sweep->posts = 0;
495 sweep->node.cb = ws_async_sweep_post_cb;
496 sweep->node.ctx = sweep;
497 sweep->next = NULL;
498 pthread_mutex_init(&sweep->lock, NULL);
499
500 ws->sweep = sweep;
501 ws->sweep_next = NULL;
502
503 pthread_t thread;
504 if (pthread_create(&thread, NULL, ws_async_sweep_watchdog, sweep) != 0) {
505 ws->sweep = NULL;
506 pthread_mutex_destroy(&sweep->lock);
507 cwist_free(sweep);
508 return;
509 }
510 pthread_detach(thread);
511
512 pthread_mutex_lock(&g_ws_sweeps_lock);
513 sweep->next = g_ws_sweeps;
514 g_ws_sweeps = sweep;
515 pthread_mutex_unlock(&g_ws_sweeps_lock);
516}
517
518/* ------------------------------------------------------------------ */
519/* Parked (POLLOUT-resumable) writes */
520/* ------------------------------------------------------------------ */
521
533static uint8_t *ws_async_serialize_frame(cwist_ws_opcode_t opcode, const uint8_t *data, size_t len,
534 size_t *out_total) {
535 uint8_t head[10];
536 size_t head_len = 2;
537
538 head[0] = 0x80 | (opcode & 0x0F);
539 if (len < 126) {
540 head[1] = (uint8_t)len;
541 } else if (len < 65536) {
542 head[1] = 126;
543 head[2] = (uint8_t)((len >> 8) & 0xFF);
544 head[3] = (uint8_t)(len & 0xFF);
545 head_len += 2;
546 } else {
547 head[1] = 127;
548 for (int i = 0; i < 8; i++) head[2 + i] = (uint8_t)(len >> ((7 - i) * 8));
549 head_len += 8;
550 }
551
552 uint8_t *buf = (uint8_t *)cwist_alloc(head_len + len);
553 if (!buf) return NULL;
554 memcpy(buf, head, head_len);
555 if (len > 0) memcpy(buf + head_len, data, len);
556 *out_total = head_len + len;
557 return buf;
558}
559
575static int ws_async_queue_frame(cwist_websocket_async *ws, cwist_ws_opcode_t opcode,
576 const uint8_t *data, size_t len) {
577 size_t frame_len = 0;
578 uint8_t *frame = ws_async_serialize_frame(opcode, data, len, &frame_len);
579 if (!frame) return -1;
580
581 size_t pending = ws->park_len - ws->park_off;
582 if (pending > 0) {
583 /* Append behind the unsent remainder, compacting to the front. */
584 uint8_t *nb = (uint8_t *)cwist_alloc(pending + frame_len);
585 if (!nb) {
587 return -1;
588 }
589 memcpy(nb, ws->park_buf + ws->park_off, pending);
590 memcpy(nb + pending, frame, frame_len);
591 cwist_free(ws->park_buf);
592 ws->park_buf = nb;
593 ws->park_off = 0;
594 ws->park_len = pending + frame_len;
595 } else {
596 cwist_free(ws->park_buf);
597 ws->park_buf = frame;
598 ws->park_off = 0;
599 ws->park_len = frame_len;
600 frame = NULL;
601 }
603
604 int flags = MSG_NOSIGNAL | MSG_DONTWAIT;
605 while (ws->park_off < ws->park_len) {
606 ssize_t n = send(ws->fd, ws->park_buf + ws->park_off, ws->park_len - ws->park_off, flags);
607 if (n > 0) {
608 ws->park_off += (size_t)n;
609 continue;
610 }
611 if (n < 0 && errno == EINTR) continue;
612 if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) break;
613 return -1; /* Fatal send error. */
614 }
615
616 if (ws->park_off == ws->park_len) {
617 /* Fully drained. */
618 cwist_free(ws->park_buf);
619 ws->park_buf = NULL;
620 ws->park_off = ws->park_len = 0;
621 ws_async_touch(ws);
622 return 0;
623 }
624
625 ws->park_deadline_sec = ws_async_park_deadline();
626 ws_async_slot_t slot = {.ws = ws};
627 if (!cwist_reactor_add_out(ws->reactor, ws->fd, ws_async_write_ready, &slot, sizeof(slot)))
628 return -1;
629 return 0;
630}
631
644static void ws_async_write_ready(int fd, void *ctx) {
645 ws_async_slot_t *slot = (ws_async_slot_t *)ctx;
646 cwist_websocket_async *ws = slot->ws;
647 int flags = MSG_NOSIGNAL | MSG_DONTWAIT;
648
649 /* Deferred termination: shutdown() woke this slot so it can perform the
650 * real teardown. */
651 if (ws->closing) {
652 ws_async_teardown(ws);
653 return;
654 }
655
656 if (ws_async_idle_reap(ws)) return;
657
658 while (ws->park_off < ws->park_len) {
659 ssize_t n = send(fd, ws->park_buf + ws->park_off, ws->park_len - ws->park_off, flags);
660 if (n > 0) {
661 ws->park_off += (size_t)n;
662 /* Progress resets the budget so a slow-but-alive client can drain. */
663 ws->park_deadline_sec = ws_async_park_deadline();
664 ws_async_touch(ws);
665 continue;
666 }
667 if (n < 0 && errno == EINTR) continue;
668 if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK) &&
669 cwist_fast_monotonic_sec() <= ws->park_deadline_sec &&
670 cwist_reactor_add_out(ws->reactor, fd, ws_async_write_ready, slot, sizeof(*slot))) {
671 /* Payload copied into the new slot; buffer ownership moves with it. */
672 return;
673 }
674 ws_async_terminate(ws);
675 return;
676 }
677
678 cwist_free(ws->park_buf);
679 ws->park_buf = NULL;
680 ws->park_off = ws->park_len = 0;
681
682 if (ws->terminate_pending) {
683 ws_async_terminate(ws);
684 return;
685 }
686 /* Re-arm the read side only when the read turn is not already armed. */
687 if (!ws->read_armed) {
688 ws_async_slot_t rslot = {.ws = ws};
689 if (cwist_reactor_add(ws->reactor, fd, ws_async_read_ready, &rslot, sizeof(rslot)))
690 ws->read_armed = true;
691 else
692 ws_async_terminate(ws);
693 }
694}
695
710int cwist_websocket_async_send(cwist_websocket_async *ws, cwist_ws_opcode_t opcode,
711 const uint8_t *data, size_t len) {
712 if (!ws || ws->closed || (!data && len > 0)) return -1;
713 return ws_async_queue_frame(ws, opcode, data, len);
714}
715
725void cwist_websocket_async_close(cwist_websocket_async *ws) {
726 if (!ws || ws->closed) return;
727 ws->closed = true;
728 /* Best-effort CLOSE frame; failure still tears the connection down. */
729 if (ws_async_queue_frame(ws, CWIST_WS_FRAME_CLOSE, NULL, 0) != 0) {
730 ws->terminate_pending = true;
731 return;
732 }
733 if (ws->park_off == ws->park_len) {
734 /* Nothing parked: the CLOSE frame is on the wire; tear down now. The
735 * enclosing read turn (we run on the reactor thread) performs the
736 * actual free via terminate_pending. */
737 ws->terminate_pending = true;
738 }
739 /* Otherwise the parked-write completion terminates after draining. */
740}
741
754void cwist_websocket_async_close_code(cwist_websocket_async *ws, uint16_t code) {
755 if (!ws || ws->closed) return;
756 ws->closed = true;
757 uint8_t body[2] = {(uint8_t)(code >> 8), (uint8_t)(code & 0xFF)};
758 if (ws_async_queue_frame(ws, CWIST_WS_FRAME_CLOSE, body, sizeof(body)) != 0) {
759 ws->terminate_pending = true;
760 return;
761 }
762 if (ws->park_off == ws->park_len) {
763 /* CLOSE frame drained: the enclosing reactor turn performs the free
764 * via terminate_pending, mirroring cwist_websocket_async_close(). */
765 ws->terminate_pending = true;
766 }
767}
768
769/* ------------------------------------------------------------------ */
770/* Incremental frame parsing (mirrors cwist_websocket_receive) */
771/* ------------------------------------------------------------------ */
772
782static bool ws_async_stash_append(cwist_websocket_async *ws, const uint8_t *src, size_t len) {
783 if (len == 0) return true;
784 size_t need = ws->stash_len + len;
785 if (need > ws->stash_cap) {
786 size_t new_cap = ws->stash_cap ? ws->stash_cap * 2 : 4096;
787 if (new_cap < need) new_cap = need;
789 if (!nb) return false;
790 if (ws->stash_len) memcpy(nb, ws->stash, ws->stash_len);
791 cwist_free(ws->stash);
792 ws->stash = nb;
793 ws->stash_cap = new_cap;
794 }
795 memcpy(ws->stash + ws->stash_len, src, len);
796 ws->stash_len += len;
797 return true;
798}
799
811static bool ws_async_frag_append(cwist_websocket_async *ws, const uint8_t *src, size_t len) {
812 if (len == 0) return true;
813 if (ws->frag_len + len > WS_ASYNC_MAX_MESSAGE_BYTES) return false;
814 size_t need = ws->frag_len + len + 1; /* +1 for null terminator */
815 if (need > ws->frag_cap) {
816 size_t new_cap = ws->frag_cap ? ws->frag_cap * 2 : 4096;
817 if (new_cap < need) new_cap = need;
819 if (!nb) return false;
820 if (ws->frag_len) memcpy(nb, ws->frag_buf, ws->frag_len);
821 cwist_free(ws->frag_buf);
822 ws->frag_buf = nb;
823 ws->frag_cap = new_cap;
824 }
825 memcpy(ws->frag_buf + ws->frag_len, src, len);
826 ws->frag_len += len;
827 return true;
828}
829
848static int ws_async_process_frame(cwist_websocket_async *ws, bool fin, cwist_ws_opcode_t opcode,
849 uint8_t *payload, size_t payload_len) {
850 bool is_control = (opcode >= 0x8);
851
852 /* --- Fragmented-message reassembly (RFC 6455 section 5.4) -------------- */
853 if (!is_control && !fin) {
854 /* Fragment with FIN=0: start or continue reassembly. */
855 if (opcode == CWIST_WS_FRAME_CONTINUATION) {
856 /* CONTINUATION is only valid when a fragmented message is
857 * already in progress. */
858 if (ws->frag_len == 0) {
859 cwist_free(payload);
860 return -1;
861 }
862 } else {
863 /* First fragment: save opcode (text vs binary). */
864 ws->frag_opcode = opcode;
865 }
866 if (!ws_async_frag_append(ws, payload, payload_len)) {
867 cwist_free(payload);
868 return -1;
869 }
870 cwist_free(payload);
871 return 0;
872 }
873
874 /* A CONTINUATION frame is only valid inside an active fragmented-message
875 * sequence; an orphan CONTINUATION with FIN=1 must not be delivered as a
876 * complete message (mirrors the blocking path, issue #158). */
877 if (!is_control && opcode == CWIST_WS_FRAME_CONTINUATION && ws->frag_len == 0) {
878 cwist_free(payload);
879 return -1;
880 }
881
882 /* FIN=1 data frame: may be the last fragment of a multi-frame message. */
883 if (!is_control && ws->frag_len > 0) {
884 if (!ws_async_frag_append(ws, payload, payload_len)) {
885 cwist_free(payload);
886 cwist_free(ws->frag_buf);
887 ws->frag_buf = NULL;
888 ws->frag_len = ws->frag_cap = 0;
889 return -1;
890 }
891 cwist_free(payload);
892
893 /* Hand ownership of the reassembly buffer to the delivered frame. */
894 payload = ws->frag_buf;
895 payload_len = ws->frag_len;
896 payload[payload_len] = '\0';
897 opcode = ws->frag_opcode;
898 fin = true;
899
900 ws->frag_buf = NULL;
901 ws->frag_len = ws->frag_cap = 0;
902 }
903
904 /* --- Deliver the complete frame ---------------------------------- */
905 if (opcode == CWIST_WS_FRAME_CLOSE) {
906 /* RFC 6455 section 5.5.1: a CLOSE body, if present, MUST be >= 2 bytes. */
907 if (payload_len == 1) {
908 cwist_free(payload);
909 return -1;
910 }
911 /* Echo the status code (first 2 bytes) back before closing. */
912 ws_async_queue_frame(ws, CWIST_WS_FRAME_CLOSE, payload, (payload_len >= 2) ? 2 : 0);
913 cwist_free(payload);
914 ws->closed = true;
915 ws->terminate_pending = true;
916 return 0;
917 }
918 if (opcode == CWIST_WS_FRAME_PING) {
919 /* RFC 6455 section 5.5.3: respond to every PING with a PONG carrying
920 * the same payload (up to 125 bytes per section 5.5). */
921 ws_async_queue_frame(ws, CWIST_WS_FRAME_PONG, payload, payload_len);
922 }
923
925 if (!frame) {
926 cwist_free(payload);
927 return -1;
928 }
929 frame->fin = fin;
930 frame->opcode = opcode;
931 frame->payload = payload;
932 frame->payload_len = payload_len;
933 /* Ownership moves to the callback; it must destroy the frame. */
934 ws->on_message(ws, frame, ws->user_data);
935 return 0;
936}
937
949static int ws_async_feed(cwist_websocket_async *ws) {
950 size_t off = 0;
951
952 while (ws->stash_len - off >= 2) {
953 const uint8_t *base = ws->stash + off;
954 bool fin = (base[0] & 0x80) != 0;
955 cwist_ws_opcode_t opcode = base[0] & 0x0F;
956
957 /* RFC 6455 section 5.2: RSV1/RSV2/RSV3 MUST be 0 unless an extension
958 * defines their meaning. CWIST has no such extension. */
959 if (base[0] & 0x70) return -1;
960
961 /* Client-to-server frames must be masked per RFC 6455 section 5.3. */
962 if (!(base[1] & 0x80)) return -1;
963
964 uint64_t payload_len = base[1] & 0x7F;
965 size_t hdr = 2;
966 if (payload_len == 126) {
967 if (ws->stash_len - off < 4) break;
968 payload_len = ((uint64_t)base[2] << 8) | (uint64_t)base[3];
969 hdr = 4;
970 } else if (payload_len == 127) {
971 if (ws->stash_len - off < 10) break;
972 const uint8_t *e = base + 2;
973 payload_len = ((uint64_t)e[0] << 56) | ((uint64_t)e[1] << 48) | ((uint64_t)e[2] << 40) |
974 ((uint64_t)e[3] << 32) | ((uint64_t)e[4] << 24) | ((uint64_t)e[5] << 16) |
975 ((uint64_t)e[6] << 8) | (uint64_t)e[7];
976 hdr = 10;
977 }
978
979 /* Reject frames that would force a huge allocation before any
980 * payload is read (OOM DoS vector). */
981 if (payload_len > WS_ASYNC_MAX_PAYLOAD_BYTES) return -1;
982
983 /* RFC 6455 section 5.5: control frames MUST NOT carry more than 125
984 * bytes and MUST NOT be fragmented. */
985 bool is_control_early = (opcode >= 0x8);
986 if (is_control_early && (payload_len > 125 || !fin)) return -1;
987
988 size_t total = hdr + 4 + (size_t)payload_len;
989 if (ws->stash_len - off < total) break; /* Partial frame: wait for more. */
990
991 const uint8_t *mask = base + hdr;
992 const uint8_t *src = base + hdr + 4;
993 uint8_t *payload = NULL;
994 if (payload_len > 0) {
995 payload = (uint8_t *)cwist_alloc((size_t)payload_len + 1);
996 if (!payload) return -1;
997 for (uint64_t i = 0; i < payload_len; i++) payload[i] = src[i] ^ mask[i % 4];
998 payload[payload_len] = '\0';
999 }
1000
1001 off += total;
1002 if (ws_async_process_frame(ws, fin, opcode, payload, (size_t)payload_len) != 0) return -1;
1003 /* The CLOSE handler marks the connection for teardown; keep parsing
1004 * nothing further — the caller terminates. */
1005 if (ws->terminate_pending) break;
1006 }
1007
1008 if (off > 0) {
1009 memmove(ws->stash, ws->stash + off, ws->stash_len - off);
1010 ws->stash_len -= off;
1011 }
1012 return 0;
1013}
1014
1015/* ------------------------------------------------------------------ */
1016/* Reactor integration */
1017/* ------------------------------------------------------------------ */
1018
1032static void ws_async_read_ready(int fd, void *ctx) {
1033 ws_async_slot_t *slot = (ws_async_slot_t *)ctx;
1034 cwist_websocket_async *ws = slot->ws;
1036 bool eof = false, err = false, got_bytes = false;
1037
1038 ws->read_armed = false;
1039
1040 /* Deferred termination (sweep reap / async_close with slots pending):
1041 * shutdown() already signalled the peer; this woken slot is the only one
1042 * left, so it performs the real teardown. */
1043 if (ws->closing) {
1044 ws_async_teardown(ws);
1045 return;
1046 }
1047
1048 /* Lazy idle reaper (mirrors http_async_event_cb): checked before any
1049 * work so an event arriving after the deadline closes the connection
1050 * without touching the stash. */
1051 if (ws_async_idle_reap(ws)) return;
1052
1053 /* Drain everything the kernel has without blocking. */
1054 for (;;) {
1055 ssize_t n = recv(fd, buf, sizeof(buf), MSG_DONTWAIT);
1056 if (n > 0) {
1057 got_bytes = true;
1058 if (!ws_async_stash_append(ws, (const uint8_t *)buf, (size_t)n)) {
1059 err = true;
1060 break;
1061 }
1062 continue;
1063 }
1064 if (n == 0) {
1065 eof = true;
1066 break;
1067 }
1068 if (errno == EINTR) continue;
1069 if (errno == EAGAIN || errno == EWOULDBLOCK) break;
1070 err = true;
1071 break;
1072 }
1073
1074 if (!err && ws->stash_len > 0 && ws_async_feed(ws) != 0) err = true;
1075
1076 /* Bytes on the wire (and therefore every complete frame, PING/PONG
1077 * included) count as activity for the idle reaper. */
1078 if (got_bytes && !err && !eof) ws_async_touch(ws);
1079
1080 /* CLOSE-from-peer, async_close() from the callback, EOF and errors all
1081 * converge here: tear the connection down exactly once. */
1082 if (err || eof || ws->terminate_pending) {
1083 ws_async_terminate(ws);
1084 return;
1085 }
1086
1087 /* One-shot slot consumed: re-arm for the next read event. */
1088 if (!cwist_reactor_add(ws->reactor, fd, ws_async_read_ready, slot, sizeof(*slot))) {
1089 ws_async_terminate(ws);
1090 return;
1091 }
1092 ws->read_armed = true;
1093}
1094
1117 cwist_ws_on_message_t on_message,
1118 cwist_ws_on_close_t on_close,
1119 void *user_data, const uint8_t *initial,
1120 size_t initial_len) {
1121 if (fd < 0 || !reactor || !on_message) return NULL;
1122
1124 if (!ws) {
1125 close(fd);
1126 return NULL;
1127 }
1128 ws->fd = fd;
1129 ws->reactor = reactor;
1130 ws->on_message = on_message;
1131 ws->on_close = on_close;
1132 ws->user_data = user_data;
1133 ws->stash = NULL;
1134 ws->stash_len = ws->stash_cap = 0;
1135 ws->frag_buf = NULL;
1136 ws->frag_len = ws->frag_cap = 0;
1137 ws->frag_opcode = CWIST_WS_FRAME_CONTINUATION;
1138 ws->park_buf = NULL;
1139 ws->park_off = ws->park_len = 0;
1140 ws->park_deadline_sec = 0;
1141 ws->last_active_sec = cwist_fast_monotonic_sec();
1142 ws->sweep = NULL;
1143 ws->sweep_next = NULL;
1144 ws->closed = false;
1145 ws->closing = false;
1146 ws->terminate_pending = false;
1147 ws->read_armed = false;
1148
1149 /* The fd must never block the reactor thread. */
1150 int fl = fcntl(fd, F_GETFL, 0);
1151 if (fl >= 0) fcntl(fd, F_SETFL, fl | O_NONBLOCK);
1152
1153 /* Bytes already read past the upgrade request (conn->rbuf) go through the
1154 * parser before the first reactor wait. Delivery can happen inline here,
1155 * including a cwist_websocket_async_close() from the callback. */
1156 bool fail = false;
1157 if (initial_len > 0) {
1158 if (!ws_async_stash_append(ws, initial, initial_len)) fail = true;
1159 if (!fail && ws_async_feed(ws) != 0) fail = true;
1160 }
1161 if (fail || ws->terminate_pending) {
1162 /* No reactor slot was registered yet: nothing to cancel. on_close
1163 * does not fire here; attach failed, so the caller keeps ownership of
1164 * any state installed during inline initial-bytes delivery. */
1165 close(fd);
1166 cwist_free(ws->stash);
1167 cwist_free(ws->frag_buf);
1168 cwist_free(ws->park_buf);
1169 cwist_free(ws);
1170 return NULL;
1171 }
1172
1173 /* Everything the reactor thread reads is set before the slot is added:
1174 * once cwist_reactor_add() returns, the read callback may already run
1175 * on the reactor thread (attach can be called from any thread), so this
1176 * thread must not write ws any more (#351).
1177 *
1178 * Join the reactor's idle sweep (no-op when reaping is disabled). */
1179 ws_async_sweep_link(ws, reactor);
1180 ws->read_armed = true;
1181
1182 ws_async_slot_t slot = {.ws = ws};
1183 if (!cwist_reactor_add(reactor, fd, ws_async_read_ready, &slot, sizeof(slot))) {
1184 ws->read_armed = false;
1185 ws_async_sweep_unlink(ws);
1186 close(fd);
1187 cwist_free(ws->stash);
1188 cwist_free(ws->frag_buf);
1189 cwist_free(ws->park_buf);
1190 cwist_free(ws);
1191 return NULL;
1192 }
1193 return ws;
1194}
1195
1212 cwist_ws_on_message_t on_message, void *user_data,
1213 const uint8_t *initial, size_t initial_len) {
1214 return cwist_websocket_async_attach_ex(fd, reactor, on_message, NULL, user_data, initial,
1215 initial_len) != NULL;
1216}
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
#define MSG_DONTWAIT
Definition http3.c:82
cwist_ws_opcode_t
Definition websocket.h:13
@ CWIST_WS_FRAME_PING
Definition websocket.h:18
@ 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
#define CWIST_REACTOR_PAYLOAD_SIZE
Definition reactor.h:19
struct cwist_reactor cwist_reactor_t
Definition reactor.h:14
bool cwist_reactor_add(cwist_reactor_t *reactor, int fd, cwist_reactor_cb_t cb, const void *payload, size_t payload_size)
Definition reactor.c:1357
bool cwist_reactor_del(cwist_reactor_t *reactor, int fd)
Definition reactor.c:1410
bool cwist_reactor_add_out(cwist_reactor_t *reactor, int fd, cwist_reactor_cb_t cb, const void *payload, size_t payload_size)
Definition reactor.c:1362
Definition reactor.h:46
#define WS_ASYNC_MAX_MESSAGE_BYTES
struct ws_async_sweep ws_async_sweep_t
int cwist_websocket_async_send(cwist_websocket_async *ws, cwist_ws_opcode_t opcode, const uint8_t *data, size_t len)
Send one complete WebSocket message frame on an async connection.
void cwist_websocket_async_close(cwist_websocket_async *ws)
Initiate the WebSocket close handshake with an empty CLOSE frame.
void cwist_websocket_async_close_code(cwist_websocket_async *ws, uint16_t code)
Initiate the WebSocket close handshake with a CLOSE frame carrying a status code.
#define WS_ASYNC_READ_CHUNK
cwist_websocket_async * cwist_websocket_async_attach_ex(int fd, cwist_reactor_t *reactor, cwist_ws_on_message_t on_message, cwist_ws_on_close_t on_close, void *user_data, const uint8_t *initial, size_t initial_len)
Attach an upgraded, HTTP-connection-owned fd to the reactor as an async WebSocket connection.
bool cwist_websocket_async_attach(int fd, cwist_reactor_t *reactor, cwist_ws_on_message_t on_message, void *user_data, const uint8_t *initial, size_t initial_len)
Attach an upgraded fd to the reactor as an async WebSocket connection (no on_close callback variant).
#define WS_ASYNC_SWEEP_SLICE_MS
#define WS_ASYNC_MAX_PAYLOAD_BYTES
#define WS_ASYNC_PARK_DEADLINE_SEC
Reactor-driven, callback-shaped non-blocking WebSocket API (C1M mode).
struct cwist_websocket_async cwist_websocket_async
void(* cwist_ws_on_message_t)(cwist_websocket_async *ws, cwist_ws_frame *frame, void *user_data)
Callback invoked for each complete WebSocket message on the C1M path.
void(* cwist_ws_on_close_t)(cwist_websocket_async *ws, void *user_data)
Optional teardown hook for the callback-shaped WS path (see cwist_websocket_async_attach_ex in ws_asy...
Zero-latency speculative fast-path writer and P2C worker scheduler.
uint32_t cwist_fast_monotonic_sec(void)
Fast monotonic clock in seconds (vDSO coarse where available).