ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
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)
55#endif
56
57#include <ascii-chat/platform/network.h> // Consolidates platform-specific network headers
58
59#include <errno.h>
60#include <limits.h>
61#include <signal.h>
62#include <stdbool.h>
63#include <stdio.h>
64#include <stdlib.h>
65#include <stdint.h>
66#include <string.h>
67#include <sys/types.h>
68#include <sys/stat.h>
69#include <time.h>
70
71#include "main.h"
72#include <ascii-chat/common.h>
73#include <ascii-chat/util/endian.h>
74#include <ascii-chat/util/ip.h>
75#include <ascii-chat/util/time.h>
76#include <ascii-chat/uthash/uthash.h>
77#include <ascii-chat/platform/abstraction.h>
78#include <ascii-chat/platform/socket.h>
79#include <ascii-chat/platform/init.h>
80#include <ascii-chat/platform/question.h>
81#include <ascii-chat/platform/terminal.h>
82#include <ascii-chat/video/image.h>
83#include <ascii-chat/video/simd/ascii_simd.h>
84#include <ascii-chat/video/simd/common.h>
85#include <ascii-chat/asciichat_errno.h>
86#include <ascii-chat/network/network.h>
87#include <ascii-chat/network/tcp/server.h>
88#include <ascii-chat/network/websocket/server.h>
89#include <ascii-chat/network/acip/acds_client.h>
90#include <ascii-chat/discovery/strings.h>
91#include <ascii-chat/network/webrtc/stun.h>
92#include <ascii-chat/thread_pool.h>
93#include <ascii-chat/options/options.h>
94#include <ascii-chat/options/rcu.h> // For RCU-based options access
95#include <ascii-chat/buffer_pool.h>
96#include <ascii-chat/audio/mixer.h>
97#include <ascii-chat/audio/audio.h>
98#include "client.h"
99#include "stream.h"
100#include "stats.h"
101#include <ascii-chat/platform/string.h>
102#include <ascii-chat/platform/symbols.h>
103#include <ascii-chat/platform/system.h>
104#include <ascii-chat/crypto/keys.h>
105#include <ascii-chat/crypto/handshake/server.h>
106#include <ascii-chat/crypto/handshake/common.h>
107#include <ascii-chat/network/rate_limit/rate_limit.h>
108#include <ascii-chat/network/mdns/mdns.h>
109#include <ascii-chat/network/mdns/discovery.h>
110#include <ascii-chat/network/errors.h>
111#include <ascii-chat/network/nat/upnp.h>
112#include <ascii-chat/network/webrtc/peer_manager.h>
113#include <ascii-chat/network/webrtc/webrtc.h>
114#include <ascii-chat/network/acip/send.h>
115#include <ascii-chat/network/acip/protocol.h>
116#include <ascii-chat/network/acip/transport.h>
117#include <ascii-chat/network/acip/handlers.h>
118#include <ascii-chat/network/acip/server.h>
119#include <ascii-chat/network/acip/client.h>
120#include <ascii-chat/ui/server_status.h>
121#include <ascii-chat/log/interactive_grep.h>
122#include <ascii-chat/log/json.h>
123#include <ascii-chat/platform/keyboard.h>
124#include <ascii-chat/debug/memory.h>
125
126/* ============================================================================
127 * Global State
128 * ============================================================================
129 */
130
143atomic_bool g_server_should_exit = false;
144
152static bool check_shutdown(void) {
153 return atomic_load(&g_server_should_exit);
154}
155
167mixer_t *volatile g_audio_mixer = NULL;
168
176static_cond_t g_shutdown_cond = STATIC_COND_INIT;
177
197rate_limiter_t *g_rate_limiter = NULL;
198
211static tcp_server_t g_tcp_server;
212
221static websocket_server_t g_websocket_server;
222
228static asciichat_thread_t g_websocket_server_thread;
229static bool g_websocket_server_thread_started = false;
230
237static time_t g_server_start_time = 0;
238
245static uint64_t g_last_status_update = 0; // Microseconds from platform_get_monotonic_time_us()
246
253static asciichat_thread_t g_status_screen_thread;
254
263static asciichat_thread_t g_keyboard_thread;
264static _Atomic bool g_keyboard_thread_running = false;
265
269#define KEYBOARD_QUEUE_SIZE 256
270static keyboard_key_t g_keyboard_queue[KEYBOARD_QUEUE_SIZE];
271static _Atomic size_t g_keyboard_queue_head = 0;
272static _Atomic size_t g_keyboard_queue_tail = 0;
273
274static bool keyboard_queue_push(keyboard_key_t key) {
275 size_t head = atomic_load(&g_keyboard_queue_head);
276 size_t next_head = (head + 1) % KEYBOARD_QUEUE_SIZE;
277 if (next_head == atomic_load(&g_keyboard_queue_tail)) {
278 return false;
279 }
280 g_keyboard_queue[head] = key;
281 atomic_store(&g_keyboard_queue_head, next_head);
282 return true;
283}
284
285static keyboard_key_t keyboard_queue_pop(void) {
286 size_t tail = atomic_load(&g_keyboard_queue_tail);
287 if (tail == atomic_load(&g_keyboard_queue_head)) {
288 return KEY_NONE;
289 }
290 keyboard_key_t key = g_keyboard_queue[tail];
291 atomic_store(&g_keyboard_queue_tail, (tail + 1) % KEYBOARD_QUEUE_SIZE);
292 return key;
293}
294
303static void *keyboard_thread_func(void *arg) {
304 (void)arg;
305 log_debug("Keyboard thread started (polling mode, 100ms interval)");
306
307 while (atomic_load(&g_keyboard_thread_running)) {
308 keyboard_key_t key = keyboard_read_with_timeout(100);
309 if (key != KEY_NONE) {
310 keyboard_queue_push(key);
311 }
312 }
313
314 log_debug("Keyboard thread exiting");
315 return NULL;
316}
317
324static char g_session_string[64] = {0};
325
329static bool g_session_is_mdns_only = false;
330
340static nat_upnp_context_t *g_upnp_ctx = NULL;
341
351static asciichat_mdns_t *g_mdns_ctx = NULL;
352
362static acds_client_t *g_acds_client = NULL;
363
372static acip_transport_t *g_acds_transport = NULL;
373
380static uint8_t g_server_participant_id[16] = {0};
381
391static webrtc_peer_manager_t *g_webrtc_peer_manager = NULL;
392
402static asciichat_thread_t g_acds_receive_thread;
403
411static bool g_acds_receive_thread_started = false;
412
422static asciichat_thread_t g_acds_ping_thread;
423
431static bool g_acds_ping_thread_started = false;
432
439static thread_pool_t *g_server_worker_pool = NULL;
440
441/* ============================================================================
442 * Server Crypto State
443 * ============================================================================
444 */
445
457
469private_key_t g_server_private_key = {0};
470
482private_key_t g_server_identity_keys[MAX_IDENTITY_KEYS] = {0};
483
493
505public_key_t g_client_whitelist[MAX_CLIENTS] = {0};
506
516
517/* ============================================================================
518 * Signal Handlers
519 * ============================================================================
520 */
521
563static void server_handle_sigint(int sigint) {
564 (void)(sigint);
565
566 // If in grep input mode, cancel grep instead of shutting down.
567 // Uses atomic reads only (no mutex) - safe from signal context.
570 return;
571 }
572
573 static _Atomic int sigint_count = 0;
574 int count = atomic_fetch_add(&sigint_count, 1) + 1;
575 if (count > 1) {
576 platform_force_exit(1);
577 }
578
579 // STEP 1: Set atomic shutdown flag (checked by all worker threads)
580 atomic_store(&g_server_should_exit, true);
581
582 // STEP 2: Use raw write() for user feedback (async-signal-safe).
583 // log_info_nofile() is NOT safe here - it calls SAFE_MALLOC (via ansi_strip_escapes),
584 // SAFE_FREE, and server_status_log_append (mutex). If the signal interrupts a thread
585 // holding the memory tracking lock or log buffer lock, the handler deadlocks.
586 // Since SIGINT auto-masks during handler execution, a second Ctrl+C would be
587 // blocked, preventing platform_force_exit() from ever running.
588 static const char sigint_msg[] = "\nSIGINT received - shutting down server...\n";
589 (void)write(STDERR_FILENO, sigint_msg, sizeof(sigint_msg) - 1);
590
591 // STEP 3: Signal TCP server to stop and close listening sockets
592 // This interrupts the accept() call in the main loop
593 atomic_store(&g_tcp_server.running, false);
594 if (g_tcp_server.listen_socket != INVALID_SOCKET_VALUE) {
595 socket_close(g_tcp_server.listen_socket);
596 g_tcp_server.listen_socket = INVALID_SOCKET_VALUE;
597 }
598 if (g_tcp_server.listen_socket6 != INVALID_SOCKET_VALUE) {
599 socket_close(g_tcp_server.listen_socket6);
600 g_tcp_server.listen_socket6 = INVALID_SOCKET_VALUE;
601 }
602
603 // STEP 3b: Signal WebSocket server to stop
604 // Without this, lws_service() could block for up to 50ms before checking the running flag.
605 // websocket_server_cancel_service() is async-signal-safe (uses only atomic operations).
606 atomic_store(&g_websocket_server.running, false);
607 websocket_server_cancel_service(&g_websocket_server);
608
609 // STEP 4: DO NOT access client data structures in signal handler
610 // Signal handlers CANNOT safely use mutexes, rwlocks, or access complex data structures
611 // This causes deadlocks and memory access violations because:
612 // 1. Signal may interrupt a thread that already holds these locks
613 // 2. Attempting to acquire locks in signal handler = instant deadlock
614 // 3. Client array might be in an inconsistent state during modification
615 //
616 // SOLUTION: The listening socket closure above is sufficient to unblock accept_with_timeout()
617 // The main thread will detect g_server_should_exit and properly close client sockets with timeouts
618
619 // NOTE: Do NOT call log_destroy() here - it's not async-signal-safe
620 // The main thread will handle cleanup when it detects g_server_should_exit
621}
622
623// bind_and_listen() function removed - now using lib/network/tcp_server abstraction
624
625/* ============================================================================
626 * WebRTC Callbacks
627 * ============================================================================
628 */
629
643static asciichat_error_t server_send_sdp(const uint8_t session_id[16], const uint8_t recipient_id[16],
644 const char *sdp_type, const char *sdp, void *user_data) {
645 (void)user_data;
646
647 if (!g_acds_transport) {
648 return SET_ERRNO(ERROR_INVALID_STATE, "ACDS transport not available for SDP relay");
649 }
650
651 // Calculate SDP length
652 size_t sdp_len = strlen(sdp);
653 if (sdp_len == 0 || sdp_len >= 8192) {
654 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid SDP length: %zu", sdp_len);
655 }
656
657 // Allocate packet buffer (header + SDP string)
658 size_t total_len = sizeof(acip_webrtc_sdp_t) + sdp_len;
659 uint8_t *packet = SAFE_MALLOC(total_len, uint8_t *);
660 if (!packet) {
661 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate SDP packet");
662 }
663
664 // Fill header
665 acip_webrtc_sdp_t *header = (acip_webrtc_sdp_t *)packet;
666 memcpy(header->session_id, session_id, 16);
667 // Use server's participant_id from SESSION_JOIN as sender
668 memcpy(header->sender_id, g_server_participant_id, 16);
669 memcpy(header->recipient_id, recipient_id, 16);
670 header->sdp_type = (strcmp(sdp_type, "offer") == 0) ? 0 : 1;
671 header->sdp_len = HOST_TO_NET_U16((uint16_t)sdp_len);
672
673 // Copy SDP string after header
674 memcpy(packet + sizeof(acip_webrtc_sdp_t), sdp, sdp_len);
675
676 log_debug("Server sending WebRTC SDP %s to participant (sender=%02x%02x..., recipient=%02x%02x...) via ACDS",
677 sdp_type, g_server_participant_id[0], g_server_participant_id[1], recipient_id[0], recipient_id[1]);
678
679 // Send via ACDS transport using generic packet sender
680 asciichat_error_t result =
681 packet_send_via_transport(g_acds_transport, PACKET_TYPE_ACIP_WEBRTC_SDP, packet, total_len, 0);
682
683 SAFE_FREE(packet);
684
685 if (result != ASCIICHAT_OK) {
686 return SET_ERRNO(result, "Failed to send SDP via ACDS");
687 }
688
689 return ASCIICHAT_OK;
690}
691
705static asciichat_error_t server_send_ice(const uint8_t session_id[16], const uint8_t recipient_id[16],
706 const char *candidate, const char *mid, void *user_data) {
707 (void)user_data;
708
709 if (!g_acds_transport) {
710 return SET_ERRNO(ERROR_INVALID_STATE, "ACDS transport not available for ICE relay");
711 }
712
713 // Calculate payload length (candidate + null + mid + null)
714 size_t candidate_len = strlen(candidate);
715 size_t mid_len = strlen(mid);
716 size_t payload_len = candidate_len + 1 + mid_len + 1;
717
718 if (payload_len >= 8192) {
719 return SET_ERRNO(ERROR_INVALID_PARAM, "ICE payload too large: %zu", payload_len);
720 }
721
722 // Allocate packet buffer (header + payload)
723 size_t total_len = sizeof(acip_webrtc_ice_t) + payload_len;
724 uint8_t *packet = SAFE_MALLOC(total_len, uint8_t *);
725 if (!packet) {
726 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate ICE packet");
727 }
728
729 // Fill header
730 acip_webrtc_ice_t *header = (acip_webrtc_ice_t *)packet;
731 memcpy(header->session_id, session_id, 16);
732 // Use server's participant_id from SESSION_JOIN as sender
733 memcpy(header->sender_id, g_server_participant_id, 16);
734 memcpy(header->recipient_id, recipient_id, 16);
735 header->candidate_len = HOST_TO_NET_U16((uint16_t)candidate_len); // FIXED: Use candidate length, not total payload
736
737 // Copy candidate and mid after header
738 uint8_t *payload = packet + sizeof(acip_webrtc_ice_t);
739 memcpy(payload, candidate, candidate_len);
740 payload[candidate_len] = '\0';
741 memcpy(payload + candidate_len + 1, mid, mid_len);
742 payload[candidate_len + 1 + mid_len] = '\0';
743
744 log_debug("Server sending WebRTC ICE candidate to participant (%.8s..., mid=%s) via ACDS", (const char *)recipient_id,
745 mid);
746 log_debug(" [2] Before ACDS send - candidate: '%s' (len=%zu)", candidate, strlen(candidate));
747 log_debug(" [2] Before ACDS send - mid: '%s' (len=%zu)", mid, strlen(mid));
748 log_debug(" [2] Before ACDS send - payload_len=%zu, header.candidate_len=%u", payload_len,
749 NET_TO_HOST_U16(header->candidate_len));
750
751 // Hex dump payload for debugging
752 log_debug(" [2] Hex dump of payload being sent (first 100 bytes):");
753 for (size_t i = 0; i < 100 && i < payload_len; i += 16) {
754 char hex[64] = {0};
755 char ascii[20] = {0};
756 for (size_t j = 0; j < 16 && (i + j) < 100 && (i + j) < payload_len; j++) {
757 snprintf(hex + j * 3, sizeof(hex) - j * 3, "%02x ", payload[i + j]);
758 ascii[j] = (payload[i + j] >= 32 && payload[i + j] < 127) ? payload[i + j] : '.';
759 }
760 log_debug(" [%04zx] %-48s %s", i, hex, ascii);
761 }
762
763 // Send via ACDS transport using generic packet sender
764 asciichat_error_t result =
765 packet_send_via_transport(g_acds_transport, PACKET_TYPE_ACIP_WEBRTC_ICE, packet, total_len, 0);
766
767 SAFE_FREE(packet);
768
769 if (result != ASCIICHAT_OK) {
770 return SET_ERRNO(result, "Failed to send ICE via ACDS");
771 }
772
773 return ASCIICHAT_OK;
774}
775
786static void on_webrtc_transport_ready(acip_transport_t *transport, const uint8_t participant_id[16], void *user_data) {
787 server_context_t *server_ctx = (server_context_t *)user_data;
788 if (!transport || !participant_id || !server_ctx) {
789 log_error("on_webrtc_transport_ready: Invalid parameters");
790 if (transport) {
791 acip_transport_destroy(transport);
792 }
793 return;
794 }
795
796 log_debug("WebRTC transport ready for participant %.8s...", (const char *)participant_id);
797
798 // Convert participant_id to string for logging (ASCII-safe portion)
799 char participant_str[33];
800 for (int i = 0; i < 16; i++) {
801 safe_snprintf(participant_str + (i * 2), 3, "%02x", participant_id[i]);
802 }
803 participant_str[32] = '\0';
804
805 // Add client to server (calls add_webrtc_client internally)
806 // For WebRTC DataChannel: start threads immediately (crypto already handled by ACDS)
807 int client_id = add_webrtc_client(server_ctx, transport, participant_str, true);
808 if (client_id < 0) {
809 log_error("Failed to add WebRTC client for participant %s", participant_str);
810 acip_transport_destroy(transport);
811 return;
812 }
813
814 log_debug("Successfully added WebRTC client ID=%d for participant %s", client_id, participant_str);
815}
816
827static void on_webrtc_sdp_server(const acip_webrtc_sdp_t *sdp, size_t total_len, void *ctx) {
828 (void)ctx;
829
830 if (!sdp || !g_webrtc_peer_manager) {
831 log_error("on_webrtc_sdp_server: Invalid parameters or peer_manager not initialized");
832 return;
833 }
834
835 // Validate packet length
836 uint16_t sdp_len = NET_TO_HOST_U16(sdp->sdp_len);
837 if (total_len < sizeof(acip_webrtc_sdp_t) + sdp_len) {
838 log_error("on_webrtc_sdp_server: Invalid packet length (total=%zu, expected>=%zu)", total_len,
839 sizeof(acip_webrtc_sdp_t) + sdp_len);
840 return;
841 }
842
843 // Determine SDP type (offer=0, answer=1)
844 const char *sdp_type = (sdp->sdp_type == 0) ? "offer" : "answer";
845
846 log_debug("Received WebRTC SDP %s from participant %.8s... (len=%u)", sdp_type, (const char *)sdp->sender_id,
847 sdp_len);
848
849 // Forward to peer_manager (pass full packet structure)
850 asciichat_error_t result = webrtc_peer_manager_handle_sdp(g_webrtc_peer_manager, sdp);
851
852 if (result != ASCIICHAT_OK) {
853 log_error("Failed to handle remote SDP from participant %.8s...: %s", (const char *)sdp->sender_id,
854 asciichat_error_string(result));
855 }
856}
857
868static void on_webrtc_ice_server(const acip_webrtc_ice_t *ice, size_t total_len, void *ctx) {
869 (void)ctx;
870
871 if (!ice || !g_webrtc_peer_manager) {
872 log_error("on_webrtc_ice_server: Invalid parameters or peer_manager not initialized");
873 return;
874 }
875
876 // Validate packet length
877 uint16_t payload_len = NET_TO_HOST_U16(ice->candidate_len);
878 if (total_len < sizeof(acip_webrtc_ice_t) + payload_len) {
879 log_error("on_webrtc_ice_server: Invalid packet length (total=%zu, expected>=%zu)", total_len,
880 sizeof(acip_webrtc_ice_t) + payload_len);
881 return;
882 }
883
884 log_debug("Received WebRTC ICE candidate from participant %.8s...", (const char *)ice->sender_id);
885
886 // Forward to peer_manager (pass full packet structure)
887 asciichat_error_t result = webrtc_peer_manager_handle_ice(g_webrtc_peer_manager, ice);
888
889 if (result != ASCIICHAT_OK) {
890 log_error("Failed to handle remote ICE candidate from participant %.8s...: %s", (const char *)ice->sender_id,
891 asciichat_error_string(result));
892 }
893}
894
904static void advertise_mdns_with_session(const char *session_string, uint16_t port) {
905 if (!g_mdns_ctx) {
906 log_debug("mDNS context not initialized, skipping advertisement");
907 return;
908 }
909
910 // Build session name from hostname for mDNS service name
911 char hostname[256] = {0};
912 char session_name[256] = "ascii-chat-Server";
913 if (gethostname(hostname, sizeof(hostname) - 1) == 0 && strlen(hostname) > 0) {
914 safe_snprintf(session_name, sizeof(session_name), "%s", hostname);
915 }
916
917 // Prepare TXT records with session string and host public key
918 char txt_session_string[512];
919 char txt_host_pubkey[512];
920 const char *txt_records[2];
921 int txt_count = 0;
922
923 // Add session string to TXT records (for client discovery)
924 safe_snprintf(txt_session_string, sizeof(txt_session_string), "session_string=%s", session_string);
925 txt_records[txt_count++] = txt_session_string;
926
927 // Add host public key to TXT records (for cryptographic verification)
928 // Convert server's Ed25519 public key to hex format
930 char hex_pubkey[65];
931 pubkey_to_hex(g_server_private_key.public_key, hex_pubkey);
932 safe_snprintf(txt_host_pubkey, sizeof(txt_host_pubkey), "host_pubkey=%s", hex_pubkey);
933 txt_records[txt_count++] = txt_host_pubkey;
934 log_debug("mDNS: Host pubkey=%s", hex_pubkey);
935 } else {
936 // If encryption is disabled, still advertise a zero pubkey for clients to detect
937 safe_snprintf(txt_host_pubkey, sizeof(txt_host_pubkey), "host_pubkey=");
938 for (int i = 0; i < 32; i++) {
939 safe_snprintf(txt_host_pubkey + strlen(txt_host_pubkey), sizeof(txt_host_pubkey) - strlen(txt_host_pubkey), "00");
940 }
941 txt_records[txt_count++] = txt_host_pubkey;
942 log_debug("mDNS: Encryption disabled, advertising zero pubkey");
943 }
944
945 asciichat_mdns_service_t service = {
946 .name = session_name,
947 .type = "_ascii-chat._tcp",
948 .host = hostname,
949 .port = port,
950 .txt_records = txt_records,
951 .txt_count = txt_count,
952 };
953
954 asciichat_error_t mdns_advertise_result = asciichat_mdns_advertise(g_mdns_ctx, &service);
955 if (mdns_advertise_result != ASCIICHAT_OK) {
956 LOG_ERRNO_IF_SET("Failed to advertise mDNS service");
957 log_warn("mDNS advertising failed - LAN discovery disabled");
958 asciichat_mdns_destroy(g_mdns_ctx);
959 g_mdns_ctx = NULL;
960 } else {
961 log_info("🌐 mDNS: Server advertised as '%s.local' on LAN", session_name);
962 log_debug("mDNS: Service advertised as '%s.local' (name=%s, port=%d, session=%s, txt_count=%d)", service.type,
963 service.name, service.port, session_string, service.txt_count);
964 }
965}
966
976static void *acds_ping_thread(void *arg) {
977 (void)arg;
978
979 log_debug("ACDS keepalive ping thread started");
980
981 while (!atomic_load(&g_server_should_exit)) {
982 if (!g_acds_transport) {
983 log_debug("ACDS transport destroyed, exiting ping thread");
984 break;
985 }
986
987 // Send PING every 10 seconds to keep connection alive
988 socket_t acds_socket = g_acds_transport->methods->get_socket(g_acds_transport);
989 if (acds_socket != INVALID_SOCKET_VALUE) {
990 asciichat_error_t ping_result = packet_send(acds_socket, PACKET_TYPE_PING, NULL, 0);
991 if (ping_result == ASCIICHAT_OK) {
992 log_debug("ACDS keepalive: Sent periodic PING");
993 } else {
994 log_warn("ACDS keepalive: Failed to send PING: %s", asciichat_error_string(ping_result));
995 }
996 }
997
998 // Sleep for 10 seconds before next ping (well before 15s timeout)
999 for (int i = 0; i < 100 && !atomic_load(&g_server_should_exit); i++) {
1000 platform_sleep_ms(100); // Check exit flag every 100ms
1001 }
1002 }
1003
1004 log_debug("ACDS keepalive ping thread exiting");
1005 return NULL;
1006}
1007
1020static void on_acds_ping(void *ctx) {
1021 (void)ctx;
1022 log_debug("ACDS keepalive: Received PING from ACDS, responding with PONG");
1023 if (g_acds_transport) {
1024 packet_send_via_transport(g_acds_transport, PACKET_TYPE_PONG, NULL, 0, 0);
1025 }
1026}
1027
1031static void on_acds_pong(void *ctx) {
1032 (void)ctx;
1033 log_debug("ACDS keepalive: Received PONG from ACDS server");
1034}
1035
1036static void *acds_receive_thread(void *arg) {
1037 (void)arg;
1038
1039 log_debug("ACDS receive thread started");
1040
1041 // Configure callbacks for WebRTC signaling packets and keepalive
1042 acip_client_callbacks_t callbacks = {
1043 .on_ascii_frame = NULL,
1044 .on_audio = NULL,
1045 .on_webrtc_sdp = on_webrtc_sdp_server,
1046 .on_webrtc_ice = on_webrtc_ice_server,
1047 .on_session_joined = NULL,
1048 .on_ping = on_acds_ping,
1049 .on_pong = on_acds_pong,
1050 .app_ctx = NULL,
1051 };
1052
1053 // Receive loop - just handle incoming packets
1054 // Keepalive is handled by separate ping thread
1055 while (!atomic_load(&g_server_should_exit)) {
1056 if (!g_acds_transport) {
1057 log_warn("ACDS transport is NULL, exiting receive thread");
1058 break;
1059 }
1060
1061 asciichat_error_t result = acip_client_receive_and_dispatch(g_acds_transport, &callbacks);
1062
1063 if (result != ASCIICHAT_OK) {
1064 // Check error context to see if connection actually closed
1065 asciichat_error_context_t err_ctx;
1066 bool has_context = HAS_ERRNO(&err_ctx);
1067
1068 // Timeouts are normal when there are no packets - just continue waiting
1069 if (result == ERROR_NETWORK_TIMEOUT) {
1070 continue;
1071 }
1072
1073 // ERROR_NETWORK could be:
1074 // 1. Receive timeout (non-fatal - continue waiting)
1075 // 2. EOF/connection closed (fatal - exit thread)
1076 // Check the error context message to distinguish
1077 if (result == ERROR_NETWORK) {
1078 if (has_context && strstr(err_ctx.context_message, "Failed to receive packet") != NULL) {
1079 // Generic receive failure (likely timeout) - continue waiting
1080 log_debug("ACDS receive timeout, continuing to wait for packets");
1081 continue;
1082 } else if (has_context && (strstr(err_ctx.context_message, "EOF") != NULL ||
1083 strstr(err_ctx.context_message, "closed") != NULL)) {
1084 // Connection actually closed
1085 log_warn("ACDS connection closed: %s", err_ctx.context_message);
1086 break;
1087 } else {
1088 // Unknown ERROR_NETWORK - log and exit
1089 log_warn("ACDS connection error: %s", has_context ? err_ctx.context_message : "unknown");
1090 break;
1091 }
1092 }
1093
1094 // Other errors - exit thread
1095 log_error("ACDS receive error: %s, exiting receive thread", asciichat_error_string(result));
1096 break;
1097 }
1098 }
1099
1100 if (atomic_load(&g_server_should_exit)) {
1101 log_debug("ACDS receive thread exiting (server shutdown)");
1102 } else {
1103 log_warn("ACDS receive thread exiting unexpectedly");
1104 }
1105 return NULL;
1106}
1107
1108/* ============================================================================
1109 * Signal Handlers
1110 * ============================================================================
1111 */
1112
1132static void server_handle_sigterm(int sigterm) {
1133 (void)(sigterm);
1134 atomic_store(&g_server_should_exit, true);
1135
1136 // Log to console only (text or JSON depending on --json flag)
1137 // Using log_console() which is signal-safe and handles both formats
1138 log_console(LOG_INFO, "SIGTERM received - shutting down server...");
1139
1140 // Stop the TCP server accept loop immediately.
1141 // Without this, the select() call with ACCEPT_TIMEOUT could delay shutdown.
1142 atomic_store(&g_tcp_server.running, false);
1143 if (g_tcp_server.listen_socket != INVALID_SOCKET_VALUE) {
1144 socket_close(g_tcp_server.listen_socket);
1145 g_tcp_server.listen_socket = INVALID_SOCKET_VALUE;
1146 }
1147 if (g_tcp_server.listen_socket6 != INVALID_SOCKET_VALUE) {
1148 socket_close(g_tcp_server.listen_socket6);
1149 g_tcp_server.listen_socket6 = INVALID_SOCKET_VALUE;
1150 }
1151
1152 // Stop the WebSocket server event loop immediately.
1153 // Without this, lws_service() could block for up to 50ms before checking the running flag.
1154 atomic_store(&g_websocket_server.running, false);
1155 websocket_server_cancel_service(&g_websocket_server);
1156}
1157
1158/* ============================================================================
1159 * Status Screen Update Callback (for tcp_server integration)
1160 * ============================================================================
1161 */
1162
1183static void *websocket_server_thread_wrapper(void *arg) {
1184 websocket_server_t *server = (websocket_server_t *)arg;
1185 asciichat_error_t result = websocket_server_run(server);
1186 if (result != ASCIICHAT_OK) {
1187 log_error("WebSocket server thread exited with error");
1188 }
1189 return NULL;
1190}
1191
1192static void *status_screen_thread(void *arg) {
1193 (void)arg; // Unused
1194
1195 uint32_t fps = GET_OPTION(fps);
1196 if (fps == 0) {
1197 fps = 60; // Default
1198 }
1199 uint64_t frame_interval_us = US_PER_SEC_INT / fps;
1200
1201 log_debug("Status screen thread started (target %u FPS)", fps);
1202
1203 // Initialize keyboard for interactive grep (cross-platform)
1204 bool keyboard_enabled = false;
1206 log_info("Terminal is interactive, initializing keyboard...");
1207 if (keyboard_init() == ASCIICHAT_OK) {
1208 atomic_store(&g_keyboard_thread_running, true);
1209 if (asciichat_thread_create(&g_keyboard_thread, keyboard_thread_func, NULL) == 0) {
1210 keyboard_enabled = true;
1211 log_info("Keyboard thread started - press '/' to activate grep");
1212 } else {
1213 log_warn("Failed to create keyboard thread");
1214 atomic_store(&g_keyboard_thread_running, false);
1215 keyboard_destroy();
1216 }
1217 } else {
1218 log_warn("Failed to initialize keyboard");
1219 }
1220 } else {
1221 log_warn("Terminal is NOT interactive, keyboard disabled");
1222 }
1223
1224 // Track when we just entered grep mode to skip the triggering '/' from buffer
1225 bool skip_next_slash = false;
1226 bool grep_was_just_cancelled = false;
1227
1228 while (!atomic_load(&g_server_should_exit)) {
1229 uint64_t frame_start = platform_get_monotonic_time_us();
1230
1231 // Check if SIGINT handler cancelled grep mode (Ctrl+C while in grep).
1232 // The signal handler sets an atomic flag instead of touching mutex-protected state.
1235 grep_was_just_cancelled = true;
1236 }
1237
1238 // Process all keys from the queue (keyboard thread captures them).
1239 // Ctrl+C is NOT in the queue (ISIG is enabled, so it generates SIGINT
1240 // which is handled above). The queue only has regular keypresses.
1241 if (keyboard_enabled) {
1242 // Reset the "just cancelled" flag each frame
1243 grep_was_just_cancelled = false;
1244
1245 // Pull all keys from queue
1246 keyboard_key_t key = keyboard_queue_pop();
1247 while (key != KEY_NONE && !grep_was_just_cancelled) {
1248 // Skip the '/' that just triggered grep mode entry
1249 if (skip_next_slash && key == '/') {
1250 skip_next_slash = false;
1251 key = keyboard_queue_pop(); // Get next key from queue
1252 continue;
1253 }
1254 skip_next_slash = false;
1255
1256 // Let grep handle its keys
1258 bool was_not_in_grep = !interactive_grep_is_entering();
1259 bool was_in_grep = interactive_grep_is_entering();
1260
1262
1263 // If we just entered grep mode with '/', skip next '/' from buffer
1264 if (was_not_in_grep && interactive_grep_is_entering() && key == '/') {
1265 skip_next_slash = true;
1266 }
1267
1268 // If grep was cancelled, stop processing keys
1269 if (was_in_grep && !interactive_grep_is_entering()) {
1270 grep_was_just_cancelled = true;
1271 break;
1272 }
1273 }
1274
1275 // Get next key from queue
1276 key = keyboard_queue_pop();
1277 }
1278 }
1279
1280 // Get the IPv4 and IPv6 addresses from TCP server config
1281 const char *ipv4_address = g_tcp_server.config.ipv4_address;
1282 const char *ipv6_address = g_tcp_server.config.ipv6_address;
1283
1284 // Render status screen (server_status_update handles rate limiting internally)
1285 server_status_update(&g_tcp_server, g_session_string, ipv4_address, ipv6_address, GET_OPTION(port),
1286 g_server_start_time, "Server", g_session_is_mdns_only, &g_last_status_update);
1287
1288 // Sleep to maintain frame rate
1289 uint64_t frame_end = platform_get_monotonic_time_us();
1290 uint64_t frame_time = frame_end - frame_start;
1291
1292 // Maintain frame rate (keyboard thread captures keys instantly)
1293 if (frame_time < frame_interval_us) {
1294 platform_sleep_us(frame_interval_us - frame_time);
1295 }
1296 }
1297
1298 // Stop keyboard thread (polls at 100ms, so join completes within ~100ms)
1299 if (keyboard_enabled) {
1300 log_debug("Stopping keyboard thread...");
1301 atomic_store(&g_keyboard_thread_running, false);
1302 asciichat_thread_join(&g_keyboard_thread, NULL);
1303 log_debug("Keyboard thread stopped");
1304 keyboard_destroy();
1305 }
1306
1307 log_debug("Status screen thread exiting");
1308 return NULL;
1309}
1310
1311/* ============================================================================
1312 * Client Handler Thread (for tcp_server integration)
1313 * ============================================================================
1314 */
1315
1329static void *ascii_chat_client_handler(void *arg) {
1330 tcp_client_context_t *ctx = (tcp_client_context_t *)arg;
1331 if (!ctx) {
1332 log_error("Client handler: NULL context");
1333 return NULL;
1334 }
1335
1336 // Extract server context from user_data
1337 server_context_t *server_ctx = (server_context_t *)ctx->user_data;
1338 if (!server_ctx) {
1339 log_error("Client handler: NULL server context");
1340 socket_close(ctx->client_socket);
1341 SAFE_FREE(ctx);
1342 return NULL;
1343 }
1344
1345 socket_t client_socket = ctx->client_socket;
1346
1347 // Extract client IP and port using tcp_server helpers
1348 char client_ip[INET6_ADDRSTRLEN] = {0};
1349 if (!tcp_client_context_get_ip(ctx, client_ip, sizeof(client_ip))) {
1350 safe_snprintf(client_ip, sizeof(client_ip), "unknown");
1351 }
1352
1353 int client_port = tcp_client_context_get_port(ctx);
1354 if (client_port < 0) {
1355 client_port = 0;
1356 }
1357
1358 log_debug("Client handler started for %s:%d", client_ip, client_port);
1359
1360 // Check connection rate limit (prevent DoS attacks)
1361 if (server_ctx->rate_limiter) {
1362 bool allowed = false;
1363 asciichat_error_t rate_check =
1364 rate_limiter_check(server_ctx->rate_limiter, client_ip, RATE_EVENT_CONNECTION, NULL, &allowed);
1365 if (rate_check != ASCIICHAT_OK || !allowed) {
1366 tcp_server_reject_client(client_socket, "Connection rate limit exceeded");
1367 SAFE_FREE(ctx);
1368 return NULL;
1369 }
1370 // Record successful connection attempt
1371 rate_limiter_record(server_ctx->rate_limiter, client_ip, RATE_EVENT_CONNECTION);
1372 }
1373
1374 // Add client (initializes structures, spawns workers via tcp_server_spawn_thread)
1375 int client_id = add_client(server_ctx, client_socket, client_ip, client_port);
1376 if (client_id < 0) {
1377 if (HAS_ERRNO(&asciichat_errno_context)) {
1378 PRINT_ERRNO_CONTEXT(&asciichat_errno_context);
1379 CLEAR_ERRNO();
1380 }
1381 tcp_server_reject_client(client_socket, "Failed to add client");
1382 SAFE_FREE(ctx);
1383 return NULL;
1384 }
1385
1386 log_debug("Client %d added successfully from %s:%d", client_id, client_ip, client_port);
1387
1388 // Block until client disconnects (active flag is set by receive thread)
1389 client_info_t *client = find_client_by_id((uint32_t)client_id);
1390 if (!client) {
1391 log_error("CRITICAL: Client %d not found after successful add! (not in hash table?)", client_id);
1392 } else {
1393 log_debug("HANDLER: Client %d found, waiting for disconnect (active=%d)", client_id, atomic_load(&client->active));
1394 int wait_count = 0;
1395 while (atomic_load(&client->active) && !atomic_load(server_ctx->server_should_exit)) {
1396 wait_count++;
1397 if (wait_count % 10 == 0) {
1398 // Log every 1 second (10 * 100ms)
1399 log_debug("HANDLER: Client %d still active (waited %d seconds), active=%d", client_id, wait_count / 10,
1400 atomic_load(&client->active));
1401 }
1402 platform_sleep_ms(100); // Check every 100ms
1403 }
1404 log_info("Client %d disconnected from %s:%d (waited %d seconds, active=%d, server_should_exit=%d)", client_id,
1405 client_ip, client_port, wait_count / 10, atomic_load(&client->active),
1406 atomic_load(server_ctx->server_should_exit));
1407 }
1408
1409 // Cleanup (this will call tcp_server_stop_client_threads internally)
1410 if (remove_client(server_ctx, (uint32_t)client_id) != 0) {
1411 log_error("CRITICAL BUG: Failed to remove client %d from server (potential zombie client leak!)", client_id);
1412 }
1413
1414 // Close socket and free context
1415 socket_close(client_socket);
1416 SAFE_FREE(ctx);
1417
1418 log_debug("Client handler finished for %s:%d", client_ip, client_port);
1419 return NULL;
1420}
1421
1431static void *websocket_client_handler(void *arg) {
1432 websocket_client_context_t *ctx = (websocket_client_context_t *)arg;
1433 if (!ctx) {
1434 log_error("WebSocket client handler: NULL context");
1435 return NULL;
1436 }
1437
1438 log_info("WebSocket client connected from %s:%d", ctx->client_ip, ctx->client_port);
1439 log_debug("[WS_HANDLER] ===== ENTRY: WebSocket client handler called for %s:%d =====", ctx->client_ip,
1440 ctx->client_port);
1441
1442 // Extract server context from user_data
1443 server_context_t *server_ctx = (server_context_t *)ctx->user_data;
1444 if (!server_ctx) {
1445 log_error("WebSocket client handler: NULL server context");
1446 if (ctx->transport) {
1447 acip_transport_destroy(ctx->transport);
1448 }
1449 SAFE_FREE(ctx);
1450 return NULL;
1451 }
1452
1453 // STEP 1: Add client first (creates client structure WITHOUT starting threads yet)
1454 log_debug("[WS_HANDLER] STEP 1: Calling add_webrtc_client()...");
1455 int client_id = add_webrtc_client(server_ctx, ctx->transport, ctx->client_ip, false);
1456 if (client_id < 0) {
1457 log_error("[WS_HANDLER] FAILED: add_webrtc_client returned %d", client_id);
1458 if (ctx->transport) {
1459 acip_transport_destroy(ctx->transport);
1460 }
1461 SAFE_FREE(ctx);
1462 return NULL;
1463 }
1464
1465 // Transport ownership transferred to client structure - don't destroy it here
1466 ctx->transport = NULL;
1467
1468 log_info("WebSocket client %d added successfully", client_id);
1469 log_debug("[WS_HANDLER] add_webrtc_client returned client_id=%d, transport ownership transferred", client_id);
1470
1471 // STEP 2: Get client structure and initialize crypto handshake
1472 log_debug("[WS_HANDLER] STEP 2: Finding client structure for ID %d...", client_id);
1473 client_info_t *client = find_client_by_id((uint32_t)client_id);
1474 if (!client) {
1475 log_error("[WS_HANDLER] FAILED: Client %d not found after successful add", client_id);
1476 SAFE_FREE(ctx);
1477 return NULL;
1478 }
1479 log_debug("[WS_HANDLER] Found client structure: client=%p, transport=%p, crypto_initialized=%d", (void *)client,
1480 (void *)client->transport, client->crypto_initialized);
1481
1482 // Initialize crypto handshake context directly in client structure
1483 // This must happen BEFORE sending KEY_EXCHANGE_INIT so that when the client
1484 // responds, the receive thread has a properly initialized context
1485 log_debug("[WS_HANDLER] Calling crypto_handshake_init()...");
1486 asciichat_error_t handshake_init_result = crypto_handshake_init(&client->crypto_handshake_ctx, true /* is_server */);
1487 if (handshake_init_result != ASCIICHAT_OK) {
1488 log_error("[WS_HANDLER] FAILED: crypto_handshake_init returned %d: %s", handshake_init_result,
1489 asciichat_error_string(handshake_init_result));
1490 remove_client(server_ctx, (uint32_t)client_id);
1491 SAFE_FREE(ctx);
1492 return NULL;
1493 }
1494 log_debug("[WS_HANDLER] crypto_handshake_init OK, handshake_ctx.state=%d", client->crypto_handshake_ctx.state);
1495
1496 // Mark crypto as NOT initialized yet (will be set to true when handshake completes)
1497 log_debug("[WS_HANDLER] Setting crypto_initialized from %d to false...", client->crypto_initialized);
1498 client->crypto_initialized = false;
1499
1500 log_debug("Initialized crypto handshake context for WebSocket client %d", client_id);
1501 log_debug("[WS_HANDLER] crypto_initialized now = %d", client->crypto_initialized);
1502
1503 // STEP 3: Now send KEY_EXCHANGE_INIT to start the handshake
1504 // The receive thread is already running, but it will use the properly initialized context
1505 log_info("[WS_HANDLER] STEP 3: Calling crypto_handshake_server_start()...");
1506 log_info("[WS_HANDLER] Arguments: ctx=%p (state=%d), transport=%p (type=%d)", (void *)&client->crypto_handshake_ctx,
1507 client->crypto_handshake_ctx.state, (void *)client->transport,
1508 client->transport ? client->transport->methods->get_type(client->transport) : -1);
1509 asciichat_error_t handshake_start_result =
1510 crypto_handshake_server_start(&client->crypto_handshake_ctx, client->transport);
1511 if (handshake_start_result != ASCIICHAT_OK) {
1512 log_error("[WS_HANDLER] FAILED: crypto_handshake_server_start returned %d: %s", handshake_start_result,
1513 asciichat_error_string(handshake_start_result));
1514 remove_client(server_ctx, (uint32_t)client_id);
1515 SAFE_FREE(ctx);
1516 return NULL;
1517 }
1518
1519 log_info("Sent CRYPTO_KEY_EXCHANGE_INIT to WebSocket client %d", client_id);
1520 log_info("[WS_HANDLER] ===== SUCCESS: Handshake started for client %d =====", client_id);
1521
1522 // STEP 4: Now start the threads after crypto is initialized
1523 // This ensures receive thread doesn't try to process packets before crypto context exists
1524 log_debug("[WS_HANDLER] STEP 4: Starting client threads after crypto initialization...");
1525 if (start_webrtc_client_threads(server_ctx, (uint32_t)client_id) != 0) {
1526 log_error("[WS_HANDLER] FAILED: start_webrtc_client_threads returned error");
1527 remove_client(server_ctx, (uint32_t)client_id);
1528 SAFE_FREE(ctx);
1529 return NULL;
1530 }
1531 log_info("[WS_HANDLER] Client threads started successfully for client %d", client_id);
1532
1533 // Handler thread exit - cleanup happens in the receive thread
1534 // The receive thread is responsible for calling remove_client() when it detects connection closure.
1535 // We exit immediately instead of blocking - this prevents the LWS service thread from being blocked
1536 // in thread_join() during LWS_CALLBACK_CLOSED, which would freeze the event loop.
1537 // The removal logic is now handled by the receive thread, which:
1538 // 1. Detects transport closure (recv fails or returns error)
1539 // 2. Sets active=false and signals other threads to stop
1540 // 3. Calls remove_client() with proper self-join detection to avoid deadlock
1541 log_debug("[WS_HANDLER] STEP 5: Handler thread exiting (client_id=%d, cleanup will happen in receive thread)",
1542 client_id);
1543
1544 SAFE_FREE(ctx);
1545 return NULL;
1546}
1547
1548/* ============================================================================
1549 * Main Function
1550 * ============================================================================
1551 */
1552
1614static int init_server_crypto(void) {
1615 // Check if encryption is disabled
1616 if (GET_OPTION(no_encrypt)) {
1617 log_info("Encryption: DISABLED (--no-encrypt)");
1619 return 0;
1620 }
1621
1622 // Load server identity keys (supports multiple --key flags for multi-key mode)
1623 size_t num_keys = GET_OPTION(num_identity_keys);
1624
1625 if (num_keys > 0) {
1626 // Multi-key mode: load all identity keys from identity_keys[] array
1627 log_info("Loading %zu identity key(s) for multi-key support...", num_keys);
1628
1629 for (size_t i = 0; i < num_keys && i < MAX_IDENTITY_KEYS; i++) {
1630 const char *key_path = GET_OPTION(identity_keys[i]);
1631
1632 if (strlen(key_path) == 0) {
1633 continue; // Skip empty entries
1634 }
1635
1636 // Validate SSH key file (skip validation for special prefixes)
1637 bool is_special_key = (strncmp(key_path, "gpg:", 4) == 0 || strncmp(key_path, "github:", 7) == 0 ||
1638 strncmp(key_path, "gitlab:", 7) == 0);
1639
1640 if (!is_special_key) {
1641 if (validate_ssh_key_file(key_path) != 0) {
1642 log_warn("Skipping invalid SSH key file: %s", key_path);
1643 continue;
1644 }
1645 }
1646
1647 // Parse key (handles SSH files and gpg: prefix, rejects github:/gitlab:)
1648 log_debug("Loading identity key #%zu: %s", i + 1, key_path);
1649 if (parse_private_key(key_path, &g_server_identity_keys[g_num_server_identity_keys]) == ASCIICHAT_OK) {
1650 log_debug("Successfully loaded identity key #%zu: %s", i + 1, key_path);
1651
1652 // Display key fingerprint for verification
1653 char hex_pubkey[65];
1655 log_debug(" Key fingerprint: %s", hex_pubkey);
1656
1658 } else {
1659 log_warn("Failed to parse identity key #%zu: %s (skipping)", i + 1, key_path);
1660 }
1661 }
1662
1663 if (g_num_server_identity_keys == 0) {
1664 log_error("No valid identity keys loaded despite %zu --key flag(s)", num_keys);
1665 SET_ERRNO(ERROR_CRYPTO_KEY, "No valid identity keys loaded");
1666 return -1;
1667 }
1668
1669 // Copy first key to g_server_private_key for backward compatibility
1670 memcpy(&g_server_private_key, &g_server_identity_keys[0], sizeof(private_key_t));
1671 log_info("Loaded %zu identity key(s) total", g_num_server_identity_keys);
1672
1673 } else if (strlen(GET_OPTION(encrypt_key)) > 0) {
1674 // Single-key mode (backward compatibility): load from encrypt_key field
1675 const char *key_path = GET_OPTION(encrypt_key);
1676
1677 // Validate SSH key file (skip validation for special prefixes)
1678 bool is_special_key = (strncmp(key_path, "gpg:", 4) == 0 || strncmp(key_path, "github:", 7) == 0 ||
1679 strncmp(key_path, "gitlab:", 7) == 0);
1680
1681 if (!is_special_key) {
1682 if (validate_ssh_key_file(key_path) != 0) {
1683 SET_ERRNO(ERROR_CRYPTO_KEY, "Invalid SSH key file: %s", key_path);
1684 return -1;
1685 }
1686 }
1687
1688 // Parse key
1689 log_info("Loading key for authentication: %s", key_path);
1690 if (parse_private_key(key_path, &g_server_private_key) == ASCIICHAT_OK) {
1691 log_info("Successfully loaded server key: %s", key_path);
1692
1693 // Also store in identity_keys array for consistency
1694 memcpy(&g_server_identity_keys[0], &g_server_private_key, sizeof(private_key_t));
1696 } else {
1697 log_error("Failed to parse key: %s\n"
1698 "This may be due to:\n"
1699 " - Wrong password for encrypted key\n"
1700 " - Unsupported key type (only Ed25519 is currently supported)\n"
1701 " - Corrupted key file\n"
1702 "\n"
1703 "Note: RSA and ECDSA keys are not yet supported\n"
1704 "To generate an Ed25519 key: ssh-keygen -t ed25519\n",
1705 key_path);
1706 SET_ERRNO(ERROR_CRYPTO_KEY, "Key parsing failed: %s", key_path);
1707 return -1;
1708 }
1709 } else if (strlen(GET_OPTION(password)) == 0) {
1710 // No identity key provided - server will run in simple mode
1711 // The server will still generate ephemeral keys for encryption, but no identity key
1712 g_server_private_key.type = KEY_TYPE_UNKNOWN;
1714 log_info("Server running without identity key (simple mode)");
1715 }
1716
1717 // Load client whitelist if provided
1718 if (strlen(GET_OPTION(client_keys)) > 0) {
1719 if (parse_public_keys(GET_OPTION(client_keys), g_client_whitelist, &g_num_whitelisted_clients, MAX_CLIENTS) != 0) {
1720 SET_ERRNO(ERROR_CRYPTO_KEY, "Client key parsing failed: %s", GET_OPTION(client_keys));
1721 return -1;
1722 }
1723 log_debug("Loaded %zu whitelisted clients", g_num_whitelisted_clients);
1724 log_info("Server will only accept %zu whitelisted clients", g_num_whitelisted_clients);
1725 }
1726
1728 return 0;
1729}
1730
1731int server_main(void) {
1732 // Common initialization (options, logging, lock debugging) now happens in main.c before dispatch
1733 // This function focuses on server-specific initialization
1734
1735 // Register shutdown check callback for library code
1736 shutdown_register_callback(check_shutdown);
1737
1738 // Initialize status screen log buffer if enabled AND terminal is interactive
1739 // In non-interactive mode (piped output), logs go directly to stdout/stderr
1740 if (GET_OPTION(status_screen) && terminal_is_interactive()) {
1742
1743 // Initialize interactive grep for status screen
1745 }
1746
1747 // Initialize crypto after logging is ready
1748 log_debug("Initializing crypto...");
1749 if (init_server_crypto() != 0) {
1750 // Print detailed error context if available
1751 LOG_ERRNO_IF_SET("Crypto initialization failed");
1752 FATAL(ERROR_CRYPTO, "Crypto initialization failed");
1753 }
1754 log_debug("Crypto initialized successfully");
1755
1756 // Handle keepawake: check for mutual exclusivity and apply mode default
1757 // Server default: keepawake DISABLED (use --keepawake to enable)
1758 if (GET_OPTION(enable_keepawake) && GET_OPTION(disable_keepawake)) {
1759 FATAL(ERROR_INVALID_PARAM, "--keepawake and --no-keepawake are mutually exclusive");
1760 }
1761 if (GET_OPTION(enable_keepawake)) {
1763 }
1764
1765 log_info("ascii-chat server starting...");
1766
1767 // log_info("SERVER: Options initialized, using log file: %s", log_filename);
1768 int port = GET_OPTION(port);
1769 if (port < 1 || port > 65535) {
1770 log_error("Invalid port configuration: %d", port);
1771 FATAL(ERROR_CONFIG, "Invalid port configuration: %d", port);
1772 }
1773
1776
1777 // Simple signal handling (temporarily disable complex threading signal handling)
1778 log_debug("Setting up simple signal handlers...");
1779
1780 // Handle Ctrl+C for cleanup
1781 platform_signal(SIGINT, server_handle_sigint);
1782 // Handle termination signal (SIGTERM is defined with limited support on Windows)
1783 platform_signal(SIGTERM, server_handle_sigterm);
1784#ifndef _WIN32
1785 // SIGPIPE not supported on Windows
1786 platform_signal(SIGPIPE, SIG_IGN);
1787 // Note: SIGUSR1 is registered in src/main.c for all modes
1788#endif
1789
1790#ifndef NDEBUG
1791 // Lock debug thread is now started in src/main.c (all modes)
1792 // Initialize statistics system
1793 if (stats_init() != 0) {
1794 FATAL(ERROR_THREAD, "Statistics system initialization failed");
1795 }
1796#endif
1797
1798 // Create background worker thread pool for server operations
1799 g_server_worker_pool = thread_pool_create("server_workers");
1800 if (!g_server_worker_pool) {
1801 LOG_ERRNO_IF_SET("Failed to create server worker thread pool");
1802 FATAL(ERROR_MEMORY, "Failed to create server worker thread pool");
1803 }
1804
1805 // Spawn statistics logging thread in worker pool
1806 if (thread_pool_spawn(g_server_worker_pool, stats_logger_thread, NULL, 0, "stats_logger") != ASCIICHAT_OK) {
1807 LOG_ERRNO_IF_SET("Statistics logger thread creation failed");
1808 } else {
1809 log_debug("Statistics logger thread started");
1810 }
1811
1812 // Network setup - Use tcp_server abstraction for dual-stack IPv4/IPv6 binding
1813 log_debug("Config check: GET_OPTION(address)='%s', GET_OPTION(address6)='%s'", GET_OPTION(address),
1814 GET_OPTION(address6));
1815
1816 bool ipv4_has_value = (strlen(GET_OPTION(address)) > 0);
1817 bool ipv6_has_value = (strlen(GET_OPTION(address6)) > 0);
1818 // Check if address is localhost (any loopback address, not just exact default)
1819 bool ipv4_is_default = is_localhost_ipv4(GET_OPTION(address));
1820 bool ipv6_is_default = is_localhost_ipv6(GET_OPTION(address6));
1821
1822 log_debug("Binding decision: ipv4_has_value=%d, ipv6_has_value=%d, ipv4_is_default=%d, ipv6_is_default=%d",
1823 ipv4_has_value, ipv6_has_value, ipv4_is_default, ipv6_is_default);
1824
1825 // Determine bind configuration
1826 bool bind_ipv4 = false;
1827 bool bind_ipv6 = false;
1828 const char *ipv4_address = NULL;
1829 const char *ipv6_address = NULL;
1830
1831 if (ipv4_has_value && ipv6_has_value && ipv4_is_default && ipv6_is_default) {
1832 // Both are defaults: dual-stack with default localhost addresses
1833 bind_ipv4 = true;
1834 bind_ipv6 = true;
1835 ipv4_address = "127.0.0.1";
1836 ipv6_address = "::1";
1837 log_info("Default dual-stack: binding to 127.0.0.1 (IPv4) and ::1 (IPv6)");
1838 } else if (ipv4_has_value && !ipv4_is_default && (!ipv6_has_value || ipv6_is_default)) {
1839 // IPv4 explicitly set, IPv6 is default or empty: bind only IPv4
1840 bind_ipv4 = true;
1841 bind_ipv6 = false;
1842 ipv4_address = GET_OPTION(address);
1843 log_info("Binding only to IPv4 address: %s", ipv4_address);
1844 } else if (ipv6_has_value && !ipv6_is_default && (ipv4_is_default || !ipv4_has_value)) {
1845 // IPv6 explicitly set, IPv4 is default or empty: bind only IPv6
1846 bind_ipv4 = false;
1847 bind_ipv6 = true;
1848 ipv6_address = GET_OPTION(address6);
1849 log_info("Binding only to IPv6 address: %s", ipv6_address);
1850 } else {
1851 // Both explicitly set or one explicit + one default: dual-stack
1852 bind_ipv4 = true;
1853 bind_ipv6 = true;
1854 ipv4_address = ipv4_has_value ? GET_OPTION(address) : "127.0.0.1";
1855 ipv6_address = ipv6_has_value ? GET_OPTION(address6) : "::1";
1856 log_info("Dual-stack binding: IPv4=%s, IPv6=%s", ipv4_address, ipv6_address);
1857 }
1858
1859 // Create server context - encapsulates all server state for passing to client handlers
1860 // This reduces global state and improves modularity by using tcp_server.user_data
1861 server_context_t server_ctx = {
1862 .tcp_server = &g_tcp_server,
1863 .rate_limiter = g_rate_limiter,
1864 .client_manager = &g_client_manager,
1865 .client_manager_rwlock = &g_client_manager_rwlock,
1866 .server_should_exit = &g_server_should_exit,
1867 .audio_mixer = g_audio_mixer,
1868 .stats = &g_stats,
1869 .stats_mutex = &g_stats_mutex,
1870 .encryption_enabled = g_server_encryption_enabled,
1871 .server_private_key = &g_server_private_key,
1872 .client_whitelist = g_client_whitelist,
1873 .num_whitelisted_clients = g_num_whitelisted_clients,
1874 .session_host = NULL, // Will be created after TCP server init
1875 };
1876
1877 // Configure TCP server
1878 tcp_server_config_t tcp_config = {
1879 .port = port,
1880 .ipv4_address = ipv4_address,
1881 .ipv6_address = ipv6_address,
1882 .bind_ipv4 = bind_ipv4,
1883 .bind_ipv6 = bind_ipv6,
1884 .accept_timeout_sec = ACCEPT_TIMEOUT,
1885 .client_handler = ascii_chat_client_handler,
1886 .user_data = &server_ctx, // Pass server context to client handlers
1887 .status_update_fn = NULL, // Status screen runs in its own thread
1888 .status_update_data = NULL,
1889 };
1890
1891 // Initialize TCP server (creates and binds sockets)
1892 memset(&g_tcp_server, 0, sizeof(g_tcp_server));
1893 asciichat_error_t tcp_init_result = tcp_server_init(&g_tcp_server, &tcp_config);
1894 if (tcp_init_result != ASCIICHAT_OK) {
1895 FATAL(ERROR_NETWORK, "Failed to initialize TCP server");
1896 }
1897
1898 // Initialize WebSocket server (for browser clients)
1899 websocket_server_config_t ws_config = {
1900 .port = GET_OPTION(websocket_port),
1901 .client_handler = websocket_client_handler,
1902 .user_data = &server_ctx,
1903 };
1904
1905 memset(&g_websocket_server, 0, sizeof(g_websocket_server));
1906 asciichat_error_t ws_init_result = websocket_server_init(&g_websocket_server, &ws_config);
1907 if (ws_init_result != ASCIICHAT_OK) {
1908 log_warn("Failed to initialize WebSocket server - browser clients will not be supported");
1909 } else {
1910 log_info("WebSocket server initialized on port %d", GET_OPTION(websocket_port));
1911 }
1912
1913 // =========================================================================
1914 // UPnP Port Mapping (Quick Win for Direct TCP)
1915 // =========================================================================
1916 // Track UPnP success for ACDS session type decision
1917 // If UPnP fails, we need to create a WebRTC session to enable client connectivity
1918 bool upnp_succeeded = false;
1919
1920 // Try to open port via UPnP so direct TCP works for ~70% of home users.
1921 // If this fails, clients fall back to WebRTC automatically - not fatal.
1922 //
1923 // Strategy:
1924 // 1. UPnP (works on ~90% of home routers)
1925 // 2. NAT-PMP fallback (Apple routers)
1926 // 3. If both fail: use ACDS + WebRTC (reliable, but slightly higher latency)
1927 if (GET_OPTION(enable_upnp)) {
1928 asciichat_error_t upnp_result = nat_upnp_open(port, "ascii-chat Server", &g_upnp_ctx);
1929
1930 if (upnp_result == ASCIICHAT_OK && g_upnp_ctx) {
1931 char public_addr[22];
1932 if (nat_upnp_get_address(g_upnp_ctx, public_addr, sizeof(public_addr)) == ASCIICHAT_OK) {
1933 char msg[256];
1934 safe_snprintf(msg, sizeof(msg), "🌐 Public endpoint: %s (direct TCP)", public_addr);
1935 log_console(LOG_INFO, msg);
1936 log_info("UPnP: Port mapping successful, public endpoint: %s", public_addr);
1937 upnp_succeeded = true;
1938 }
1939 } else {
1940 log_info("UPnP: Port mapping unavailable or failed - will use WebRTC fallback");
1941 log_console(LOG_INFO, "📡 Clients behind strict NATs will use WebRTC fallback");
1942 }
1943 } else {
1944 log_debug("UPnP: Disabled (use --upnp to enable automatic port mapping)");
1945 }
1946
1947 // Initialize synchronization primitives
1949 FATAL(ERROR_THREAD, "Failed to initialize client manager rwlock");
1950 }
1951
1952 // Lock debug system already initialized earlier in main()
1953
1954 // Check if SIGINT/SIGTERM was received during initialization
1955 // If so, skip the accept loop entirely and go to cleanup
1956 if (atomic_load(&g_server_should_exit)) {
1957 log_info("Shutdown signal received during initialization, skipping server startup");
1958 goto cleanup;
1959 }
1960
1961 // Lock debug thread already started earlier in main()
1962
1963 // NOTE: g_client_manager is already zero-initialized in client.c with = {0}
1964 // We only need to initialize the mutex
1966
1967 // Initialize uthash head pointer for O(1) lookup (uthash requires NULL initialization)
1969
1970 // Initialize connection rate limiter (prevents DoS attacks)
1971 if (!atomic_load(&g_server_should_exit)) {
1972 log_debug("Initializing connection rate limiter...");
1974 if (!g_rate_limiter) {
1975 LOG_ERRNO_IF_SET("Failed to initialize rate limiter");
1976 if (!atomic_load(&g_server_should_exit)) {
1977 FATAL(ERROR_MEMORY, "Failed to create connection rate limiter");
1978 }
1979 } else {
1980 log_info("Connection rate limiter initialized (50 connections/min per IP)");
1981 }
1982 }
1983
1984 // Initialize audio mixer (always enabled on server)
1985 if (!atomic_load(&g_server_should_exit)) {
1986 log_debug("Initializing audio mixer for per-client audio rendering...");
1987 g_audio_mixer = mixer_create(MAX_CLIENTS, AUDIO_SAMPLE_RATE);
1988 if (!g_audio_mixer) {
1989 LOG_ERRNO_IF_SET("Failed to initialize audio mixer");
1990 if (!atomic_load(&g_server_should_exit)) {
1991 FATAL(ERROR_AUDIO, "Failed to initialize audio mixer");
1992 }
1993 } else {
1994 if (!atomic_load(&g_server_should_exit)) {
1995 log_debug("Audio mixer initialized successfully for per-client audio rendering");
1996 }
1997 }
1998 }
1999
2000 // Initialize mDNS context for LAN service discovery (optional)
2001 // mDNS allows clients on the LAN to discover this server without knowing its IP
2002 // Can be disabled with --no-mdns-advertise
2003 // Note: Actual advertisement is deferred until after ACDS session creation (if --acds is enabled)
2004 if (!atomic_load(&g_server_should_exit) && !GET_OPTION(no_mdns_advertise)) {
2005 log_debug("Initializing mDNS for LAN service discovery...");
2006 g_mdns_ctx = asciichat_mdns_init();
2007 if (!g_mdns_ctx) {
2008 LOG_ERRNO_IF_SET("Failed to initialize mDNS (non-fatal, LAN discovery disabled)");
2009 log_warn("mDNS disabled - LAN service discovery will not be available");
2010 g_mdns_ctx = NULL;
2011 } else {
2012 log_debug("mDNS context initialized, advertisement deferred until session string is ready");
2013 }
2014 } else if (GET_OPTION(no_mdns_advertise)) {
2015 log_info("mDNS service advertisement disabled via --no-mdns-advertise");
2016 }
2017
2018 // ========================================================================
2019 // Session Host Creation (for discovery mode support)
2020 // ========================================================================
2021 // Create session_host to track clients in a transport-agnostic way.
2022 // This enables future discovery mode where participants can become hosts.
2023 if (!atomic_load(&g_server_should_exit)) {
2024 session_host_config_t host_config = {
2025 .port = port,
2026 .ipv4_address = ipv4_address,
2027 .ipv6_address = ipv6_address,
2028 .max_clients = GET_OPTION(max_clients),
2029 .encryption_enabled = g_server_encryption_enabled,
2030 .key_path = GET_OPTION(encrypt_key),
2031 .password = GET_OPTION(password),
2032 .callbacks = {0}, // No callbacks for now
2033 .user_data = NULL,
2034 };
2035
2036 server_ctx.session_host = session_host_create(&host_config);
2037 if (!server_ctx.session_host) {
2038 // Non-fatal: session_host is optional, server can work without it
2039 log_warn("Failed to create session_host (discovery mode support disabled)");
2040 } else {
2041 log_debug("Session host created for discovery mode support");
2042 }
2043 }
2044
2045 // ========================================================================
2046 // MAIN CONNECTION LOOP - Delegated to tcp_server
2047 // ========================================================================
2048 //
2049 // The tcp_server module handles:
2050 // 1. Dual-stack IPv4/IPv6 accept loop with select() timeout
2051 // 2. Spawning client_handler threads for each connection
2052 // 3. Responsive shutdown when g_tcp_server.running is set to false
2053 //
2054 // Client lifecycle is managed by ascii_chat_client_handler() which:
2055 // - Performs rate limiting
2056 // - Calls add_client() to initialize structures and spawn workers
2057 // - Blocks until client disconnects
2058 // - Calls remove_client() to cleanup and stop worker threads
2059
2060 // ACDS Session Creation: Register this server with discovery service
2061 // This also determines the session string for mDNS (if --acds is enabled)
2062 char session_string[64] = {0};
2063 bool session_is_mdns_only = false;
2064
2065 // ACDS Registration (conditional on --discovery flag)
2066 if (GET_OPTION(discovery)) {
2067 // Security Requirement Check (Issue #239):
2068 // Server IP must be protected by password, identity verification, or explicit opt-in
2069
2070 // Auto-detection: Check if password or identity verification is configured
2071 const char *password = GET_OPTION(password);
2072 bool has_password = password && strlen(password) > 0;
2073 const char *encrypt_key = GET_OPTION(encrypt_key);
2074 bool has_identity = encrypt_key && strlen(encrypt_key) > 0;
2075 bool explicit_expose = GET_OPTION(discovery_expose_ip) != 0;
2076
2077 // Validate security configuration BEFORE attempting ACDS connection
2078 bool acds_expose_ip_flag = false;
2079
2080 if (has_password || has_identity) {
2081 // Auto-enable privacy: IP revealed only after verification
2082 acds_expose_ip_flag = false;
2083 log_plain("🔒 ACDS privacy enabled: IP disclosed only after %s verification",
2084 has_password ? "password" : "identity");
2085 } else if (explicit_expose) {
2086 // Explicit opt-in to public IP disclosure
2087 // Only prompt if running interactively (can prompt user)
2088 // When non-interactive (automated/scripted), treat explicit flag as confirmation
2089 bool is_interactive = terminal_can_prompt_user();
2090
2091 if (is_interactive) {
2092 log_plain_stderr("");
2093 log_plain_stderr("⚠️ WARNING: You are about to allow PUBLIC IP disclosure!");
2094 log_plain_stderr("⚠️ Anyone with the session string will be able to see your IP address.");
2095 log_plain_stderr("⚠️ This is NOT RECOMMENDED unless you understand the privacy implications.");
2096 log_plain_stderr("");
2097
2098 if (!platform_prompt_yes_no("Do you want to proceed with public IP disclosure", false)) {
2099 log_plain_stderr("");
2100 log_plain_stderr("❌ IP disclosure not confirmed. Server will run WITHOUT discovery service.");
2101 goto skip_acds_session;
2102 }
2103 }
2104
2105 // User confirmed (or running non-interactively with explicit flag) - proceed with public IP disclosure
2106 acds_expose_ip_flag = true;
2107 log_plain_stderr("");
2108 log_plain_stderr("⚠️ Public IP disclosure CONFIRMED");
2109 log_plain_stderr("⚠️ Your IP address will be visible to anyone with the session string");
2110 } else {
2111 // Security violation: No password, no identity, no explicit opt-in
2112 log_plain_stderr("❌ Cannot create ACDS session: No security configured!");
2113 log_plain_stderr(" You must either:");
2114 log_plain_stderr(" 1. Set a password: --password \"your-secret\"");
2115 log_plain_stderr(" 2. Use identity key: --key ~/.ssh/id_ed25519");
2116 log_plain_stderr(" 3. Explicitly allow public IP: --acds-expose-ip (NOT RECOMMENDED)");
2117 log_plain_stderr("");
2118 log_plain_stderr("Server will run WITHOUT discovery service.");
2119 goto skip_acds_session;
2120 }
2121
2122 // Security is configured, proceed with ACDS connection
2123 const char *acds_server = GET_OPTION(discovery_server);
2124 uint16_t acds_port = (uint16_t)GET_OPTION(discovery_port);
2125
2126 log_info("Attempting to create session on ACDS server at %s:%d...", acds_server, acds_port);
2127
2128 acds_client_config_t acds_config;
2130 SAFE_STRNCPY(acds_config.server_address, acds_server, sizeof(acds_config.server_address));
2131 acds_config.server_port = acds_port;
2132 acds_config.timeout_ms = 5 * MS_PER_SEC_INT;
2133
2134 // Allocate ACDS client on heap for server lifecycle
2135 g_acds_client = SAFE_MALLOC(sizeof(acds_client_t), acds_client_t *);
2136 if (!g_acds_client) {
2137 log_error("Failed to allocate ACDS client");
2138 goto skip_acds_session;
2139 }
2140
2141 asciichat_error_t acds_connect_result = acds_client_connect(g_acds_client, &acds_config);
2142 if (acds_connect_result != ASCIICHAT_OK) {
2143 log_error("Failed to connect to ACDS server at %s:%d: %s", acds_server, acds_port,
2144 asciichat_error_string(acds_connect_result));
2145 goto skip_acds_session;
2146 }
2147 if (acds_connect_result == ASCIICHAT_OK) {
2148 // Prepare session creation parameters
2149 acds_session_create_params_t create_params;
2150 memset(&create_params, 0, sizeof(create_params));
2151
2152 // Use server's Ed25519 identity public key if available
2153 if (g_server_encryption_enabled && has_identity) {
2154 memcpy(create_params.identity_pubkey, g_server_private_key.public_key, 32);
2155 log_debug("Using server identity key for ACDS session");
2156 } else {
2157 // No identity key available - use zero key
2158 // ACDS will accept this if identity verification is not required
2159 memset(create_params.identity_pubkey, 0, 32);
2160 log_debug("No server identity key - using zero key for ACDS session");
2161 }
2162
2163 create_params.capabilities = 0x03; // Video + Audio
2164 create_params.max_participants = GET_OPTION(max_clients);
2165 log_debug("ACDS: max_clients option value = %d", GET_OPTION(max_clients));
2166
2167 // Set password if configured
2168 create_params.has_password = has_password;
2169 if (has_password) {
2170 // TODO: Hash password with Argon2id
2171 SAFE_STRNCPY(create_params.password, password, sizeof(create_params.password));
2172 }
2173
2174 // Set IP disclosure policy (determined above)
2175 create_params.acds_expose_ip = acds_expose_ip_flag;
2176 log_info("DEBUG: Server setting acds_expose_ip=%d (explicit_expose=%d, has_password=%d, has_identity=%d)",
2177 create_params.acds_expose_ip, explicit_expose, has_password, has_identity);
2178
2179 // Set session type (Direct TCP or WebRTC)
2180 // Auto-detect: Use WebRTC if UPnP failed OR if explicitly requested via --webrtc
2181 // Exception: If bind address is 0.0.0.0, server is on public IP - use Direct TCP
2182 const char *bind_addr = GET_OPTION(address);
2183 bool bind_all_interfaces = (strcmp(bind_addr, "0.0.0.0") == 0);
2184
2185 // Determine session type: prefer WebRTC by default (unless explicitly disabled)
2186 // Priority: explicit --webrtc flag > connection type detection > UPnP > default
2187 if (GET_OPTION(webrtc)) {
2188 // Explicit WebRTC request
2189 create_params.session_type = SESSION_TYPE_WEBRTC;
2190 log_info("ACDS session type: WebRTC (explicitly requested via --webrtc)");
2191 } else if (bind_all_interfaces) {
2192 // Bind to 0.0.0.0: use WebRTC as default (better NAT compatibility)
2193 create_params.session_type = SESSION_TYPE_WEBRTC;
2194 log_info("ACDS session type: WebRTC (default for 0.0.0.0 binding, provides NAT-agnostic connections)");
2195 } else if (upnp_succeeded) {
2196 // UPnP port mapping worked - can use direct TCP
2197 create_params.session_type = SESSION_TYPE_DIRECT_TCP;
2198 log_info("ACDS session type: Direct TCP (UPnP succeeded, server is publicly accessible)");
2199 } else {
2200 // UPnP failed and not on public IP - use WebRTC for NAT traversal
2201 create_params.session_type = SESSION_TYPE_WEBRTC;
2202 log_info("ACDS session type: WebRTC (UPnP failed, server behind NAT)");
2203 }
2204
2205 // Server connection information (where clients should connect)
2206 // If bind address is 0.0.0.0, leave server_address empty for ACDS to auto-detect public IP
2207 if (bind_all_interfaces) {
2208 create_params.server_address[0] = '\0'; // Empty - ACDS will use connection source IP
2209 log_debug("Bind address is 0.0.0.0, ACDS will auto-detect public IP from connection");
2210 } else {
2211 SAFE_STRNCPY(create_params.server_address, bind_addr, sizeof(create_params.server_address));
2212 }
2213 create_params.server_port = port;
2214
2215 // DEBUG: Log what we're sending to ACDS
2216 log_info("DEBUG: Before SESSION_CREATE - expose_ip_publicly=%d, server_address='%s' port=%u, session_type=%u",
2217 create_params.acds_expose_ip, create_params.server_address, create_params.server_port,
2218 create_params.session_type);
2219
2220 // Create session
2221 acds_session_create_result_t create_result;
2222 asciichat_error_t create_err = acds_session_create(g_acds_client, &create_params, &create_result);
2223
2224 if (create_err == ASCIICHAT_OK) {
2225 SAFE_STRNCPY(session_string, create_result.session_string, sizeof(session_string));
2226 SAFE_STRNCPY(g_session_string, create_result.session_string, sizeof(g_session_string));
2227 session_is_mdns_only = false; // Session is now registered with ACDS (globally discoverable)
2228 log_info("Session created: %s", session_string);
2229
2230 // Server must join its own session so ACDS can route signaling messages
2231 log_debug("Server joining session as first participant for WebRTC signaling...");
2232 acds_session_join_params_t join_params = {0};
2233 join_params.session_string = session_string;
2234
2235 // Use same identity key as session creation
2236 memcpy(join_params.identity_pubkey, create_params.identity_pubkey, 32);
2237
2238 // Include password if session is password-protected
2239 if (has_password) {
2240 join_params.has_password = true;
2241 SAFE_STRNCPY(join_params.password, password, sizeof(join_params.password));
2242 }
2243
2244 acds_session_join_result_t join_result = {0};
2245 asciichat_error_t join_err = acds_session_join(g_acds_client, &join_params, &join_result);
2246 if (join_err != ASCIICHAT_OK || !join_result.success) {
2247 log_error("Failed to join own session: %s (error: %s)", asciichat_error_string(join_err),
2248 join_result.error_message[0] ? join_result.error_message : "unknown");
2249 // Continue anyway - this is not fatal for Direct TCP sessions
2250 } else {
2251 log_debug("Server joined session successfully (participant_id: %02x%02x...)", join_result.participant_id[0],
2252 join_result.participant_id[1]);
2253 // Store participant ID for WebRTC signaling (needed to identify server in SDP/ICE messages)
2254 memcpy(g_server_participant_id, join_result.participant_id, 16);
2255 log_debug("Stored server participant_id for signaling: %02x%02x...", g_server_participant_id[0],
2256 g_server_participant_id[1]);
2257 memcpy(create_result.session_id, join_result.session_id, 16);
2258 }
2259
2260 // Keep ACDS connection alive for WebRTC signaling relay
2261 log_debug("Server staying connected to ACDS for signaling relay");
2262
2263 // Create ACDS transport wrapper for sending signaling packets
2264 g_acds_transport = acip_tcp_transport_create(g_acds_client->socket, NULL);
2265 if (!g_acds_transport) {
2266 log_error("Failed to create ACDS transport wrapper");
2267 } else {
2268 log_debug("ACDS transport wrapper created for signaling");
2269
2270 // Start ACDS ping thread to keep connection alive (for ALL session types)
2271 int ping_thread_result = asciichat_thread_create(&g_acds_ping_thread, acds_ping_thread, NULL);
2272 if (ping_thread_result != 0) {
2273 log_error("Failed to create ACDS ping thread: %d", ping_thread_result);
2274 } else {
2275 log_debug("ACDS ping thread started to keep connection alive");
2276 g_acds_ping_thread_started = true;
2277 }
2278 }
2279
2280 // Initialize WebRTC peer_manager if session type is WebRTC
2281 if (create_params.session_type == SESSION_TYPE_WEBRTC) {
2282 log_debug("Initializing WebRTC library and peer manager for session (role=CREATOR)...");
2283
2284 // Initialize WebRTC library (libdatachannel)
2285 asciichat_error_t webrtc_init_result = webrtc_init();
2286 if (webrtc_init_result != ASCIICHAT_OK) {
2287 log_error("Failed to initialize WebRTC library: %s", asciichat_error_string(webrtc_init_result));
2288 g_webrtc_peer_manager = NULL;
2289 } else {
2290 log_debug("WebRTC library initialized successfully");
2291
2292 // Configure STUN servers for ICE gathering (static to persist for peer_manager lifetime)
2293 static stun_server_t stun_servers[4] = {0};
2294 static unsigned int g_stun_init_refcount = 0;
2295 static static_mutex_t g_stun_init_mutex = STATIC_MUTEX_INIT;
2296 static int stun_count = 0; // Store actual count separately
2297
2298 static_mutex_lock(&g_stun_init_mutex);
2299 if (g_stun_init_refcount == 0) {
2300 log_debug("Parsing STUN servers from options: '%s'", GET_OPTION(stun_servers));
2301 int count =
2302 stun_servers_parse(GET_OPTION(stun_servers), OPT_ENDPOINT_STUN_SERVERS_DEFAULT, stun_servers, 4);
2303 if (count > 0) {
2304 stun_count = count;
2305 log_debug("Parsed %d STUN servers", count);
2306 for (int i = 0; i < count; i++) {
2307 log_debug(" STUN[%d]: '%s' (len=%d)", i, stun_servers[i].host, stun_servers[i].host_len);
2308 }
2309 } else {
2310 log_warn("Failed to parse STUN servers, using defaults");
2311 stun_count = stun_servers_parse(OPT_ENDPOINT_STUN_SERVERS_DEFAULT, OPT_ENDPOINT_STUN_SERVERS_DEFAULT,
2312 stun_servers, 4);
2313 log_debug("Using default STUN servers, count=%d", stun_count);
2314 for (int i = 0; i < stun_count; i++) {
2315 log_debug(" STUN[%d]: '%s' (len=%d)", i, stun_servers[i].host, stun_servers[i].host_len);
2316 }
2317 }
2318 g_stun_init_refcount = 1;
2319 }
2320 static_mutex_unlock(&g_stun_init_mutex);
2321
2322 // Configure peer_manager
2323 webrtc_peer_manager_config_t pm_config = {
2324 .role = WEBRTC_ROLE_CREATOR, // Server accepts offers, generates answers
2325 .stun_servers = stun_servers,
2326 .stun_count = stun_count,
2327 .turn_servers = NULL, // No TURN for server (clients should have public IP or use TURN)
2328 .turn_count = 0,
2329 .on_transport_ready = on_webrtc_transport_ready,
2330 .user_data = &server_ctx,
2331 .crypto_ctx = NULL // WebRTC handles crypto internally
2332 };
2333
2334 // Configure signaling callbacks for relaying SDP/ICE via ACDS
2335 webrtc_signaling_callbacks_t signaling_callbacks = {
2336 .send_sdp = server_send_sdp, .send_ice = server_send_ice, .user_data = NULL};
2337
2338 // Create peer_manager
2339 asciichat_error_t pm_result =
2340 webrtc_peer_manager_create(&pm_config, &signaling_callbacks, &g_webrtc_peer_manager);
2341 if (pm_result != ASCIICHAT_OK) {
2342 log_error("Failed to create WebRTC peer_manager: %s", asciichat_error_string(pm_result));
2343 g_webrtc_peer_manager = NULL;
2344 } else {
2345 log_debug("WebRTC peer_manager initialized successfully");
2346
2347 // Start ACDS receive thread for WebRTC signaling relay
2348 int thread_result = asciichat_thread_create(&g_acds_receive_thread, acds_receive_thread, NULL);
2349 if (thread_result != 0) {
2350 log_error("Failed to create ACDS receive thread: %d", thread_result);
2351 // Cleanup peer_manager since signaling won't work
2352 webrtc_peer_manager_destroy(g_webrtc_peer_manager);
2353 g_webrtc_peer_manager = NULL;
2354 } else {
2355 log_debug("ACDS receive thread started for WebRTC signaling relay");
2356 g_acds_receive_thread_started = true;
2357 }
2358 }
2359 } // Close else block from webrtc_init() success
2360 } else {
2361 log_debug("Session type is DIRECT_TCP, skipping WebRTC peer_manager initialization");
2362 }
2363
2364 // Advertise mDNS with ACDS session string
2365 // This ensures both mDNS and ACDS discovery return the same session string
2366 advertise_mdns_with_session(session_string, (uint16_t)port);
2367 } else {
2368 log_warn("Failed to create session on ACDS server (server will run without discovery)");
2369 // Clean up failed ACDS client
2370 if (g_acds_client) {
2371 acds_client_disconnect(g_acds_client);
2372 SAFE_FREE(g_acds_client);
2373 g_acds_client = NULL;
2374 }
2375 }
2376 } else {
2377 log_warn("Could not connect to ACDS server at %s:%d (server will run without discovery)", acds_server, acds_port);
2378 }
2379 } else {
2380 log_info("ACDS registration disabled (use --acds to enable)");
2381 }
2382
2383skip_acds_session:
2384 // Fallback: If no session string was set by ACDS (either disabled or failed),
2385 // generate a random session string for mDNS discovery only
2386 if (session_string[0] == '\0' && g_mdns_ctx) {
2387 log_debug("No ACDS session string available, generating random session for mDNS");
2388
2389 // Use the proper session string generation from discovery module
2390 // This generates adjective-noun-noun format using the full wordlists
2391 if (acds_string_generate(session_string, sizeof(session_string)) != ASCIICHAT_OK) {
2392 log_error("Failed to generate session string for mDNS");
2393 return 1;
2394 }
2395
2396 log_debug("Generated random session string for mDNS: '%s'", session_string);
2397
2398 // Mark that this session is mDNS-only (not globally discoverable via ACDS)
2399 session_is_mdns_only = true;
2400
2401 // Advertise mDNS with random session string
2402 advertise_mdns_with_session(session_string, (uint16_t)port);
2403 }
2404
2405 // ====================================================================
2406 // Display session string prominently as the FINAL startup message
2407 // This ensures users see the connection info clearly without logs
2408 // wiping it away
2409 // ====================================================================
2410 if (session_string[0] != '\0') {
2411 if (session_is_mdns_only) {
2412 log_plain("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n📋 Session String: %s (LAN only via "
2413 "mDNS)\n🔗 Share with others on your LAN to join:\n ascii-chat "
2414 "%s\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
2415 session_string, session_string);
2416 } else {
2417 log_plain("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n📋 Session String: %s\n🔗 Share this "
2418 "globally to join:\n ascii-chat %s\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
2419 session_string, session_string);
2420 }
2421 }
2422
2423 // Copy session info to globals for status screen display
2424 SAFE_STRNCPY(g_session_string, session_string, sizeof(g_session_string));
2425 g_session_is_mdns_only = session_is_mdns_only;
2426
2427 log_debug("Server entering accept loop (port %d)...", port);
2428
2429 // Initialize status screen
2430 g_server_start_time = time(NULL);
2431 g_last_status_update = platform_get_monotonic_time_us();
2432
2433 // Clear status screen log buffer to discard initialization logs
2434 // This ensures only NEW logs (generated after status screen starts) are displayed
2435 if (GET_OPTION(status_screen)) {
2436 extern void server_status_log_clear(void);
2438 }
2439
2440 // Start status screen thread if enabled
2441 // Runs independently at target FPS (default 60 Hz), decoupled from network accept loop
2442 if (GET_OPTION(status_screen)) {
2443 if (asciichat_thread_create(&g_status_screen_thread, status_screen_thread, NULL) != 0) {
2444 log_error("Failed to create status screen thread");
2445 goto cleanup;
2446 }
2447 log_debug("Status screen thread started");
2448 }
2449
2450 // Start WebSocket server thread if initialized
2451 if (g_websocket_server.context != NULL) {
2452 if (asciichat_thread_create(&g_websocket_server_thread, websocket_server_thread_wrapper, &g_websocket_server) !=
2453 0) {
2454 log_error("Failed to create WebSocket server thread");
2455 } else {
2456 g_websocket_server_thread_started = true;
2457 log_info("WebSocket server thread started");
2458 }
2459 }
2460
2461 // Run TCP server (blocks until shutdown signal received)
2462 // tcp_server_run() handles:
2463 // - select() on IPv4/IPv6 sockets with timeout
2464 // - accept() new connections
2465 // - Spawn ascii_chat_client_handler() thread for each connection
2466 // - Responsive shutdown when atomic_store(&g_tcp_server.running, false)
2467 asciichat_error_t run_result = tcp_server_run(&g_tcp_server);
2468 if (run_result != ASCIICHAT_OK) {
2469 log_error("TCP server exited with error");
2470 }
2471
2472 log_debug("Server accept loop exited");
2473
2474cleanup:
2475 // Signal status screen thread to exit
2476 atomic_store(&g_server_should_exit, true);
2477
2478 // Wait for status screen thread to finish if it was started
2479 if (GET_OPTION(status_screen)) {
2480 log_debug("Waiting for status screen thread to exit...");
2481 int join_result = asciichat_thread_join_timeout(&g_status_screen_thread, NULL, NS_PER_SEC_INT); // 1 second
2482 if (join_result != 0) {
2483 log_warn("Status screen thread did not exit cleanly (timeout)");
2484 } else {
2485 log_debug("Status screen thread exited");
2486 }
2487 }
2488
2489 // Cleanup status screen log capture
2491
2492 // Cleanup
2493 log_debug("Server shutting down...");
2494 memset(g_session_string, 0, sizeof(g_session_string)); // Clear session string for status screen
2495
2496 // Wake up any threads that might be blocked on condition variables
2497 // (like packet queues) to ensure responsive shutdown
2498 // This must happen BEFORE client cleanup to wake up any blocked threads
2499 static_cond_broadcast(&g_shutdown_cond);
2500 // NOTE: Do NOT call cond_destroy() on statically-initialized condition variables
2501 // g_shutdown_cond uses STATIC_COND_INIT which doesn't allocate resources that need cleanup
2502 // Calling cond_destroy() on a static cond is undefined behavior on some platforms
2503
2504 // Close all client sockets immediately to unblock receive threads.
2505 // The signal handler only closed the listening socket, but client receive threads
2506 // are still blocked in recv_with_timeout(). We need to close their sockets to unblock them.
2507 log_debug("Closing all client sockets to unblock receive threads...");
2508
2509 // Use write lock since we're modifying client->socket
2510 rwlock_wrlock(&g_client_manager_rwlock);
2511 for (int i = 0; i < MAX_CLIENTS; i++) {
2512 client_info_t *client = &g_client_manager.clients[i];
2513 if (atomic_load(&client->client_id) != 0 && client->socket != INVALID_SOCKET_VALUE) {
2514 socket_close(client->socket);
2515 client->socket = INVALID_SOCKET_VALUE;
2516 }
2517 }
2518 rwlock_wrunlock(&g_client_manager_rwlock);
2519
2520 log_debug("Signaling all clients to stop (sockets closed, g_server_should_exit set)...");
2521
2522 // Stop and destroy server worker thread pool (stats logger, etc.)
2523 if (g_server_worker_pool) {
2524 thread_pool_destroy(g_server_worker_pool);
2525 g_server_worker_pool = NULL;
2526 log_debug("Server worker thread pool stopped");
2527 }
2528
2529 // Destroy rate limiter
2530 if (g_rate_limiter) {
2532 g_rate_limiter = NULL;
2533 }
2534
2535 // Shutdown WebSocket server BEFORE removing clients.
2536 // The LWS event thread runs callbacks that access transport data (impl_data).
2537 // If we destroy transports via remove_client while LWS is still running,
2538 // callbacks access freed memory (heap-use-after-free).
2539 // lws_context_destroy fires LWS_CALLBACK_CLOSED for all connections, which
2540 // closes transports and NULLs the per-session conn_data->transport pointer.
2541 // After this, remove_client can safely destroy the transport objects.
2542 if (g_websocket_server.context != NULL) {
2543 log_debug("Shutting down WebSocket server before client cleanup");
2544 atomic_store(&g_websocket_server.running, false);
2545 // Wake the LWS event loop immediately instead of waiting for the 50ms
2546 // lws_service timeout. This is safe because the event loop thread can't
2547 // destroy the context until after it exits its loop (which requires
2548 // seeing running=false, which we just set).
2549 websocket_server_cancel_service(&g_websocket_server);
2550
2551 // Join with 2-second timeout only if thread was successfully created.
2552 // lws_context_destroy() can take 5+ seconds if called from a different thread
2553 // (waiting for close handshake), but we're destroying from the event loop thread
2554 // so it's fast. 2s accounts for any slow lws_service() calls or LWS cleanup.
2555 if (g_websocket_server_thread_started) {
2556 int join_result =
2557 asciichat_thread_join_timeout(&g_websocket_server_thread, NULL, 2 * NS_PER_SEC_INT); // 2 seconds in ns
2558 if (join_result != 0) {
2559 log_warn("WebSocket thread did not exit cleanly (timeout), forcing cleanup");
2560 }
2561 g_websocket_server_thread_started = false;
2562 }
2563
2564 // Context is destroyed by websocket_server_run from the event loop thread.
2565 // websocket_server_destroy handles the case where it's already NULL or context already destroyed.
2566 websocket_server_destroy(&g_websocket_server);
2567 log_debug("WebSocket server shut down");
2568 }
2569
2570 // Clean up all connected clients
2571 log_debug("Cleaning up connected clients...");
2572 // FIXED: Simplified to collect client IDs first, then remove them without holding locks
2573 uint32_t clients_to_remove[MAX_CLIENTS];
2574 int client_count = 0;
2575
2576 rwlock_rdlock(&g_client_manager_rwlock);
2577 for (int i = 0; i < MAX_CLIENTS; i++) {
2578 client_info_t *client = &g_client_manager.clients[i];
2579
2580 // Only attempt to clean up clients that were actually connected
2581 // (client_id is 0 for uninitialized clients, starts from 1 for connected clients)
2582 // FIXED: Only access mutex for initialized clients to avoid accessing uninitialized mutex
2583 if (atomic_load(&client->client_id) == 0) {
2584 continue; // Skip uninitialized clients
2585 }
2586
2587 // Use snapshot pattern to avoid holding both locks simultaneously
2588 // This prevents deadlock by not acquiring client_state_mutex while holding rwlock
2589 uint32_t client_id_snapshot = atomic_load(&client->client_id); // Atomic read is safe under rwlock
2590
2591 // Clean up ANY client that was allocated, whether active or not
2592 // (disconnected clients may not be active but still have resources)
2593 clients_to_remove[client_count++] = client_id_snapshot;
2594 }
2595 rwlock_rdunlock(&g_client_manager_rwlock);
2596
2597 // Remove all clients without holding any locks
2598 for (int i = 0; i < client_count; i++) {
2599 if (remove_client(&server_ctx, clients_to_remove[i]) != 0) {
2600 log_error("Failed to remove client %u during shutdown", clients_to_remove[i]);
2601 }
2602 }
2603
2604 // Clean up hash table
2605 // Clean up uthash table (uthash handles deletion when the last item is removed,
2606 // but we should clear it here just in case there are stragglers)
2608 client_info_t *current_client, *tmp;
2609 HASH_ITER(hh, g_client_manager.clients_by_id, current_client, tmp) {
2610 HASH_DELETE(hh, g_client_manager.clients_by_id, current_client);
2611 // Note: We don't free current_client here because it's part of the clients[] array
2612 }
2614 }
2615
2616 // Clean up audio mixer
2617 if (g_audio_mixer) {
2618 // Set to NULL first before destroying.
2619 // Client handler threads may still be running and checking g_audio_mixer.
2620 // Setting it to NULL first prevents use-after-free race condition.
2621 // volatile ensures this write is visible to other threads immediately
2622 mixer_t *mixer_to_destroy = g_audio_mixer;
2623 g_audio_mixer = NULL;
2624 mixer_destroy(mixer_to_destroy);
2625 }
2626
2627 // Clean up mDNS context
2628 if (g_mdns_ctx) {
2629 asciichat_mdns_destroy(g_mdns_ctx);
2630 g_mdns_ctx = NULL;
2631 log_debug("mDNS context shut down");
2632 }
2633
2634 // Clean up synchronization primitives
2635 rwlock_destroy(&g_client_manager_rwlock);
2637
2638#ifdef NDEBUG
2639 // Clean up statistics system
2640 stats_cleanup();
2641#endif
2642
2643#ifndef NDEBUG
2644 // Clean up lock debugging system (always, regardless of build type)
2645 // Lock debug records are allocated in debug builds too, so they must be cleaned up
2647#endif
2648
2649 // Destroy session host (before TCP server shutdown)
2650 if (server_ctx.session_host) {
2651 log_debug("Destroying session host");
2653 server_ctx.session_host = NULL;
2654 }
2655
2656 // Shutdown TCP server (closes listen sockets and cleans up)
2657 tcp_server_destroy(&g_tcp_server);
2658
2659 // WebSocket server already shut down before client cleanup (above).
2660
2661 // Join ACDS threads (if started)
2662 // NOTE: Must be done BEFORE destroying transport to ensure clean shutdown
2663 if (g_acds_ping_thread_started) {
2664 log_debug("Joining ACDS ping thread");
2665 asciichat_thread_join(&g_acds_ping_thread, NULL);
2666 g_acds_ping_thread_started = false;
2667 log_debug("ACDS ping thread joined");
2668 }
2669
2670 if (g_acds_receive_thread_started) {
2671 log_debug("Joining ACDS receive thread");
2672 asciichat_thread_join(&g_acds_receive_thread, NULL);
2673 g_acds_receive_thread_started = false;
2674 log_debug("ACDS receive thread joined");
2675 }
2676
2677 // Clean up WebRTC peer manager (if initialized for ACDS signaling relay)
2678 if (g_webrtc_peer_manager) {
2679 log_debug("Destroying WebRTC peer manager");
2680 webrtc_peer_manager_destroy(g_webrtc_peer_manager);
2681 g_webrtc_peer_manager = NULL;
2682 }
2683
2684 // Clean up ACDS transport wrapper (if created)
2685 if (g_acds_transport) {
2686 log_debug("Destroying ACDS transport wrapper");
2687 acip_transport_destroy(g_acds_transport);
2688 g_acds_transport = NULL;
2689 }
2690
2691 // Disconnect from ACDS server (if connected for WebRTC signaling relay)
2692 if (g_acds_client) {
2693 log_debug("Disconnecting from ACDS server");
2694 acds_client_disconnect(g_acds_client);
2695 SAFE_FREE(g_acds_client);
2696 g_acds_client = NULL;
2697 }
2698
2699 // Clean up SIMD caches
2701
2702 // Clean up symbol cache
2703 // This must be called BEFORE log_destroy() as symbol_cache_destroy() uses log_debug()
2704 // Safe to call even if atexit() runs - it's idempotent (checks g_symbol_cache_initialized)
2705 // Also called via platform_destroy() atexit handler, but explicit call ensures proper ordering
2707
2708 // Clean up global buffer pool (explicitly, as atexit may not run on Ctrl-C)
2709 // Note: This is also registered with atexit(), but calling it explicitly is safe (idempotent)
2710 // Safe to call even if atexit() runs - it checks g_global_buffer_pool and sets it to NULL
2712
2713 // Disable keepawake mode (re-allow OS to sleep)
2715
2716 // Clean up binary path cache explicitly
2717 // Note: This is also called by platform_destroy() via atexit(), but it's idempotent
2718 // (checks g_cache_initialized and sets it to false, sets g_bin_path_cache to NULL)
2719 // Safe to call even if atexit() runs later
2721
2722 // Clean up errno context (allocated strings, backtrace symbols)
2724
2725 // Clean up RCU-based options state
2727
2728 // Clean up platform-specific resources (Windows: Winsock cleanup, timer restoration)
2729 // POSIX: minimal cleanup (symbol cache already handled above on Windows)
2730 socket_cleanup();
2731 platform_restore_timer_resolution(); // Restore timer resolution (no-op on POSIX)
2732
2733#ifndef NDEBUG
2734 // Join the lock debug thread as one of the very last things before exit
2736#endif
2737
2738 log_info("Server shutdown complete");
2739
2741 log_destroy();
2742
2743 // Use exit() to allow atexit() handlers to run
2744 // Cleanup functions are idempotent (check if initialized first)
2745 exit(0);
2746}
void acds_client_config_init_defaults(acds_client_config_t *config)
Definition acds_client.c:45
asciichat_error_t acds_session_create(acds_client_t *client, const acds_session_create_params_t *params, acds_session_create_result_t *result)
asciichat_error_t acds_session_join(acds_client_t *client, const acds_session_join_params_t *params, acds_session_join_result_t *result)
asciichat_error_t acds_client_connect(acds_client_t *client, const acds_client_config_t *config)
Definition acds_client.c:60
void acds_client_disconnect(acds_client_t *client)
void ascii_simd_init(void)
Definition ascii_simd.c:98
void asciichat_error_stats_print(void)
void asciichat_errno_destroy(void)
__thread asciichat_error_context_t asciichat_errno_context
void buffer_pool_cleanup_global(void)
Per-client state management and lifecycle orchestration.
asciichat_error_t crypto_handshake_init(crypto_handshake_context_t *ctx, bool is_server)
asciichat_error_t acds_string_generate(char *output, size_t output_size)
void pubkey_to_hex(const uint8_t pubkey[32], char hex_out[65])
Definition discovery.c:49
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)
Definition host.c:222
session_host_t * session_host_create(const session_host_config_t *config)
Definition host.c:166
int socket_t
void interactive_grep_exit_mode(bool accept)
bool interactive_grep_is_entering(void)
bool interactive_grep_check_signal_cancel(void)
bool interactive_grep_is_entering_atomic(void)
void interactive_grep_signal_cancel(void)
asciichat_error_t interactive_grep_init(void)
asciichat_error_t interactive_grep_handle_key(keyboard_key_t key)
bool interactive_grep_should_handle(int key)
int is_localhost_ipv6(const char *ip)
Definition ip.c:1320
int is_localhost_ipv4(const char *ip)
Definition ip.c:1299
asciichat_error_t parse_private_key(const char *key_path, private_key_t *key_out)
Definition keys.c:194
asciichat_error_t parse_public_keys(const char *input, public_key_t *keys_out, size_t *num_keys, size_t max_keys)
Definition keys.c:345
asciichat_error_t crypto_handshake_server_start(crypto_handshake_context_t *ctx, acip_transport_t *transport)
asciichat_error_t acip_client_receive_and_dispatch(acip_transport_t *transport, const acip_client_callbacks_t *callbacks)
void tcp_server_destroy(tcp_server_t *server)
void tcp_server_reject_client(socket_t socket, const char *reason)
asciichat_error_t tcp_server_init(tcp_server_t *server, const tcp_server_config_t *config)
int tcp_client_context_get_port(const tcp_client_context_t *ctx)
asciichat_error_t tcp_server_run(tcp_server_t *server)
const char * tcp_client_context_get_ip(const tcp_client_context_t *ctx, char *buf, size_t len)
asciichat_error_t webrtc_init(void)
asciichat_error_t websocket_server_init(websocket_server_t *server, const websocket_server_config_t *config)
asciichat_error_t websocket_server_run(websocket_server_t *server)
void websocket_server_destroy(websocket_server_t *server)
void websocket_server_cancel_service(websocket_server_t *server)
void platform_disable_keepawake(void)
asciichat_error_t platform_enable_keepawake(void)
void lock_debug_cleanup_thread(void)
Definition lock.c:1371
void lock_debug_destroy(void)
Definition lock.c:1370
void log_destroy(void)
void asciichat_mdns_destroy(asciichat_mdns_t *mdns)
Definition mdns.c:66
asciichat_mdns_t * asciichat_mdns_init(void)
Definition mdns.c:35
asciichat_error_t asciichat_mdns_advertise(asciichat_mdns_t *mdns, const asciichat_mdns_service_t *service)
Definition mdns.c:80
mixer_t * mixer_create(int max_sources, int sample_rate)
Definition mixer.c:218
void mixer_destroy(mixer_t *mixer)
Definition mixer.c:347
const float weight_blue
const float weight_red
const float weight_green
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:288
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)
asciichat_error_t webrtc_peer_manager_handle_ice(webrtc_peer_manager_t *manager, const acip_webrtc_ice_t *ice)
asciichat_error_t webrtc_peer_manager_handle_sdp(webrtc_peer_manager_t *manager, const acip_webrtc_sdp_t *sdp)
void webrtc_peer_manager_destroy(webrtc_peer_manager_t *manager)
bool terminal_can_prompt_user(void)
bool terminal_is_interactive(void)
void platform_sleep_us(unsigned int us)
uint64_t platform_get_monotonic_time_us(void)
void platform_sleep_ms(unsigned int ms)
asciichat_error_t rate_limiter_record(rate_limiter_t *limiter, const char *ip_address, rate_event_type_t event_type)
Definition rate_limit.c:145
void rate_limiter_destroy(rate_limiter_t *limiter)
Definition rate_limit.c:116
rate_limiter_t * rate_limiter_create_memory(void)
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)
Definition rate_limit.c:128
void options_state_destroy(void)
Definition rcu.c:309
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
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.
#define KEYBOARD_QUEUE_SIZE
Thread-safe keyboard queue (lock-free SPSC ring buffer)
atomic_bool g_server_should_exit
Global atomic shutdown flag shared across all threads.
static_cond_t g_shutdown_cond
Global shutdown condition variable for waking blocked threads.
ascii-chat Server Mode Entry Point Header
void server_status_log_destroy(void)
void server_status_log_clear(void)
void server_status_update(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, uint64_t *last_update_ns)
void server_status_log_init(void)
uint8_t session_id[16]
uint8_t participant_id[16]
int start_webrtc_client_threads(server_context_t *server_ctx, uint32_t client_id)
Start threads for a WebRTC client after crypto initialization.
client_info_t * find_client_by_id(uint32_t client_id)
Fast O(1) client lookup by ID using hash table.
int 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.
int remove_client(server_context_t *server_ctx, uint32_t client_id)
int 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.
client_manager_t g_client_manager
Global client manager singleton - central coordination point.
asciichat_error_t validate_ssh_key_file(const char *key_path)
Definition ssh_keys.c:940
server_stats_t g_stats
Global server statistics structure.
Definition stats.c:162
void * stats_logger_thread(void *arg)
Main statistics collection and reporting thread function.
Definition stats.c:336
int stats_init(void)
Initialize the stats mutex.
Definition stats.c:189
mutex_t g_stats_mutex
Mutex protecting global server statistics.
Definition stats.c:171
void stats_cleanup(void)
Cleanup the stats mutex.
Definition stats.c:203
Server performance statistics tracking.
Multi-client video mixing and ASCII frame generation.
Internal mDNS context structure.
Definition mdns.c:23
client_info_t * clients_by_id
uthash head pointer for O(1) client_id -> client_info_t* lookups
Definition client.h:67
mutex_t mutex
Legacy mutex (mostly replaced by rwlock)
Definition client.h:71
client_info_t clients[MAX_CLIENTS]
Array of client_info_t structures (backing storage)
Definition client.h:65
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.
atomic_bool * server_should_exit
Shutdown flag.
rate_limiter_t * rate_limiter
Connection and packet rate limiter.
WebRTC peer manager structure.
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
void symbol_cache_destroy(void)
Definition symbols.c:292
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:456
void platform_cleanup_binary_path_cache(void)
Cleanup the binary PATH cache.
Definition system.c:304
acip_transport_t * acip_tcp_transport_create(socket_t sockfd, crypto_context_t *crypto_ctx)
void acip_transport_destroy(acip_transport_t *transport)
void thread_pool_destroy(thread_pool_t *pool)
Definition thread_pool.c:48
thread_pool_t * thread_pool_create(const char *pool_name)
Definition thread_pool.c:17
asciichat_error_t thread_pool_spawn(thread_pool_t *pool, void *(*thread_func)(void *), void *thread_arg, int stop_id, const char *thread_name)
Definition thread_pool.c:70
int mutex_init(mutex_t *mutex)
Definition threading.c:16
int asciichat_thread_create(asciichat_thread_t *thread, void *(*start_routine)(void *), void *arg)
Definition threading.c:42
int rwlock_init(rwlock_t *rwlock)
Definition threading.c:63
int asciichat_thread_join(asciichat_thread_t *thread, void **retval)
Definition threading.c:46
int mutex_destroy(mutex_t *mutex)
Definition threading.c:21
asciichat_error_t nat_upnp_open(uint16_t internal_port, const char *description, nat_upnp_context_t **ctx)
Definition upnp.c:236
asciichat_error_t nat_upnp_get_address(const nat_upnp_context_t *ctx, char *addr, size_t addr_len)
Definition upnp.c:310
bool platform_prompt_yes_no(const char *question, bool default_yes)
Definition util.c:81
void precalc_rgb_palettes(const float red, const float green, const float blue)
void simd_caches_destroy_all(void)