100 asciichat_error_t result;
104 if (GET_OPTION(enable_keepawake) && GET_OPTION(disable_keepawake)) {
105 log_error(
"--keepawake and --no-keepawake are mutually exclusive");
106 return ERROR_INVALID_PARAM;
108 if (GET_OPTION(enable_keepawake)) {
114 struct rlimit rl = {.rlim_cur = 65536, .rlim_max = 65536};
115 if (setrlimit(RLIMIT_NOFILE, &rl) < 0) {
116 log_warn(
"couldn't raise fd limit: %s", strerror(errno));
118#elif defined(__APPLE__)
119 struct rlimit rl = {.rlim_cur = 10240, .rlim_max = RLIM_INFINITY};
120 setrlimit(RLIMIT_NOFILE, &rl);
126 log_info(
"ascii-chat Discovery Service (acds) starting...");
127 log_info(
"Version: %s (%s, %s)", ASCII_CHAT_VERSION_FULL, ASCII_CHAT_BUILD_TYPE, ASCII_CHAT_BUILD_DATE);
131 if (result != ASCIICHAT_OK) {
132 log_error(
"Failed to initialize session string generator");
137 uint8_t public_key[32];
138 uint8_t secret_key[64];
140 const char *acds_key_path = GET_OPTION(encrypt_key);
143 if (!acds_key_path || acds_key_path[0] ==
'\0') {
146 safe_snprintf(default_key_path,
sizeof(default_key_path),
"%sdiscovery_identity", config_dir);
147 acds_key_path = default_key_path;
150 log_info(
"Loading identity key from %s", acds_key_path);
153 if (result != ASCIICHAT_OK) {
154 log_info(
"Identity key not found, generating new key...");
157 if (result != ASCIICHAT_OK) {
158 log_error(
"Failed to generate identity key");
163 if (result != ASCIICHAT_OK) {
164 log_error(
"Failed to save identity key to %s", acds_key_path);
168 log_info(
"Saved new identity key to %s", acds_key_path);
172 char fingerprint[65];
174 log_info(
"Discovery server identity: SHA256:%s", fingerprint);
176 safe_snprintf(msg,
sizeof(msg),
"🔑 Server fingerprint: SHA256:%s", fingerprint);
177 log_console(LOG_INFO, msg);
182 int port_num = GET_OPTION(port);
183 if (port_num < 1 || port_num > 65535) {
184 log_error(
"Invalid port: %d (must be 1-65535)", port_num);
185 return ERROR_INVALID_PARAM;
187 config.
port = port_num;
189 const char *address = opts && opts->address[0] !=
'\0' ? opts->address :
"127.0.0.1";
190 const char *address6 = opts && opts->address6[0] !=
'\0' ? opts->address6 :
"::1";
191 const char *log_file = opts && opts->log_file[0] !=
'\0' ? opts->log_file :
"acds.log";
197 config.
log_level = GET_OPTION(log_level);
203 log_info(
"Security: Requiring signed identity from servers creating sessions");
206 log_info(
"Security: Requiring signed identity from clients joining sessions");
212 const char *stun_servers_str = GET_OPTION(stun_servers);
213 if (stun_servers_str && stun_servers_str[0] !=
'\0') {
214 char stun_copy[OPTIONS_BUFF_SIZE];
215 SAFE_STRNCPY(stun_copy, stun_servers_str,
sizeof(stun_copy));
217 char *saveptr = NULL;
218 char *token = platform_strtok_r(stun_copy,
",", &saveptr);
221 while (*token ==
' ' || *token ==
'\t')
223 size_t len = strlen(token);
224 while (len > 0 && (token[len - 1] ==
' ' || token[len - 1] ==
'\t')) {
228 if (len > 0 && len <
sizeof(config.
stun_servers[0].host)) {
232 log_info(
"Added STUN server: %s", token);
234 }
else if (len > 0) {
235 log_warn(
"STUN server URL too long (max 63 chars): %s", token);
238 token = platform_strtok_r(NULL,
",", &saveptr);
245 const char *turn_servers_str = GET_OPTION(turn_servers);
246 const char *turn_username_str = GET_OPTION(turn_username);
247 const char *turn_credential_str = GET_OPTION(turn_credential);
249 if (turn_servers_str && turn_servers_str[0] !=
'\0') {
250 char turn_copy[OPTIONS_BUFF_SIZE];
251 SAFE_STRNCPY(turn_copy, turn_servers_str,
sizeof(turn_copy));
253 char *saveptr = NULL;
254 char *token = platform_strtok_r(turn_copy,
",", &saveptr);
257 while (*token ==
' ' || *token ==
'\t')
259 size_t len = strlen(token);
260 while (len > 0 && (token[len - 1] ==
' ' || token[len - 1] ==
'\t')) {
264 if (len > 0 && len <
sizeof(config.
turn_servers[0].url)) {
270 if (turn_username_str && turn_username_str[0] !=
'\0') {
271 size_t username_len = strlen(turn_username_str);
272 if (username_len <
sizeof(config.
turn_servers[0].username)) {
280 if (turn_credential_str && turn_credential_str[0] !=
'\0') {
281 size_t credential_len = strlen(turn_credential_str);
282 if (credential_len <
sizeof(config.
turn_servers[0].credential)) {
289 log_info(
"Added TURN server: %s (username: %s)", token,
290 turn_username_str && turn_username_str[0] ? turn_username_str :
"<none>");
292 }
else if (len > 0) {
293 log_warn(
"TURN server URL too long (max 63 chars): %s", token);
296 token = platform_strtok_r(NULL,
",", &saveptr);
301 const char *turn_secret_str = GET_OPTION(turn_secret);
302 if (turn_secret_str && turn_secret_str[0] !=
'\0') {
304 log_info(
"TURN dynamic credential generation enabled");
311 memset(&server, 0,
sizeof(server));
315 if (result != ASCIICHAT_OK) {
316 log_error(
"Server initialization failed");
322 websocket_server_config_t ws_config = {
323 .port = GET_OPTION(websocket_port),
324 .client_handler = NULL,
325 .user_data = &server,
328 memset(&g_websocket_server, 0,
sizeof(g_websocket_server));
330 if (ws_init_result != ASCIICHAT_OK) {
331 log_warn(
"Failed to initialize WebSocket server - browser clients will not be supported");
333 log_info(
"WebSocket server initialized on port %d", GET_OPTION(websocket_port));
337 g_discovery_start_time = time(NULL);
338 g_last_status_update = g_discovery_start_time;
339 if (GET_OPTION(status_screen)) {
340 server.
tcp_server.config.status_update_fn = discovery_status_update_callback;
341 server.
tcp_server.config.status_update_data = &server;
345 if (acds_should_exit()) {
346 log_info(
"Shutdown signal received during initialization, skipping server startup");
362 if (acds_should_exit()) {
363 log_info(
"Shutdown signal received before UPnP initialization");
364 result = ASCIICHAT_OK;
365 goto cleanup_resources;
368 if (GET_OPTION(enable_upnp)) {
369 asciichat_error_t upnp_result =
nat_upnp_open(config.
port,
"ascii-chat ACDS", &g_upnp_ctx);
371 if (upnp_result == ASCIICHAT_OK && g_upnp_ctx) {
372 char public_addr[22];
375 safe_snprintf(msg,
sizeof(msg),
"🌐 Public endpoint: %s (direct TCP)", public_addr);
376 log_console(LOG_INFO, msg);
377 log_info(
"UPnP: Port mapping successful, public endpoint: %s", public_addr);
380 log_info(
"UPnP: Port mapping unavailable or failed - will use WebRTC fallback");
381 log_console(LOG_INFO,
"📡 Clients behind strict NATs will use WebRTC fallback");
384 log_debug(
"UPnP: Disabled (use --upnp to enable automatic port mapping)");
388 if (acds_should_exit()) {
389 log_info(
"Shutdown signal received before mDNS initialization");
390 result = ASCIICHAT_OK;
391 goto cleanup_resources;
397 log_debug(
"Initializing mDNS for ACDS LAN service discovery...");
400 LOG_ERRNO_IF_SET(
"Failed to initialize mDNS (non-fatal, LAN discovery disabled)");
401 log_warn(
"mDNS disabled for ACDS - LAN discovery of discovery service will not be available");
405 char hostname[256] = {0};
406 gethostname(hostname,
sizeof(hostname) - 1);
408 asciichat_mdns_service_t service = {
409 .name =
"ascii-chat-Discovery-Service",
410 .type =
"_ascii-chat-discovery-service._tcp",
418 if (mdns_advertise_result != ASCIICHAT_OK) {
419 LOG_ERRNO_IF_SET(
"Failed to advertise ACDS mDNS service");
420 log_warn(
"mDNS advertising failed for ACDS - LAN discovery disabled");
424 log_console(LOG_INFO,
"🌐 mDNS: ACDS advertised as '_ascii-chat-discovery-service._tcp.local' on LAN");
425 log_info(
"mDNS: ACDS advertised as '_ascii-chat-discovery-service._tcp.local' (port=%d)", config.
port);
430 signal(SIGINT, acds_handle_signal);
431 signal(SIGTERM, acds_handle_signal);
434 if (GET_OPTION(status_screen)) {
435 discovery_status_t status;
437 config.
port, g_discovery_start_time, &status) == ASCIICHAT_OK) {
439 g_last_status_update = g_discovery_start_time;
445 if (result != ASCIICHAT_OK) {
446 log_error(
"Server run failed");
454 log_info(
"Shutting down discovery server...");
460 log_debug(
"WebSocket server destroyed");
465 log_debug(
"UPnP port mapping closed");
472 log_debug(
"mDNS context shut down");
475 log_info(
"Discovery server stopped");