ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
server/main.c
Go to the documentation of this file.
1
51#ifdef _WIN32
52#include <io.h>
53#else
54#include <unistd.h> // For write(), STDERR_FILENO (signal handler), dup(), dup2()
55#include <fcntl.h> // For open() and O_WRONLY
56#endif
57
58#include <ascii-chat/platform/network.h> // Consolidates platform-specific network headers
60
61#include <errno.h>
62#include <limits.h>
63#include <signal.h>
64#include <stdbool.h>
65#include <stdio.h>
66#include <stdlib.h>
67#include <stdint.h>
68#include <string.h>
69#include <sys/types.h>
70#include <sys/stat.h>
71#include <time.h>
72
73#include "main.h"
74#include "../main.h" // For should_exit() and signal_exit() API
76#include <ascii-chat/common.h>
78#include <ascii-chat/util/ip.h>
81#include <ascii-chat/uthash.h>
98#include <ascii-chat/options/rcu.h> // For RCU-based options access
102#include "session/h265/server.h"
103#include "client.h"
104#include "stream.h"
105#include "stats.h"
125#include <ascii-chat/ui/status.h>
127#include <ascii-chat/log/json.h>
131#include <ascii-chat/atomic.h>
132
133/* ============================================================================
134 * Global State
135 * ============================================================================
136 */
137
160static bool check_shutdown(void) {
161 return should_exit();
162}
163
175mixer_t *volatile g_audio_mixer = NULL;
176
188
197
218
219// TCP server is now owned by session_server_like; access via session_server_like_get_tcp_server().
220
221// WebSocket server is now owned by session_server_like; access via session_server_like_get_websocket_server().
222
229static time_t g_server_start_time = 0;
230
237static uint64_t g_last_status_update = 0; // Microseconds from platform_get_monotonic_time_us()
238
239// Status screen thread, keyboard thread, and SPSC queue are now owned by session_server_like.
240
247static char g_session_string[64] = {0};
248
252static bool g_session_is_mdns_only = false;
253
254// UPnP and mDNS contexts are now owned by session_server_like; access via accessors.
255
265static acds_client_t *g_acds_client = NULL;
266
275static acip_transport_t *g_acds_transport = NULL;
276
283static uint8_t g_server_participant_id[16] = {0};
284
294static webrtc_peer_manager_t *g_webrtc_peer_manager = NULL;
295
305static asciichat_thread_t g_acds_receive_thread;
306
314static bool g_acds_receive_thread_started = false;
315
325static asciichat_thread_t g_acds_ping_thread;
326
334static bool g_acds_ping_thread_started = false;
335
342static thread_pool_t *g_server_worker_pool = NULL;
343
344/* ============================================================================
345 * Server Crypto State
346 * ============================================================================
347 */
348
360
373
386
396
409
419
420/* ============================================================================
421 * Signal Handlers
422 * ============================================================================
423 */
424
466static void server_handle_sigint(int sig) {
467 (void)sig;
468 // If in grep input mode, cancel grep instead of shutting down.
469 // Uses atomic reads only (no mutex) - safe from signal context.
472 return;
473 }
474
475 static atomic_t sigint_count = {0};
476 int count = atomic_fetch_add_int(&sigint_count, 1) + 1;
477 if (count > 1) {
479 }
480
481 // STEP 1: Set atomic shutdown flag (checked by all worker threads)
483
484 // STEP 2: Use raw write() for user feedback (async-signal-safe).
485 // log_info_nofile() is NOT safe here - it calls SAFE_MALLOC (via ansi_strip_escapes),
486 // SAFE_FREE, and server_like_status_log_append (mutex). If the signal interrupts a thread
487 // holding the memory tracking lock or log buffer lock, the handler deadlocks.
488 // Since SIGINT auto-masks during handler execution, a second Ctrl+C would be
489 // blocked, preventing platform_force_exit() from ever running.
490 // INFO: commented out because it's too noisy, uncomment to debug
491 // static const char sigint_msg[] = "SIGINT received - shutting down server...\n";
492 // platform_write_all(STDERR_FILENO, sigint_msg, sizeof(sigint_msg) - 1);
493
494 // STEP 3: Signal TCP server to stop and close listening sockets
495 // This interrupts the accept() call in the main loop
497 if (tcp) {
498 atomic_store_bool(&tcp->running, false);
502 }
506 }
507 }
508
509 // STEP 3b: Signal WebSocket server to stop
510 // Without this, lws_service() could block for up to 50ms before checking the running flag.
511 // websocket_server_cancel_service() is async-signal-safe (uses only atomic operations).
513 if (ws) {
514 atomic_store_u64(&ws->running, false);
516 }
517
518 // STEP 4: DO NOT access client data structures in signal handler
519 // Signal handlers CANNOT safely use mutexes, rwlocks, or access complex data structures
520 // This causes deadlocks and memory access violations because:
521 // 1. Signal may interrupt a thread that already holds these locks
522 // 2. Attempting to acquire locks in signal handler = instant deadlock
523 // 3. Client array might be in an inconsistent state during modification
524 //
525 // SOLUTION: The listening socket closure above is sufficient to unblock accept_with_timeout()
526 // The main thread will detect g_should_exit and properly close client sockets with timeouts
527
528 // NOTE: Do NOT call log_destroy() here - it's not async-signal-safe
529 // The main thread will handle cleanup when it detects g_should_exit
530}
531
532// bind_and_listen() function removed - now using lib/network/tcp_server abstraction
533
534/* ============================================================================
535 * WebRTC Callbacks
536 * ============================================================================
537 */
538
552static asciichat_error_t server_send_sdp(const uint8_t session_id[16], const uint8_t recipient_id[16],
553 const char *sdp_type, const char *sdp, void *user_data) {
554 (void)user_data;
555
556 if (!g_acds_transport) {
557 return SET_ERRNO(ERROR_INVALID_STATE, "ACDS transport not available for SDP relay");
558 }
559
560 // Calculate SDP length
561 size_t sdp_len = strlen(sdp);
562 if (sdp_len == 0 || sdp_len >= 8192) {
563 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid SDP length: %zu", sdp_len);
564 }
565
566 // Allocate packet buffer (header + SDP string)
567 size_t total_len = sizeof(acip_webrtc_sdp_t) + sdp_len;
568 uint8_t *packet = SAFE_MALLOC(total_len, uint8_t *);
569 if (!packet) {
570 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate SDP packet");
571 }
572
573 // Fill header
574 acip_webrtc_sdp_t *header = (acip_webrtc_sdp_t *)packet;
575 memcpy(header->session_id, session_id, 16);
576 // Use server's participant_id from SESSION_JOIN as sender
577 memcpy(header->sender_id, g_server_participant_id, 16);
578 memcpy(header->recipient_id, recipient_id, 16);
579 header->sdp_type = (strcmp(sdp_type, "offer") == 0) ? 0 : 1;
580 header->sdp_len = HOST_TO_NET_U16((uint16_t)sdp_len);
581
582 // Copy SDP string after header (binary data, not null-terminated)
583 // NOLINTNEXTLINE(bugprone-not-null-terminated-result)
584 memcpy(packet + sizeof(acip_webrtc_sdp_t), sdp, sdp_len);
585
586 log_debug("Server sending WebRTC SDP %s to participant (sender=%02x%02x..., recipient=%02x%02x...) via ACDS",
587 sdp_type, g_server_participant_id[0], g_server_participant_id[1], recipient_id[0], recipient_id[1]);
588
589 // Send via ACDS transport using generic packet sender
590 asciichat_error_t result =
591 packet_send_via_transport(g_acds_transport, PACKET_TYPE_ACIP_WEBRTC_SDP, packet, total_len, 0);
592
593 SAFE_FREE(packet);
594
595 if (result != ASCIICHAT_OK) {
596 return SET_ERRNO(result, "Failed to send SDP via ACDS");
597 }
598
599 return ASCIICHAT_OK;
600}
601
615static asciichat_error_t server_send_ice(const uint8_t session_id[16], const uint8_t recipient_id[16],
616 const char *candidate, const char *mid, void *user_data) {
617 (void)user_data;
618
619 if (!g_acds_transport) {
620 return SET_ERRNO(ERROR_INVALID_STATE, "ACDS transport not available for ICE relay");
621 }
622
623 // Calculate payload length (candidate + null + mid + null)
624 size_t candidate_len = strlen(candidate);
625 size_t mid_len = strlen(mid);
626 size_t payload_len = candidate_len + 1 + mid_len + 1;
627
628 if (payload_len >= 8192) {
629 return SET_ERRNO(ERROR_INVALID_PARAM, "ICE payload too large: %zu", payload_len);
630 }
631
632 // Allocate packet buffer (header + payload)
633 size_t total_len = sizeof(acip_webrtc_ice_t) + payload_len;
634 uint8_t *packet = SAFE_MALLOC(total_len, uint8_t *);
635 if (!packet) {
636 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate ICE packet");
637 }
638
639 // Fill header
640 acip_webrtc_ice_t *header = (acip_webrtc_ice_t *)packet;
641 memcpy(header->session_id, session_id, 16);
642 // Use server's participant_id from SESSION_JOIN as sender
643 memcpy(header->sender_id, g_server_participant_id, 16);
644 memcpy(header->recipient_id, recipient_id, 16);
645 header->candidate_len = HOST_TO_NET_U16((uint16_t)candidate_len); // FIXED: Use candidate length, not total payload
646
647 // Copy candidate and mid after header
648 uint8_t *payload = packet + sizeof(acip_webrtc_ice_t);
649 memcpy(payload, candidate, candidate_len);
650 payload[candidate_len] = '\0';
651 memcpy(payload + candidate_len + 1, mid, mid_len);
652 payload[candidate_len + 1 + mid_len] = '\0';
653
654 log_debug("Server sending WebRTC ICE candidate to participant (%.8s..., mid=%s) via ACDS", (const char *)recipient_id,
655 mid);
656 log_debug(" [2] Before ACDS send - candidate: '%s' (len=%zu)", candidate, strlen(candidate));
657 log_debug(" [2] Before ACDS send - mid: '%s' (len=%zu)", mid, strlen(mid));
658 log_debug(" [2] Before ACDS send - payload_len=%zu, header.candidate_len=%u", payload_len,
659 NET_TO_HOST_U16(header->candidate_len));
660
661 // Hex dump payload for debugging
662 log_debug(" [2] Hex dump of payload being sent (first 100 bytes):");
663 for (size_t i = 0; i < 100 && i < payload_len; i += 16) {
664 char hex[64] = {0};
665 char ascii[20] = {0};
666 for (size_t j = 0; j < 16 && (i + j) < 100 && (i + j) < payload_len; j++) {
667 snprintf(hex + j * 3, sizeof(hex) - j * 3, "%02x ", payload[i + j]);
668 ascii[j] = (payload[i + j] >= 32 && payload[i + j] < 127) ? payload[i + j] : '.';
669 }
670 log_debug(" [%04zx] %-48s %s", i, hex, ascii);
671 }
672
673 // Send via ACDS transport using generic packet sender
674 asciichat_error_t result =
675 packet_send_via_transport(g_acds_transport, PACKET_TYPE_ACIP_WEBRTC_ICE, packet, total_len, 0);
676
677 SAFE_FREE(packet);
678
679 if (result != ASCIICHAT_OK) {
680 return SET_ERRNO(result, "Failed to send ICE via ACDS");
681 }
682
683 return ASCIICHAT_OK;
684}
685
696static void on_webrtc_transport_ready(acip_transport_t *transport, const uint8_t participant_id[16], void *user_data) {
697 server_context_t *server_ctx = (server_context_t *)user_data;
698 if (!transport || !participant_id || !server_ctx) {
699 log_error("on_webrtc_transport_ready: Invalid parameters");
700 if (transport) {
701 acip_transport_destroy(transport);
702 }
703 return;
704 }
705
706 log_debug("WebRTC transport ready for participant %.8s...", (const char *)participant_id);
707
708 // Convert participant_id to string for logging (ASCII-safe portion)
709 char participant_str[33];
710 for (int i = 0; i < 16; i++) {
711 safe_snprintf(participant_str + (i * 2), 3, "%02x", participant_id[i]);
712 }
713 participant_str[32] = '\0';
714
715 // Add client to server (calls add_webrtc_client internally)
716 // For WebRTC DataChannel: start threads immediately (crypto already handled by ACDS)
717 client_info_t *client = add_webrtc_client(server_ctx, transport, participant_str, true);
718 if (!client) {
719 log_error("Failed to add WebRTC client for participant %s", participant_str);
720 acip_transport_destroy(transport);
721 return;
722 }
723
724 log_debug("Successfully added WebRTC client ID=%s for participant %s", client->client_id, participant_str);
725}
726
737static void on_webrtc_sdp_server(const acip_webrtc_sdp_t *sdp, size_t total_len, void *ctx) {
738 (void)ctx;
739
740 if (!sdp || !g_webrtc_peer_manager) {
741 log_error("on_webrtc_sdp_server: Invalid parameters or peer_manager not initialized");
742 return;
743 }
744
745 // Validate packet length
746 uint16_t sdp_len = NET_TO_HOST_U16(sdp->sdp_len);
747 if (total_len < sizeof(acip_webrtc_sdp_t) + sdp_len) {
748 log_error("on_webrtc_sdp_server: Invalid packet length (total=%zu, expected>=%zu)", total_len,
749 sizeof(acip_webrtc_sdp_t) + sdp_len);
750 return;
751 }
752
753 // Determine SDP type (offer=0, answer=1)
754 const char *sdp_type = (sdp->sdp_type == 0) ? "offer" : "answer";
755
756 log_debug("Received WebRTC SDP %s from participant %.8s... (len=%u)", sdp_type, (const char *)sdp->sender_id,
757 sdp_len);
758
759 // Forward to peer_manager (pass full packet structure)
760 asciichat_error_t result = webrtc_peer_manager_handle_sdp(g_webrtc_peer_manager, sdp);
761
762 if (result != ASCIICHAT_OK) {
763 log_error("Failed to handle remote SDP from participant %.8s...: %s", (const char *)sdp->sender_id,
764 asciichat_error_string(result));
765 }
766}
767
778static void on_webrtc_ice_server(const acip_webrtc_ice_t *ice, size_t total_len, void *ctx) {
779 (void)ctx;
780
781 if (!ice || !g_webrtc_peer_manager) {
782 log_error("on_webrtc_ice_server: Invalid parameters or peer_manager not initialized");
783 return;
784 }
785
786 // Validate packet length
787 uint16_t payload_len = NET_TO_HOST_U16(ice->candidate_len);
788 if (total_len < sizeof(acip_webrtc_ice_t) + payload_len) {
789 log_error("on_webrtc_ice_server: Invalid packet length (total=%zu, expected>=%zu)", total_len,
790 sizeof(acip_webrtc_ice_t) + payload_len);
791 return;
792 }
793
794 log_debug("Received WebRTC ICE candidate from participant %02x%02x%02x%02x...", ice->sender_id[0],
795 ice->sender_id[1], ice->sender_id[2], ice->sender_id[3]);
796
797 // Forward to peer_manager (pass full packet structure)
798 asciichat_error_t result = webrtc_peer_manager_handle_ice(g_webrtc_peer_manager, ice);
799
800 if (result != ASCIICHAT_OK) {
801 log_error("Failed to handle remote ICE candidate from participant %02x%02x%02x%02x...: %s", ice->sender_id[0],
802 ice->sender_id[1], ice->sender_id[2], ice->sender_id[3], asciichat_error_string(result));
803 }
804}
805
815static void advertise_mdns_with_session(const char *session_string, uint16_t port) {
817 if (!mdns_ctx) {
818 log_debug("mDNS context not initialized, skipping advertisement");
819 return;
820 }
821
822 // Build session name from hostname for mDNS service name
823 char hostname[256] = {0};
824 char session_name[256] = "ascii-chat-Server";
825 if (gethostname(hostname, sizeof(hostname) - 1) == 0 && strlen(hostname) > 0) {
826 safe_snprintf(session_name, sizeof(session_name), "%s", hostname);
827 }
828
829 // Prepare TXT records with session string and host public key
830 char txt_session_string[512];
831 char txt_host_pubkey[512];
832 const char *txt_records[2];
833 int txt_count = 0;
834
835 // Add session string to TXT records (for client discovery)
836 safe_snprintf(txt_session_string, sizeof(txt_session_string), "session_string=%s", session_string);
837 txt_records[txt_count++] = txt_session_string;
838
839 // Add host public key to TXT records (for cryptographic verification)
840 // Convert server's Ed25519 public key to hex format
842 char hex_pubkey[65];
844 safe_snprintf(txt_host_pubkey, sizeof(txt_host_pubkey), "host_pubkey=%s", hex_pubkey);
845 txt_records[txt_count++] = txt_host_pubkey;
846 log_debug("mDNS: Host pubkey=%s", hex_pubkey);
847 } else {
848 // If encryption is disabled, still advertise a zero pubkey for clients to detect
849 safe_snprintf(txt_host_pubkey, sizeof(txt_host_pubkey), "host_pubkey=");
850 for (int i = 0; i < 32; i++) {
851 safe_snprintf(txt_host_pubkey + strlen(txt_host_pubkey), sizeof(txt_host_pubkey) - strlen(txt_host_pubkey), "00");
852 }
853 txt_records[txt_count++] = txt_host_pubkey;
854 log_debug("mDNS: Encryption disabled, advertising zero pubkey");
855 }
856
857 asciichat_mdns_service_t service = {
858 .name = session_name,
859 .type = "_ascii-chat._tcp",
860 .host = hostname,
861 .port = port,
862 .txt_records = txt_records,
863 .txt_count = txt_count,
864 };
865
866 asciichat_error_t mdns_advertise_result = asciichat_mdns_advertise(mdns_ctx, &service);
867 if (mdns_advertise_result != ASCIICHAT_OK) {
868 LOG_ERRNO_IF_SET("Failed to advertise mDNS service");
869 log_warn("mDNS advertising failed - LAN discovery disabled");
870 } else {
871 log_info("🌐 mDNS: Server advertised as '%s.local' on LAN", session_name);
872 log_debug("mDNS: Service advertised as '%s.local' (name=%s, port=%d, session=%s, txt_count=%d)", service.type,
873 service.name, service.port, session_string, service.txt_count);
874 }
875}
876
886static void *acds_ping_thread(void *arg) {
887 (void)arg;
888
889 log_debug("ACDS keepalive ping thread started");
890
892 if (!g_acds_transport) {
893 log_debug("ACDS transport destroyed, exiting ping thread");
894 break;
895 }
896
897 // Send PING every 10 seconds to keep connection alive
898 socket_t acds_socket = g_acds_transport->methods->get_socket(g_acds_transport);
899 if (acds_socket != INVALID_SOCKET_VALUE) {
900 asciichat_error_t ping_result = packet_send(acds_socket, PACKET_TYPE_PING, NULL, 0);
901 if (ping_result == ASCIICHAT_OK) {
902 log_debug("ACDS keepalive: Sent periodic PING");
903 } else {
904 log_warn("ACDS keepalive: Failed to send PING: %s", asciichat_error_string(ping_result));
905 }
906 }
907
908 // Sleep for 10 seconds before next ping (well before 15s timeout)
909 for (int i = 0; i < 100 && !atomic_load_bool(&g_should_exit); i++) {
910 APP_CALLBACK_VOID(platform_pump_events);
911 platform_sleep_ms(100); // Check exit flag every 100ms
912 }
913 }
914
915 log_debug("ACDS keepalive ping thread exiting");
916 return NULL;
917}
918
931static void on_acds_ping(void *ctx) {
932 (void)ctx;
933 log_debug("ACDS keepalive: Received PING from ACDS, responding with PONG");
934 if (g_acds_transport) {
935 packet_send_via_transport(g_acds_transport, PACKET_TYPE_PONG, NULL, 0, 0);
936 }
937}
938
942static void on_acds_pong(void *ctx) {
943 (void)ctx;
944 log_debug("ACDS keepalive: Received PONG from ACDS server");
945}
946
947static void *acds_receive_thread(void *arg) {
948 (void)arg;
949
950 log_debug("ACDS receive thread started");
951
952 // Configure callbacks for WebRTC signaling packets and keepalive
953 acip_client_callbacks_t callbacks = {
954 .on_ascii_frame = NULL,
955 .on_audio = NULL,
956 .on_webrtc_sdp = on_webrtc_sdp_server,
957 .on_webrtc_ice = on_webrtc_ice_server,
958 .on_session_joined = NULL,
959 .on_ping = on_acds_ping,
960 .on_pong = on_acds_pong,
961 .app_ctx = NULL,
962 };
963
964 // Receive loop - just handle incoming packets
965 // Keepalive is handled by separate ping thread
967 if (!g_acds_transport) {
968 log_warn("ACDS transport is NULL, exiting receive thread");
969 break;
970 }
971
972 asciichat_error_t result = acip_client_receive_and_dispatch(g_acds_transport, &callbacks);
973
974 if (result != ASCIICHAT_OK) {
975 // Check error context to see if connection actually closed
977 bool has_context = HAS_ERRNO(&err_ctx);
978
979 // Timeouts are normal when there are no packets - just continue waiting
980 if (result == ERROR_NETWORK_TIMEOUT) {
981 continue;
982 }
983
984 // ERROR_NETWORK could be:
985 // 1. Receive timeout (non-fatal - continue waiting)
986 // 2. EOF/connection closed (fatal - exit thread)
987 // Check the error context message to distinguish
988 if (result == ERROR_NETWORK) {
989 if (has_context && strstr(err_ctx.context_message, "Failed to receive packet") != NULL) {
990 // Generic receive failure (likely timeout) - continue waiting
991 log_debug("ACDS receive timeout, continuing to wait for packets");
992 continue;
993 } else if (has_context && (strstr(err_ctx.context_message, "EOF") != NULL ||
994 strstr(err_ctx.context_message, "closed") != NULL)) {
995 // Connection actually closed
996 log_warn("ACDS connection closed: %s", err_ctx.context_message);
997 break;
998 } else {
999 // Unknown ERROR_NETWORK - log and exit
1000 log_warn("ACDS connection error: %s", has_context ? err_ctx.context_message : "unknown");
1001 break;
1002 }
1003 }
1004
1005 // Other errors - exit thread
1006 log_error("ACDS receive error: %s, exiting receive thread", asciichat_error_string(result));
1007 break;
1008 }
1009 }
1010
1012 log_debug("ACDS receive thread exiting (server shutdown)");
1013 } else {
1014 log_warn("ACDS receive thread exiting unexpectedly");
1015 }
1016 return NULL;
1017}
1018
1019/* ============================================================================
1020 * Signal Handlers
1021 * ============================================================================
1022 */
1023
1024// SIGTERM handling is now done by session_server_like (both SIGINT and SIGTERM call interrupt_fn).
1025// WebSocket server thread wrapper and status screen thread are now in session_server_like.
1026
1027/* ============================================================================
1028 * Client Handler Thread (for tcp_server integration)
1029 * ============================================================================
1030 */
1031
1045static void *ascii_chat_client_handler(void *arg) {
1047 if (!ctx) {
1048 log_error("Client handler: NULL context");
1049 return NULL;
1050 }
1051
1052 // Extract server context from user_data
1053 server_context_t *server_ctx = (server_context_t *)ctx->user_data;
1054 if (!server_ctx) {
1055 log_error("Client handler: NULL server context");
1057 SAFE_FREE(ctx);
1058 return NULL;
1059 }
1060
1061 socket_t client_socket = ctx->client_socket;
1062
1063 // Extract client IP and port using tcp_server helpers
1064 char client_ip[INET6_ADDRSTRLEN] = {0};
1065 if (!tcp_client_context_get_ip(ctx, client_ip, sizeof(client_ip))) {
1066 safe_snprintf(client_ip, sizeof(client_ip), "unknown");
1067 }
1068
1069 int client_port = tcp_client_context_get_port(ctx);
1070 if (client_port < 0) {
1071 client_port = 0;
1072 }
1073
1074 log_debug("Client handler started for %s:%d", client_ip, client_port);
1075
1076 // Check connection rate limit (prevent DoS attacks)
1077 if (server_ctx->rate_limiter) {
1078 bool allowed = false;
1079 asciichat_error_t rate_check =
1080 rate_limiter_check(server_ctx->rate_limiter, client_ip, RATE_EVENT_CONNECTION, NULL, &allowed);
1081 if (rate_check != ASCIICHAT_OK || !allowed) {
1082 tcp_server_reject_client(client_socket, "Connection rate limit exceeded");
1083 SAFE_FREE(ctx);
1084 return NULL;
1085 }
1086 // Record successful connection attempt
1088 }
1089
1090 // Add client (initializes structures, spawns workers via tcp_server_spawn_thread)
1091 client_info_t *client = add_client(server_ctx, client_socket, client_ip, client_port);
1092 if (!client) {
1095 CLEAR_ERRNO();
1096 }
1097 tcp_server_reject_client(client_socket, "Failed to add client");
1098 SAFE_FREE(ctx);
1099 return NULL;
1100 }
1101
1102 log_debug("Client %s added successfully from %s:%d", client->client_id, client_ip, client_port);
1103
1104 // Block until client disconnects (active flag is set by receive thread)
1105 if (client) {
1106 log_debug("HANDLER: Client %s found, waiting for disconnect (active=%d)", client->client_id,
1107 atomic_load_bool(&client->active));
1108 int wait_count = 0;
1109 while (atomic_load_bool(&client->active) && !atomic_load_bool(server_ctx->server_should_exit)) {
1110 wait_count++;
1111 if (wait_count % 10 == 0) {
1112 // Log every 1 second (10 * 100ms)
1113 log_debug("HANDLER: Client %s still active (waited %d seconds), active=%d", client->client_id, wait_count / 10,
1114 atomic_load_bool(&client->active));
1115 }
1116 APP_CALLBACK_VOID(platform_pump_events);
1117 platform_sleep_ms(100); // Check every 100ms
1118 }
1119 log_info("Client %s disconnected from %s:%d (waited %d seconds, active=%d, server_should_exit=%d)",
1120 client->client_id, client_ip, client_port, wait_count / 10, atomic_load_bool(&client->active),
1122 }
1123
1124 // Cleanup (this will call tcp_server_stop_client_threads internally)
1125 if (remove_client(server_ctx, client->client_id) != 0) {
1126 log_error("CRITICAL BUG: Failed to remove client %s from server (potential zombie client leak!)",
1127 client->client_id);
1128 }
1129
1130 // Close socket and free context
1131 socket_close(client_socket);
1132 SAFE_FREE(ctx);
1133
1134 log_debug("Client handler finished for %s:%d", client_ip, client_port);
1135 return NULL;
1136}
1137
1147static void *websocket_client_handler(void *arg) {
1149 if (!ctx) {
1150 log_error("WebSocket client handler: NULL context");
1151 return NULL;
1152 }
1153
1154 log_info("WebSocket client connected from %s:%d", ctx->client_ip, ctx->client_port);
1155 log_debug("[WS_HANDLER] ===== ENTRY: WebSocket client handler called for %s:%d =====", ctx->client_ip,
1156 ctx->client_port);
1157
1158 // Extract server context from user_data
1159 server_context_t *server_ctx = (server_context_t *)ctx->user_data;
1160 if (!server_ctx) {
1161 log_error("WebSocket client handler: NULL server context");
1162 if (ctx->transport) {
1164 }
1165 SAFE_FREE(ctx);
1166 return NULL;
1167 }
1168
1169 // STEP 1: Add client first (creates client structure WITHOUT starting threads yet)
1170 log_debug("[WS_HANDLER] STEP 1: Calling add_webrtc_client()...");
1171 client_info_t *client = add_webrtc_client(server_ctx, ctx->transport, ctx->client_ip, false);
1172 if (!client) {
1173 log_error("[WS_HANDLER] FAILED: add_webrtc_client returned NULL");
1174 if (ctx->transport) {
1176 }
1177 SAFE_FREE(ctx);
1178 return NULL;
1179 }
1180
1181 // Transport ownership transferred to client structure - don't destroy it here
1182 ctx->transport = NULL;
1183
1184 // TLS encrypts the transport; configured credentials still require authentication.
1185 if (ctx->is_secure && GET_OPTION(password)[0] == '\0' && g_num_whitelisted_clients == 0) {
1186 client->transport_encrypted = true;
1187 if (start_webrtc_client_threads(server_ctx, client->client_id) != 0) {
1188 remove_client(server_ctx, client->client_id);
1189 }
1190 SAFE_FREE(ctx);
1191 return NULL;
1192 }
1193
1194 log_info("WebSocket client %s added successfully", client->client_id);
1195 log_debug("[WS_HANDLER] add_webrtc_client returned client_id=%s, transport ownership transferred", client->client_id);
1196
1197 // STEP 2: Client structure is already initialized - set up crypto handshake
1198 log_debug("[WS_HANDLER] STEP 2: Client structure obtained: client=%p, transport=%p, crypto_initialized=%d",
1199 (void *)client, (void *)client->transport, client->crypto_initialized);
1200
1201 // Initialize crypto handshake context directly in client structure
1202 // This must happen BEFORE sending KEY_EXCHANGE_INIT so that when the client
1203 // responds, the receive thread has a properly initialized context
1204 // Create debug name with client_id
1205 char crypto_name[64];
1206 SAFE_SNPRINTF(crypto_name, sizeof(crypto_name), "crypto_client_%s", client->client_id);
1207
1208 log_info("[WS_HANDLER] ★★★ LOCK STATE BEFORE crypto_handshake_init()");
1209 // debug_sync_print_state(); // Disabled: causes AddressSanitizer stack-use-after-return crash
1210 log_debug("[WS_HANDLER] Calling crypto_handshake_init()...");
1211 asciichat_error_t handshake_init_result =
1212 crypto_handshake_init(crypto_name, &client->crypto_handshake_ctx, true /* is_server */);
1213 log_info("[WS_HANDLER] ★★★ DEBUG: Printing lock state after crypto_handshake_init");
1214 // debug_sync_print_state(); // Disabled: causes AddressSanitizer stack-use-after-return crash
1215 if (handshake_init_result != ASCIICHAT_OK) {
1216 log_error("[WS_HANDLER] FAILED: crypto_handshake_init returned %d: %s", handshake_init_result,
1217 asciichat_error_string(handshake_init_result));
1218 remove_client(server_ctx, client->client_id);
1219 SAFE_FREE(ctx);
1220 return NULL;
1221 }
1222 log_debug("[WS_HANDLER] crypto_handshake_init OK, handshake_ctx.state=%d", client->crypto_handshake_ctx.state);
1223
1224 // Mark crypto as NOT initialized yet (will be set to true when handshake completes)
1225 log_debug("[WS_HANDLER] Setting crypto_initialized from %d to false...", client->crypto_initialized);
1226 client->crypto_initialized = false;
1227
1228 log_debug("Initialized crypto handshake context for WebSocket client %s", client->client_id);
1229 log_debug("[WS_HANDLER] crypto_initialized now = %d", client->crypto_initialized);
1230
1231 // STEP 3: Send CRYPTO_PARAMETERS then KEY_EXCHANGE_INIT to start the handshake
1232 // The receive thread is already running, but it will use the properly initialized context
1233 log_info("[WS_HANDLER] STEP 3: Sending CRYPTO_PARAMETERS + KEY_EXCHANGE_INIT...");
1234 asciichat_error_t handshake_start_result =
1235 crypto_handshake_server_send_parameters(&client->crypto_handshake_ctx, client->transport);
1236 if (handshake_start_result != ASCIICHAT_OK) {
1237 log_error("[WS_HANDLER] FAILED: crypto_handshake_server_send_parameters returned %d: %s", handshake_start_result,
1238 asciichat_error_string(handshake_start_result));
1239 remove_client(server_ctx, client->client_id);
1240 SAFE_FREE(ctx);
1241 return NULL;
1242 }
1243 handshake_start_result = crypto_handshake_server_start(&client->crypto_handshake_ctx, client->transport);
1244 if (handshake_start_result != ASCIICHAT_OK) {
1245 log_error("[WS_HANDLER] FAILED: crypto_handshake_server_start returned %d: %s", handshake_start_result,
1246 asciichat_error_string(handshake_start_result));
1247 remove_client(server_ctx, client->client_id);
1248 SAFE_FREE(ctx);
1249 return NULL;
1250 }
1251
1252 log_info("Sent CRYPTO_KEY_EXCHANGE_INIT to WebSocket client %s", client->client_id);
1253 log_info("[WS_HANDLER] ===== SUCCESS: Handshake started for client %s =====", client->client_id);
1254
1255 // STEP 4: Now start the threads after crypto is initialized
1256 // This ensures receive thread doesn't try to process packets before crypto context exists
1257 log_debug("[WS_HANDLER] STEP 4: Starting client threads after crypto initialization...");
1258 if (start_webrtc_client_threads(server_ctx, client->client_id) != 0) {
1259 log_error("[WS_HANDLER] FAILED: start_webrtc_client_threads returned error");
1260 remove_client(server_ctx, client->client_id);
1261 SAFE_FREE(ctx);
1262 return NULL;
1263 }
1264 log_info("[WS_HANDLER] Client threads started successfully for client %s", client->client_id);
1265
1266 // Handler thread exit - cleanup happens in the receive thread
1267 // The receive thread is responsible for calling remove_client() when it detects connection closure.
1268 // We exit immediately instead of blocking - this prevents the LWS service thread from being blocked
1269 // in thread_join() during LWS_CALLBACK_CLOSED, which would freeze the event loop.
1270 // The removal logic is now handled by the receive thread, which:
1271 // 1. Detects transport closure (recv fails or returns error)
1272 // 2. Sets active=false and signals other threads to stop
1273 // 3. Calls remove_client() with proper self-join detection to avoid deadlock
1274 log_debug("[WS_HANDLER] STEP 5: Handler thread exiting (client_id=%s, cleanup will happen in receive thread)",
1275 client->client_id);
1276
1277 SAFE_FREE(ctx);
1278 return NULL;
1279}
1280
1281/* ============================================================================
1282 * Main Function
1283 * ============================================================================
1284 */
1285
1347static int init_server_crypto(void) {
1348 // Check if encryption is disabled
1349 if (GET_OPTION(no_encrypt)) {
1350 log_info("Encryption: DISABLED (--no-encrypt)");
1352 return 0;
1353 }
1354
1355 // Load server identity keys (supports multiple --key flags for multi-key mode)
1356 size_t num_keys = GET_OPTION(num_identity_keys);
1357
1358 if (num_keys > 0) {
1359 // Multi-key mode: load all identity keys from identity_keys[] array
1360 log_info("Loading %zu identity key(s) for multi-key support...", num_keys);
1361
1362 for (size_t i = 0; i < num_keys && i < MAX_IDENTITY_KEYS; i++) {
1363 const char *key_path = GET_OPTION(identity_keys[i]);
1364
1365 if (strlen(key_path) == 0) {
1366 continue; // Skip empty entries
1367 }
1368
1369 // Validate SSH key file (skip validation for special prefixes)
1370 bool is_special_key = (strncmp(key_path, "gpg:", 4) == 0 || strncmp(key_path, "github:", 7) == 0 ||
1371 strncmp(key_path, "gitlab:", 7) == 0);
1372
1373 if (!is_special_key) {
1374 if (validate_ssh_key_file(key_path) != 0) {
1375 log_warn("Skipping invalid SSH key file: %s", key_path);
1376 continue;
1377 }
1378 }
1379
1380 // Parse key (handles SSH files and gpg: prefix, rejects github:/gitlab:)
1381 log_debug("Loading identity key #%zu: %s", i + 1, key_path);
1383 log_debug("Successfully loaded identity key #%zu: %s", i + 1, key_path);
1384
1385 // Display key fingerprint for verification
1386 char hex_pubkey[65];
1388 log_debug(" Key fingerprint: %s", hex_pubkey);
1389
1391 } else {
1392 log_warn("Failed to parse identity key #%zu: %s (skipping)", i + 1, key_path);
1393 }
1394 }
1395
1396 if (g_num_server_identity_keys == 0) {
1397 log_error("No valid identity keys loaded despite %zu --key flag(s)", num_keys);
1398 SET_ERRNO(ERROR_CRYPTO_KEY, "No valid identity keys loaded");
1399 return -1;
1400 }
1401
1402 // Copy first key to g_server_private_key for backward compatibility
1404 log_info("Loaded %zu identity key(s) total", g_num_server_identity_keys);
1405
1406 } else if (strlen(GET_OPTION(encrypt_key)) > 0) {
1407 // Single-key mode (backward compatibility): load from encrypt_key field
1408 const char *key_path = GET_OPTION(encrypt_key);
1409
1410 // Validate SSH key file (skip validation for special prefixes)
1411 bool is_special_key = (strncmp(key_path, "gpg:", 4) == 0 || strncmp(key_path, "github:", 7) == 0 ||
1412 strncmp(key_path, "gitlab:", 7) == 0);
1413
1414 if (!is_special_key) {
1415 if (validate_ssh_key_file(key_path) != 0) {
1416 SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid SSH key file: %s", key_path);
1417 return -1;
1418 }
1419 }
1420
1421 // Parse key
1422 log_info("Loading key for authentication: %s", key_path);
1424 log_info("Successfully loaded server key: %s", key_path);
1425
1426 // Also store in identity_keys array for consistency
1429 } else {
1430 log_error("Failed to parse key: %s\n"
1431 "This may be due to:\n"
1432 " - Wrong password for encrypted key\n"
1433 " - Unsupported key type (only Ed25519 is currently supported)\n"
1434 " - Corrupted key file\n"
1435 "\n"
1436 "Note: RSA and ECDSA keys are not yet supported\n"
1437 "To generate an Ed25519 key: ssh-keygen -t ed25519\n",
1438 key_path);
1439 SET_ERRNO(ERROR_CRYPTO_KEY, "Key parsing failed: %s", key_path);
1440 return -1;
1441 }
1442 } else if (strlen(GET_OPTION(password)) == 0) {
1443 // No identity key provided - server will run in simple mode
1444 // The server will still generate ephemeral keys for encryption, but no identity key
1447 log_info("Server running without identity key (simple mode)");
1448 }
1449
1450 // Load client whitelist if provided
1451 if (strlen(GET_OPTION(client_keys)) > 0) {
1453 SET_ERRNO(ERROR_CRYPTO_KEY, "Client key parsing failed: %s", GET_OPTION(client_keys));
1454 return -1;
1455 }
1456 log_debug("Loaded %zu whitelisted clients", g_num_whitelisted_clients);
1457 log_info("Server will only accept %zu whitelisted clients", g_num_whitelisted_clients);
1458 }
1459
1461 return 0;
1462}
1463
1464/* ============================================================================
1465 * Static server context (populated during init, used by callbacks)
1466 * ============================================================================ */
1467static server_context_t g_server_ctx;
1468
1469/* ============================================================================
1470 * Status screen callback (populates ui_status_t from server state)
1471 * ============================================================================ */
1472static void server_status_fn(void *user_data, ui_status_t *out_status) {
1473 (void)user_data;
1474
1476 if (!tcp) {
1477 return;
1478 }
1479
1480 ui_status_gather(tcp, g_session_string, tcp->config.ipv4_address, tcp->config.ipv6_address, GET_OPTION(port),
1481 g_server_start_time, "Server", g_session_is_mdns_only, out_status);
1482}
1483
1484/* ============================================================================
1485 * Mode-specific initialization callback
1486 * ============================================================================ */
1487static asciichat_error_t server_init_fn(void *user_data) {
1488 (void)user_data;
1489 int port = GET_OPTION(port);
1490
1491 // Get TCP server from server_like (it's already initialized at this point)
1493 if (!tcp) {
1494 return SET_ERRNO(ERROR_INVALID_STATE, "TCP server not available from server_like");
1495 }
1496
1497 // Point server context at the server_like-owned TCP server
1498 g_server_ctx.tcp_server = tcp;
1499
1500 // UPnP success check for ACDS session type decision
1502 bool upnp_succeeded = (upnp_ctx != NULL);
1503
1504 // Initialize synchronization primitives
1505 if (rwlock_init(&g_client_manager_rwlock, "clients") != 0) {
1506 return SET_ERRNO(ERROR_THREAD, "Failed to initialize client manager rwlock");
1507 }
1509
1510 // Register global sync primitives for debugging
1511 NAMED_REGISTER_RWLOCK(&g_client_manager_rwlock, "client_manager_rwlock", NULL);
1512
1513 // Check if SIGINT/SIGTERM was received during initialization
1515 log_info("Shutdown signal received during initialization, skipping server startup");
1516 return ASCIICHAT_OK;
1517 }
1518
1519 // NOTE: g_client_manager is already zero-initialized in client.c with = {0}
1520 // We only need to initialize the mutex
1521 mutex_init(&g_client_manager.mutex, "client_manager");
1522
1523 // Initialize uthash head pointer for O(1) lookup (uthash requires NULL initialization)
1525
1526 // Initialize connection rate limiter (prevents DoS attacks)
1528 log_debug("Initializing connection rate limiter...");
1530 if (!g_rate_limiter) {
1531 LOG_ERRNO_IF_SET("Failed to initialize rate limiter");
1533 return SET_ERRNO(ERROR_MEMORY, "Failed to create connection rate limiter");
1534 }
1535 } else {
1536 log_info("Connection rate limiter initialized (50 connections/min per IP)");
1537 }
1538 }
1539
1540 // Initialize audio mixer (always enabled on server)
1542 log_debug("Initializing audio mixer for per-client audio rendering...");
1544 if (!g_audio_mixer) {
1545 LOG_ERRNO_IF_SET("Failed to initialize audio mixer");
1547 return SET_ERRNO(ERROR_AUDIO, "Failed to initialize audio mixer");
1548 }
1549 } else {
1551 log_debug("Audio mixer initialized successfully for per-client audio rendering");
1552 }
1553 }
1554 }
1555
1556 // Initialize H.265 codec server context (always enabled on server)
1558 log_debug("Initializing H.265 codec server context...");
1560 if (!g_h265_server) {
1561 LOG_ERRNO_IF_SET("Failed to initialize H.265 codec context");
1563 return SET_ERRNO(ERROR_MEDIA_INIT, "Failed to initialize H.265 codec context");
1564 }
1565 } else {
1567 log_debug("H.265 codec server context initialized successfully");
1568 }
1569 }
1570 }
1571
1572 // Session Host Creation (for discovery mode support)
1573 // Create session_host to track clients in a transport-agnostic way.
1575 const char *ipv4_address = tcp->config.ipv4_address;
1576 const char *ipv6_address = tcp->config.ipv6_address;
1577
1578 session_host_config_t host_config = {
1579 .port = port,
1580 .ipv4_address = ipv4_address,
1581 .ipv6_address = ipv6_address,
1582 .max_clients = GET_OPTION(max_clients),
1583 .encryption_enabled = g_server_encryption_enabled,
1584 .key_path = GET_OPTION(encrypt_key),
1585 .password = GET_OPTION(password),
1586 .callbacks = {0}, // No callbacks for now
1587 .user_data = NULL,
1588 };
1589
1590 g_server_ctx.session_host = session_host_create(&host_config);
1591 if (!g_server_ctx.session_host) {
1592 // Non-fatal: session_host is optional, server can work without it
1593 log_warn("Failed to create session_host (discovery mode support disabled)");
1594 } else {
1595 log_debug("Session host created for discovery mode support");
1596 }
1597 }
1598
1599 // Update server context fields that depend on init results
1600 g_server_ctx.rate_limiter = g_rate_limiter;
1601 g_server_ctx.audio_mixer = g_audio_mixer;
1602 g_server_ctx.h265_server = g_h265_server; // Sync H.265 context created in this function
1603
1604 // ACDS Session Creation: Register this server with discovery service
1605 // This also determines the session string for mDNS (if --acds is enabled)
1606 char session_string[64] = {0};
1607 bool session_is_mdns_only = false;
1608
1609 // ACDS Registration (conditional on --discovery flag)
1610 if (GET_OPTION(discovery)) {
1611 // Security Requirement Check (Issue #239):
1612 // Server IP must be protected by password, identity verification, or explicit opt-in
1613
1614 // Auto-detection: Check if password or identity verification is configured
1615 const char *password = GET_OPTION(password);
1616 bool has_password = password && strlen(password) > 0;
1617 const char *encrypt_key = GET_OPTION(encrypt_key);
1618 bool has_identity = encrypt_key && strlen(encrypt_key) > 0;
1619 bool explicit_expose = GET_OPTION(discovery_expose_ip) != 0;
1620
1621 // Validate security configuration BEFORE attempting ACDS connection
1622 bool acds_expose_ip_flag = false;
1623
1624 if (has_password || has_identity) {
1625 // Auto-enable privacy: IP revealed only after verification
1626 acds_expose_ip_flag = false;
1627 log_plain("ACDS privacy enabled: IP disclosed only after %s verification",
1628 has_password ? "password" : "identity");
1629 } else if (explicit_expose) {
1630 // Explicit opt-in to public IP disclosure
1631 // Only prompt if running interactively (can prompt user)
1632 // When non-interactive (automated/scripted), treat explicit flag as confirmation
1633 bool is_interactive = terminal_can_prompt_user();
1634
1635 if (is_interactive) {
1636 log_warn("You are about to allow PUBLIC IP disclosure.\n"
1637 "Anyone with the session string will be able to see your IP address.\n"
1638 "This is NOT RECOMMENDED unless you understand the privacy implications.");
1639
1640 if (!platform_prompt_yes_no("Do you want to proceed with public IP disclosure", false)) {
1641 log_info("IP disclosure not confirmed. Server will run WITHOUT discovery service.");
1642 goto skip_acds_session;
1643 }
1644 }
1645
1646 // User confirmed (or running non-interactively with explicit flag) - proceed with public IP disclosure
1647 acds_expose_ip_flag = true;
1648 log_info("Public IP disclosure CONFIRMED.\n"
1649 "Your IP address will be visible to anyone with the session string");
1650 } else {
1651 // Security violation: No password, no identity, no explicit opt-in
1652 log_error("Cannot create ACDS session: No security configured!\n"
1653 "You must either:\n"
1654 "1. Set a password: --password \"your-secret\"\n"
1655 "2. Use identity key: --key ~/.ssh/id_ed25519\n"
1656 "3. Explicitly allow public IP: --acds-expose-ip (NOT RECOMMENDED)\n"
1657 "\n"
1658 "Server will run WITHOUT discovery service.");
1659 goto skip_acds_session;
1660 }
1661
1662 // Security is configured, proceed with ACDS connection
1663 const char *acds_server = GET_OPTION(discovery_server);
1664 uint16_t acds_port = (uint16_t)GET_OPTION(discovery_port);
1665
1666 log_info("Attempting to create session on ACDS server at %s:%d...", acds_server, acds_port);
1667
1668 acds_client_config_t acds_config;
1670 SAFE_STRNCPY(acds_config.server_address, acds_server, sizeof(acds_config.server_address));
1671 acds_config.server_port = acds_port;
1672 acds_config.timeout_ms = 5 * MS_PER_SEC_INT;
1673
1674 // Allocate ACDS client on heap for server lifecycle
1675 g_acds_client = SAFE_MALLOC(sizeof(acds_client_t), acds_client_t *);
1676 if (!g_acds_client) {
1677 log_error("Failed to allocate ACDS client");
1678 goto skip_acds_session;
1679 }
1680
1681 asciichat_error_t acds_connect_result = acds_client_connect(g_acds_client, &acds_config);
1682 if (acds_connect_result != ASCIICHAT_OK) {
1683 log_error("Failed to connect to ACDS server at %s:%d: %s", acds_server, acds_port,
1684 asciichat_error_string(acds_connect_result));
1685 goto skip_acds_session;
1686 }
1687 if (acds_connect_result == ASCIICHAT_OK) {
1688 // Prepare session creation parameters
1689 acds_session_create_params_t create_params;
1690 memset(&create_params, 0, sizeof(create_params));
1691
1692 // Use server's Ed25519 identity public key if available
1693 if (g_server_encryption_enabled && has_identity) {
1694 memcpy(create_params.identity_pubkey, g_server_private_key.public_key, 32);
1695 log_debug("Using server identity key for ACDS session");
1696 } else {
1697 // No identity key available - use zero key
1698 // ACDS will accept this if identity verification is not required
1699 memset(create_params.identity_pubkey, 0, 32);
1700 log_debug("No server identity key - using zero key for ACDS session");
1701 }
1702
1703 create_params.capabilities = 0x03; // Video + Audio
1704 create_params.max_participants = GET_OPTION(max_clients);
1705 log_debug("ACDS: max_clients option value = %d", GET_OPTION(max_clients));
1706
1707 // Set password if configured
1708 create_params.has_password = has_password;
1709 if (has_password) {
1710 // TODO: Hash password with Argon2id
1711 SAFE_STRNCPY(create_params.password, password, sizeof(create_params.password));
1712 }
1713
1714 // Set IP disclosure policy (determined above)
1715 create_params.acds_expose_ip = acds_expose_ip_flag;
1716 log_info("DEBUG: Server setting acds_expose_ip=%d (explicit_expose=%d, has_password=%d, has_identity=%d)",
1717 create_params.acds_expose_ip, explicit_expose, has_password, has_identity);
1718
1719 // Set session type (Direct TCP or WebRTC)
1720 // Auto-detect: Use WebRTC if UPnP failed OR if explicitly requested via --webrtc
1721 // Exception: If bind address is 0.0.0.0, server is on public IP - use Direct TCP
1722 const char *bind_addr = GET_OPTION(address);
1723 bool bind_all_interfaces = (strcmp(bind_addr, "0.0.0.0") == 0);
1724
1725 // Determine session type: prefer WebRTC by default (unless explicitly disabled)
1726 // Priority: explicit --webrtc flag > connection type detection > UPnP > default
1727 if (GET_OPTION(webrtc)) {
1728 // Explicit WebRTC request
1729 create_params.session_type = SESSION_TYPE_WEBRTC;
1730 log_info("ACDS session type: WebRTC (explicitly requested via --webrtc)");
1731 } else if (bind_all_interfaces) {
1732 // Bind to 0.0.0.0: use WebRTC as default (better NAT compatibility)
1733 create_params.session_type = SESSION_TYPE_WEBRTC;
1734 log_info("ACDS session type: WebRTC (default for 0.0.0.0 binding, provides NAT-agnostic connections)");
1735 } else if (upnp_succeeded) {
1736 // UPnP port mapping worked - can use direct TCP
1737 create_params.session_type = SESSION_TYPE_DIRECT_TCP;
1738 log_info("ACDS session type: Direct TCP (UPnP succeeded, server is publicly accessible)");
1739 } else {
1740 // UPnP failed and not on public IP - use WebRTC for NAT traversal
1741 create_params.session_type = SESSION_TYPE_WEBRTC;
1742 log_info("ACDS session type: WebRTC (UPnP failed, server behind NAT)");
1743 }
1744
1745 // Server connection information (where clients should connect)
1746 // If bind address is 0.0.0.0, leave server_address empty for ACDS to auto-detect public IP
1747 if (bind_all_interfaces) {
1748 create_params.server_address[0] = '\0'; // Empty - ACDS will use connection source IP
1749 log_debug("Bind address is 0.0.0.0, ACDS will auto-detect public IP from connection");
1750 } else {
1751 SAFE_STRNCPY(create_params.server_address, bind_addr, sizeof(create_params.server_address));
1752 }
1753 create_params.server_port = port;
1754
1755 // DEBUG: Log what we're sending to ACDS
1756 log_info("DEBUG: Before SESSION_CREATE - expose_ip_publicly=%d, server_address='%s' port=%u, session_type=%u",
1757 create_params.acds_expose_ip, create_params.server_address, create_params.server_port,
1758 create_params.session_type);
1759
1760 // Create session
1761 acds_session_create_result_t create_result;
1762 asciichat_error_t create_err = acds_session_create(g_acds_client, &create_params, &create_result);
1763
1764 if (create_err == ASCIICHAT_OK) {
1765 SAFE_STRNCPY(session_string, create_result.session_string, sizeof(session_string));
1766 SAFE_STRNCPY(g_session_string, create_result.session_string, sizeof(g_session_string));
1767 session_is_mdns_only = false; // Session is now registered with ACDS (globally discoverable)
1768 log_info("Session created: %s", session_string);
1769
1770 // Server must join its own session so ACDS can route signaling messages
1771 log_debug("Server joining session as first participant for WebRTC signaling...");
1772 acds_session_join_params_t join_params = {0};
1773 join_params.session_string = session_string;
1774
1775 // Use same identity key as session creation
1776 memcpy(join_params.identity_pubkey, create_params.identity_pubkey, 32);
1777
1778 // Include password if session is password-protected
1779 if (has_password) {
1780 join_params.has_password = true;
1781 SAFE_STRNCPY(join_params.password, password, sizeof(join_params.password));
1782 }
1783
1784 acds_session_join_result_t join_result = {0};
1785 asciichat_error_t join_err = acds_session_join(g_acds_client, &join_params, &join_result);
1786 if (join_err != ASCIICHAT_OK || !join_result.success) {
1787 log_error("Failed to join own session: %s (error: %s)", asciichat_error_string(join_err),
1788 join_result.error_message[0] ? join_result.error_message : "unknown");
1789 // Continue anyway - this is not fatal for Direct TCP sessions
1790 } else {
1791 log_debug("Server joined session successfully (participant_id: %02x%02x...)", join_result.participant_id[0],
1792 join_result.participant_id[1]);
1793 // Store participant ID for WebRTC signaling (needed to identify server in SDP/ICE messages)
1794 memcpy(g_server_participant_id, join_result.participant_id, 16);
1795 log_debug("Stored server participant_id for signaling: %02x%02x...", g_server_participant_id[0],
1796 g_server_participant_id[1]);
1797 memcpy(create_result.session_id, join_result.session_id, 16);
1798 }
1799
1800 // Keep ACDS connection alive for WebRTC signaling relay
1801 log_debug("Server staying connected to ACDS for signaling relay");
1802
1803 // Create ACDS transport wrapper for sending signaling packets
1804 g_acds_transport = acip_tcp_transport_create("transport_acds_server", g_acds_client->socket, NULL);
1805 if (!g_acds_transport) {
1806 log_error("Failed to create ACDS transport wrapper");
1807 } else {
1808 log_debug("ACDS transport wrapper created for signaling");
1809
1810 // Start ACDS ping thread to keep connection alive (for ALL session types)
1811 int ping_thread_result = asciichat_thread_create(&g_acds_ping_thread, "acds_ping", acds_ping_thread, NULL);
1812 if (ping_thread_result != 0) {
1813 log_error("Failed to create ACDS ping thread: %d", ping_thread_result);
1814 } else {
1815 log_debug("ACDS ping thread started to keep connection alive");
1816 g_acds_ping_thread_started = true;
1817 }
1818 }
1819
1820 // Initialize WebRTC peer_manager if session type is WebRTC
1821 if (create_params.session_type == SESSION_TYPE_WEBRTC) {
1822 log_debug("Initializing WebRTC library and peer manager for session (role=CREATOR)...");
1823
1824 // Initialize WebRTC library (libdatachannel)
1825 asciichat_error_t webrtc_init_result = webrtc_init();
1826 if (webrtc_init_result != ASCIICHAT_OK) {
1827 log_error("Failed to initialize WebRTC library: %s", asciichat_error_string(webrtc_init_result));
1828 g_webrtc_peer_manager = NULL;
1829 } else {
1830 log_debug("WebRTC library initialized successfully");
1831
1832 // Configure STUN servers for ICE gathering (static to persist for peer_manager lifetime)
1833 static stun_server_t stun_servers[4] = {0};
1834 static lifecycle_t g_stun_lc = LIFECYCLE_INIT;
1835 static int stun_count = 0; // Store actual count separately
1836
1837 if (lifecycle_init(&g_stun_lc, "stun")) {
1838 log_debug("Parsing STUN servers from options: '%s'", GET_OPTION(stun_servers));
1839 int count =
1840 stun_servers_parse(GET_OPTION(stun_servers), OPT_ENDPOINT_STUN_SERVERS_DEFAULT, stun_servers, 4);
1841 if (count > 0) {
1842 stun_count = count;
1843 log_debug("Parsed %d STUN servers", count);
1844 for (int i = 0; i < count; i++) {
1845 log_debug(" STUN[%d]: '%s' (len=%d)", i, stun_servers[i].host, stun_servers[i].host_len);
1846 }
1847 } else {
1848 log_warn("Failed to parse STUN servers, using defaults");
1850 stun_servers, 4);
1851 log_debug("Using default STUN servers, count=%d", stun_count);
1852 for (int i = 0; i < stun_count; i++) {
1853 log_debug(" STUN[%d]: '%s' (len=%d)", i, stun_servers[i].host, stun_servers[i].host_len);
1854 }
1855 }
1856 }
1857
1858 static turn_server_t turn_servers[4];
1859 static char turn_auth_username[128];
1860 static char turn_auth_credential[128];
1861 size_t turn_count = 0;
1862 const char *turn_username = GET_OPTION(turn_username);
1863 const char *turn_credential = GET_OPTION(turn_credential);
1864 SAFE_STRNCPY(turn_auth_username, turn_username, sizeof(turn_auth_username));
1865 SAFE_STRNCPY(turn_auth_credential, turn_credential, sizeof(turn_auth_credential));
1866 if (turn_username[0] && turn_credential[0]) {
1867 char urls[OPTIONS_BUFF_SIZE];
1868 SAFE_STRNCPY(urls, GET_OPTION(turn_servers), sizeof(urls));
1869 char *saveptr = NULL;
1870 char *url = platform_strtok_r(urls, ",", &saveptr);
1871 while (url && turn_count < 4) {
1872 while (*url == ' ' || *url == '\t')
1873 url++;
1874 size_t len = strlen(url);
1875 while (len && (url[len - 1] == ' ' || url[len - 1] == '\t'))
1876 url[--len] = '\0';
1877 if (len && len < sizeof(turn_servers[0].url)) {
1878 turn_server_t *turn = &turn_servers[turn_count++];
1879 SAFE_STRNCPY(turn->url, url, sizeof(turn->url));
1880 SAFE_STRNCPY(turn->username, turn_username, sizeof(turn->username));
1881 SAFE_STRNCPY(turn->credential, turn_credential, sizeof(turn->credential));
1882 turn->url_len = (uint8_t)len;
1883 turn->username_len = (uint8_t)strlen(turn->username);
1884 turn->credential_len = (uint8_t)strlen(turn->credential);
1885 }
1886 url = platform_strtok_r(NULL, ",", &saveptr);
1887 }
1888 }
1889
1890 // Configure peer_manager
1891 const char *server_address = GET_OPTION(address);
1892 bool bind_webrtc_to_server_address = server_address && server_address[0] &&
1893 strcmp(server_address, "0.0.0.0") != 0 &&
1894 strcmp(server_address, "::") != 0;
1895 webrtc_peer_manager_config_t pm_config = {
1896 .role = WEBRTC_ROLE_CREATOR, // Server accepts offers, generates answers
1897 // Honor an explicitly selected server interface for ICE. With a
1898 // wildcard bind, let libdatachannel gather candidates on all interfaces.
1899 .bind_address = bind_webrtc_to_server_address ? server_address : NULL,
1900 .stun_servers = stun_servers,
1901 .stun_count = stun_count,
1902 .turn_servers = turn_servers,
1903 .turn_count = turn_count,
1904 .relay_only = GET_OPTION(webrtc_relay_only),
1905 .turn_username = turn_auth_username,
1906 .turn_credential = turn_auth_credential,
1907 .on_transport_ready = on_webrtc_transport_ready,
1908 .user_data = &g_server_ctx,
1909 .crypto_ctx = NULL // WebRTC handles crypto internally
1910 };
1911
1912 // Configure signaling callbacks for relaying SDP/ICE via ACDS
1913 webrtc_signaling_callbacks_t signaling_callbacks = {
1914 .send_sdp = server_send_sdp, .send_ice = server_send_ice, .user_data = NULL};
1915
1916 // Create peer_manager
1917 asciichat_error_t pm_result =
1918 webrtc_peer_manager_create(&pm_config, &signaling_callbacks, &g_webrtc_peer_manager);
1919 if (pm_result != ASCIICHAT_OK) {
1920 log_error("Failed to create WebRTC peer_manager: %s", asciichat_error_string(pm_result));
1921 g_webrtc_peer_manager = NULL;
1922 } else {
1923 log_debug("WebRTC peer_manager initialized successfully");
1924
1925 // Start ACDS receive thread for WebRTC signaling relay
1926 int thread_result =
1927 asciichat_thread_create(&g_acds_receive_thread, "acds_recv", acds_receive_thread, NULL);
1928 if (thread_result != 0) {
1929 log_error("Failed to create ACDS receive thread: %d", thread_result);
1930 // Cleanup peer_manager since signaling won't work
1931 webrtc_peer_manager_destroy(g_webrtc_peer_manager);
1932 g_webrtc_peer_manager = NULL;
1933 } else {
1934 log_debug("ACDS receive thread started for WebRTC signaling relay");
1935 g_acds_receive_thread_started = true;
1936 }
1937 }
1938 } // Close else block from webrtc_init() success
1939 } else {
1940 log_debug("Session type is DIRECT_TCP, skipping WebRTC peer_manager initialization");
1941 }
1942
1943 // Advertise mDNS with ACDS session string
1944 // This ensures both mDNS and ACDS discovery return the same session string
1945 advertise_mdns_with_session(session_string, (uint16_t)port);
1946 } else {
1947 log_warn("Failed to create session on ACDS server (server will run without discovery)");
1948 // Clean up failed ACDS client
1949 if (g_acds_client) {
1950 acds_client_disconnect(g_acds_client);
1951 SAFE_FREE(g_acds_client);
1952 g_acds_client = NULL;
1953 }
1954 }
1955 } else {
1956 log_warn("Could not connect to ACDS server at %s:%d (server will run without discovery)", acds_server, acds_port);
1957 }
1958 } else {
1959 log_info("ACDS registration disabled (use --acds to enable)");
1960 }
1961
1962skip_acds_session:
1963 // Fallback: If no session string was set by ACDS (either disabled or failed),
1964 // generate a random session string for mDNS discovery only
1965 if (session_string[0] == '\0' && session_server_like_get_mdns_ctx()) {
1966 log_debug("No ACDS session string available, generating random session for mDNS");
1967
1968 // Use the proper session string generation from discovery module
1969 // This generates adjective-noun-noun format using the full wordlists
1970 if (acds_string_generate(session_string, sizeof(session_string)) != ASCIICHAT_OK) {
1971 log_error("Failed to generate session string for mDNS");
1972 return SET_ERRNO(ERROR_INVALID_STATE, "Failed to generate session string for mDNS");
1973 }
1974
1975 log_debug("Generated random session string for mDNS: '%s'", session_string);
1976
1977 // Mark that this session is mDNS-only (not globally discoverable via ACDS)
1978 session_is_mdns_only = true;
1979
1980 // Advertise mDNS with random session string
1981 advertise_mdns_with_session(session_string, (uint16_t)port);
1982 }
1983
1984 // ====================================================================
1985 // Display session string prominently as the FINAL startup message
1986 // This ensures users see the connection info clearly without logs
1987 // wiping it away
1988 // ====================================================================
1989 if (session_string[0] != '\0') {
1990 if (session_is_mdns_only) {
1991 log_plain("========================================================\nSession String: %s (LAN only via "
1992 "mDNS)\nShare with others on your LAN to join:\n ascii-chat "
1993 "%s\n========================================================",
1994 session_string, session_string);
1995 } else {
1996 log_plain("========================================================\nSession String: %s\nShare this "
1997 "globally to join:\n ascii-chat %s\n========================================================",
1998 session_string, session_string);
1999 }
2000 }
2001
2002 // Copy session info to globals for status screen display
2003 SAFE_STRNCPY(g_session_string, session_string, sizeof(g_session_string));
2004 g_session_is_mdns_only = session_is_mdns_only;
2005
2006 // Initialize status screen timestamps
2007 g_server_start_time = time(NULL);
2008 g_last_status_update = platform_get_monotonic_time_us();
2009
2010 log_debug("Server init_fn complete");
2011 return ASCIICHAT_OK;
2012}
2013
2014/* ============================================================================
2015 * Mode-specific cleanup callback
2016 * ============================================================================ */
2017static void server_cleanup_fn(void *user_data) {
2018 (void)user_data;
2019
2020 log_debug("Server shutting down...");
2021 memset(g_session_string, 0, sizeof(g_session_string)); // Clear session string for status screen
2022
2023 // Wake up any threads that might be blocked on condition variables
2024 // (like packet queues) to ensure responsive shutdown
2025 static_cond_broadcast(&g_shutdown_cond);
2026
2027 // Close all client sockets immediately to unblock receive threads.
2028 // Only do this if initialization succeeded and created the rwlock.
2029 log_debug("Closing all client sockets to unblock receive threads...");
2032 for (int i = 0; i < MAX_CLIENTS; i++) {
2034 if (client->client_id[0] != '\0' && client->socket != INVALID_SOCKET_VALUE) {
2035 socket_close(client->socket);
2036 client->socket = INVALID_SOCKET_VALUE;
2037 }
2038 }
2040 }
2041
2042 log_debug("Signaling all clients to stop (sockets closed, g_should_exit set)...");
2043
2044 // Stop and destroy server worker thread pool (stats logger, etc.)
2045 if (g_server_worker_pool) {
2046 thread_pool_destroy(g_server_worker_pool);
2047 g_server_worker_pool = NULL;
2048 log_debug("Server worker thread pool stopped");
2049 }
2050
2051 // Destroy rate limiter
2052 if (g_rate_limiter) {
2054 g_rate_limiter = NULL;
2055 }
2056
2057 // Wait for all client handler threads to exit (only if rwlock was initialized)
2059 int retry_count = 0;
2060 while (retry_count < 50) { // Max 500ms wait
2062 bool all_cleaned = true;
2063 for (int i = 0; i < MAX_CLIENTS; i++) {
2064 if (g_client_manager.clients[i].client_id[0] != '\0') {
2065 all_cleaned = false;
2066 break;
2067 }
2068 }
2070
2071 if (all_cleaned)
2072 break;
2073 platform_sleep_us(10 * US_PER_MS_INT); // 10ms
2074 retry_count++;
2075 }
2076 log_debug("Client handler threads cleaned up");
2077 }
2078
2079 // Cleanup debug sync BEFORE destroying websocket_server
2080#ifndef NDEBUG
2082#endif
2083
2084 // Clean up all connected clients (only if rwlock was initialized)
2085 log_debug("Cleaning up connected clients...");
2086 char clients_to_remove[MAX_CLIENTS][MAX_CLIENT_ID_LEN];
2087 int client_count = 0;
2088
2091 for (int i = 0; i < MAX_CLIENTS; i++) {
2093 if (client->client_id[0] == '\0') {
2094 continue;
2095 }
2096 SAFE_STRNCPY(clients_to_remove[client_count], client->client_id, MAX_CLIENT_ID_LEN - 1);
2097 client_count++;
2098 }
2100 }
2101
2102 for (int i = 0; i < client_count; i++) {
2103 if (remove_client(&g_server_ctx, clients_to_remove[i]) != 0) {
2104 log_error("Failed to remove client %s during shutdown", clients_to_remove[i]);
2105 }
2106 }
2107
2108 // Clean up hash table
2111 HASH_DELETE(hh, g_client_manager.clients_by_id, current_client);
2112 }
2113
2114 // Clean up audio mixer
2115 if (g_audio_mixer) {
2116 mixer_t *mixer_to_destroy = g_audio_mixer;
2117 g_audio_mixer = NULL;
2118 mixer_destroy(mixer_to_destroy);
2119 }
2120
2121 // Clean up H.265 codec server context
2122 if (g_h265_server) {
2123 h265_server_context_t *h265_to_destroy = g_h265_server;
2124 g_h265_server = NULL;
2125 g_server_ctx.h265_server = NULL;
2126 h265_server_context_destroy(h265_to_destroy);
2127 log_debug("H.265 codec server context shut down");
2128 }
2129
2130 // Clean up synchronization primitives (only if they were initialized)
2134 }
2136
2137#ifndef NDEBUG
2138 stats_cleanup();
2139#endif
2140
2141 // Destroy session host
2142 if (g_server_ctx.session_host) {
2143 log_debug("Destroying session host");
2144 session_host_destroy(g_server_ctx.session_host);
2145 g_server_ctx.session_host = NULL;
2146 }
2147
2148 // Destroy H.265 server codec context from server_ctx
2149 if (g_server_ctx.h265_server) {
2150 log_debug("Destroying H.265 server context");
2152 g_server_ctx.h265_server = NULL;
2153 }
2154
2155 // TCP server, WebSocket server, mDNS, UPnP are destroyed by server_like.
2156
2157 // Join ACDS threads (if started)
2158 if (g_acds_ping_thread_started) {
2159 log_debug("Joining ACDS ping thread");
2160 asciichat_thread_join(&g_acds_ping_thread, NULL);
2161 g_acds_ping_thread_started = false;
2162 log_debug("ACDS ping thread joined");
2163 }
2164
2165 if (g_acds_receive_thread_started) {
2166 log_debug("Joining ACDS receive thread");
2167 asciichat_thread_join(&g_acds_receive_thread, NULL);
2168 g_acds_receive_thread_started = false;
2169 log_debug("ACDS receive thread joined");
2170 }
2171
2172 // Clean up WebRTC peer manager
2173 if (g_webrtc_peer_manager) {
2174 log_debug("Destroying WebRTC peer manager");
2175 webrtc_peer_manager_destroy(g_webrtc_peer_manager);
2176 g_webrtc_peer_manager = NULL;
2177 }
2178
2179 // Clean up ACDS transport wrapper
2180 if (g_acds_transport) {
2181 log_debug("Destroying ACDS transport wrapper");
2182 acip_transport_destroy(g_acds_transport);
2183 g_acds_transport = NULL;
2184 }
2185
2186 // Disconnect from ACDS server
2187 if (g_acds_client) {
2188 log_debug("Disconnecting from ACDS server");
2189 acds_client_disconnect(g_acds_client);
2190 SAFE_FREE(g_acds_client);
2191 g_acds_client = NULL;
2192 }
2193
2194 // Clean up platform-specific resources
2196
2197 log_info("Server shutdown complete");
2199}
2200
2201/* ============================================================================
2202 * server_main() - Entry point, delegates to session_server_like_run()
2203 * ============================================================================ */
2204int server_main(void) {
2205 // Common initialization (options, logging, lock debugging) now happens in main.c before dispatch
2206
2207 // Register atomic for debug tracking
2209
2210 // Register shutdown check callback for library code
2211 shutdown_register_callback(check_shutdown);
2212
2213 // Initialize crypto after logging is ready
2214 log_debug("Initializing crypto...");
2215 if (init_server_crypto() != 0) {
2216 LOG_ERRNO_IF_SET("Crypto initialization failed");
2217 FATAL(ERROR_CRYPTO, "Crypto initialization failed");
2218 }
2219 log_debug("Crypto initialized successfully");
2220
2221 log_info("ascii-chat server starting...");
2222
2223 int port = GET_OPTION(port);
2224 if (port < 1 || port > 65535) {
2225 log_error("Invalid port configuration: %d", port);
2226 FATAL(ERROR_CONFIG, "Invalid port configuration: %d", port);
2227 }
2228
2231
2232#ifndef NDEBUG
2233 if (stats_init() != 0) {
2234 FATAL(ERROR_THREAD, "Statistics system initialization failed");
2235 }
2236#endif
2237
2238 // Create background worker thread pool for server operations
2239 g_server_worker_pool = thread_pool_create("server_workers");
2240 if (!g_server_worker_pool) {
2241 LOG_ERRNO_IF_SET("Failed to create server worker thread pool");
2242 FATAL(ERROR_MEMORY, "Failed to create server worker thread pool");
2243 }
2244
2245 // Spawn statistics logging thread in worker pool
2246 if (thread_pool_spawn(g_server_worker_pool, stats_logger_thread, NULL, 0, "stats_logger") != ASCIICHAT_OK) {
2247 LOG_ERRNO_IF_SET("Statistics logger thread creation failed");
2248 } else {
2249 log_debug("Statistics logger thread started");
2250 }
2251
2252 // Initialize static server context (populated further in server_init_fn)
2253 // H.265 context is initialized in server_init_fn, not here
2254 g_server_ctx = (server_context_t){
2255 .tcp_server = NULL, // Set in server_init_fn after TCP server is ready
2256 .rate_limiter = g_rate_limiter,
2257 .client_manager = &g_client_manager,
2258 .client_manager_rwlock = &g_client_manager_rwlock,
2259 .server_should_exit = &g_should_exit,
2260 .audio_mixer = g_audio_mixer,
2261 .stats = &g_stats,
2262 .stats_mutex = &g_stats_mutex,
2263 .encryption_enabled = g_server_encryption_enabled,
2264 .server_private_key = &g_server_private_key,
2265 .client_whitelist = g_client_whitelist,
2266 .num_whitelisted_clients = g_num_whitelisted_clients,
2267 .session_host = NULL,
2268 .h265_server = NULL, // Initialized in server_init_fn
2269 };
2270
2272 .init_fn = server_init_fn,
2273 .init_user_data = NULL,
2274 .interrupt_fn = server_handle_sigint,
2275 .cleanup_fn = server_cleanup_fn,
2276 .cleanup_user_data = NULL,
2277 .status_fn = server_status_fn,
2278 .status_user_data = NULL,
2279 .tcp_handler = ascii_chat_client_handler,
2280 .tcp_user_data = &g_server_ctx,
2281 .websocket =
2282 {
2283 .enabled = true,
2284 .handler = websocket_client_handler,
2285 .user_data = &g_server_ctx,
2286 },
2287 .mdns =
2288 {
2289 .enabled = !GET_OPTION(no_mdns_advertise),
2290 .service_name = NULL, // Deferred: advertised after ACDS or mDNS session string is ready
2291 .service_type = "_ascii-chat._tcp",
2292 },
2293 .upnp =
2294 {
2295 .enabled = GET_OPTION(enable_upnp),
2296 .description = "ascii-chat Server",
2297 },
2298 // Raise FD limit for high-client-count scenarios (matches discovery-service policy)
2299 // Server accepts MAX_CLIENTS connections plus WebSocket, TCP, UPnP, mDNS, etc.
2300 // Each client may use multiple FDs (socket, media buffers, file handles)
2301 // Raising to 65536 ensures we don't hit system limits under load
2302 .raise_fd_limit = true,
2303 .fd_limit_target = 65536,
2304 };
2305
2307 return (result == ASCIICHAT_OK) ? 0 : 1;
2308}
Platform abstraction layer umbrella header providing unified cross-platform API.
void acds_client_config_init_defaults(acds_client_config_t *config)
Initialize ACDS client configuration with defaults.
asciichat_error_t acds_session_create(acds_client_t *client, const acds_session_create_params_t *params, acds_session_create_result_t *result)
Create a new session on the discovery server.
asciichat_error_t acds_session_join(acds_client_t *client, const acds_session_join_params_t *params, acds_session_join_result_t *result)
Join an existing session.
asciichat_error_t acds_client_connect(acds_client_t *client, const acds_client_config_t *config)
Connect to ACDS server.
void acds_client_disconnect(acds_client_t *client)
Disconnect from ACDS server.
ACIP client-side protocol library and ACDS client connection API.
Application-level callbacks for library code.
#define APP_CALLBACK_VOID(callback_name)
⚠️‼️ Comprehensive thread-local error context system for ascii-chat
#define LOG_ERRNO_IF_SET(message)
Check if any error occurred and log it if so.
#define PRINT_ERRNO_CONTEXT(context)
void atomic_store_bool(atomic_t *a, bool value)
Atomically store a boolean value.
Definition atomic.c:177
int atomic_fetch_add_int(atomic_t *a, int delta)
Atomically add to an int and return the previous value.
Definition atomic.c:209
bool atomic_load_bool(atomic_t *a)
Atomically load a boolean value.
Definition atomic.c:169
void atomic_store_u64(atomic_t *a, uint64_t value)
Atomically store a uint64_t value.
Definition atomic.c:241
⚛️ Atomic operations abstraction layer with debug tracking
#define ATOMIC_REGISTER_AUTO(name, parent_ptr)
Register an atomic variable for debug tracking with automatic name generation.
Definition atomic.h:472
🗃️ Lock-Free Unified Memory Buffer Pool with Lazy Allocation
⚙️ Common definitions, error codes, macros, and types shared throughout the application
asciichat_error_t thread_pool_spawn(void *pool, void *(*thread_func)(void *), void *thread_arg, int stop_id, const char *name)
Spawn a worker thread in the pool.
asciichat_error_t crypto_handshake_init(const char *name, crypto_handshake_context_t *ctx, bool is_server)
Initialize named crypto handshake context.
Common declarations and data structures for cryptographic handshake.
🔍 Memory debugging helpers for tracking allocations in debug builds
Named object registry for debugging — log identifiable resource names.
asciichat_error_t acds_string_generate(char *output, size_t output_size)
Generate random session string.
void pubkey_to_hex(const uint8_t pubkey[32], char hex_out[65])
Convert Ed25519 public key to hex string.
Definition discovery.c:49
🔄 Network byte order conversion helpers
#define HOST_TO_NET_U16(val)
Definition endian.h:96
#define NET_TO_HOST_U16(val)
Definition endian.h:111
Cross-platform error handling utilities.
Network error handling utilities.
atomic_t g_should_exit
Global application exit flag (shared across all modes)
Definition globals.c:34
acip_webrtc_ice_t
Definition acds.h:650
acip_webrtc_sdp_t
Definition acds.h:604
@ SESSION_TYPE_WEBRTC
WebRTC P2P mesh with STUN/TURN relay.
Definition acds.h:146
@ SESSION_TYPE_DIRECT_TCP
Direct TCP connection to server IP:port (default)
Definition acds.h:145
#define AUDIO_SAMPLE_RATE
Audio sample rate (48kHz professional quality, Opus-compatible)
mixer_t * mixer_create(int max_sources, int sample_rate)
Create a new audio mixer.
Definition mixer.c:262
void mixer_destroy(mixer_t *mixer)
Destroy a mixer and free all resources.
Definition mixer.c:396
unsigned short uint16_t
Definition common.h:57
#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_SNPRINTF(buffer, buffer_size,...)
Definition common.h:492
#define FATAL(code,...)
Exit with error code and custom message, with stack trace in debug builds.
Definition common.h:158
unsigned long long uint64_t
Definition common.h:59
unsigned char uint8_t
Definition common.h:56
#define NAMED_REGISTER_RWLOCK(lock, name, parent_ptr)
Register an rwlock with automatic format specifier.
void debug_sync_destroy(void)
Destroy debug synchronization system.
Definition sync.c:670
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
#define HAS_ERRNO(var)
Check if an error occurred and get full context.
void asciichat_error_stats_print(void)
Print error statistics to stderr.
#define CLEAR_ERRNO()
Clear the current error state.
__thread asciichat_error_context_t asciichat_errno_context
Thread-local error context storage.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ERROR_INVALID_STATE
@ ERROR_CRYPTO_KEY
Definition error_codes.h:97
@ ERROR_NETWORK
Definition error_codes.h:77
@ ERROR_AUDIO
Definition error_codes.h:67
@ ERROR_MEMORY
Definition error_codes.h:56
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_MEDIA_INIT
Definition error_codes.h:70
@ ERROR_CONFIG
Definition error_codes.h:57
@ ERROR_CRYPTO
Definition error_codes.h:96
@ ERROR_INVALID_PARAM
@ ERROR_NETWORK_TIMEOUT
Definition error_codes.h:80
@ ERROR_THREAD
asciichat_error_t parse_private_key(const char *key_path, private_key_t *key_out)
Parse SSH private key from file.
Definition keys.c:193
key_type_t type
Definition key_types.h:92
asciichat_error_t parse_public_keys(const char *input, public_key_t *keys_out, size_t *num_keys, size_t max_keys)
Parse all SSH/GPG public keys from any format (returns all keys)
Definition keys.c:344
uint8_t public_key[32]
Definition key_types.h:99
asciichat_error_t validate_ssh_key_file(const char *key_path)
Validate SSH key file before parsing.
Definition ssh_keys.c:939
@ KEY_TYPE_UNKNOWN
Definition key_types.h:51
#define MAX_CLIENTS
Maximum possible clients (static array size) - actual runtime limit set by –max-clients (1-32)
Definition limits.h:26
#define MAX_CLIENT_ID_LEN
Maximum client ID length - format: "noun.N (transport:port)" e.g., "mountain.0 (tcp:48036)".
Definition limits.h:23
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
#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 log_plain(...)
Plain logging - writes to both log file and stderr without timestamps or log levels.
Definition log/log.h:618
#define US_PER_MS_INT
Definition time.h:160
#define MS_PER_SEC_INT
Definition time.h:164
asciichat_error_t packet_send(socket_t sockfd, packet_type_t type, const void *data, size_t len)
Send a packet with proper header and CRC32.
Definition packet.c:292
#define GET_OPTION(field)
Safely get a specific option field (lock-free read)
#define MAX_IDENTITY_KEYS
Maximum number of identity keys that can be loaded (for multi-key support)
#define OPT_ENDPOINT_STUN_SERVERS_DEFAULT
Default STUN servers (comma-separated list)
const float weight_blue
Blue weight for luminance calculation.
const float weight_red
Red weight for luminance calculation.
#define OPTIONS_BUFF_SIZE
Buffer size for option string values.
const float weight_green
Green weight for luminance calculation.
@ PACKET_TYPE_ACIP_WEBRTC_ICE
WebRTC ICE candidate (bidirectional)
Definition packet.h:428
@ PACKET_TYPE_PONG
Keepalive pong response.
Definition packet.h:385
@ PACKET_TYPE_ACIP_WEBRTC_SDP
WebRTC SDP offer/answer (bidirectional)
Definition packet.h:426
@ PACKET_TYPE_PING
Keepalive ping packet.
Definition packet.h:383
#define rwlock_wrunlock(lock)
Release a write lock (with debug tracking in debug builds)
Definition rwlock.h:349
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:148
bool platform_prompt_yes_no(const char *question, bool default_yes)
Prompt the user for a yes/no answer.
Definition util.c:83
int rwlock_init(rwlock_t *rwlock, const char *name)
Initialize a read-write lock with a name.
Definition threading.c:65
int rwlock_destroy(rwlock_t *lock)
Destroy a read-write lock.
void socket_cleanup(void)
Cleanup socket subsystem.
#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
#define rwlock_rdlock(lock)
Acquire a read lock (with debug tracking in debug builds)
Definition rwlock.h:294
int socket_close(socket_t sock)
Close a socket.
void platform_force_exit(int exit_code)
Forcefully terminate the process immediately without cleanup.
Definition misc.c:121
uint64_t platform_get_monotonic_time_us(void)
Get monotonic time in microseconds.
#define rwlock_wrlock(lock)
Acquire a write lock (with debug tracking in debug builds)
Definition rwlock.h:313
void platform_sleep_ms(unsigned int ms)
Sleep for a specified number of milliseconds.
void platform_sleep_us(unsigned int us)
High-precision sleep function with microsecond precision.
char * platform_strtok_r(char *str, const char *delim, char **saveptr)
Thread-safe string tokenization (strtok_r replacement)
#define rwlock_rdunlock(lock)
Release a read lock (with debug tracking in debug builds)
Definition rwlock.h:331
int mutex_destroy(mutex_t *mutex)
Destroy a mutex.
Definition threading.c:22
size_t g_num_whitelisted_clients
Number of whitelisted clients.
bool g_server_encryption_enabled
Global flag indicating if server encryption is enabled.
public_key_t g_client_whitelist[MAX_CLIENTS]
Global client public key whitelist.
private_key_t g_server_private_key
Global server private key (first identity key, for backward compatibility)
private_key_t g_server_identity_keys[MAX_IDENTITY_KEYS]
Global server identity keys array (multi-key support)
size_t g_num_server_identity_keys
Number of loaded server identity keys.
void session_host_destroy(session_host_t *host)
Destroy session host and free resources.
Definition host.c:249
session_host_t * session_host_create(const session_host_config_t *config)
Create a new session host.
Definition host.c:193
void shutdown_register_callback(shutdown_check_fn callback)
Register application's shutdown check function.
Definition common.c:69
stun_server_t
Definition stun.h:75
int stun_servers_parse(const char *csv_servers, const char *default_csv, stun_server_t *out_servers, int max_count)
Parse comma-separated STUN server URLs into stun_server_t array.
Definition stun.c:51
turn_server_t
Definition turn.h:101
void precalc_rgb_palettes(const float red, const float green, const float blue)
Precalculate RGB palettes with color adjustment.
void ascii_simd_init(void)
Initialize SIMD subsystem.
asciichat_error_t webrtc_peer_manager_create(const webrtc_peer_manager_config_t *config, const webrtc_signaling_callbacks_t *signaling_callbacks, webrtc_peer_manager_t **manager_out)
Create a WebRTC peer manager.
asciichat_error_t webrtc_peer_manager_handle_ice(webrtc_peer_manager_t *manager, const acip_webrtc_ice_t *ice)
Handle incoming ICE candidate from ACDS.
asciichat_error_t webrtc_peer_manager_handle_sdp(webrtc_peer_manager_t *manager, const acip_webrtc_sdp_t *sdp)
Handle incoming SDP message from ACDS.
void webrtc_peer_manager_destroy(webrtc_peer_manager_t *manager)
Destroy peer manager and close all connections.
@ WEBRTC_ROLE_CREATOR
Session creator - accepts offers, generates answers.
asciichat_error_t webrtc_init(void)
Initialize WebRTC library (libdatachannel)
ACIP protocol packet handlers (transport-agnostic)
int socket_t
🔊 Audio Capture and Playback Interface for ascii-chat
Server-side handshake functions.
📝 JSON structured logging output
ACIP client-side protocol API.
ascii-chat Binary Network Protocol (complete system including ACIP discovery service)
ACIP server-side protocol API.
WebSocket server for accepting browser client connections.
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
Platform initialization and static synchronization helpers.
Capture stdout/stderr and redirect to logging system.
🌍 IP Address Parsing and Formatting Utilities
🎮 Cross-platform keyboard input interface for ascii-chat
asciichat_error_t crypto_handshake_server_send_parameters(crypto_handshake_context_t *ctx, acip_transport_t *transport)
Server: Send CRYPTO_PARAMETERS and set handshake context sizes.
asciichat_error_t crypto_handshake_server_start(crypto_handshake_context_t *ctx, acip_transport_t *transport)
Server: Start crypto handshake by sending public key.
asciichat_error_t acip_client_receive_and_dispatch(acip_transport_t *transport, const acip_client_callbacks_t *callbacks)
Receive packet from server and dispatch to callbacks.
void tcp_server_reject_client(socket_t socket, const char *reason)
Reject client connection with reason.
int tcp_client_context_get_port(const tcp_client_context_t *ctx)
Get port number from client context.
const char * tcp_client_context_get_ip(const tcp_client_context_t *ctx, char *buf, size_t len)
Get formatted IP address from client context.
void websocket_server_cancel_service(websocket_server_t *server)
Wake the WebSocket server event loop immediately.
#define asciichat_thread_create(thread_ptr, attr, start_routine, arg)
void * asciichat_thread_t
#define asciichat_thread_join(thread, timeout_ms)
bool lifecycle_init(lifecycle_t *lc, const char *name)
Definition lifecycle.c:26
Lock-free module lifecycle state machine using pure stdatomic.
#define LIFECYCLE_INIT
Static initializer for module-global lifecycle variables (no sync primitive)
Definition lifecycle.h:68
Application limits and constraints.
bool should_exit(void)
Definition misc.c:131
Multi-Source Audio Mixing and Processing System.
asciichat_error_t asciichat_mdns_advertise(asciichat_mdns_t *mdns, const asciichat_mdns_service_t *service)
Advertise a service on the local network.
🌐 Core network I/O operations with timeout support
Cross-platform network headers and socket definitions.
Cross-platform string operations.
bool terminal_can_prompt_user(void)
Determine if interactive user prompts are appropriate.
Cross-platform interactive prompting utilities.
asciichat_error_t rate_limiter_record(rate_limiter_t *limiter, const char *ip_address, rate_event_type_t event_type)
Record a rate limit event.
Definition rate_limit.c:145
void rate_limiter_destroy(rate_limiter_t *limiter)
Destroy rate limiter and free resources.
Definition rate_limit.c:116
rate_limiter_t * rate_limiter_create_memory(void)
Create in-memory rate limiter.
Definition rate_limit.c:73
asciichat_error_t rate_limiter_check(rate_limiter_t *limiter, const char *ip_address, rate_event_type_t event_type, const rate_limit_config_t *config, bool *allowed)
Check if an event from an IP address should be rate limited.
Definition rate_limit.c:128
🚦 Rate limiting API with pluggable backends
@ RATE_EVENT_CONNECTION
New connection.
Definition rate_limit.h:53
void log_search_signal_cancel(void)
Cancel grep mode from a signal handler (async-signal-safe)
Definition search.c:376
bool log_search_is_entering_atomic(void)
Check if grep is in entering mode (async-signal-safe)
Definition search.c:372
Interactive search filtering for terminal screens.
asciichat_error_t packet_send_via_transport(acip_transport_t *transport, packet_type_t type, const void *payload, size_t payload_len, uint32_t client_id)
Send packet via transport with proper header (exported for generic wrappers)
Definition send.c:41
ACIP shared/bidirectional packet sending functions.
h265_server_context_t *volatile g_h265_server
Global H.265 codec server context.
rate_limiter_t * g_rate_limiter
Global rate limiter for connection attempts and packet processing.
mixer_t *volatile g_audio_mixer
Global audio mixer instance for multi-client audio processing.
int server_main(void)
Server mode entry point for unified binary.
static_cond_t g_shutdown_cond
Global shutdown condition variable for waking blocked threads.
ascii-chat Server Mode Entry Point Header
websocket_server_t * session_server_like_get_websocket_server(void)
asciichat_mdns_t * session_server_like_get_mdns_ctx(void)
tcp_server_t * session_server_like_get_tcp_server(void)
asciichat_error_t session_server_like_run(const session_server_like_config_t *config)
nat_upnp_context_t * session_server_like_get_upnp_ctx(void)
Shared initialization, networking, and teardown for server-like modes.
Cross-platform crash signal and exception handling.
Cross-platform socket interface for ascii-chat.
uint8_t session_id[16]
uint8_t participant_id[16]
client_info_t * add_client(server_context_t *server_ctx, socket_t socket, const char *client_ip, int port)
rwlock_t g_client_manager_rwlock
Reader-writer lock protecting the global client manager.
client_info_t * add_webrtc_client(server_context_t *server_ctx, acip_transport_t *transport, const char *client_ip, bool start_threads)
Register a WebRTC client with the server.
bool g_client_manager_rwlock_initialized
int start_webrtc_client_threads(server_context_t *server_ctx, const char *client_id)
Start threads for a WebRTC client after crypto initialization.
client_manager_t g_client_manager
Global client manager singleton - central coordination point.
int remove_client(server_context_t *server_ctx, const char *client_id)
Client-side H.265 media capture and encoding pipeline.
h265_server_context_t * h265_server_context_create(void)
void h265_server_context_destroy(h265_server_context_t *ctx)
Server-side H.265 decoding pipeline for multi-client streams.
server_stats_t g_stats
Global server statistics structure.
Definition stats.c:163
void * stats_logger_thread(void *arg)
Main statistics collection and reporting thread function.
Definition stats.c:327
int stats_init(void)
Initialize the stats mutex.
Definition stats.c:188
mutex_t g_stats_mutex
Mutex protecting global server statistics.
Definition stats.c:172
void stats_cleanup(void)
Cleanup the stats mutex.
Definition stats.c:201
Server performance statistics tracking.
asciichat_error_t ui_status_gather(tcp_server_t *server, const char *session_string, const char *ipv4_address, const char *ipv6_address, uint16_t port, time_t start_time, const char *mode_name, bool session_is_mdns_only, ui_status_t *out_status)
Gather current status information.
Definition status.c:52
📊 Status screen display and management for server/discovery service modes
ClangTool/LibTooling compatibility shim for stdbool.h.
Multi-client video mixing and ASCII frame generation.
ACDS client connection configuration.
char server_address[256]
ACDS server address (e.g., "discovery.ascii.chat" or "127.0.0.1")
uint32_t timeout_ms
Connection timeout in milliseconds.
uint16_t server_port
ACDS server port (default: 27225)
ACDS client connection handle.
socket_t socket
TCP socket to ACDS server.
Session creation request parameters.
uint16_t server_port
Server port where clients should connect.
bool acds_expose_ip
Explicitly allow public IP disclosure (–acds-expose-ip opt-in)
uint8_t max_participants
Maximum participants (1-8)
uint8_t identity_pubkey[32]
Ed25519 public key (host identity)
char server_address[64]
Server address where clients should connect.
bool has_password
Password protection enabled.
uint8_t capabilities
Bit 0: video, Bit 1: audio.
char password[128]
Optional password (if has_password)
uint8_t session_type
acds_session_type_t: 0=DIRECT_TCP (default), 1=WEBRTC
Session creation result.
uint8_t session_id[16]
Session UUID.
char session_string[49]
Generated session string (null-terminated)
Session join parameters.
uint8_t identity_pubkey[32]
Participant's Ed25519 public key.
bool has_password
Password provided.
char password[128]
Password (if has_password)
const char * session_string
Session to join.
Session join result.
char error_message[129]
Error message (if !success, null-terminated)
uint8_t participant_id[16]
Participant UUID (if success)
bool success
Join succeeded.
uint8_t session_id[16]
Session UUID (if success)
Client-side packet handler callbacks.
Definition handlers.h:51
void(* on_ascii_frame)(const ascii_frame_packet_t *header, const void *frame_data, size_t data_len, void *ctx)
Called when ASCII frame received from server.
Definition handlers.h:53
socket_t(* get_socket)(acip_transport_t *transport)
Get underlying socket (if applicable)
Definition transport.h:175
Transport instance structure.
Definition transport.h:214
const acip_transport_methods_t * methods
Method table (virtual functions)
Definition transport.h:215
Error context structure.
char * context_message
Optional custom message (dynamically allocated, owned by system)
Service information for advertisement.
Internal mDNS context structure.
Atomic value wrapper for integral/boolean types.
Definition atomic.h:76
Per-client state structure for server-side client management.
bool transport_encrypted
TLS or DTLS already encrypts this client's transport.
char client_id[MAX_CLIENT_ID_LEN]
client_info_t * clients_by_id
uthash head pointer for O(1) client_id -> client_info_t* lookups
mutex_t mutex
Legacy mutex (mostly replaced by rwlock)
client_info_t clients[MAX_CLIENTS]
Array of client_info_t structures (backing storage)
Main mixer structure for multi-source audio processing.
Definition mixer.h:325
Handle to UPnP context.
Definition upnp.h:25
Private key structure (for server –ssh-key)
Definition key_types.h:91
Public key structure.
Definition key_types.h:69
Rate limiter structure.
Definition rate_limit.c:64
Server context - encapsulates all server state.
tcp_server_t * tcp_server
TCP server managing connections.
session_host_t * session_host
Session host for discovery mode support.
mixer_t * audio_mixer
Multi-client audio mixer.
atomic_t * server_should_exit
Shutdown flag.
h265_server_context_t * h265_server
H.265 decoder pool for all clients.
rate_limiter_t * rate_limiter
Connection and packet rate limiter.
Configuration for session host.
Definition host.h:154
int port
Port to listen on (default: 27224)
Definition host.h:156
session_server_like_init_fn init_fn
Static condition variable structure for global condition variables requiring static initialization.
socket_t client_socket
Client connection socket.
void * user_data
User-provided data from config.
const char * ipv4_address
IPv4 bind address (NULL or empty = don't bind)
const char * ipv6_address
IPv6 bind address (NULL or empty = don't bind)
atomic_t running
Server running flag (set false to shutdown)
tcp_server_config_t config
Server configuration.
socket_t listen_socket
IPv4 listen socket.
socket_t listen_socket6
IPv6 listen socket.
Status information for server/discovery service modes.
Definition status.h:30
Peer manager configuration.
webrtc_peer_role_t role
Session role (creator or joiner)
WebRTC peer manager structure.
Signaling callbacks for sending SDP/ICE.
webrtc_send_sdp_callback_t send_sdp
Send SDP via ACDS.
Context passed to client handler thread.
acip_transport_t * transport
ACIP transport for this client.
char client_ip[64]
Client IP address (for logging/rate limiting)
void * user_data
User-provided data from server config.
bool is_secure
True if connection is using TLS (wss://)
void thread_pool_destroy(thread_pool_t *pool)
Destroy a thread pool.
void thread_pool_t
thread_pool_t * thread_pool_create(int num_threads)
Symbol Resolution Cache for Backtrace Addresses.
Cross-platform system functions interface for ascii-chat.
acip_transport_t * acip_tcp_transport_create(const char *name, socket_t sockfd, crypto_context_t *crypto_ctx)
Create TCP transport from existing socket.
void acip_transport_destroy(acip_transport_t *transport)
Destroy transport and free all resources.
🖥️ Cross-platform terminal interface for ascii-chat
🧵 Generic thread pool abstraction for managing worker threads
⏱️ High-precision timing utilities using sokol_time.h and uthash
Transport abstraction layer for ACIP protocol.
UPnP/NAT-PMP port mapping for direct TCP connectivity.
🔤 String Manipulation and Shell Escaping Utilities
SIMD-optimized ASCII conversion interface.