ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
main.h File Reference

Go to the source code of this file.

Data Structures

struct  acds_config_t
 Discovery server configuration. More...
 

Functions

int acds_main (void)
 ACDS (discovery-service mode) entry point.
 

Function Documentation

◆ acds_main()

int acds_main ( void  )

ACDS (discovery-service mode) entry point.

Expects options already parsed and shared initialization complete. Called by main.c mode dispatcher after options_init() and asciichat_shared_init().

Returns
Exit code (0 = success, non-zero = error)

Definition at line 99 of file discovery-service/main.c.

99 {
100 asciichat_error_t result;
101
102 // Handle keepawake: check for mutual exclusivity and apply mode default
103 // Discovery-service default: keepawake DISABLED (use --keepawake to enable)
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;
107 }
108 if (GET_OPTION(enable_keepawake)) {
110 }
111
112 // Increase file descriptor limit for many concurrent connections
113#ifdef __linux__
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));
117 }
118#elif defined(__APPLE__)
119 struct rlimit rl = {.rlim_cur = 10240, .rlim_max = RLIM_INFINITY};
120 setrlimit(RLIMIT_NOFILE, &rl);
121#endif
122
123 // Options already parsed and shared initialization complete (done by main.c)
124 const options_t *opts = options_get();
125
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);
128
129 // Initialize session string generator (libsodium)
130 result = acds_string_init();
131 if (result != ASCIICHAT_OK) {
132 log_error("Failed to initialize session string generator");
133 return result;
134 }
135
136 // Load or generate identity keys
137 uint8_t public_key[32];
138 uint8_t secret_key[64];
139
140 const char *acds_key_path = GET_OPTION(encrypt_key);
141 // If no key provided, use default path in config directory
142 char default_key_path[PLATFORM_MAX_PATH_LENGTH] = {0};
143 if (!acds_key_path || acds_key_path[0] == '\0') {
144 const char *config_dir = get_config_dir();
145 if (config_dir) {
146 safe_snprintf(default_key_path, sizeof(default_key_path), "%sdiscovery_identity", config_dir);
147 acds_key_path = default_key_path;
148 }
149 }
150 log_info("Loading identity key from %s", acds_key_path);
151 result = acds_identity_load(acds_key_path, public_key, secret_key);
152
153 if (result != ASCIICHAT_OK) {
154 log_info("Identity key not found, generating new key...");
155
156 result = acds_identity_generate(public_key, secret_key);
157 if (result != ASCIICHAT_OK) {
158 log_error("Failed to generate identity key");
159 return result;
160 }
161
162 result = acds_identity_save(acds_key_path, public_key, secret_key);
163 if (result != ASCIICHAT_OK) {
164 log_error("Failed to save identity key to %s", acds_key_path);
165 return result;
166 }
167
168 log_info("Saved new identity key to %s", acds_key_path);
169 }
170
171 // Display server fingerprint
172 char fingerprint[65];
173 acds_identity_fingerprint(public_key, fingerprint);
174 log_info("Discovery server identity: SHA256:%s", fingerprint);
175 char msg[256];
176 safe_snprintf(msg, sizeof(msg), "🔑 Server fingerprint: SHA256:%s", fingerprint);
177 log_console(LOG_INFO, msg);
178
179 // Create config from options for server initialization
180 acds_config_t config;
181 // Read validated port option (discovery-service listens on 'port', not 'acds_port')
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;
186 }
187 config.port = port_num;
188
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";
192 SAFE_STRNCPY(config.address, address, sizeof(config.address));
193 SAFE_STRNCPY(config.address6, address6, sizeof(config.address6));
194 SAFE_STRNCPY(config.database_path, GET_OPTION(discovery_database_path), sizeof(config.database_path));
195 SAFE_STRNCPY(config.key_path, acds_key_path, sizeof(config.key_path));
196 SAFE_STRNCPY(config.log_file, log_file, sizeof(config.log_file));
197 config.log_level = GET_OPTION(log_level);
198 config.require_server_identity = GET_OPTION(require_server_identity) != 0;
199 config.require_client_identity = GET_OPTION(require_client_identity) != 0;
200
201 // Log security policy
202 if (config.require_server_identity) {
203 log_info("Security: Requiring signed identity from servers creating sessions");
204 }
205 if (config.require_client_identity) {
206 log_info("Security: Requiring signed identity from clients joining sessions");
207 }
208
209 // Parse STUN servers from comma-separated list
210 config.stun_count = 0;
211 memset(config.stun_servers, 0, sizeof(config.stun_servers));
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));
216
217 char *saveptr = NULL;
218 char *token = platform_strtok_r(stun_copy, ",", &saveptr);
219 while (token && config.stun_count < 4) {
220 // Trim whitespace
221 while (*token == ' ' || *token == '\t')
222 token++;
223 size_t len = strlen(token);
224 while (len > 0 && (token[len - 1] == ' ' || token[len - 1] == '\t')) {
225 token[--len] = '\0';
226 }
227
228 if (len > 0 && len < sizeof(config.stun_servers[0].host)) {
229 config.stun_servers[config.stun_count].host_len = (uint8_t)len;
230 SAFE_STRNCPY(config.stun_servers[config.stun_count].host, token,
231 sizeof(config.stun_servers[config.stun_count].host));
232 log_info("Added STUN server: %s", token);
233 config.stun_count++;
234 } else if (len > 0) {
235 log_warn("STUN server URL too long (max 63 chars): %s", token);
236 }
237
238 token = platform_strtok_r(NULL, ",", &saveptr);
239 }
240 }
241
242 // Parse TURN servers from comma-separated list
243 config.turn_count = 0;
244 memset(config.turn_servers, 0, sizeof(config.turn_servers));
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);
248
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));
252
253 char *saveptr = NULL;
254 char *token = platform_strtok_r(turn_copy, ",", &saveptr);
255 while (token && config.turn_count < 4) {
256 // Trim whitespace
257 while (*token == ' ' || *token == '\t')
258 token++;
259 size_t len = strlen(token);
260 while (len > 0 && (token[len - 1] == ' ' || token[len - 1] == '\t')) {
261 token[--len] = '\0';
262 }
263
264 if (len > 0 && len < sizeof(config.turn_servers[0].url)) {
265 config.turn_servers[config.turn_count].url_len = (uint8_t)len;
266 SAFE_STRNCPY(config.turn_servers[config.turn_count].url, token,
267 sizeof(config.turn_servers[config.turn_count].url));
268
269 // Set username if provided
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)) {
273 config.turn_servers[config.turn_count].username_len = (uint8_t)username_len;
274 SAFE_STRNCPY(config.turn_servers[config.turn_count].username, turn_username_str,
275 sizeof(config.turn_servers[config.turn_count].username));
276 }
277 }
278
279 // Set credential if provided
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)) {
283 config.turn_servers[config.turn_count].credential_len = (uint8_t)credential_len;
284 SAFE_STRNCPY(config.turn_servers[config.turn_count].credential, turn_credential_str,
285 sizeof(config.turn_servers[config.turn_count].credential));
286 }
287 }
288
289 log_info("Added TURN server: %s (username: %s)", token,
290 turn_username_str && turn_username_str[0] ? turn_username_str : "<none>");
291 config.turn_count++;
292 } else if (len > 0) {
293 log_warn("TURN server URL too long (max 63 chars): %s", token);
294 }
295
296 token = platform_strtok_r(NULL, ",", &saveptr);
297 }
298 }
299
300 // Copy TURN secret for dynamic credential generation
301 const char *turn_secret_str = GET_OPTION(turn_secret);
302 if (turn_secret_str && turn_secret_str[0] != '\0') {
303 SAFE_STRNCPY(config.turn_secret, turn_secret_str, sizeof(config.turn_secret));
304 log_info("TURN dynamic credential generation enabled");
305 } else {
306 config.turn_secret[0] = '\0';
307 }
308
309 // Initialize server
310 acds_server_t server;
311 memset(&server, 0, sizeof(server));
312 g_server = &server;
313
314 result = acds_server_init(&server, &config);
315 if (result != ASCIICHAT_OK) {
316 log_error("Server initialization failed");
317 g_server = NULL;
318 return result;
319 }
320
321 // Initialize WebSocket server (for browser clients)
322 websocket_server_config_t ws_config = {
323 .port = GET_OPTION(websocket_port),
324 .client_handler = NULL, // TODO: Implement WebSocket client handler for discovery-service
325 .user_data = &server,
326 };
327
328 memset(&g_websocket_server, 0, sizeof(g_websocket_server));
329 asciichat_error_t ws_init_result = websocket_server_init(&g_websocket_server, &ws_config);
330 if (ws_init_result != ASCIICHAT_OK) {
331 log_warn("Failed to initialize WebSocket server - browser clients will not be supported");
332 } else {
333 log_info("WebSocket server initialized on port %d", GET_OPTION(websocket_port));
334 }
335
336 // Set up status screen callback if enabled
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;
342 }
343
344 // Check if shutdown was requested during initialization
345 if (acds_should_exit()) {
346 log_info("Shutdown signal received during initialization, skipping server startup");
347 return 0;
348 }
349
350 // =========================================================================
351 // UPnP Port Mapping (Quick Win for Direct TCP)
352 // =========================================================================
353 // Try to open port via UPnP so direct TCP works for ~70% of home users.
354 // If this fails, clients fall back to WebRTC automatically - not fatal.
355 //
356 // Strategy:
357 // 1. UPnP (works on ~90% of home routers)
358 // 2. NAT-PMP fallback (Apple routers)
359 // 3. If both fail: use ACDS + WebRTC (reliable, but slightly higher latency)
360
361 // Check again before expensive UPnP initialization (might timeout trying to reach router)
362 if (acds_should_exit()) {
363 log_info("Shutdown signal received before UPnP initialization");
364 result = ASCIICHAT_OK;
365 goto cleanup_resources;
366 }
367
368 if (GET_OPTION(enable_upnp)) {
369 asciichat_error_t upnp_result = nat_upnp_open(config.port, "ascii-chat ACDS", &g_upnp_ctx);
370
371 if (upnp_result == ASCIICHAT_OK && g_upnp_ctx) {
372 char public_addr[22];
373 if (nat_upnp_get_address(g_upnp_ctx, public_addr, sizeof(public_addr)) == ASCIICHAT_OK) {
374 char msg[256];
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);
378 }
379 } else {
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");
382 }
383 } else {
384 log_debug("UPnP: Disabled (use --upnp to enable automatic port mapping)");
385 }
386
387 // Check if shutdown was requested before initializing mDNS
388 if (acds_should_exit()) {
389 log_info("Shutdown signal received before mDNS initialization");
390 result = ASCIICHAT_OK;
391 goto cleanup_resources;
392 }
393
394 // Initialize mDNS for LAN discovery of ACDS server
395 // This allows clients on the local network to discover the discovery service itself
396
397 log_debug("Initializing mDNS for ACDS LAN service discovery...");
398 g_mdns_ctx = asciichat_mdns_init();
399 if (!g_mdns_ctx) {
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");
402 } else {
403 // Advertise ACDS service on the LAN
404 // Build hostname if available
405 char hostname[256] = {0};
406 gethostname(hostname, sizeof(hostname) - 1);
407
408 asciichat_mdns_service_t service = {
409 .name = "ascii-chat-Discovery-Service",
410 .type = "_ascii-chat-discovery-service._tcp",
411 .host = hostname,
412 .port = config.port,
413 .txt_records = NULL,
414 .txt_count = 0,
415 };
416
417 asciichat_error_t mdns_advertise_result = asciichat_mdns_advertise(g_mdns_ctx, &service);
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");
421 asciichat_mdns_destroy(g_mdns_ctx);
422 g_mdns_ctx = NULL;
423 } else {
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);
426 }
427 }
428
429 // Install signal handlers for clean shutdown
430 signal(SIGINT, acds_handle_signal);
431 signal(SIGTERM, acds_handle_signal);
432
433 // Display initial status if enabled
434 if (GET_OPTION(status_screen)) {
435 discovery_status_t status;
436 if (discovery_status_gather(&server.tcp_server, (discovery_database_t *)server.db, config.address, config.address6,
437 config.port, g_discovery_start_time, &status) == ASCIICHAT_OK) {
439 g_last_status_update = g_discovery_start_time;
440 }
441 }
442
443 // Run server
444 result = acds_server_run(&server);
445 if (result != ASCIICHAT_OK) {
446 log_error("Server run failed");
447 }
448
449cleanup_resources:
450 // Cleanup
451 // Disable keepawake before exit
453
454 log_info("Shutting down discovery server...");
455 acds_server_shutdown(&server);
456 g_server = NULL;
457
458 // Destroy WebSocket server
459 websocket_server_destroy(&g_websocket_server);
460 log_debug("WebSocket server destroyed");
461
462 // Clean up UPnP port mapping
463 if (g_upnp_ctx) {
464 nat_upnp_close(&g_upnp_ctx);
465 log_debug("UPnP port mapping closed");
466 }
467
468 // Clean up mDNS context
469 if (g_mdns_ctx) {
470 asciichat_mdns_destroy(g_mdns_ctx);
471 g_mdns_ctx = NULL;
472 log_debug("mDNS context shut down");
473 }
474
475 log_info("Discovery server stopped");
476 return result;
477}
asciichat_error_t acds_string_init(void)
void discovery_status_display(const discovery_status_t *status)
asciichat_error_t discovery_status_gather(tcp_server_t *server, discovery_database_t *db, const char *ipv4_address, const char *ipv6_address, uint16_t port, time_t start_time, discovery_status_t *out_status)
asciichat_error_t acds_identity_load(const char *path, uint8_t public_key[32], uint8_t secret_key[64])
Definition identity.c:32
asciichat_error_t acds_identity_save(const char *path, const uint8_t public_key[32], const uint8_t secret_key[64])
Definition identity.c:61
void acds_identity_fingerprint(const uint8_t public_key[32], char fingerprint[65])
Definition identity.c:104
asciichat_error_t acds_identity_generate(uint8_t public_key[32], uint8_t secret_key[64])
Definition identity.c:18
asciichat_error_t websocket_server_init(websocket_server_t *server, const websocket_server_config_t *config)
void websocket_server_destroy(websocket_server_t *server)
void platform_disable_keepawake(void)
asciichat_error_t platform_enable_keepawake(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
char * get_config_dir(void)
Definition path.c:493
const options_t * options_get(void)
Definition rcu.c:347
void acds_server_shutdown(acds_server_t *server)
Shutdown discovery server.
asciichat_error_t acds_server_init(acds_server_t *server, const acds_config_t *config)
Initialize discovery server.
asciichat_error_t acds_server_run(acds_server_t *server)
Run discovery server main loop.
Discovery server configuration.
char address[256]
IPv4 bind address (empty = all interfaces)
turn_server_t turn_servers[4]
TURN server configurations.
char database_path[512]
SQLite database path.
bool require_server_identity
Require servers to provide signed identity when creating sessions.
uint8_t turn_count
Number of configured TURN servers (0-4)
int port
TCP listen port (default 27225)
char turn_secret[256]
Shared secret for TURN credential generation (HMAC-SHA1)
uint8_t stun_count
Number of configured STUN servers (0-4)
char key_path[512]
Ed25519 identity key file path.
char log_file[512]
Log file path (empty = stderr)
char address6[256]
IPv6 bind address (empty = all interfaces)
bool require_client_identity
Require clients to provide signed identity when joining sessions.
stun_server_t stun_servers[4]
STUN server configurations.
log_level_t log_level
Logging verbosity level.
Discovery server state.
tcp_server_t tcp_server
TCP server abstraction.
sqlite3 * db
SQLite database handle.
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:456
#define PLATFORM_MAX_PATH_LENGTH
Definition system.c:64
void nat_upnp_close(nat_upnp_context_t **ctx)
Definition upnp.c:276
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

References acds_identity_fingerprint(), acds_identity_generate(), acds_identity_load(), acds_identity_save(), acds_server_init(), acds_server_run(), acds_server_shutdown(), acds_string_init(), acds_config_t::address, acds_config_t::address6, asciichat_mdns_advertise(), asciichat_mdns_destroy(), asciichat_mdns_init(), acds_config_t::database_path, acds_server_t::db, discovery_status_display(), discovery_status_gather(), get_config_dir(), acds_config_t::key_path, acds_config_t::log_file, acds_config_t::log_level, nat_upnp_close(), nat_upnp_get_address(), nat_upnp_open(), options_get(), platform_disable_keepawake(), platform_enable_keepawake(), PLATFORM_MAX_PATH_LENGTH, acds_config_t::port, acds_config_t::require_client_identity, acds_config_t::require_server_identity, safe_snprintf(), acds_config_t::stun_count, acds_config_t::stun_servers, acds_server_t::tcp_server, acds_config_t::turn_count, acds_config_t::turn_secret, acds_config_t::turn_servers, websocket_server_destroy(), and websocket_server_init().