ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
websocket/transport.c
Go to the documentation of this file.
1
36#include <ascii-chat/log/log.h>
45#include <libwebsockets.h>
46#include <openssl/err.h>
47#include <string.h>
48#include <unistd.h>
49#include <pthread.h>
50#include <errno.h>
51#include <limits.h>
52
60#define WEBSOCKET_MESSAGE_QUEUE_SIZE_INCOMING 64
61
69#define WEBSOCKET_MESSAGE_QUEUE_SIZE_OUTGOING 512
70
71// Legacy names for backward compatibility
72#define WEBSOCKET_RECV_QUEUE_SIZE WEBSOCKET_MESSAGE_QUEUE_SIZE_INCOMING
73#define WEBSOCKET_SEND_QUEUE_SIZE WEBSOCKET_MESSAGE_QUEUE_SIZE_OUTGOING
74
75// Shared internal types (websocket_recv_msg_t, websocket_transport_data_t)
77
78// Forward declaration for libwebsockets callback
79static int websocket_callback(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len);
80
81// REMOVED: lws_service_with_timeout() was creating a race condition
82// by spawning multiple threads that all call lws_service() on the same context.
83// LibWebSockets is NOT thread-safe for concurrent calls.
84//
85// Instead: Service thread calls lws_service(1) directly in a tight loop.
86// timeout=1 means: block up to 1ms on internal poll(), then return.
87// This is fast enough to keep 60fps (16.6ms per frame budget) while
88// letting the TLS state machine progress incrementally.
89
90// =============================================================================
91// Service Thread (Client-side only)
92// =============================================================================
93
100static void deferred_buffer_free(websocket_transport_data_t *ws_data, uint8_t *ptr, size_t size) {
101 if (!ws_data || !ptr)
102 return;
103
104 pending_free_item_t item = {.ptr = ptr, .size = size};
106 ringbuffer_write(ws_data->pending_free_queue, &item);
108}
109
116static void drain_pending_free_queue(websocket_transport_data_t *ws_data) {
117 if (!ws_data)
118 return;
119
122 while (ringbuffer_read(ws_data->pending_free_queue, &item)) {
123 buffer_pool_free(NULL, item.ptr, item.size);
124 }
126}
127
134static void *websocket_service_thread(void *arg) {
136
137 log_info(">>> SERVICE_THREAD_START: owns_context=%d, wsi=%p, context=%p, send_queue=%p", ws_data->owns_context,
138 (void *)ws_data->wsi, (void *)ws_data->context, (void *)ws_data->send_queue);
139
140 int loop_count = 0;
141 uint64_t last_service_call = 0; // Force lws_service() on first iteration
142
143 while (ws_data->service_running) {
144 loop_count++;
145
146 // CRITICAL: Check if we're destroying FIRST before doing anything else
147 // This prevents accessing invalid pointers or contexts
148 if (atomic_load_bool(&ws_data->is_destroying) || !ws_data->context) {
149 if (loop_count <= 10) {
150 log_info("[LOOP %d] Destroying flag set or context NULL, exiting service thread", loop_count);
151 }
152 break;
153 }
154
155 // Periodically drain pending-free queue to free buffers deferred from compression
156 // Do this at the start of each loop to ensure buffers are freed after compression completes
157 if (loop_count % 10 == 0) {
158 drain_pending_free_queue(ws_data);
159 }
160
161 // ALWAYS log first 10 loops to debug why queue checks aren't working
162 if (loop_count <= 10) {
163 log_info("[LOOP %d] owns_context=%d, wsi=%p, service_running=%d", loop_count, ws_data->owns_context,
164 (void *)ws_data->wsi, ws_data->service_running);
165 }
166
167 // Check if THIS transport (client or server) has data queued to send
168 // Note: we only check for CLIENT transports here (owns_context=true)
169 // because SERVER transports are already handled by the SERVER_WRITEABLE callback.
170 // Clients need explicit triggering via lws_callback_on_writable().
171 if (ws_data->owns_context && ws_data->wsi) {
172 if (loop_count <= 10) {
173 log_info("[LOOP %d] CLIENT condition TRUE - checking queue", loop_count);
174 }
175
176 // Wait for connection to be established before sending
177 // The CLIENT_ESTABLISHED callback sets is_connected = true
178 // If we try to send before handshake completes, lws_write() fails
179 mutex_lock(&ws_data->state_mutex);
180 bool connected = ws_data->is_connected;
181 mutex_unlock(&ws_data->state_mutex);
182
183 if (!connected) {
184 if (loop_count <= 10) {
185 log_info("[LOOP %d] CLIENT not connected yet, skipping queue drain", loop_count);
186 }
187
188 // Check if connection attempt failed (CONNECTION_ERROR was called)
189 // If so, break from service loop to avoid indefinite retry
190 mutex_lock(&ws_data->state_mutex);
191 bool failed = ws_data->connection_failed;
192 mutex_unlock(&ws_data->state_mutex);
193
194 if (loop_count <= 50) {
195 log_debug("[LOOP %d] Connection failed check: %d", loop_count, failed);
196 }
197
198 if (failed) {
199 log_info("[LOOP %d] Connection attempt failed, exiting service thread", loop_count);
200 break;
201 }
202
203 // Do NOT sleep here - let lws_service() loop control timing
204 // During TLS handshake, lws_service() needs to be called every 100us
205 // Sleeping here blocks that and causes the handshake to stall
206 } else {
207 // This is a CLIENT transport - request WRITEABLE callback to send queued messages
208 // lws_write() must be called from within LWS callbacks, not from external threads
209
210 // Only request writeable callback when there is data queued to send.
211 // Unconditionally requesting on every loop iteration causes a tight spam loop
212 // of LWS_CALLBACK_CLIENT_WRITEABLE callbacks (~300/s), flooding logs and causing
213 // deadlocks when stderr pipe fills and blocks the service thread while holding
214 // the log mutex (other threads that try to log then deadlock).
215 mutex_lock(&ws_data->send_mutex);
216 bool has_data = !ringbuffer_is_empty(ws_data->send_queue);
217
218 if (loop_count <= 10) {
219 log_info("[LOOP %d] Queue check: has_data=%d", loop_count, has_data);
220 }
221
222 if (has_data) {
223 lws_callback_on_writable(ws_data->wsi);
224 }
225
226 mutex_unlock(&ws_data->send_mutex);
227
228 if (loop_count <= 10 && has_data) {
229 log_info(">>> SERVICE_THREAD: CLIENT queue has data (callback will process it)");
230 }
231 } // End of if (connected) block
232 } else {
233 if (loop_count <= 10) {
234 log_info("[LOOP %d] CLIENT condition FALSE (owns_context=%d, wsi=%p)", loop_count, ws_data->owns_context,
235 (void *)ws_data->wsi);
236 }
237 }
238
239 // Service libwebsockets (processes network events, triggers callbacks)
240 // Call frequently to handle both incoming AND outgoing data responsively
241 // During TLS handshake: every 100us (1/10 of 1ms) for rapid protocol progression
242 // During data transfer: every 1ms to balance responsiveness vs CPU usage
243 uint64_t now_ns = time_get_ns();
244 uint64_t time_since_last_service = now_ns - last_service_call;
245 int result = 0;
246
247 // Call lws_service() frequently during handshake, less frequently after connection
248 // Use 100us interval during handshake, 1ms after connected
249 uint64_t service_interval = ws_data->is_connected ? 1000000ULL : 100000ULL; // 1ms : 100us
250
251 if (loop_count <= 10) {
252 log_debug("[LOOP %d] Timing check: time_since_last=%lu us, interval=%lu us, will_service=%d", loop_count,
253 time_since_last_service / 1000, service_interval / 1000,
254 (int)(time_since_last_service >= service_interval));
255 }
256
257 if (time_since_last_service >= service_interval) {
258 last_service_call = now_ns;
259 uint64_t service_start_ns = now_ns;
260
261 bool is_destroying = atomic_load_bool(&ws_data->is_destroying);
262 if (!ws_data->context || is_destroying) {
263 // Context is invalid or being destroyed, don't call lws_service
264 if (loop_count <= 50) {
265 log_info("[LOOP %d] Skipping lws_service: context=%p, destroying=%d", loop_count, (void *)ws_data->context,
266 (int)is_destroying);
267 }
268 } else {
269 // Call lws_service with short timeout (1ms)
270 // This lets lws_service() block briefly on internal poll() if there's no socket activity,
271 // but returns quickly to let service loop continue if needed.
272 // 1ms timeout is short enough to maintain 60fps (16.6ms per frame) while being responsive.
273 //
274 // Why timeout=1, not timeout=0?
275 // - timeout=0: select() returns immediately, but TLS state machine might not progress
276 // - timeout=1: select() blocks up to 1ms for socket events, letting TLS negotiate faster
277 // while still being responsive to multiple service calls per frame
278 if (loop_count <= 10) {
279 log_info("[LOOP %d] >>> CALLING lws_service() NOW (is_connected=%d, wsi=%p, timeout=1ms)", loop_count,
280 ws_data->is_connected, (void *)ws_data->wsi);
281 }
282
283 result = lws_service(ws_data->context, 1);
284
285 if (loop_count <= 10) {
286 log_info("[LOOP %d] <<< lws_service() RETURNED (result=%d)", loop_count, result);
287 }
288 }
289 uint64_t service_end_ns = time_get_ns();
290
291 if (loop_count <= 50) {
292 char service_duration_str[32];
293 time_pretty(service_end_ns - service_start_ns, -1, service_duration_str, sizeof(service_duration_str));
294 log_info("[LOOP %d] lws_service() returned %d, duration %s", loop_count, result, service_duration_str);
295 }
296
297 if (result < 0) {
298 log_fatal("🔴 lws_service error: %d at loop %d", result, loop_count);
299 break;
300 }
301 } else {
302 // Sleep briefly between lws_service() calls to avoid busy-waiting
303 // During handshake: 0us sleep (busy-wait for maximum responsiveness during TLS)
304 // lws_service is now non-blocking, so tight polling won't block
305 // During transfer: 100us sleep (normal cadence)
306 uint64_t sleep_us = ws_data->is_connected ? 100 : 0;
307 if (loop_count <= 50) {
308 log_debug("[LOOP %d] Time since service: %lu us, interval: %lu us - sleeping %lu us", loop_count,
309 time_since_last_service / 1000, service_interval / 1000, sleep_us);
310 }
311 if (sleep_us > 0) {
312 platform_sleep_us(sleep_us);
313 }
314 // else: no sleep during handshake - busy-wait with non-blocking lws_service calls
315 }
316
317 // Check if connection is still alive
318 if (loop_count <= 50 && ws_data->is_connected) {
319 log_info("[LOOP %d] After lws_service: is_connected=true, wsi=%p", loop_count, (void *)ws_data->wsi);
320 } else if (loop_count <= 50) {
321 log_warn("[LOOP %d] After lws_service: is_connected=false, wsi=%p", loop_count, (void *)ws_data->wsi);
322 }
323 }
324
325 // log_debug("WebSocket service thread exiting"); // Disabled: causes AddressSanitizer heap-use-after-free crash
326 return NULL;
327}
328
329// =============================================================================
330// libwebsockets Callbacks
331// =============================================================================
332
339static int websocket_callback(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len) {
340 websocket_transport_data_t *ws_data = wsi ? lws_get_opaque_user_data(wsi) : NULL;
341 if (!ws_data) {
342 ws_data = (websocket_transport_data_t *)user;
343 }
344
345 // Log all callback reasons for TLS investigation
346 static uint64_t callback_count = 0;
347 uint64_t now_ns = time_get_ns();
348 if (reason == LWS_CALLBACK_PROTOCOL_INIT || reason == LWS_CALLBACK_CLIENT_ESTABLISHED ||
349 reason == LWS_CALLBACK_CLIENT_WRITEABLE || reason == LWS_CALLBACK_CLIENT_RECEIVE ||
350 reason == LWS_CALLBACK_CLIENT_CONNECTION_ERROR || reason == LWS_CALLBACK_CLOSED) {
351 callback_count++;
352 log_info(">>> CALLBACK #%lu: reason=%d (0x%x), wsi=%p, len=%zu, timestamp=%llu", callback_count, reason, reason,
353 (void *)wsi, len, (unsigned long long)now_ns);
354 }
355
356 // Validate ws_data exists before accessing it (prevents heap-buffer-overflow in TLS callbacks)
357 // For unknown callbacks (especially TLS internal callbacks from OpenSSL), return early
358 if (!ws_data) {
359 return 0;
360 }
361
362 switch (reason) {
363 case LWS_CALLBACK_CLIENT_ESTABLISHED: {
364 // Don't process if we're in shutdown mode
365 if (atomic_load_bool(&ws_data->is_destroying)) {
366 return 0;
367 }
368 uint64_t now_ns = time_get_ns();
369 log_fatal("🟢🟢🟢 WebSocket CLIENT_ESTABLISHED! wsi=%p, ws_data=%p, timestamp=%llu, elapsed_from_start=%llu",
370 (void *)wsi, (void *)ws_data, (unsigned long long)now_ns, (unsigned long long)(now_ns / 1000000000ULL));
371 if (ws_data) {
372 mutex_lock(&ws_data->state_mutex);
373 log_fatal(" [ESTABLISHED] Setting is_connected=true (was %d)", ws_data->is_connected);
374 ws_data->is_connected = true;
375 cond_signal(&ws_data->state_cond);
376 mutex_unlock(&ws_data->state_mutex);
377 log_fatal(" [ESTABLISHED] State updated, wsi=%p ready for send/recv", (void *)wsi);
378 }
379 break;
380 }
381
382 case LWS_CALLBACK_CLIENT_FILTER_PRE_ESTABLISH: {
383 log_info(">>> CALLBACK: LWS_CALLBACK_CLIENT_FILTER_PRE_ESTABLISH (wsi=%p) - WebSocket upgrade starting",
384 (void *)wsi);
385 break;
386 }
387
388 case LWS_CALLBACK_PROTOCOL_INIT: {
389 log_info(">>> CALLBACK: LWS_CALLBACK_PROTOCOL_INIT (wsi=%p, ws_data=%p)", (void *)wsi, (void *)ws_data);
390 if (atomic_load_bool(&ws_data->is_destroying)) {
391 return 0;
392 }
393 break;
394 }
395
396 case LWS_CALLBACK_CLIENT_RECEIVE: {
397 // Received data from server - may be fragmented for large messages
398 if (atomic_load_bool(&ws_data->is_destroying)) {
399 return 0;
400 }
401 uint64_t now_ns = time_get_ns();
402 if (!in || len == 0) {
403 log_debug("CLIENT_RECEIVE: in=%p, len=%zu - skipping", in, len);
404 break;
405 }
406
407 // Detect first/final bits from WebSocket frame using the LWS API
408 bool is_first = lws_is_first_fragment(wsi);
409 bool is_final = lws_is_final_fragment(wsi);
410
411 log_info("🟡 LWS_CALLBACK_CLIENT_RECEIVE: %zu bytes (first=%d, final=%d), wsi=%p, timestamp=%llu", len, is_first,
412 is_final, (void *)wsi, (unsigned long long)now_ns);
413
414 // Queue this fragment immediately with first/final flags.
415 // Per LWS design, each fragment is processed individually by the callback.
416 // We must NOT manually reassemble fragments - that breaks LWS's internal state machine.
417 // Instead, queue each fragment with metadata, and let the receiver decide on reassembly.
418
420 msg.data = buffer_pool_alloc(NULL, len);
421 if (!msg.data) {
422 log_error("Failed to allocate buffer for fragment (%zu bytes)", len);
423 break;
424 }
425
426 memcpy(msg.data, in, len);
427 msg.len = len;
428 msg.first = is_first;
429 msg.final = is_final;
430
431 mutex_lock(&ws_data->recv_mutex);
432 bool success = ringbuffer_write(ws_data->recv_queue, &msg);
433 if (!success) {
434 // Queue is full - drop the fragment and log warning
435 log_warn("WebSocket receive queue full - dropping fragment (len=%zu, first=%d, final=%d)", len, is_first,
436 is_final);
437 buffer_pool_free(NULL, msg.data, msg.len);
438 mutex_unlock(&ws_data->recv_mutex);
439 break;
440 }
441
442 // Signal waiting recv() call that a fragment is available
443 cond_signal(&ws_data->recv_cond);
444 mutex_unlock(&ws_data->recv_mutex);
445 break;
446 }
447
448 case LWS_CALLBACK_CLIENT_CLOSED:
449 case LWS_CALLBACK_CLOSED: {
450 uint64_t now_ns = time_get_ns();
451 log_fatal("🔴🔴🔴 WebSocket connection CLOSED! reason=%d, wsi=%p, ws_data=%p, is_connected=%d, timestamp=%llu",
452 reason, (void *)wsi, (void *)ws_data, ws_data ? ws_data->is_connected : -1, (unsigned long long)now_ns);
453 if (ws_data) {
454 mutex_lock(&ws_data->state_mutex);
455 log_fatal(" [CLOSE] Setting is_connected=false (was %d)", ws_data->is_connected);
456 ws_data->is_connected = false;
457 mutex_unlock(&ws_data->state_mutex);
458
459 // Wake any blocking recv() calls
460 cond_broadcast(&ws_data->recv_cond);
461 }
462 break;
463 }
464
465 case LWS_CALLBACK_CLIENT_CONNECTION_ERROR: {
466 uint64_t now_ns = time_get_ns();
467
468 // Get detailed OpenSSL error if available
469 unsigned long ssl_err = ERR_get_error();
470 char ssl_err_str[256] = "no SSL error";
471 if (ssl_err) {
472 ERR_error_string_n(ssl_err, ssl_err_str, sizeof(ssl_err_str));
473 }
474
475 log_fatal(
476 "🔴🔴🔴 WebSocket CONNECTION ERROR! reason=%d, error=%s, wsi=%p, ws_data=%p, ssl_err=%lu (%s), timestamp=%llu",
477 reason, in ? (const char *)in : "unknown", (void *)wsi, (void *)ws_data, ssl_err, ssl_err_str,
478 (unsigned long long)now_ns);
479 if (ws_data) {
480 mutex_lock(&ws_data->state_mutex);
481 ws_data->is_connected = false;
482 ws_data->connection_failed = true; // Signal service thread to exit
483 cond_signal(&ws_data->state_cond); // Wake anyone waiting on connection
484 mutex_unlock(&ws_data->state_mutex);
485
486 // Wake any blocking recv() calls
487 cond_broadcast(&ws_data->recv_cond);
488 }
489 break;
490 }
491
492 case LWS_CALLBACK_CLIENT_WRITEABLE: {
493 // Socket is writable - process queued messages with FRAGMENTATION
494 // CRITICAL FIX: Fragment large messages into ~4KB chunks to avoid internal buffering
495 // libwebsockets #464: Sending messages > rx_buffer_size causes ultra-slow buffering
496 uint64_t now_ns = time_get_ns();
497 log_dev("LWS_CALLBACK_CLIENT_WRITEABLE FIRED for wsi=%p, ws_data=%p, is_connected=%d, timestamp=%llu", (void *)wsi,
498 (void *)ws_data, ws_data ? ws_data->is_connected : -1, (unsigned long long)now_ns);
499
500 if (!ws_data || atomic_load_bool(&ws_data->is_destroying)) {
501 break;
502 }
503
504 // CRITICAL: Don't try to write if not fully connected
505 // libwebsockets can hit assertion if we write before connection establishment completes
506 mutex_lock(&ws_data->state_mutex);
507 bool connected = ws_data->is_connected;
508 mutex_unlock(&ws_data->state_mutex);
509
510 if (!connected) {
511 log_debug(" [CLIENT_WRITEABLE] Skipping write - not connected yet");
512 break;
513 }
514
516 int message_count = 0;
517
518 // Process all messages in queue until pipe is choked or queue is empty
519 // CRITICAL: Do NOT hold send_mutex while calling lws_write()
520 // lws_write() can block for seconds during TLS operations, causing other threads to deadlock
521 while (1) {
522 // Dequeue one message (lock only for queue access)
523 mutex_lock(&ws_data->send_mutex);
524 if (!ringbuffer_peek(ws_data->send_queue, &msg)) {
525 // Queue is empty
526 mutex_unlock(&ws_data->send_mutex);
527 break;
528 }
529 // Check pipe state BEFORE dequeuing
530 if (lws_send_pipe_choked(ws_data->wsi)) {
531 log_debug("Pipe choked, stopping message processing");
532 mutex_unlock(&ws_data->send_mutex);
533 break;
534 }
535 // Dequeue the message
536 ringbuffer_read(ws_data->send_queue, &msg);
537 mutex_unlock(&ws_data->send_mutex); // Release mutex BEFORE calling lws_write()
538
539 log_debug("WebSocket CLIENT_WRITEABLE: sending queued %zu bytes in fragments (msg %d)", msg.len,
540 message_count + 1);
541
542 // CRITICAL: Let libwebsockets handle fragmentation internally
543 // We send the entire message at once with LWS_WRITE_BINARY
544 // libwebsockets automatically fragments based on its MTU and manages FIN bits
545 log_debug("WebSocket sending %zu bytes (first=%d) with LWS_WRITE_BINARY", msg.len, msg.first);
546
547 int result = lws_write(ws_data->wsi, msg.data + LWS_PRE, msg.len, LWS_WRITE_BINARY);
548
549 if (result < 0) {
550 // Error - pipe is full or other error
551 log_debug("WebSocket write returned %d (pipe choked), re-queueing", result);
552 // Re-queue the message (lock only for queue access)
553 mutex_lock(&ws_data->send_mutex);
554 ringbuffer_write(ws_data->send_queue, &msg);
555 mutex_unlock(&ws_data->send_mutex);
556 break; // Stop processing, wait for next writeable callback
557 } else {
558 // Success - lws_write handles ALL fragmentation internally
559 // The entire message is queued for sending (fragmented internally by libwebsockets)
560 deferred_buffer_free(ws_data, msg.data, LWS_PRE + msg.len);
561 message_count++;
562 log_debug("Message queued for send, will be fragmented by libwebsockets");
563 }
564 } // End while loop - continues until queue empty or pipe choked
565
566 // Request another callback if more messages are queued
567 // Must request even if pipe was choked so we drain queue once TCP buffer drains
568 mutex_lock(&ws_data->send_mutex);
569 bool has_more = ringbuffer_peek(ws_data->send_queue, &msg);
570 mutex_unlock(&ws_data->send_mutex);
571
572 if (has_more) {
573 lws_callback_on_writable(ws_data->wsi);
574 }
575 break;
576 }
577
578 default:
579 break;
580 }
581
582 return 0;
583}
584
585// =============================================================================
586// WebSocket Transport Methods
587// =============================================================================
588
589static asciichat_error_t websocket_send(acip_transport_t *transport, const void *data, size_t len) {
590 // NOTE: Disabled START_TIMER/STOP_TIMER due to thread safety issues with timer hashtable
591 // causing heap-use-after-free crashes in ASAN. The timer system has race conditions
592 // when accessed from the service thread context. See: line 325 (similar issue).
594
595 // For server-side transports (owns_context=false), the connection is already established.
596 // For client-side transports, packets may be queued before the asynchronous WebSocket
597 // handshake completes. The service thread drains that queue after CLIENT_ESTABLISHED.
598 if (ws_data->owns_context) {
599 // Check connection state without blocking the service thread.
600 mutex_lock(&ws_data->state_mutex);
601 bool connected = ws_data->is_connected;
602 bool connection_failed = ws_data->connection_failed;
603 mutex_unlock(&ws_data->state_mutex);
604
605 if (connection_failed) {
606 log_error("[WEBSOCKET_SEND] Connection failed - cannot send");
607 return SET_ERRNO(ERROR_NETWORK, "WebSocket connection failed");
608 }
609
610 if (!connected) {
611 log_debug("[WEBSOCKET_SEND] Connection is opening; queueing packet for delivery after handshake");
612 }
613
614 log_dev_every(1000000, "websocket_send (client): is_connected=%d, wsi=%p, send_len=%zu", connected,
615 (void *)ws_data->wsi, len);
616 } else {
618 "[WEBSOCKET_SEND_SERVER] Server transport send: wsi=%p, len=%zu (bypassing is_connected check)",
619 (void *)ws_data->wsi, len);
620 }
621
622 // Check if encryption is needed (matching tcp_send logic)
623 const void *send_data = data;
624 size_t send_len = len;
625 uint8_t *encrypted_packet = NULL;
626 size_t encrypted_packet_size = 0;
627
628 if (len >= sizeof(packet_header_t) && transport->crypto_ctx && crypto_is_ready(transport->crypto_ctx)) {
629 const packet_header_t *header = (const packet_header_t *)data;
630 uint16_t packet_type = NET_TO_HOST_U16(header->type);
631
632 if (!packet_is_handshake_type((packet_type_t)packet_type)) {
633 // Encrypt the entire packet (header + payload)
634 size_t ciphertext_size = len + CRYPTO_NONCE_SIZE + CRYPTO_MAC_SIZE;
635 // Use SAFE_MALLOC (not buffer pool - encrypted_packet also uses pool and causes overlap)
636 uint8_t *ciphertext = SAFE_MALLOC(ciphertext_size, uint8_t *);
637 if (!ciphertext) {
638 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate ciphertext buffer for WebSocket");
639 }
640
641 size_t ciphertext_len;
642 crypto_result_t result =
643 crypto_encrypt(transport->crypto_ctx, data, len, ciphertext, ciphertext_size, &ciphertext_len);
644 if (result != CRYPTO_OK) {
645 SAFE_FREE(ciphertext);
646 return SET_ERRNO(ERROR_CRYPTO, "Failed to encrypt WebSocket packet: %s", crypto_result_to_string(result));
647 }
648
649 // Build PACKET_TYPE_ENCRYPTED wrapper: header + ciphertext
650 size_t total_encrypted_size = sizeof(packet_header_t) + ciphertext_len;
651 encrypted_packet = buffer_pool_alloc(NULL, total_encrypted_size);
652 if (!encrypted_packet) {
653 SAFE_FREE(ciphertext);
654 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate encrypted packet buffer");
655 }
656
657 packet_header_t encrypted_header;
658 encrypted_header.magic = HOST_TO_NET_U64(PACKET_MAGIC);
659 encrypted_header.type = HOST_TO_NET_U16(PACKET_TYPE_ENCRYPTED);
660 encrypted_header.length = HOST_TO_NET_U32((uint32_t)ciphertext_len);
661 encrypted_header.crc32 = HOST_TO_NET_U32(asciichat_crc32(ciphertext, ciphertext_len));
662 encrypted_header.client_id = 0;
663
664 memcpy(encrypted_packet, &encrypted_header, sizeof(encrypted_header));
665 memcpy(encrypted_packet + sizeof(encrypted_header), ciphertext, ciphertext_len);
666 SAFE_FREE(ciphertext);
667
668 send_data = encrypted_packet;
669 send_len = total_encrypted_size;
670 encrypted_packet_size = total_encrypted_size;
671
672 log_dev_every(1000000, "WebSocket: encrypted packet (original type %d as PACKET_TYPE_ENCRYPTED, %zu bytes)",
673 packet_type, send_len);
674 }
675 }
676
677 // libwebsockets requires LWS_PRE bytes before the payload for protocol headers
678 // Allocate a temporary buffer for this send to avoid thread-safety issues
679 // Each send() call gets its own buffer, preventing race conditions with concurrent sends
680 size_t required_size = LWS_PRE + send_len;
681 uint8_t *send_buffer = SAFE_MALLOC(required_size, uint8_t *);
682 if (!send_buffer) {
683 if (encrypted_packet)
684 buffer_pool_free(NULL, encrypted_packet, encrypted_packet_size);
685 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate WebSocket send buffer");
686 }
687
688 // Copy data after LWS_PRE offset
689 memcpy(send_buffer + LWS_PRE, send_data, send_len);
690
691 // Server-side transports cannot call lws_write() directly
692 // They must queue data and send from LWS_CALLBACK_SERVER_WRITEABLE
693 if (!ws_data->owns_context) {
694 // Queue the data for server-side sending
695 // IMPORTANT: Allocate with LWS_PRE padding because lws_write() needs to write
696 // the WebSocket frame header backwards into the LWS_PRE region
697 // Use buffer_pool instead of SAFE_MALLOC to avoid use-after-free with
698 // permessage-deflate compression (same issue as client-side sends)
700 size_t buffer_size = LWS_PRE + send_len;
702 if (!msg.data) {
703 SAFE_FREE(send_buffer);
704 if (encrypted_packet)
705 buffer_pool_free(NULL, encrypted_packet, send_len);
706 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate send queue buffer");
707 }
708 // Copy data AFTER the LWS_PRE region
709 memcpy(msg.data + LWS_PRE, send_data, send_len);
710 msg.len = send_len;
711 msg.first = 1;
712 msg.final = 1;
713
714 mutex_lock(&ws_data->send_mutex);
715 bool success = ringbuffer_write(ws_data->send_queue, &msg);
716
717 if (!success) {
718 mutex_unlock(&ws_data->send_mutex);
719 log_error("WebSocket server send queue FULL - cannot queue %zu bytes (queue size=%d)", send_len,
722 SAFE_FREE(send_buffer);
723 if (encrypted_packet)
724 buffer_pool_free(NULL, encrypted_packet, encrypted_packet_size);
725 return SET_ERRNO(ERROR_NETWORK, "Send queue full (cannot queue %zu bytes)", send_len);
726 }
727 mutex_unlock(&ws_data->send_mutex);
728
729 log_debug(">>> SERVER FRAME QUEUED: %zu bytes for wsi=%p", send_len, (void *)ws_data->wsi);
730
731 // Wake the LWS service thread so it processes the queued data.
732 // lws_callback_on_writable() is NOT safe from non-service threads. Instead, use
733 // lws_cancel_service() which safely interrupts the blocking lws_service() poll.
734 // The service thread handles LWS_CALLBACK_EVENT_WAIT_CANCELLED by calling
735 // lws_callback_on_writable_all_protocol() for all protocols.
736 START_TIMER("ws_callback");
737 lws_cancel_service(ws_data->context);
738 STOP_TIMER_AND_LOG(dev, 0, "ws_callback", "[WEBSOCKET] lws_cancel_service");
739 log_debug(">>> CANCELLED SERVICE to wake event loop for wsi=%p", (void *)ws_data->wsi);
740
741 SAFE_FREE(send_buffer);
742 if (encrypted_packet)
743 buffer_pool_free(NULL, encrypted_packet, encrypted_packet_size);
744 return ASCIICHAT_OK;
745 }
746
747 // Client-side: queue entire message for service thread to send
748 // Avoid fragmentation race conditions by sending atomic messages from service thread
749 // libwebsockets handles automatic fragmentation internally when needed
750 // Allocate with LWS_PRE padding because lws_write() needs to write
751 // the WebSocket frame header backwards into the LWS_PRE region
752 // Use buffer_pool instead of SAFE_MALLOC to avoid use-after-free with permessage-deflate
753 // compression. libwebsockets may buffer this data asynchronously after lws_write() returns,
754 // so we can't free it immediately. buffer_pool_free() safely returns it to the pool instead
755 // of deallocating, preventing the use-after-free race condition with the compression layer.
757 size_t buffer_size = LWS_PRE + send_len;
759 if (!msg.data) {
760 SAFE_FREE(send_buffer);
761 if (encrypted_packet)
762 buffer_pool_free(NULL, encrypted_packet, encrypted_packet_size);
763 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate client send queue buffer");
764 }
765 // Copy data AFTER the LWS_PRE region
766 memcpy(msg.data + LWS_PRE, send_data, send_len);
767 msg.len = send_len;
768 msg.first = 1;
769 msg.final = 1;
770
771 mutex_lock(&ws_data->send_mutex);
772 bool success = ringbuffer_write(ws_data->send_queue, &msg);
773
774 if (!success) {
775 mutex_unlock(&ws_data->send_mutex);
776 log_error("WebSocket client send queue FULL - cannot queue %zu bytes (queue size=%d)", send_len,
779 SAFE_FREE(send_buffer);
780 if (encrypted_packet)
781 buffer_pool_free(NULL, encrypted_packet, encrypted_packet_size);
782 return SET_ERRNO(ERROR_NETWORK, "Client send queue full (cannot queue %zu bytes)", send_len);
783 }
784 mutex_unlock(&ws_data->send_mutex);
785
786 log_debug(">>> QUEUED CLIENT MESSAGE: %zu bytes queued at %p for service thread (wsi=%p)", send_len,
787 (void *)ws_data->wsi, (void *)ws_data->wsi);
788
789 // Wake the client's service thread from its blocking lws_service() call.
790 // Without this, the service thread may block in poll() for up to its timeout
791 // duration before checking the send queue, causing multi-second send delays.
792 lws_cancel_service(ws_data->context);
793
794 log_dev_every(1000000, "WebSocket client: queued %zu bytes for service thread to send", send_len);
795 SAFE_FREE(send_buffer);
796 if (encrypted_packet)
797 buffer_pool_free(NULL, encrypted_packet, encrypted_packet_size);
798 return ASCIICHAT_OK;
799}
800
801static asciichat_error_t websocket_recv(acip_transport_t *transport, void **buffer, size_t *out_len,
802 void **out_allocated_buffer) {
804
805 // Client transports are returned before the asynchronous WebSocket upgrade
806 // completes, so their first recv() must wait for the connection state. A
807 // server transport is created from LWS_CALLBACK_ESTABLISHED and may already
808 // have queued the client's first packet before its handler starts; applying
809 // the client-only gate there can hide that packet after an early close.
810 if (ws_data->owns_context) {
811 mutex_lock(&ws_data->state_mutex);
812 bool connected = ws_data->is_connected;
813 bool connection_failed = ws_data->connection_failed;
814 uint64_t start_time = time_get_ns();
815
816 if (!connected && !connection_failed) {
817 // Connection not yet established and hasn't failed - wait for it with timeout
818 log_info("WEBSOCKET_RECV: Connection not yet established, waiting (timeout=5s)");
819 // Wait up to 5 seconds for the state_cond to signal that is_connected changed
820 int wait_result = cond_timedwait(&ws_data->state_cond, &ws_data->state_mutex, 5 * 1000000000ULL);
821
822 uint64_t elapsed_ns = time_get_ns() - start_time;
823 uint64_t elapsed_ms = elapsed_ns / 1000000ULL;
824
825 log_info("WEBSOCKET_RECV: Wait returned (result=%d, elapsed=%llums)", wait_result,
826 (unsigned long long)elapsed_ms);
827
828 // Re-check after wait
829 connected = ws_data->is_connected;
830 connection_failed = ws_data->connection_failed;
831
832 log_info("WEBSOCKET_RECV: After wait: connected=%d, connection_failed=%d", connected, connection_failed);
833 }
834
835 mutex_unlock(&ws_data->state_mutex);
836
837 if (connection_failed && !connected) {
838 log_error("🔴 WEBSOCKET_RECV: Connection failed during establishment");
839 return SET_ERRNO(ERROR_NETWORK, "WebSocket connection failed");
840 }
841
842 if (!connected) {
843 log_error("🔴 WEBSOCKET_RECV: Connection timeout (waited 5 seconds)");
844 return SET_ERRNO(ERROR_NETWORK, "WebSocket connection timeout");
845 }
846 }
847
848 mutex_lock(&ws_data->recv_mutex);
849
850 // Even if connection is closed, we should try to deliver any buffered data
851 // Check if there's data in the queue
852 bool has_queued_data = !ringbuffer_is_empty(ws_data->recv_queue);
853
854 // Connection is established at this point, but might close during data transfer
855 // Deliver buffered data if available, otherwise wait
856 if (!has_queued_data && ws_data->partial_size == 0) {
857 // Check connection state again
858 mutex_lock(&ws_data->state_mutex);
859 bool still_connected = ws_data->is_connected;
860 mutex_unlock(&ws_data->state_mutex);
861
862 if (!still_connected) {
863 uint64_t now_ns = time_get_ns();
864 log_fatal("🔴 WEBSOCKET_RECV: Connection closed! has_queued_data=%d, partial_size=%zu, wsi=%p, "
865 "timestamp=%llu",
866 has_queued_data, ws_data->partial_size, (void *)ws_data->wsi, (unsigned long long)now_ns);
867 mutex_unlock(&ws_data->recv_mutex);
868 return SET_ERRNO(ERROR_NETWORK, "Connection closed");
869 }
870 }
871
872 // Reassemble fragmented WebSocket messages with SHORT timeout
873 // We queue each fragment from the LWS callback with first/final flags.
874 // Key insight: if we wait too long for final fragment, connection times out.
875 // Return partial messages quickly (500ms) to avoid blocking handler thread.
876 //
877 // CRITICAL FIX: Use persistent partial_buffer to handle packet boundaries
878 // that don't align with WebSocket frame boundaries. This prevents data loss
879 // when a single WebSocket message contains multiple ACIP packets.
880
881 uint8_t *assembled_buffer = NULL;
882 size_t assembled_size = 0;
883 size_t assembled_capacity = 0;
884 int fragment_count = 0;
885
886 // Start with leftover data from previous recv() call if available
887 if (ws_data->partial_size > 0) {
888 log_info("[WS_REASSEMBLE] Starting with leftover data from previous packet: %zu bytes", ws_data->partial_size);
889 assembled_buffer = ws_data->partial_buffer;
890 assembled_size = ws_data->partial_size;
891 assembled_capacity = ws_data->partial_capacity;
892
893 // Clear the partial buffer state so next call starts fresh if no more leftovers
894 ws_data->partial_buffer = NULL;
895 ws_data->partial_size = 0;
896 ws_data->partial_capacity = 0;
897 }
898
899 while (true) {
900 // Wait for fragment if queue is empty
901 while (ringbuffer_is_empty(ws_data->recv_queue)) {
902 // Don't timeout waiting for fragments. If the connection is dead, libwebsockets will
903 // set is_connected=false and we'll detect it below. Arbitrary timeouts just cause
904 // unnecessary disconnections while data is actually being transmitted.
905
906 // Check connection state - but don't fail immediately if connection closes
907 // while reassembling. Instead, return what we have so the handler can process it.
908 // This prevents losing buffered data due to connection timeouts.
909 mutex_lock(&ws_data->state_mutex);
910 bool still_connected = ws_data->is_connected;
911 mutex_unlock(&ws_data->state_mutex);
912
913 if (!still_connected && assembled_size > 0) {
914 // Connection closed but we have partial data - return it
915 log_info("[WS_REASSEMBLE] Connection closed mid-reassembly, returning %zu bytes received so far",
916 assembled_size);
917 *buffer = assembled_buffer;
918 *out_len = assembled_size;
919 *out_allocated_buffer = assembled_buffer;
920 mutex_unlock(&ws_data->recv_mutex);
921 return ASCIICHAT_OK;
922 }
923
924 if (!still_connected && assembled_size == 0) {
925 // Connection closed and no data yet
926 if (assembled_buffer) {
927 buffer_pool_free(NULL, assembled_buffer, assembled_capacity);
928 }
929 mutex_unlock(&ws_data->recv_mutex);
930 return SET_ERRNO(ERROR_NETWORK, "Connection closed");
931 }
932
933 // Wait for next fragment with 1ms timeout to allow signals to interrupt
934 cond_timedwait(&ws_data->recv_cond, &ws_data->recv_mutex, 1 * 1000000ULL);
935 }
936
937 // Read next fragment from queue
939 bool success = ringbuffer_read(ws_data->recv_queue, &frag);
940 if (!success) {
941 if (assembled_buffer) {
942 buffer_pool_free(NULL, assembled_buffer, assembled_capacity);
943 }
944 mutex_unlock(&ws_data->recv_mutex);
945 return SET_ERRNO(ERROR_NETWORK, "Failed to read fragment from queue");
946 }
947
948 fragment_count++;
949 if (frag.len > 100 || fragment_count == 1) {
950 log_info("[WS_REASSEMBLE] Fragment #%d: %zu bytes, first=%d, final=%d, assembled_so_far=%zu", fragment_count,
951 frag.len, frag.first, frag.final, assembled_size);
952 } else {
953 log_debug("[WS_REASSEMBLE] Fragment #%d: %zu bytes, first=%d, final=%d", fragment_count, frag.len, frag.first,
954 frag.final);
955 }
956
957 // Sanity check: first fragment must have first=1, continuations must have first=0
958 if (assembled_size == 0 && !frag.first) {
959 log_error("[WS_REASSEMBLE] ERROR: Expected first=1 for first fragment, got first=%d", frag.first);
960 buffer_pool_free(NULL, frag.data, frag.len);
961 if (assembled_buffer) {
962 buffer_pool_free(NULL, assembled_buffer, assembled_capacity);
963 }
964 mutex_unlock(&ws_data->recv_mutex);
965 return SET_ERRNO(ERROR_NETWORK, "Protocol error: continuation fragment without first fragment");
966 }
967
968 // Grow assembled buffer if needed
969 size_t required_size = assembled_size + frag.len;
970 if (required_size > assembled_capacity) {
971 // Start with 8KB, grow by 1.5x, but cap at 4MB to prevent unbounded allocation
972 const size_t MAX_REASSEMBLY_SIZE = (4 * 1024 * 1024); // 4MB limit
973
974 size_t new_capacity = (assembled_capacity == 0) ? 8192 : (assembled_capacity * 3 / 2);
975 if (new_capacity < required_size) {
976 new_capacity = required_size;
977 }
978
979 // Enforce maximum reassembly buffer size
980 if (new_capacity > MAX_REASSEMBLY_SIZE) {
981 log_error("[WS_REASSEMBLE] Frame too large: need %zu bytes, max allowed is %zu", new_capacity,
982 MAX_REASSEMBLY_SIZE);
983 buffer_pool_free(NULL, frag.data, frag.len);
984 if (assembled_buffer) {
985 buffer_pool_free(NULL, assembled_buffer, assembled_capacity);
986 }
987 mutex_unlock(&ws_data->recv_mutex);
988 return SET_ERRNO(ERROR_NETWORK, "WebSocket frame exceeds maximum size (4MB)");
989 }
990
991 uint8_t *new_buffer = buffer_pool_alloc(NULL, new_capacity);
992 if (!new_buffer) {
993 log_error("[WS_REASSEMBLE] Failed to allocate reassembly buffer (%zu bytes)", new_capacity);
994 buffer_pool_free(NULL, frag.data, frag.len);
995 if (assembled_buffer) {
996 buffer_pool_free(NULL, assembled_buffer, assembled_capacity);
997 }
998 mutex_unlock(&ws_data->recv_mutex);
999 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate fragment reassembly buffer");
1000 }
1001
1002 // Copy existing data to new buffer
1003 if (assembled_size > 0) {
1004 memcpy(new_buffer, assembled_buffer, assembled_size);
1005 }
1006
1007 // Free old buffer
1008 if (assembled_buffer) {
1009 buffer_pool_free(NULL, assembled_buffer, assembled_capacity);
1010 }
1011
1012 assembled_buffer = new_buffer;
1013 assembled_capacity = new_capacity;
1014 }
1015
1016 // Append fragment data with bounds checking
1017 if (frag.len > 0 && frag.data) {
1018 // Safety check: ensure we don't overflow the buffer
1019 if (assembled_size + frag.len > assembled_capacity) {
1020 log_error("[WS_REASSEMBLE] CRITICAL: Buffer overflow detected! assembled_size=%zu, frag.len=%zu, capacity=%zu",
1021 assembled_size, frag.len, assembled_capacity);
1022 buffer_pool_free(NULL, frag.data, frag.len);
1023 if (assembled_buffer) {
1024 buffer_pool_free(NULL, assembled_buffer, assembled_capacity);
1025 }
1026 mutex_unlock(&ws_data->recv_mutex);
1027 return SET_ERRNO(ERROR_MEMORY, "Fragment reassembly buffer overflow");
1028 }
1029
1030 memcpy(assembled_buffer + assembled_size, frag.data, frag.len);
1031 assembled_size += frag.len;
1032 }
1033
1034 // Free fragment data after copying (allocated in LWS callback with buffer_pool_alloc)
1035 if (frag.data) {
1036 // Use the fragment length that was stored when we allocated it
1037 // This must match the size passed to buffer_pool_alloc in the LWS callback
1038 buffer_pool_free(NULL, frag.data, frag.len);
1039 frag.data = NULL; // Prevent accidental double-free
1040 }
1041
1042 // Try to detect packet boundary using protocol structure
1043 // ACIP packet header: magic(8) + type(2) + length(4) + crc(4) + client_id(4) = 22 bytes
1044 // The length field (bytes 10-13) tells us the payload size
1045 if (assembled_size >= 14) { // Need at least 14 bytes to read length field at offset 10
1046 const uint8_t *data = assembled_buffer;
1047
1048 // Parse length field at offset 10 (4 bytes, big-endian)
1049 uint32_t msg_payload_len = (data[10] << 24) | (data[11] << 16) | (data[12] << 8) | data[13];
1050
1051 // Sanity check: payload length should be reasonable (< 5MB)
1052 if (msg_payload_len > 0 && msg_payload_len <= 5 * 1024 * 1024) {
1053 // Total packet size = header(14 bytes up to and including length field) + payload
1054 // Actually full header is 22 bytes but we can work with just the length field
1055 const size_t HEADER_SIZE = 22;
1056 size_t expected_size = HEADER_SIZE + msg_payload_len;
1057
1058 if (assembled_size >= expected_size) {
1059 // Complete packet assembled based on header length field
1060 log_info("[WS_REASSEMBLE] Complete message by length field: %zu bytes in %d fragments (payload=%u)",
1061 expected_size, fragment_count, msg_payload_len);
1062
1063 // Check for leftover data after this packet
1064 size_t leftover_size = assembled_size - expected_size;
1065 if (leftover_size > 0) {
1066 // Save leftover data for next recv() call
1067 log_info("[WS_REASSEMBLE] Saving %zu bytes leftover for next recv() call", leftover_size);
1068
1069 // Create buffer for leftover (or reuse if we can)
1070 uint8_t *leftover_buffer = buffer_pool_alloc(NULL, leftover_size);
1071 if (leftover_buffer) {
1072 memcpy(leftover_buffer, assembled_buffer + expected_size, leftover_size);
1073 ws_data->partial_buffer = leftover_buffer;
1074 ws_data->partial_size = leftover_size;
1075 ws_data->partial_capacity = leftover_size;
1076 } else {
1077 log_error("[WS_REASSEMBLE] Failed to allocate leftover buffer (%zu bytes)", leftover_size);
1078 // Fall through - return the packet anyway, we'll lose the leftover
1079 }
1080 }
1081
1082 // Create a new buffer with just the complete packet (not the leftover)
1083 uint8_t *packet_buffer = buffer_pool_alloc(NULL, expected_size);
1084 if (!packet_buffer) {
1085 log_error("[WS_REASSEMBLE] Failed to allocate packet buffer (%zu bytes)", expected_size);
1086 buffer_pool_free(NULL, assembled_buffer, assembled_capacity);
1087 mutex_unlock(&ws_data->recv_mutex);
1088 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate packet buffer");
1089 }
1090
1091 memcpy(packet_buffer, assembled_buffer, expected_size);
1092 buffer_pool_free(NULL, assembled_buffer, assembled_capacity);
1093
1094 *buffer = packet_buffer;
1095 *out_len = expected_size;
1096 *out_allocated_buffer = packet_buffer;
1097 mutex_unlock(&ws_data->recv_mutex);
1098 return ASCIICHAT_OK;
1099 }
1100 // Need more fragments to complete this message
1101 }
1102 }
1103
1104 // Fallback: check if we have the final WebSocket fragment
1105 // BUT: Only return if we have a COMPLETE ACIP packet!
1106 // ACIP packets can span multiple WebSocket frames, so final=true doesn't mean
1107 // we have the complete ACIP packet - we must verify using the ACIP header's length field
1108 if (frag.final) {
1109 // Check if this is actually a complete ACIP packet by validating the length field
1110 if (assembled_size >= 14) { // Minimum to read ACIP length field
1111 const uint8_t *data = (const uint8_t *)assembled_buffer;
1112 uint32_t msg_payload_len = (data[10] << 24) | (data[11] << 16) | (data[12] << 8) | data[13];
1113 const size_t HEADER_SIZE = 22;
1114 size_t expected_size = HEADER_SIZE + msg_payload_len;
1115
1116 if (assembled_size >= expected_size) {
1117 // We have a complete ACIP packet
1118 log_info("[WS_REASSEMBLE] Complete ACIP packet by WebSocket final fragment: %zu bytes in %d fragments",
1119 expected_size, fragment_count);
1120
1121 // Check for leftover data after this packet
1122 size_t leftover_size = assembled_size - expected_size;
1123 if (leftover_size > 0) {
1124 // Save leftover data for next recv() call
1125 log_info("[WS_REASSEMBLE] Saving %zu bytes leftover for next recv() call", leftover_size);
1126
1127 // Create buffer for leftover
1128 uint8_t *leftover_buffer = buffer_pool_alloc(NULL, leftover_size);
1129 if (leftover_buffer) {
1130 memcpy(leftover_buffer, assembled_buffer + expected_size, leftover_size);
1131 ws_data->partial_buffer = leftover_buffer;
1132 ws_data->partial_size = leftover_size;
1133 ws_data->partial_capacity = leftover_size;
1134 } else {
1135 log_error("[WS_REASSEMBLE] Failed to allocate leftover buffer (%zu bytes)", leftover_size);
1136 }
1137 }
1138
1139 // Create a new buffer with just the complete packet
1140 uint8_t *packet_buffer = buffer_pool_alloc(NULL, expected_size);
1141 if (!packet_buffer) {
1142 log_error("[WS_REASSEMBLE] Failed to allocate packet buffer (%zu bytes)", expected_size);
1143 buffer_pool_free(NULL, assembled_buffer, assembled_capacity);
1144 mutex_unlock(&ws_data->recv_mutex);
1145 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate packet buffer");
1146 }
1147
1148 memcpy(packet_buffer, assembled_buffer, expected_size);
1149 buffer_pool_free(NULL, assembled_buffer, assembled_capacity);
1150
1151 *buffer = packet_buffer;
1152 *out_len = expected_size;
1153 *out_allocated_buffer = packet_buffer;
1154 mutex_unlock(&ws_data->recv_mutex);
1155 return ASCIICHAT_OK;
1156 } else {
1157 // ACIP packet is incomplete even though WebSocket frame is final
1158 // This shouldn't happen - WebSocket is delivering corrupt data
1159 log_error("[WS_REASSEMBLE] ERROR: WebSocket final fragment but incomplete ACIP packet (have %zu, need %zu)",
1160 assembled_size, expected_size);
1161 buffer_pool_free(NULL, assembled_buffer, assembled_capacity);
1162 mutex_unlock(&ws_data->recv_mutex);
1163 return SET_ERRNO(ERROR_NETWORK, "WebSocket final fragment but incomplete ACIP packet");
1164 }
1165 }
1166 }
1167
1168 // More fragments coming, continue reassembling
1169 }
1170
1171 // Unreachable unless while loop breaks without returning
1172 mutex_unlock(&ws_data->recv_mutex);
1173 return SET_ERRNO(ERROR_NETWORK, "Unexpected exit from fragment reassembly loop");
1174}
1175
1176static asciichat_error_t websocket_close(acip_transport_t *transport) {
1178 uint64_t now_ns = time_get_ns();
1179
1180 // CRITICAL: Stop service thread BEFORE calling lws_close_reason()
1181 // Mark as destroying FIRST to signal service thread to stop immediately
1182 // This prevents service thread from trying to call lws_service on a destroying/destroyed context
1183 atomic_store_bool(&ws_data->is_destroying, true);
1184
1185 if (ws_data->service_running) {
1186 log_debug("[websocket_close] Stopping service thread to prevent deadlock during lws_close_reason()");
1187 ws_data->service_running = false;
1188 // Give service thread time to notice is_destroying flag
1189 platform_sleep_us(10000); // 10ms
1190 asciichat_thread_join(&ws_data->service_thread, NULL);
1191 log_debug("[websocket_close] Service thread stopped");
1192 }
1193
1194 mutex_lock(&ws_data->state_mutex);
1195
1196 if (!ws_data->is_connected) {
1197 mutex_unlock(&ws_data->state_mutex);
1198 log_info("websocket_close: Already closed (is_connected=false), wsi=%p, timestamp=%llu", (void *)ws_data->wsi,
1199 (unsigned long long)now_ns);
1200 return ASCIICHAT_OK; // Already closed
1201 }
1202
1203 log_fatal("🔴 websocket_close called! Setting is_connected=false, wsi=%p, timestamp=%llu", (void *)ws_data->wsi,
1204 (unsigned long long)now_ns);
1205 ws_data->is_connected = false;
1206 mutex_unlock(&ws_data->state_mutex);
1207
1208 // Close WebSocket connection
1209 // Safe to call lws_close_reason() now - service thread is stopped, so no callback contention
1210 if (ws_data->wsi) {
1211 log_fatal(" [websocket_close] Calling lws_close_reason for wsi=%p", (void *)ws_data->wsi);
1212 lws_close_reason(ws_data->wsi, LWS_CLOSE_STATUS_NORMAL, NULL, 0);
1213 log_fatal(" [websocket_close] lws_close_reason returned");
1214 }
1215
1216 // Wake any blocking recv() calls and send() waits
1217 // CRITICAL: Signal both recv_cond and state_cond to unblock all waiting threads
1218 // before the transport is destroyed. This prevents use-after-free when threads
1219 // wake from cond_timedwait() after the structure has been freed.
1220 cond_broadcast(&ws_data->recv_cond);
1221 cond_broadcast(&ws_data->state_cond);
1222
1223 log_info("WebSocket transport closed, wsi=%p", (void *)ws_data->wsi);
1224 return ASCIICHAT_OK;
1225}
1226
1227static acip_transport_type_t websocket_get_type(acip_transport_t *transport) {
1228 (void)transport;
1230}
1231
1232static socket_t websocket_get_socket(acip_transport_t *transport) {
1233 (void)transport;
1234 return INVALID_SOCKET_VALUE; // WebSocket has no underlying socket handle we can expose
1235}
1236
1237static bool websocket_is_connected(acip_transport_t *transport) {
1239
1240 mutex_lock(&ws_data->state_mutex);
1241 bool connected = ws_data->is_connected;
1242 mutex_unlock(&ws_data->state_mutex);
1243
1244 return connected;
1245}
1246
1247static bool websocket_has_pending_data(acip_transport_t *transport) {
1249 if (!ws_data || !ws_data->recv_queue) {
1250 return false;
1251 }
1252
1253 mutex_lock(&ws_data->recv_mutex);
1254 bool has_data = !ringbuffer_is_empty(ws_data->recv_queue);
1255 mutex_unlock(&ws_data->recv_mutex);
1256 return has_data;
1257}
1258
1259// =============================================================================
1260// WebSocket Transport Destroy Implementation
1261// =============================================================================
1262
1270static void websocket_destroy_impl(acip_transport_t *transport) {
1271 if (!transport || !transport->impl_data) {
1272 return;
1273 }
1274
1276
1277 // Mark transport as destroying and broadcast condition variables
1278 // This signals all waiting threads to wake and exit naturally.
1279 log_debug("[WEBSOCKET_DESTROY] Marking transport as destroying");
1280 atomic_store_bool(&ws_data->is_destroying, true);
1281
1282 // Broadcast all condition variables to wake waiting threads so they can check
1283 // the is_destroying flag and exit gracefully
1284 cond_broadcast(&ws_data->state_cond);
1285 cond_broadcast(&ws_data->recv_cond);
1286
1287 // Cancel any pending libwebsockets service calls to interrupt blocking lws_service()
1288 // This prevents the service thread from getting stuck in lws_service()
1289 if (ws_data->context) {
1290 log_debug("[WEBSOCKET_DESTROY] Cancelling libwebsockets service");
1291 lws_cancel_service(ws_data->context);
1292 }
1293
1294 // Give threads a brief moment to detect the flag and exit
1295 // Threads should check is_destroying before acquiring mutexes
1296 platform_sleep_us(50 * US_PER_MS_INT); // 50ms for threads to detect flag
1297
1298 // Stop service thread (client-side only)
1299 if (ws_data->service_running) {
1300 log_debug("Stopping WebSocket service thread");
1301 ws_data->service_running = false;
1302 asciichat_thread_join(&ws_data->service_thread, NULL);
1303 log_debug("WebSocket service thread stopped");
1304 }
1305
1306 // Close WebSocket connection gracefully before destroying context
1307 // Skip lws_wsi_close() to avoid segfault in libwebsockets during cleanup
1308 // The context_destroy below will handle cleanup properly
1309 ws_data->wsi = NULL;
1310
1311 // Destroy WebSocket context (only if we own it - client transports only)
1312 if (ws_data->context && ws_data->owns_context) {
1313 lws_context_destroy(ws_data->context);
1314 ws_data->context = NULL;
1315 }
1316
1317 // Clear receive queue and free buffered messages
1318 if (ws_data->recv_queue) {
1319 mutex_lock(&ws_data->recv_mutex);
1320
1322 while (ringbuffer_read(ws_data->recv_queue, &msg)) {
1323 if (msg.data) {
1324 buffer_pool_free(NULL, msg.data, msg.len);
1325 }
1326 }
1327
1328 mutex_unlock(&ws_data->recv_mutex);
1329 ringbuffer_destroy(ws_data->recv_queue);
1330 ws_data->recv_queue = NULL;
1331
1332 if (ws_data->send_queue) {
1333 // Drain send queue before destroying to free allocated message data
1334 // Use buffer_pool_free to match buffer_pool_alloc used for send messages
1336 while (ringbuffer_read(ws_data->send_queue, &msg)) {
1337 if (msg.data) {
1338 buffer_pool_free(NULL, msg.data, LWS_PRE + msg.len);
1339 }
1340 }
1341 ringbuffer_destroy(ws_data->send_queue);
1342 ws_data->send_queue = NULL;
1343 }
1344 }
1345
1346 // Free send buffer
1347 if (ws_data->send_buffer) {
1348 SAFE_FREE(ws_data->send_buffer);
1349 ws_data->send_buffer = NULL;
1350 }
1351
1352 // Free partial buffer (leftover data from previous recv())
1353 if (ws_data->partial_buffer) {
1354 buffer_pool_free(NULL, ws_data->partial_buffer, ws_data->partial_capacity);
1355 ws_data->partial_buffer = NULL;
1356 ws_data->partial_size = 0;
1357 ws_data->partial_capacity = 0;
1358 }
1359
1360 // Destroy synchronization primitives
1361 cond_destroy(&ws_data->state_cond);
1362 mutex_destroy(&ws_data->state_mutex);
1363 cond_destroy(&ws_data->recv_cond);
1364 mutex_destroy(&ws_data->recv_mutex);
1365 mutex_destroy(&ws_data->send_mutex);
1366 mutex_destroy(&ws_data->pending_free_mutex);
1367
1368 // Destroy pending-free queue
1369 if (ws_data->pending_free_queue) {
1370 ringbuffer_destroy(ws_data->pending_free_queue);
1371 ws_data->pending_free_queue = NULL;
1372 }
1373
1374 // Deregister websocket implementation data
1375 NAMED_UNREGISTER(ws_data);
1376
1377 // Clear impl_data pointer BEFORE freeing to prevent use-after-free in callbacks
1378 transport->impl_data = NULL;
1379
1380 // Free the websocket transport data structure
1381 SAFE_FREE(ws_data);
1382
1383 log_debug("Destroyed WebSocket transport resources");
1384}
1385
1386// =============================================================================
1387// WebSocket Transport Method Table
1388// =============================================================================
1389
1390static const acip_transport_methods_t websocket_methods = {
1391 .send = websocket_send,
1392 .recv = websocket_recv,
1393 .close = websocket_close,
1394 .get_type = websocket_get_type,
1395 .get_socket = websocket_get_socket,
1396 .is_connected = websocket_is_connected,
1397 .has_pending_data = websocket_has_pending_data,
1398 .destroy_impl = websocket_destroy_impl,
1399};
1400
1401// =============================================================================
1402// WebSocket Transport Creation
1403// =============================================================================
1404
1413 crypto_context_t *crypto_ctx) {
1414 if (!name) {
1415 SET_ERRNO(ERROR_INVALID_STATE, "Transport name is required");
1416 return NULL;
1417 }
1418
1419 if (!url) {
1420 SET_ERRNO(ERROR_INVALID_PARAM, "url is required");
1421 return NULL;
1422 }
1423
1424 // Parse URL to extract host, port, and path
1425 // Format: ws://host:port/path or wss://host:port/path
1426 const char *protocol_end = strstr(url, "://");
1427 if (!protocol_end) {
1428 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid WebSocket URL format (missing ://)");
1429 return NULL;
1430 }
1431
1432 bool use_ssl = (strncmp(url, "wss://", 6) == 0);
1433 const char *host_start = protocol_end + 3;
1434
1435 // Find port (if specified)
1436 const char *port_start = strchr(host_start, ':');
1437 const char *path_start = strchr(host_start, '/');
1438
1439 char host[256] = {0};
1440 int port = use_ssl ? 443 : 27226; // Default: wss:// uses 443, ws:// uses 27226 (ascii-chat WebSocket port)
1441 char path[256] = "/";
1442
1443 if (port_start && (!path_start || port_start < path_start)) {
1444 // Port is specified
1445 size_t host_len = port_start - host_start;
1446 if (host_len >= sizeof(host)) {
1447 SET_ERRNO(ERROR_INVALID_PARAM, "Host name too long");
1448 return NULL;
1449 }
1450 memcpy(host, host_start, host_len);
1451 host[host_len] = '\0';
1452
1453 // Extract port
1454 char *endptr;
1455 errno = 0;
1456 long port_val = strtol(port_start + 1, &endptr, 10);
1457 if (*endptr != '\0' || errno != 0 || port_val <= 0 || port_val > 65535) {
1458 log_error("websocket_transport_create: Invalid port number: %s", port_start + 1);
1459 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid port number");
1460 return NULL;
1461 }
1462 port = (uint16_t)port_val;
1463 } else {
1464 // No port specified, use default
1465 size_t host_len = path_start ? (size_t)(path_start - host_start) : strlen(host_start);
1466 if (host_len >= sizeof(host)) {
1467 SET_ERRNO(ERROR_INVALID_PARAM, "Host name too long");
1468 return NULL;
1469 }
1470 memcpy(host, host_start, host_len);
1471 host[host_len] = '\0';
1472 }
1473
1474 // Extract path
1475 if (path_start) {
1476 SAFE_STRNCPY(path, path_start, sizeof(path) - 1);
1477 path[sizeof(path) - 1] = '\0';
1478 }
1479
1480 log_info("Connecting to WebSocket: %s (host=%s, port=%d, path=%s, ssl=%d)", url, host, port, path, use_ssl);
1481
1482 // Allocate transport structure
1484 if (!transport) {
1485 SET_ERRNO(ERROR_MEMORY, "Failed to allocate WebSocket transport");
1486 return NULL;
1487 }
1488
1489 // Allocate WebSocket-specific data
1492 if (!ws_data) {
1493 SAFE_FREE(transport);
1494 SET_ERRNO(ERROR_MEMORY, "Failed to allocate WebSocket transport data");
1495 return NULL;
1496 }
1497
1498 // Initialize destruction state flag (CALLOC zero-initializes, but explicit for clarity)
1499 atomic_store_bool(&ws_data->is_destroying, false);
1500
1501 // Create receive queue
1503 if (!ws_data->recv_queue) {
1504 SAFE_FREE(ws_data);
1505 SAFE_FREE(transport);
1506 SET_ERRNO(ERROR_MEMORY, "Failed to create receive queue");
1507 return NULL;
1508 }
1509
1510 // Create send queue for client transport to buffer outgoing messages
1512 if (!ws_data->send_queue) {
1514 SAFE_FREE(ws_data);
1515 SAFE_FREE(transport);
1516 SET_ERRNO(ERROR_MEMORY, "Failed to create send queue");
1517 return NULL;
1518 }
1519
1520 // Initialize synchronization primitives with transport-aware names
1521 char recv_name[64], state_name[64];
1522 snprintf(recv_name, sizeof(recv_name), "recv_%s", name);
1523 snprintf(state_name, sizeof(state_name), "state_%s", name);
1524
1525 if (mutex_init(&ws_data->recv_mutex, recv_name) != 0) {
1528 SAFE_FREE(ws_data);
1529 SAFE_FREE(transport);
1530 SET_ERRNO(ERROR_INTERNAL, "Failed to initialize recv mutex");
1531 return NULL;
1532 }
1533
1534 if (cond_init(&ws_data->recv_cond, recv_name) != 0) {
1535 mutex_destroy(&ws_data->recv_mutex);
1538 SAFE_FREE(ws_data);
1539 SAFE_FREE(transport);
1540 SET_ERRNO(ERROR_INTERNAL, "Failed to initialize recv condition variable");
1541 return NULL;
1542 }
1543
1544 if (mutex_init(&ws_data->state_mutex, state_name) != 0) {
1545 cond_destroy(&ws_data->recv_cond);
1546 mutex_destroy(&ws_data->recv_mutex);
1549 SAFE_FREE(ws_data);
1550 SAFE_FREE(transport);
1551 SET_ERRNO(ERROR_INTERNAL, "Failed to initialize state mutex");
1552 return NULL;
1553 }
1554
1555 if (cond_init(&ws_data->state_cond, state_name) != 0) {
1556 mutex_destroy(&ws_data->state_mutex);
1557 cond_destroy(&ws_data->recv_cond);
1558 mutex_destroy(&ws_data->recv_mutex);
1561 SAFE_FREE(ws_data);
1562 SAFE_FREE(transport);
1563 SET_ERRNO(ERROR_INTERNAL, "Failed to initialize state condition variable");
1564 return NULL;
1565 }
1566
1567 // Allocate initial send buffer
1568 ws_data->send_buffer_capacity = LWS_PRE + 524288; // Match tx_packet_size (512KB)
1569 ws_data->send_buffer = SAFE_MALLOC(ws_data->send_buffer_capacity, uint8_t *);
1570 if (!ws_data->send_buffer) {
1571 mutex_destroy(&ws_data->state_mutex);
1572 cond_destroy(&ws_data->recv_cond);
1573 mutex_destroy(&ws_data->recv_mutex);
1576 SAFE_FREE(ws_data);
1577 SAFE_FREE(transport);
1578 SET_ERRNO(ERROR_MEMORY, "Failed to allocate send buffer");
1579 return NULL;
1580 }
1581
1582 // Create libwebsockets context
1583 // Protocol array must persist for lifetime of context - use static
1584 // Register "http" protocol for WebSocket connections - matches server-side protocol name
1585 // This ensures libwebsockets will invoke our websocket_callback during upgrade handshake and communication
1586 static struct lws_protocols client_protocols[] = {
1587 {
1588 "http", // Default HTTP protocol - handles WebSocket upgrade and ACIP communication
1589 websocket_callback,
1590 0, // Per-session data - using connect_info.userdata instead
1591 524288, // RX buffer size
1592 0, // ID
1593 NULL, // User pointer (set from connect_info.userdata)
1594 524288 // TX packet size
1595 },
1596 {
1597 "acip", // Application subprotocol selected by the ACIP WebSocket client
1598 websocket_callback,
1599 0,
1600 524288,
1601 0,
1602 NULL,
1603 524288
1604 },
1605 {NULL, NULL, 0, 0, 0, NULL, 0} // Terminator
1606 };
1607
1608 // Disable client compression for now - causes assertion in lws_set_extension_option()
1609 // This is a known issue with libwebsockets permessage-deflate negotiation
1610 // Server-side compression is still enabled, data will be compressed from server to client
1611 // but client->server traffic remains uncompressed (acceptable since client sends less data)
1612 // static const struct lws_extension client_extensions[] = {
1613 // {"permessage-deflate", lws_extension_callback_pm_deflate, "permessage-deflate; client_max_window_bits=15"},
1614 // {NULL, NULL, NULL}};
1615
1616 struct lws_context_creation_info info;
1617 memset(&info, 0, sizeof(info));
1618 info.port = CONTEXT_PORT_NO_LISTEN; // Client mode - no listening
1619 info.protocols = client_protocols;
1620 info.gid = (gid_t)-1; // Cast to avoid undefined behavior with unsigned type
1621 info.uid = (uid_t)-1; // Cast to avoid undefined behavior with unsigned type
1622 // Initialize SSL globally for client (required even for clients to use OpenSSL)
1623 // The LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT flag works for both servers and clients
1624 info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
1625 info.extensions = NULL; // Disable client compression due to lws_set_extension_option() assertion
1626
1627 // Increase per-thread service buffer to prevent fragmentation of large messages
1628 // Default is 4KB, causing 291KB frames to fragment into 73 × 4KB chunks
1629 // Increase to 512KB to match server and allow larger WebSocket frames without fragmentation
1630 info.pt_serv_buf_size = 512 * 1024; // 512KB per-thread service buffer
1631
1632 // Configure keep-alive to prevent idle disconnects during handshake
1633 // PING every 30 seconds if idle, close after 35 seconds total with no response
1634 static const lws_retry_bo_t client_keep_alive_policy = {
1635 .secs_since_valid_ping = 30, // Send PING after 30s idle
1636 .secs_since_valid_hangup = 35, // Hangup if still idle after 35s
1637 };
1638 info.retry_and_idle_policy = &client_keep_alive_policy;
1639
1640 // Enable libwebsockets logging through centralized logging system
1642
1643 ws_data->context = lws_create_context(&info);
1644
1645 if (!ws_data->context) {
1646 SAFE_FREE(ws_data->send_buffer);
1647 cond_destroy(&ws_data->state_cond);
1648 mutex_destroy(&ws_data->state_mutex);
1649 cond_destroy(&ws_data->recv_cond);
1650 mutex_destroy(&ws_data->recv_mutex);
1653 SAFE_FREE(ws_data);
1654 SAFE_FREE(transport);
1655 SET_ERRNO(ERROR_NETWORK, "Failed to create libwebsockets context");
1656 return NULL;
1657 }
1658
1659 // Initialize connection state flags BEFORE calling lws_client_connect_via_info
1660 // The callback might fire immediately, so all fields must be ready
1661 ws_data->is_connected = false; // Will be set to true in LWS_CALLBACK_CLIENT_ESTABLISHED
1662 ws_data->connection_failed = false; // Set to true in LWS_CALLBACK_CLIENT_CONNECTION_ERROR
1663 ws_data->owns_context = true; // Client transport owns the context
1664
1665 // Connect to WebSocket server
1666 log_debug("Initiating WebSocket connection to %s:%d%s", host, port, path);
1667 struct lws_client_connect_info connect_info;
1668 memset(&connect_info, 0, sizeof(connect_info));
1669 connect_info.context = ws_data->context;
1670 connect_info.address = host;
1671 connect_info.port = port;
1672 connect_info.path = path;
1673 connect_info.host = host;
1674 connect_info.origin = host;
1675 // Request the ACIP subprotocol so server routes to acip_protocol handler
1676 // This ensures WebSocket upgrade includes Sec-WebSocket-Protocol: acip
1677 connect_info.local_protocol_name = "acip";
1678 connect_info.protocol = "acip";
1679 // Use SSL + skip server certificate hostname verification + allow self-signed certs (for development)
1680 connect_info.ssl_connection =
1681 use_ssl ? (LCCSCF_USE_SSL | LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK | LCCSCF_ALLOW_SELFSIGNED) : 0;
1682 connect_info.opaque_user_data = ws_data;
1683
1684 log_debug("Calling lws_client_connect_via_info...");
1685 ws_data->wsi = lws_client_connect_via_info(&connect_info);
1686 log_debug("lws_client_connect_via_info returned: %p", (void *)ws_data->wsi);
1687 if (!ws_data->wsi) {
1688 lws_context_destroy(ws_data->context);
1689 SAFE_FREE(ws_data->send_buffer);
1690 cond_destroy(&ws_data->state_cond);
1691 mutex_destroy(&ws_data->state_mutex);
1692 cond_destroy(&ws_data->recv_cond);
1693 mutex_destroy(&ws_data->recv_mutex);
1696 SAFE_FREE(ws_data);
1697 SAFE_FREE(transport);
1698 SET_ERRNO(ERROR_NETWORK, "Failed to connect to WebSocket server");
1699 return NULL;
1700 }
1701
1702 // Initialize transport
1703 transport->methods = &websocket_methods;
1704 transport->crypto_ctx = crypto_ctx;
1705 transport->impl_data = ws_data;
1706
1707 // Start service thread immediately - it will drive TLS handshake with frequent polling
1708 // The service thread checks is_destroying and context validity before calling lws_service,
1709 // so it's safe to start it right away without delays.
1710 ws_data->service_running = true;
1711 if (asciichat_thread_create(&ws_data->service_thread, "ws_service", websocket_service_thread, ws_data) != 0) {
1712 log_error("Failed to create WebSocket service thread");
1713 ws_data->service_running = false;
1714 lws_context_destroy(ws_data->context);
1715 SAFE_FREE(ws_data->send_buffer);
1716 cond_destroy(&ws_data->state_cond);
1717 mutex_destroy(&ws_data->state_mutex);
1718 cond_destroy(&ws_data->recv_cond);
1719 mutex_destroy(&ws_data->recv_mutex);
1722 SAFE_FREE(ws_data);
1723 SAFE_FREE(transport);
1724 SET_ERRNO(ERROR_INTERNAL, "Failed to create service thread");
1725 return NULL;
1726 }
1727 log_debug("WebSocket service thread started");
1728
1729 // CRITICAL FIX: Do NOT block the main thread waiting for connection!
1730 // This prevents stdin/keyboard input from being processed and causes the client to hang.
1731 // The service thread will establish the connection asynchronously.
1732 // The protocol layer (recv) will detect if connection fails and handle it there.
1733 // Return immediately so the main thread can respond to keyboard input.
1734 log_debug("WebSocket transport created, service thread will establish connection asynchronously");
1735
1736 // Register transport
1737 NAMED_REGISTER_TRANSPORT(transport, name, NULL);
1738
1739 // Register websocket implementation data with transport as parent
1740 (void)NAMED_REGISTER_WEBSOCKET_IMPL(ws_data, "impl", (uintptr_t)(const void *)(transport));
1741
1742 // Return transport immediately - connection will be established by service thread
1743 // If connection fails, recv() will detect is_connected=false and return error
1744 return transport;
1745}
1746
1758 crypto_context_t *crypto_ctx) {
1759 if (!name) {
1760 SET_ERRNO(ERROR_INVALID_STATE, "Transport name is required");
1761 return NULL;
1762 }
1763
1764 if (!wsi) {
1765 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid wsi parameter");
1766 return NULL;
1767 }
1768
1769 // Allocate transport structure
1771 if (!transport) {
1772 SET_ERRNO(ERROR_MEMORY, "Failed to allocate WebSocket transport");
1773 return NULL;
1774 }
1775
1776 // Allocate transport-specific data
1779 if (!ws_data) {
1780 SAFE_FREE(transport);
1781 SET_ERRNO(ERROR_MEMORY, "Failed to allocate WebSocket transport data");
1782 return NULL;
1783 }
1784
1785 // Initialize receive queue
1787 if (!ws_data->recv_queue) {
1788 SAFE_FREE(ws_data);
1789 SAFE_FREE(transport);
1790 SET_ERRNO(ERROR_MEMORY, "Failed to create receive queue");
1791 return NULL;
1792 }
1793
1794 // Initialize send queue (for server-side transports)
1796 if (!ws_data->send_queue) {
1798 SAFE_FREE(ws_data);
1799 SAFE_FREE(transport);
1800 SET_ERRNO(ERROR_MEMORY, "Failed to create send queue");
1801 return NULL;
1802 }
1803
1804 // Initialize pending-free queue for deferred buffer freeing
1805 // permessage-deflate compression holds buffer references asynchronously,
1806 // so we defer freeing to prevent use-after-free errors
1808 if (!ws_data->pending_free_queue) {
1811 SAFE_FREE(ws_data);
1812 SAFE_FREE(transport);
1813 SET_ERRNO(ERROR_MEMORY, "Failed to create pending-free queue");
1814 return NULL;
1815 }
1816
1817 // Initialize synchronization primitives with transport-aware names
1818 char recv_name[64], send_name[64], state_name[64], pending_free_name[64];
1819 snprintf(recv_name, sizeof(recv_name), "recv_%s", name);
1820 snprintf(send_name, sizeof(send_name), "send_%s", name);
1821 snprintf(state_name, sizeof(state_name), "state_%s", name);
1822 snprintf(pending_free_name, sizeof(pending_free_name), "pending_free_%s", name);
1823
1824 if (mutex_init(&ws_data->recv_mutex, recv_name) != 0) {
1827 SAFE_FREE(ws_data);
1828 SAFE_FREE(transport);
1829 SET_ERRNO(ERROR_NETWORK, "Failed to initialize recv mutex");
1830 return NULL;
1831 }
1832
1833 if (cond_init(&ws_data->recv_cond, recv_name) != 0) {
1834 mutex_destroy(&ws_data->recv_mutex);
1837 SAFE_FREE(ws_data);
1838 SAFE_FREE(transport);
1839 SET_ERRNO(ERROR_NETWORK, "Failed to initialize recv condition variable");
1840 return NULL;
1841 }
1842
1843 if (mutex_init(&ws_data->send_mutex, send_name) != 0) {
1844 cond_destroy(&ws_data->recv_cond);
1845 mutex_destroy(&ws_data->recv_mutex);
1848 SAFE_FREE(ws_data);
1849 SAFE_FREE(transport);
1850 SET_ERRNO(ERROR_NETWORK, "Failed to initialize send mutex");
1851 return NULL;
1852 }
1853
1854 if (mutex_init(&ws_data->state_mutex, state_name) != 0) {
1855 mutex_destroy(&ws_data->send_mutex);
1856 cond_destroy(&ws_data->recv_cond);
1857 mutex_destroy(&ws_data->recv_mutex);
1860 SAFE_FREE(ws_data);
1861 SAFE_FREE(transport);
1862 SET_ERRNO(ERROR_NETWORK, "Failed to initialize state mutex");
1863 return NULL;
1864 }
1865
1866 if (cond_init(&ws_data->state_cond, state_name) != 0) {
1867 mutex_destroy(&ws_data->state_mutex);
1868 mutex_destroy(&ws_data->send_mutex);
1869 cond_destroy(&ws_data->recv_cond);
1870 mutex_destroy(&ws_data->recv_mutex);
1873 SAFE_FREE(ws_data);
1874 SAFE_FREE(transport);
1875 SET_ERRNO(ERROR_NETWORK, "Failed to initialize state condition variable");
1876 return NULL;
1877 }
1878
1879 if (mutex_init(&ws_data->pending_free_mutex, pending_free_name) != 0) {
1880 cond_destroy(&ws_data->state_cond);
1881 mutex_destroy(&ws_data->state_mutex);
1882 mutex_destroy(&ws_data->send_mutex);
1883 cond_destroy(&ws_data->recv_cond);
1884 mutex_destroy(&ws_data->recv_mutex);
1888 SAFE_FREE(ws_data);
1889 SAFE_FREE(transport);
1890 SET_ERRNO(ERROR_NETWORK, "Failed to initialize pending-free mutex");
1891 return NULL;
1892 }
1893
1894 // Allocate send buffer with LWS_PRE padding
1895 size_t initial_capacity = 4096 + LWS_PRE;
1896 ws_data->send_buffer = SAFE_MALLOC(initial_capacity, uint8_t *);
1897 if (!ws_data->send_buffer) {
1898 mutex_destroy(&ws_data->state_mutex);
1899 mutex_destroy(&ws_data->send_mutex);
1900 cond_destroy(&ws_data->recv_cond);
1901 mutex_destroy(&ws_data->recv_mutex);
1904 SAFE_FREE(ws_data);
1905 SAFE_FREE(transport);
1906 SET_ERRNO(ERROR_MEMORY, "Failed to allocate send buffer");
1907 return NULL;
1908 }
1909 ws_data->send_buffer_capacity = initial_capacity;
1910
1911 // Store connection info (server-side: no context ownership, connection already established)
1912 ws_data->wsi = wsi;
1913 ws_data->context = lws_get_context(wsi); // Get context from wsi (not owned)
1914 ws_data->owns_context = false; // Server owns context, not transport
1915 ws_data->is_connected = true; // Already connected (server-side)
1916 log_info("[WEBSOCKET_TRANSPORT_CREATE] ★★★ SERVER TRANSPORT CREATED: is_connected=true, wsi=%p, ws_data=%p",
1917 (void *)wsi, (void *)ws_data);
1918 log_debug("Server transport created: is_connected=true, wsi=%p", (void *)wsi);
1919
1920 // Initialize transport
1921 transport->methods = &websocket_methods;
1922 transport->crypto_ctx = crypto_ctx;
1923 transport->impl_data = ws_data;
1924
1925 log_info("Created WebSocket server transport (crypto: %s)", crypto_ctx ? "enabled" : "disabled");
1926
1927 // Register transport
1928 NAMED_REGISTER_TRANSPORT(transport, name, NULL);
1929
1930 // Register websocket implementation data with transport as parent
1931 (void)NAMED_REGISTER_WEBSOCKET_IMPL(ws_data, "impl", (uintptr_t)(const void *)(transport));
1932
1933 return transport;
1934}
void atomic_store_bool(atomic_t *a, bool value)
Atomically store a boolean value.
Definition atomic.c:177
bool atomic_load_bool(atomic_t *a)
Atomically load a boolean value.
Definition atomic.c:169
🗃️ Lock-Free Unified Memory Buffer Pool with Lazy Allocation
Cross-platform condition variable interface for ascii-chat.
Hardware-Accelerated CRC32 Checksum Computation.
🔍 Memory debugging helpers for tracking allocations in debug builds
Named object registry for debugging — log identifiable resource names.
🔄 Network byte order conversion helpers
#define HOST_TO_NET_U16(val)
Definition endian.h:96
#define HOST_TO_NET_U32(val)
Definition endian.h:66
#define HOST_TO_NET_U64(val)
Definition endian.h:213
#define NET_TO_HOST_U16(val)
Definition endian.h:111
Cross-platform error handling utilities.
int buffer_size
Size of circular buffer.
Definition grep.c:90
void buffer_pool_free(buffer_pool_t *pool, const void *data, size_t size)
Free a buffer back to the pool (lock-free)
void * buffer_pool_alloc(buffer_pool_t *pool, size_t size)
Allocate a buffer from the pool (lock-free fast path)
unsigned short uint16_t
Definition common.h:57
unsigned int uint32_t
Definition common.h:58
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:414
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MALLOC(size, cast)
Definition common.h:264
#define SAFE_CALLOC(count, size, cast)
Definition common.h:274
unsigned long long uint64_t
Definition common.h:59
unsigned char uint8_t
Definition common.h:56
crypto_result_t crypto_encrypt(crypto_context_t *ctx, const uint8_t *plaintext, size_t plaintext_len, uint8_t *ciphertext_out, size_t ciphertext_out_size, size_t *ciphertext_len_out)
Encrypt data using XSalsa20-Poly1305.
#define CRYPTO_NONCE_SIZE
Nonce size (XSalsa20)
const char * crypto_result_to_string(crypto_result_t result)
Convert crypto result to human-readable string.
crypto_result_t
Cryptographic operation result codes.
#define CRYPTO_MAC_SIZE
MAC size (Poly1305)
bool crypto_is_ready(const crypto_context_t *ctx)
Check if key exchange is complete and ready for encryption.
#define NAMED_REGISTER_WEBSOCKET_IMPL(data, name, parent_ptr)
#define NAMED_REGISTER_TRANSPORT(transport, name, parent_ptr)
Register a transport with automatic format specifier.
#define NAMED_UNREGISTER(ptr)
Unregister a pointer.
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ERROR_INVALID_STATE
@ ERROR_NETWORK
Definition error_codes.h:77
@ ERROR_MEMORY
Definition error_codes.h:56
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_CRYPTO
Definition error_codes.h:96
@ ERROR_INVALID_PARAM
@ ERROR_INTERNAL
Definition error_codes.h:92
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
#define log_dev(...)
Log a DEV message (most verbose, development only)
Definition log/log.h:534
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
#define log_fatal(...)
Log a FATAL message.
Definition log/log.h:599
#define log_info(...)
Log an INFO message.
Definition log/log.h:561
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
#define STOP_TIMER_AND_LOG(log_level, threshold_ns, timer_name, msg_fmt,...)
Stop a timer and log the result with a custom message.
Definition time.h:612
uint64_t time_get_ns(void)
Get current monotonic time in nanoseconds.
Definition util/time.c:108
int time_pretty(uint64_t nanoseconds, int decimals, char *buffer, size_t buffer_size)
Format nanoseconds as pretty duration with spaces and configurable precision.
Definition util/time.c:424
#define START_TIMER(name_fmt,...)
Start a timer with formatted name.
Definition time.h:333
#define US_PER_MS_INT
Definition time.h:160
packet_type_t
Network protocol packet type enumeration.
Definition packet.h:286
#define PACKET_MAGIC
Packet magic number (alias for MAGIC_PACKET_VALID)
Definition packet.h:255
@ PACKET_TYPE_ENCRYPTED
Encrypted packet (after handshake completion)
Definition packet.h:333
#define mutex_lock(mutex)
Lock a mutex (with debug tracking in debug builds)
int cond_broadcast(cond_t *cond)
Broadcast to a condition variable (wake all waiting threads)
#define INVALID_SOCKET_VALUE
Invalid socket value (POSIX: -1)
Definition socket.h:278
int mutex_init(mutex_t *mutex, const char *name)
Initialize a mutex with a name.
Definition threading.c:16
int cond_signal(cond_t *cond)
Signal a condition variable (wake one waiting thread)
int cond_init(cond_t *cond, const char *name)
Initialize a condition variable with a name.
void platform_sleep_us(unsigned int us)
High-precision sleep function with microsecond precision.
int errno
#define mutex_unlock(mutex)
Unlock a mutex (with debug tracking in debug builds)
#define cond_timedwait(cond, mutex, timeout_ns)
Wait on a condition variable with timeout (with debug tracking in debug builds)
Definition cond.h:300
int cond_destroy(cond_t *cond)
Destroy a condition variable.
int mutex_destroy(mutex_t *mutex)
Destroy a mutex.
Definition threading.c:22
ringbuffer_t * ringbuffer_create(size_t element_size, size_t capacity)
Create a new ring buffer.
Definition ringbuffer.c:29
void ringbuffer_destroy(ringbuffer_t *rb)
Destroy a ring buffer and free its memory.
Definition ringbuffer.c:53
bool ringbuffer_is_empty(const ringbuffer_t *rb)
Check if buffer is empty.
Definition ringbuffer.c:125
bool ringbuffer_read(ringbuffer_t *rb, void *data)
Try to read an element from the ring buffer (non-blocking)
Definition ringbuffer.c:82
bool ringbuffer_write(ringbuffer_t *rb, const void *data)
Try to write an element to the ring buffer (non-blocking)
Definition ringbuffer.c:60
bool ringbuffer_peek(ringbuffer_t *rb, void *data)
Peek at the next element without removing it.
Definition ringbuffer.c:104
#define asciichat_crc32(data, len)
Main CRC32 dispatcher macro - use this in application code.
Definition crc32.h:144
int socket_t
#define asciichat_thread_create(thread_ptr, attr, start_routine, arg)
#define asciichat_thread_join(thread, timeout_ms)
Application limits and constraints.
📝 Logging API with multiple log levels and terminal output control
#define log_dev_every(interval_us, fmt,...)
Rate-limited DEV logging.
Definition log/log.h:699
Internal WebSocket implementation types shared between transport.c and server.c.
Cross-platform mutex interface for ascii-chat.
Lock-Free Ring Buffer and Frame Buffer Management.
Transport method table (virtual function table)
Definition transport.h:115
asciichat_error_t(* send)(acip_transport_t *transport, const void *data, size_t len)
Send data through this transport.
Definition transport.h:129
Transport instance structure.
Definition transport.h:214
void * impl_data
Transport-specific state.
Definition transport.h:218
const acip_transport_methods_t * methods
Method table (virtual functions)
Definition transport.h:215
crypto_context_t * crypto_ctx
Optional encryption context.
Definition transport.h:216
Cryptographic context structure.
Network packet header structure.
Definition packet.h:598
uint32_t client_id
Client ID (0 = server, >0 = client identifier)
Definition packet.h:608
uint32_t length
Payload data length in bytes (0 for header-only packets)
Definition packet.h:604
uint64_t magic
Magic number (PACKET_MAGIC = 0xA5C11C4A1 "ASCIICHAT" in hex) for packet validation.
Definition packet.h:600
uint16_t type
Packet type (packet_type_t enumeration)
Definition packet.h:602
uint32_t crc32
CRC32 checksum of payload data (0 if length == 0)
Definition packet.h:606
Pending buffer free item for deferred cleanup Used to defer buffer freeing when permessage-deflate co...
uint8_t * ptr
Pointer to buffer to free.
WebSocket message element (individual frame or fragment)
size_t len
Frame length in bytes.
uint8_t first
1 if first frame of message (or complete message)
uint8_t final
1 if final frame of message (or complete message)
uint8_t * data
Frame data (allocated, caller must free)
WebSocket transport implementation data.
struct lws_context * context
libwebsockets context (may be owned or borrowed)
mutex_t send_mutex
Protect send_queue operations only.
bool owns_context
True if transport owns context (client), false if borrowed (server)
cond_t recv_cond
Signal when recv_queue messages arrive.
mutex_t state_mutex
Protect state changes.
ringbuffer_t * send_queue
Send message queue (for server-side transports)
ringbuffer_t * pending_free_queue
Queue of buffers pending delayed free.
struct lws * wsi
libwebsockets instance (owned)
ringbuffer_t * recv_queue
Receive message queue.
size_t send_buffer_capacity
Current send buffer capacity.
cond_t state_cond
Signal when is_connected changes (for connection wait)
uint8_t * send_buffer
Send buffer with LWS_PRE padding.
bool connection_failed
Connection attempt failed (for client-side detection)
mutex_t pending_free_mutex
Protect pending_free_queue operations.
mutex_t recv_mutex
Protect recv_queue operations only.
asciichat_thread_t service_thread
Thread that services libwebsockets context.
volatile bool service_running
Service thread running flag.
atomic_t is_destroying
True when destruction has started.
Cross-platform system functions interface for ascii-chat.
Transport abstraction layer for ACIP protocol.
acip_transport_type_t
Transport type enumeration.
Definition transport.h:93
@ ACIP_TRANSPORT_WEBSOCKET
WebSocket over TCP.
Definition transport.h:95
#define WEBSOCKET_SEND_QUEUE_SIZE
acip_transport_t * acip_websocket_client_transport_create(const char *name, const char *url, crypto_context_t *crypto_ctx)
Create WebSocket client transport.
#define WEBSOCKET_RECV_QUEUE_SIZE
acip_transport_t * acip_websocket_server_transport_create(const char *name, struct lws *wsi, crypto_context_t *crypto_ctx)
Create WebSocket server transport from existing connection.
void lws_log_init_client(void)
Initialize libwebsockets logging for client mode.
Definition websocket.c:99
Libwebsockets logging integration.