ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
src/discovery-service/server.c
Go to the documentation of this file.
1
31#include <ascii-chat/log/log.h>
37#include <ascii-chat/util/ip.h>
40#include <string.h>
41
59static uint64_t get_current_time_ns(void) {
60 return time_get_ns();
61}
62
63static migration_context_t *find_or_create_migration(acds_server_t *server, const uint8_t session_id[16], bool create) {
64 if (!server || !session_id) {
65 return NULL;
66 }
67
68 // Search for existing migration
69 for (size_t i = 0; i < server->num_active_migrations; i++) {
70 if (memcmp(server->active_migrations[i].session_id, session_id, 16) == 0) {
71 return &server->active_migrations[i];
72 }
73 }
74
75 // Not found
76 if (!create) {
77 return NULL;
78 }
79
80 // Create new entry if space available
81 if (server->num_active_migrations >= 32) {
82 log_warn("Too many active migrations (max 32)");
83 return NULL;
84 }
85
87 memcpy(ctx->session_id, session_id, 16);
88 ctx->migration_start_ns = get_current_time_ns();
89 server->num_active_migrations++;
90
91 return ctx;
92}
93
109static void monitor_host_migrations(acds_server_t *server, uint64_t migration_timeout_ms) {
110 if (!server || !server->db || server->num_active_migrations == 0) {
111 return;
112 }
113
114 uint64_t now = get_current_time_ns();
115 uint64_t migration_timeout_ns = migration_timeout_ms * NS_PER_MS_INT; // Convert ms to ns
116
117 size_t i = 0;
118 while (i < server->num_active_migrations) {
119 migration_context_t *ctx = &server->active_migrations[i];
120
121 // Check if migration has timed out (stuck for >migration_timeout_ns)
122 uint64_t elapsed_ns = now - ctx->migration_start_ns;
123 if (elapsed_ns < migration_timeout_ns) {
124 i++;
125 continue;
126 }
127
128 uint64_t elapsed_ms = elapsed_ns / NS_PER_MS_INT; // Convert ns to ms for logging
129 log_warn("Host migration timeout for session %02x%02x... (elapsed %llu ms)", ctx->session_id[0], ctx->session_id[1],
130 (unsigned long long)elapsed_ms);
131
132 // Migration timed out - mark session as failed and clear migration state
134 if (result != ASCIICHAT_OK) {
135 log_warn("Failed to clear host for timed-out migration: %s", asciichat_error_string(result));
136 }
137
138 // Remove this migration from tracking (shift remaining entries down)
139 if (i < server->num_active_migrations - 1) {
140 memmove(&server->active_migrations[i], &server->active_migrations[i + 1],
141 (server->num_active_migrations - i - 1) * sizeof(migration_context_t));
142 }
143 server->num_active_migrations--;
144 }
145}
146
154static void *cleanup_thread_func(void *arg) {
155 acds_server_t *server = (acds_server_t *)arg;
156 if (!server) {
157 return NULL;
158 }
159
160 log_info("Cleanup thread started (rate limits + expired sessions)");
161
162 while (!atomic_load_bool(&server->shutdown)) {
163 // Sleep for 5 minutes (or until shutdown)
164 // Use 100ms sleep intervals for responsive shutdown on timeout
165 for (int i = 0; i < 3000 && !atomic_load_bool(&server->shutdown); i++) {
166 APP_CALLBACK_VOID(platform_pump_events);
167 platform_sleep_ms(100); // Sleep 100ms at a time for responsive shutdown
168 }
169
170 if (atomic_load_bool(&server->shutdown)) {
171 break;
172 }
173
174 // Run rate limit cleanup (delete events older than 1 hour)
175 log_debug("Running rate limit cleanup...");
176 asciichat_error_t result = rate_limiter_prune(server->rate_limiter, 3600);
177 if (result != ASCIICHAT_OK) {
178 log_warn("Rate limit cleanup failed");
179 }
180
181 // Run expired session cleanup
182 log_debug("Running expired session cleanup...");
184
185 // Monitor host migrations (check for completed windows)
186 log_debug("Checking for completed host migrations...");
187 monitor_host_migrations(server, 5000); // 5-second collection window for testing
188 }
189
190 log_info("Cleanup thread exiting");
191 return NULL;
192}
193
195 if (!server || !config) {
196 return SET_ERRNO(ERROR_INVALID_PARAM, "server or config is NULL");
197 }
198
199 memset(server, 0, sizeof(*server));
200 memcpy(&server->config, config, sizeof(acds_config_t));
201
202 // Initialize stats
203 atomic_store_u64(&server->stats.bytes_sent, 0);
207 server->stats.start_time = time(NULL);
208
209 // Open database (SQLite as single source of truth)
210 asciichat_error_t result = database_init(config->database_path, &server->db);
211 if (result != ASCIICHAT_OK) {
212 return result;
213 }
214
215 // Initialize rate limiter with SQLite backend
216 server->rate_limiter = rate_limiter_create_sqlite(NULL); // NULL = externally managed DB
217 if (!server->rate_limiter) {
218 database_close(server->db);
219 return SET_ERRNO(ERROR_MEMORY, "Failed to create rate limiter");
220 }
221
222 // Set the database handle for the rate limiter
224
225 // Get TCP server from server_like (it owns the lifecycle)
227 if (!server->tcp_server) {
229 database_close(server->db);
230 return SET_ERRNO(ERROR_INVALID_STATE, "TCP server not available from server_like");
231 }
232
233 // Initialize background worker thread pool
234 atomic_store_bool(&server->shutdown, false);
235 server->worker_pool = thread_pool_create("acds_workers");
236 if (!server->worker_pool) {
237 log_warn("Failed to create worker thread pool");
239 database_close(server->db);
240 return SET_ERRNO(ERROR_MEMORY, "Failed to create worker thread pool");
241 }
242
243 // Spawn cleanup thread in worker pool
244 if (thread_pool_spawn(server->worker_pool, cleanup_thread_func, server, 0, "cleanup") != ASCIICHAT_OK) {
245 log_warn("Failed to spawn cleanup thread (continuing without cleanup)");
246 }
247
248 log_info("Discovery server initialized successfully");
249 return ASCIICHAT_OK;
250}
251
253 if (!server) {
254 return;
255 }
256
257 // Signal shutdown to worker threads
258 atomic_store_bool(&server->shutdown, true);
259
260 // Wait for all client handler threads to exit
261 // TCP server is still alive (server_like destroys it after cleanup_fn)
262 if (server->tcp_server) {
263 size_t remaining_clients;
264 int shutdown_attempts = 0;
265 const int max_shutdown_attempts = 100; // 10 seconds (100 * 100ms)
266
267 while ((remaining_clients = tcp_server_get_client_count(server->tcp_server)) > 0 &&
268 shutdown_attempts < max_shutdown_attempts) {
269 log_debug("Waiting for %zu client handler threads to exit (attempt %d/%d)", remaining_clients,
270 shutdown_attempts + 1, max_shutdown_attempts);
271 APP_CALLBACK_VOID(platform_pump_events);
273 shutdown_attempts++;
274 }
275
276 if (remaining_clients > 0) {
277 log_warn("Server shutdown: %zu client handler threads still running after 10 seconds", remaining_clients);
278 } else if (shutdown_attempts > 0) {
279 log_debug("All client handler threads exited gracefully");
280 }
281 }
282
283 // Stop and destroy worker thread pool (cleanup thread, etc.)
284 if (server->worker_pool) {
286 server->worker_pool = NULL;
287 log_debug("Worker thread pool stopped");
288 }
289
290 // Destroy rate limiter
291 if (server->rate_limiter) {
293 server->rate_limiter = NULL;
294 }
295
296 // Close database
297 if (server->db) {
298 database_close(server->db);
299 server->db = NULL;
300 }
301
302 log_info("Server shutdown complete");
303}
304
305// =============================================================================
306// ACIP Transport Helper Macros for ACDS - REMOVED
307// =============================================================================
308// Transport is now passed to callbacks as a parameter, no need for temp creation
309
310// =============================================================================
311// Stats Tracking Helper
312// =============================================================================
313
314static inline void acds_track_packet_sent(acds_server_t *server, size_t payload_size) {
315 if (server) {
316 // Packet size includes header (same as receive)
317 size_t total_packet_size = payload_size + 14; // 4 (magic) + 1 (type) + 2 (length) + 4 (CRC) + 3 (client ID)
318 atomic_fetch_add_u64(&server->stats.bytes_sent, total_packet_size);
320 }
321}
322
323// =============================================================================
324// ACIP Callback Wrappers for ACDS
325// =============================================================================
326// These callbacks are invoked by acip_handle_acds_packet() via O(1) array dispatch.
327// Each callback implements: Rate Limit → Crypto Verify → Business Logic → DB Save
328
329static void acds_on_session_create(const acip_session_create_t *req, acip_transport_t *transport, const char *client_ip,
330 void *app_ctx) {
331 acds_server_t *server = (acds_server_t *)app_ctx;
332 acds_client_data_t *client_data = (acds_client_data_t *)transport->user_data;
333
334 log_debug("SESSION_CREATE packet from %s", client_ip);
335
336 if (!client_data) {
337 acip_send_error(transport, ERROR_INVALID_PARAM, "Client data not found");
338 return;
339 }
340
341 // === MULTI-KEY PROTOCOL: Handle key by key_index ===
342 // total_keys > 0 indicates multi-key mode
343 // key_index == 0 starts a new sequence; subsequent keys are appended
344
345 if (req->total_keys > 0) {
346 // Multi-key mode: total_keys tells us how many to expect
347 if (req->key_index == 0) {
348 // First key in sequence - initialize state
349 log_info("SESSION_CREATE starting multi-key sequence from %s: expecting %u key(s)", client_ip, req->total_keys);
350
351 // Store the first packet's session parameters
352 memcpy(&client_data->pending_session, req, sizeof(acip_session_create_t));
353
354 // Allocate for keys (should already be sized for MAX_IDENTITY_KEYS, but validate bounds)
355 if (req->total_keys > MAX_IDENTITY_KEYS) {
356 log_error("Too many keys: %u (max %u)", req->total_keys, MAX_IDENTITY_KEYS);
357 acip_send_error(transport, ERROR_INVALID_PARAM, "Too many keys");
358 return;
359 }
360
361 client_data->num_pending_keys = 0;
362 client_data->in_multikey_session_create = true;
363 }
364
365 // Store this key
366 if (req->key_index >= req->total_keys) {
367 log_error("Invalid key_index: %u >= total_keys: %u", req->key_index, req->total_keys);
368 acip_send_error(transport, ERROR_INVALID_PARAM, "Invalid key index");
369 client_data->in_multikey_session_create = false;
370 client_data->num_pending_keys = 0;
371 return;
372 }
373
374 memcpy(client_data->pending_session_keys[req->key_index], req->identity_pubkey, 32);
375 client_data->num_pending_keys = req->key_index + 1;
376
377 log_debug("SESSION_CREATE received key %u/%u from %s", req->key_index + 1, req->total_keys, client_ip);
378
379 // Check if this is the last key
380 bool is_final_key = (req->key_index == req->total_keys - 1);
381 if (!is_final_key) {
382 // More keys expected - wait for next packet
383 log_debug("Waiting for remaining %u key(s)", req->total_keys - client_data->num_pending_keys);
384 return;
385 }
386
387 // Final key received - finalize session
388 log_info("SESSION_CREATE finalize from %s: received all %zu identity key(s)", client_ip,
389 client_data->num_pending_keys);
390
391 // Auto-detect server's public IP if not provided (empty address)
392 if (client_data->pending_session.server_address[0] == '\0') {
393 log_info("ACDS: Auto-detecting server public IP from connection source: %s", client_ip);
394 SAFE_STRNCPY(client_data->pending_session.server_address, client_ip,
395 sizeof(client_data->pending_session.server_address));
396 log_info("ACDS: Auto-detected server_address='%s'", client_data->pending_session.server_address);
397 }
398
399 // Create session in database with all collected keys
400 // For now, database_session_create only supports single key, so use first key
401 // TODO: Extend database schema to support multiple keys per session
403 memset(&resp, 0, sizeof(resp));
404
405 asciichat_error_t create_result =
406 database_session_create(server->db, &client_data->pending_session, &server->config, &resp);
407 if (create_result == ASCIICHAT_OK) {
408 // Build complete payload: fixed response + variable STUN/TURN servers
409 size_t stun_size = (size_t)resp.stun_count * sizeof(stun_server_t);
410 size_t turn_size = (size_t)resp.turn_count * sizeof(turn_server_t);
411 size_t total_size = sizeof(resp) + stun_size + turn_size;
412
413 uint8_t *payload = SAFE_MALLOC(total_size, uint8_t *);
414 if (!payload) {
415 acip_send_error(transport, ERROR_MEMORY, "Out of memory building response");
416 client_data->in_multikey_session_create = false;
417 client_data->num_pending_keys = 0;
418 return;
419 }
420
421 // Copy fixed response
422 memcpy(payload, &resp, sizeof(resp));
423
424 // Append STUN servers
425 if (resp.stun_count > 0) {
426 memcpy(payload + sizeof(resp), server->config.stun_servers, stun_size);
427 }
428
429 // Append TURN servers
430 if (resp.turn_count > 0) {
431 memcpy(payload + sizeof(resp) + stun_size, server->config.turn_servers, turn_size);
432 }
433
434 // Send complete response with variable-length data
435 packet_send_via_transport(transport, PACKET_TYPE_ACIP_SESSION_CREATED, payload, total_size, 0);
436 acds_track_packet_sent(server, total_size);
437 SAFE_FREE(payload);
438
439 log_info("Session created: %.*s (UUID: %02x%02x..., %zu keys, %d STUN, %d TURN servers)", resp.session_string_len,
440 resp.session_string, resp.session_id[0], resp.session_id[1], client_data->num_pending_keys,
441 resp.stun_count, resp.turn_count);
442
443 // Store session/participant IDs and mark session as established
444 memcpy(client_data->session_id, resp.session_id, 16);
445 memcpy(client_data->participant_id, resp.participant_id, 16);
446 client_data->joined_session = true;
447 client_data->session_established = true; // Signal handler to exit receive loop
448 } else {
449 // Error - send error response using proper error code
450 acip_send_error(transport, create_result, "Failed to create session");
451 log_warn("Session creation failed for %s: %s", client_ip, asciichat_error_string(create_result));
452 }
453
454 // Clear multi-key state
455 client_data->in_multikey_session_create = false;
456 client_data->num_pending_keys = 0;
457
458 return;
459 } else {
460 // Legacy single-key mode (total_keys == 0)
461 log_debug("SESSION_CREATE single-key mode from %s", client_ip);
462 memcpy(&client_data->pending_session, req, sizeof(acip_session_create_t));
463 memcpy(client_data->pending_session_keys[0], req->identity_pubkey, 32);
464 client_data->num_pending_keys = 1;
465 client_data->in_multikey_session_create = false;
466 log_info("SESSION_CREATE finalize from %s: %zu identity key(s)", client_ip, client_data->num_pending_keys);
467
468 // Auto-detect server's public IP if not provided (empty address)
469 if (client_data->pending_session.server_address[0] == '\0') {
470 log_info("ACDS: Auto-detecting server public IP from connection source: %s", client_ip);
471 SAFE_STRNCPY(client_data->pending_session.server_address, client_ip,
472 sizeof(client_data->pending_session.server_address));
473 log_info("ACDS: Auto-detected server_address='%s'", client_data->pending_session.server_address);
474 }
475
476 // Create session in database with all collected keys
477 // For now, database_session_create only supports single key, so use first key
478 // TODO: Extend database schema to support multiple keys per session
480 memset(&resp, 0, sizeof(resp));
481
482 asciichat_error_t create_result =
483 database_session_create(server->db, &client_data->pending_session, &server->config, &resp);
484 if (create_result == ASCIICHAT_OK) {
485 // Build complete payload: fixed response + variable STUN/TURN servers
486 size_t stun_size = (size_t)resp.stun_count * sizeof(stun_server_t);
487 size_t turn_size = (size_t)resp.turn_count * sizeof(turn_server_t);
488 size_t total_size = sizeof(resp) + stun_size + turn_size;
489
490 uint8_t *payload = SAFE_MALLOC(total_size, uint8_t *);
491 if (!payload) {
492 acip_send_error(transport, ERROR_MEMORY, "Out of memory building response");
493 client_data->in_multikey_session_create = false;
494 client_data->num_pending_keys = 0;
495 return;
496 }
497
498 // Copy fixed response
499 memcpy(payload, &resp, sizeof(resp));
500
501 // Append STUN servers
502 if (resp.stun_count > 0) {
503 memcpy(payload + sizeof(resp), server->config.stun_servers, stun_size);
504 }
505
506 // Append TURN servers
507 if (resp.turn_count > 0) {
508 memcpy(payload + sizeof(resp) + stun_size, server->config.turn_servers, turn_size);
509 }
510
511 // Send complete response with variable-length data
512 packet_send_via_transport(transport, PACKET_TYPE_ACIP_SESSION_CREATED, payload, total_size, 0);
513 acds_track_packet_sent(server, total_size);
514 SAFE_FREE(payload);
515
516 log_info("Session created: %.*s (UUID: %02x%02x..., %zu keys, %d STUN, %d TURN servers)", resp.session_string_len,
517 resp.session_string, resp.session_id[0], resp.session_id[1], client_data->num_pending_keys,
518 resp.stun_count, resp.turn_count);
519
520 // Store session/participant IDs and mark session as established
521 memcpy(client_data->session_id, resp.session_id, 16);
522 memcpy(client_data->participant_id, resp.participant_id, 16);
523 client_data->joined_session = true;
524 client_data->session_established = true; // Signal handler to exit receive loop
525 } else {
526 // Error - send error response using proper error code
527 acip_send_error(transport, create_result, "Failed to create session");
528 log_warn("Session creation failed for %s: %s", client_ip, asciichat_error_string(create_result));
529 }
530
531 // Clear multi-key state
532 client_data->in_multikey_session_create = false;
533 client_data->num_pending_keys = 0;
534
535 return;
536 }
537
538 // === MULTI-KEY PROTOCOL: Add key to pending session ===
539 if (client_data->in_multikey_session_create) {
540 // Subsequent SESSION_CREATE with non-zero key - add to pending keys
541
542 // Validate we haven't exceeded max keys
543 if (client_data->num_pending_keys >= MAX_IDENTITY_KEYS) {
544 acip_send_error(transport, ERROR_INVALID_PARAM, "Maximum identity keys exceeded");
545 return;
546 }
547
548 // Validate key is different from all existing keys
549 for (size_t i = 0; i < client_data->num_pending_keys; i++) {
550 if (memcmp(client_data->pending_session_keys[i], req->identity_pubkey, 32) == 0) {
551 acip_send_error(transport, ERROR_INVALID_PARAM, "Duplicate identity key");
552 return;
553 }
554 }
555
556 // Add key to pending array
557 memcpy(client_data->pending_session_keys[client_data->num_pending_keys], req->identity_pubkey, 32);
558 client_data->num_pending_keys++;
559
560 log_debug("SESSION_CREATE key #%zu from %s (pubkey: %02x%02x...)", client_data->num_pending_keys, client_ip,
561 req->identity_pubkey[0], req->identity_pubkey[1]);
562
563 // Don't send response yet - client will send more keys or zero-key to finalize
564 return;
565 }
566
567 // === MULTI-KEY PROTOCOL: Start new session ===
568 // First SESSION_CREATE with non-zero key - start multi-key mode
569
570 // Rate limiting check (only on first SESSION_CREATE)
571 {
572 bool allowed = false;
573 asciichat_error_t rate_check =
574 rate_limiter_check(server->rate_limiter, client_ip, RATE_EVENT_SESSION_CREATE, NULL, &allowed);
575 if (rate_check != ASCIICHAT_OK || !allowed) {
576 acip_send_error(transport, ERROR_RATE_LIMITED, "Rate limit exceeded. Please try again later.");
577 log_warn("Rate limit exceeded for SESSION_CREATE from %s", client_ip);
578 return;
579 }
581 }
582
583 // Cryptographic identity verification (if required)
584 if (server->config.require_server_identity) {
585 // Validate timestamp (5 minute window)
586 if (!acds_validate_timestamp(req->timestamp, 300)) {
587 log_warn("SESSION_CREATE rejected from %s: invalid timestamp (replay attack protection)", client_ip);
588 acip_send_error(transport, ERROR_CRYPTO_VERIFICATION, "Timestamp validation failed - too old or in the future");
589 return;
590 }
591
592 // Verify Ed25519 signature
594 req->identity_pubkey, req->timestamp, req->capabilities, req->max_participants, req->signature);
595
596 if (verify_result != ASCIICHAT_OK) {
597 log_warn("SESSION_CREATE rejected from %s: invalid signature (identity verification failed)", client_ip);
598 acip_send_error(transport, ERROR_CRYPTO_VERIFICATION, "Identity signature verification failed");
599 return;
600 }
601
602 log_debug("SESSION_CREATE signature verified from %s (pubkey: %02x%02x...)", client_ip, req->identity_pubkey[0],
603 req->identity_pubkey[1]);
604 }
605
606 // Reachability verification for Direct TCP sessions
607 if (req->session_type == SESSION_TYPE_DIRECT_TCP) {
608 // Auto-detect public IP if server_address is empty
609 if (req->server_address[0] == '\0') {
610 SAFE_STRNCPY(req->server_address, client_ip, sizeof(req->server_address));
611 log_info("SESSION_CREATE from %s: auto-detected server address (bind was 0.0.0.0)", client_ip);
612 }
613
614 // Verify server_address matches connection source
615 if (strcmp(req->server_address, client_ip) != 0) {
616 log_warn("SESSION_CREATE rejected from %s: server_address '%s' does not match actual connection IP", client_ip,
617 req->server_address);
619 "Direct TCP sessions require server_address to match your actual IP");
620 return;
621 }
622 log_debug("SESSION_CREATE reachability verified: %s matches connection source", req->server_address);
623 }
624
625 // Store pending session data and first key
626 memcpy(&client_data->pending_session, req, sizeof(acip_session_create_t));
627 memcpy(client_data->pending_session_keys[0], req->identity_pubkey, 32);
628 client_data->num_pending_keys = 1;
629 client_data->in_multikey_session_create = true;
630
631 log_info("SESSION_CREATE started from %s: multi-key mode (key #1 stored, waiting for more or zero-key finalize)",
632 client_ip);
633
634 // Don't send response yet - wait for more keys or zero-key finalize
635}
636
637static void acds_on_session_lookup(const acip_session_lookup_t *req, acip_transport_t *transport, const char *client_ip,
638 void *app_ctx) {
639 acds_server_t *server = (acds_server_t *)app_ctx;
640
641 log_debug("SESSION_LOOKUP packet from %s", client_ip);
642
643 // Rate limiting check
644 {
645 bool allowed = false;
646 asciichat_error_t rate_check =
647 rate_limiter_check(server->rate_limiter, client_ip, RATE_EVENT_SESSION_LOOKUP, NULL, &allowed);
648 if (rate_check != ASCIICHAT_OK || !allowed) {
649 acip_send_error(transport, ERROR_RATE_LIMITED, "Rate limit exceeded. Please try again later.");
650 log_warn("Rate limit exceeded for SESSION_LOOKUP from %s", client_ip);
651 return;
652 }
654 }
655
657 memset(&resp, 0, sizeof(resp));
658
659 // Null-terminate session string for lookup
660 char session_string[49] = {0};
661 size_t copy_len =
662 (req->session_string_len < sizeof(session_string) - 1) ? req->session_string_len : sizeof(session_string) - 1;
663 memcpy(session_string, req->session_string, copy_len);
664
665 asciichat_error_t lookup_result = database_session_lookup(server->db, session_string, &server->config, &resp);
666 if (lookup_result == ASCIICHAT_OK) {
667 acip_send_session_info(transport, &resp);
668 log_info("Session lookup for '%s' from %s: %s", session_string, client_ip, resp.found ? "found" : "not found");
669 } else {
670 acip_send_error(transport, lookup_result, "Session lookup failed");
671 log_warn("Session lookup failed for %s: %s", client_ip, asciichat_error_string(lookup_result));
672 }
673}
674
675static void acds_on_session_join(const acip_session_join_t *req, acip_transport_t *transport, const char *client_ip,
676 void *app_ctx) {
677 acds_server_t *server = (acds_server_t *)app_ctx;
678 acds_client_data_t *client_data = (acds_client_data_t *)transport->user_data;
679
680 log_debug("SESSION_JOIN packet from %s", client_ip);
681
682 // Rate limiting check
683 {
684 bool allowed = false;
685 asciichat_error_t rate_check =
686 rate_limiter_check(server->rate_limiter, client_ip, RATE_EVENT_SESSION_JOIN, NULL, &allowed);
687 if (rate_check != ASCIICHAT_OK || !allowed) {
688 acip_send_error(transport, ERROR_RATE_LIMITED, "Rate limit exceeded. Please try again later.");
689 log_warn("Rate limit exceeded for SESSION_JOIN from %s", client_ip);
690 return;
691 }
693 }
694
695 // Cryptographic identity verification (if required)
696 if (server->config.require_client_identity) {
697 // Validate timestamp (5 minute window)
698 if (!acds_validate_timestamp(req->timestamp, 300)) {
699 log_warn("SESSION_JOIN rejected from %s: invalid timestamp (replay attack protection)", client_ip);
700 acip_session_joined_t error_resp;
701 memset(&error_resp, 0, sizeof(error_resp));
702 error_resp.success = 0;
703 error_resp.error_code = ERROR_CRYPTO_VERIFICATION;
704 SAFE_STRNCPY(error_resp.error_message, "Timestamp validation failed", sizeof(error_resp.error_message));
705 acip_send_session_joined(transport, &error_resp);
706 return;
707 }
708
709 // Verify Ed25519 signature
710 asciichat_error_t verify_result =
711 acds_verify_session_join(req->identity_pubkey, req->timestamp, req->session_string, req->signature);
712
713 if (verify_result != ASCIICHAT_OK) {
714 log_warn("SESSION_JOIN rejected from %s: invalid signature (identity verification failed)", client_ip);
715 acip_session_joined_t error_resp;
716 memset(&error_resp, 0, sizeof(error_resp));
717 error_resp.success = 0;
718 error_resp.error_code = ERROR_CRYPTO_VERIFICATION;
719 SAFE_STRNCPY(error_resp.error_message, "Identity signature verification failed",
720 sizeof(error_resp.error_message));
721 acip_send_session_joined(transport, &error_resp);
722 return;
723 }
724
725 log_debug("SESSION_JOIN signature verified from %s (pubkey: %02x%02x...)", client_ip, req->identity_pubkey[0],
726 req->identity_pubkey[1]);
727 }
728
730 memset(&resp, 0, sizeof(resp));
731
732 asciichat_error_t join_result = database_session_join(server->db, req, &server->config, &resp);
733 if (join_result == ASCIICHAT_OK && resp.success) {
734 acip_send_session_joined(transport, &resp);
735
736 // Update client data (accessed via transport->user_data)
737 if (client_data) {
738 memcpy(client_data->session_id, resp.session_id, 16);
739 memcpy(client_data->participant_id, resp.participant_id, 16);
740 client_data->joined_session = true;
741 client_data->session_established = true; // Signal handler to exit receive loop
742 }
743
744 log_info("Client %s joined session (participant %02x%02x...)", client_ip, resp.participant_id[0],
745 resp.participant_id[1]);
746
747 // Broadcast PARTICIPANT_JOINED notification to all other participants in the session
748 acip_participant_joined_t participant_joined;
749 memset(&participant_joined, 0, sizeof(participant_joined));
750 memcpy(participant_joined.session_id, resp.session_id, 16);
751 memcpy(participant_joined.new_participant_id, resp.participant_id, 16);
752 memcpy(participant_joined.new_participant_pubkey, req->identity_pubkey, 32);
753 // peer_count is the number of OTHER participants; total = peer_count + 1 (the new one)
754 participant_joined.current_participant_count = resp.peer_count + 1;
755
756 asciichat_error_t broadcast_result =
757 signaling_broadcast(server->db, server->tcp_server, resp.session_id, PACKET_TYPE_ACIP_PARTICIPANT_JOINED,
758 &participant_joined, sizeof(participant_joined), resp.participant_id);
759
760 if (broadcast_result != ASCIICHAT_OK) {
761 log_warn("Failed to broadcast PARTICIPANT_JOINED for session %02x%02x...: %d", resp.session_id[0],
762 resp.session_id[1], broadcast_result);
763 } else {
764 log_debug("Broadcasted PARTICIPANT_JOINED to other participants in session");
765 }
766 } else {
767 acip_send_session_joined(transport, &resp);
768 log_warn("Session join failed for %s: %s", client_ip, resp.error_message);
769 }
770}
771
772static asciichat_error_t acds_remove_client_from_session(acds_server_t *server, acds_client_data_t *client_data) {
773 if (!server || !client_data || !client_data->joined_session) {
774 return ASCIICHAT_OK;
775 }
776
777 session_entry_t *session = database_session_find_by_id(server->db, client_data->session_id);
778 bool was_host = false;
779 if (session) {
780 const uint8_t *host_id = session->host_established ? session->host_participant_id : session->initiator_id;
781 was_host = memcmp(host_id, client_data->participant_id, sizeof(client_data->participant_id)) == 0;
782 session_entry_destroy(session);
783 }
784
785 asciichat_error_t result = database_session_leave(server->db, client_data->session_id, client_data->participant_id);
786 if (result != ASCIICHAT_OK) {
787 return SET_ERRNO(result, "Failed to remove disconnected participant from session");
788 }
789
790 client_data->joined_session = false;
791
792 // A closed connection can no longer send SESSION_LEAVE. Notify peers after
793 // deleting the participant so reconnects do not try signaling this socket.
794 session = database_session_find_by_id(server->db, client_data->session_id);
795 if (session) {
796 acip_participant_left_t participant_left = {0};
797 memcpy(participant_left.session_id, client_data->session_id, sizeof(participant_left.session_id));
798 memcpy(participant_left.left_participant_id, client_data->participant_id,
799 sizeof(participant_left.left_participant_id));
800 participant_left.was_host = was_host ? 1 : 0;
801 participant_left.remaining_count = session->current_participants;
802 result =
804 &participant_left, sizeof(participant_left), client_data->participant_id);
805 if (result != ASCIICHAT_OK) {
806 log_warn("Failed to notify peers that participant left: %s", asciichat_error_string(result));
807 }
808 session_entry_destroy(session);
809 }
810
811 return ASCIICHAT_OK;
812}
813
814static void acds_on_session_leave(const acip_session_leave_t *req, acip_transport_t *transport, const char *client_ip,
815 void *app_ctx) {
816 acds_server_t *server = (acds_server_t *)app_ctx;
817 acds_client_data_t *client_data = (acds_client_data_t *)transport->user_data;
818
819 log_debug("SESSION_LEAVE packet from %s", client_ip);
820
821 if (!client_data || !client_data->joined_session ||
822 memcmp(req->session_id, client_data->session_id, sizeof(client_data->session_id)) != 0 ||
823 memcmp(req->participant_id, client_data->participant_id, sizeof(client_data->participant_id)) != 0) {
824 acip_send_error(transport, ERROR_INVALID_PARAM, "SESSION_LEAVE does not match this connection");
825 log_warn("Rejected SESSION_LEAVE from %s because it did not match the joined participant", client_ip);
826 return;
827 }
828
829 asciichat_error_t leave_result = acds_remove_client_from_session(server, client_data);
830 if (leave_result == ASCIICHAT_OK) {
831 log_info("Client %s left session", client_ip);
832 } else {
833 acip_send_error(transport, leave_result, asciichat_error_string(leave_result));
834 log_warn("Session leave failed for %s: %s", client_ip, asciichat_error_string(leave_result));
835 }
836}
837
838static void acds_on_webrtc_sdp(const acip_webrtc_sdp_t *sdp, size_t payload_len, acip_transport_t *transport,
839 const char *client_ip, void *app_ctx) {
840 acds_server_t *server = (acds_server_t *)app_ctx;
841 (void)transport; // Transport not needed for relay
842
843 log_debug("WEBRTC_SDP packet from %s", client_ip);
844
845 // Validation is done in the library handler (lib/network/acip/acds_handlers.c)
846 asciichat_error_t relay_result = signaling_relay_sdp(server->db, server->tcp_server, sdp, payload_len);
847 if (relay_result != ASCIICHAT_OK) {
848 acip_send_error(transport, relay_result, "SDP relay failed");
849 log_warn("SDP relay failed from %s: %s", client_ip, asciichat_error_string(relay_result));
850 }
851}
852
853static void acds_on_webrtc_ice(const acip_webrtc_ice_t *ice, size_t payload_len, acip_transport_t *transport,
854 const char *client_ip, void *app_ctx) {
855 acds_server_t *server = (acds_server_t *)app_ctx;
856 (void)transport; // Transport not needed for relay
857
858 log_debug("WEBRTC_ICE packet from %s", client_ip);
859
860 // Validation is done in the library handler (lib/network/acip/acds_handlers.c)
861 asciichat_error_t relay_result = signaling_relay_ice(server->db, server->tcp_server, ice, payload_len);
862 if (relay_result != ASCIICHAT_OK) {
863 acip_send_error(transport, relay_result, "ICE relay failed");
864 log_warn("ICE relay failed from %s: %s", client_ip, asciichat_error_string(relay_result));
865 }
866}
867
868static void acds_on_network_quality(const void *payload, size_t payload_len, acip_transport_t *transport,
869 const char *client_ip, void *app_ctx) {
870 acds_server_t *server = (acds_server_t *)app_ctx;
871 (void)transport; // Transport not needed for relay
872
873 log_info("★ ACDS_ON_NETWORK_QUALITY: received from %s, payload_len=%zu", client_ip, payload_len);
874
875 // Cast payload to acip_nat_quality_t for relay
876 const acip_nat_quality_t *quality = (const acip_nat_quality_t *)payload;
877
878 log_debug("NETWORK_QUALITY: session=%02x%02x..., participant=%02x%02x...", quality->session_id[0],
879 quality->session_id[1], quality->participant_id[0], quality->participant_id[1]);
880
881 // Validation and relay
882 asciichat_error_t relay_result =
883 signaling_relay_network_quality(server->db, server->tcp_server, quality, payload_len);
884 if (relay_result != ASCIICHAT_OK) {
885 acip_send_error(transport, relay_result, "NETWORK_QUALITY relay failed");
886 log_warn("NETWORK_QUALITY relay failed from %s: %s", client_ip, asciichat_error_string(relay_result));
887 } else {
888 log_info("★ ACDS_ON_NETWORK_QUALITY: relay completed successfully for %s", client_ip);
889 }
890}
891
892static void acds_on_host_announcement(const acip_host_announcement_t *announcement, acip_transport_t *transport,
893 const char *client_ip, void *app_ctx) {
894 acds_server_t *server = (acds_server_t *)app_ctx;
895
896 log_info("HOST_ANNOUNCEMENT from %s: host_id=%02x%02x..., address=%s:%u, conn_type=%d", client_ip,
897 announcement->host_id[0], announcement->host_id[1], announcement->host_address, announcement->host_port,
898 announcement->connection_type);
899
900 // Update session host in database
901 asciichat_error_t result =
902 database_session_update_host(server->db, announcement->session_id, announcement->host_id,
903 announcement->host_address, announcement->host_port, announcement->connection_type);
904
905 if (result != ASCIICHAT_OK) {
906 acip_send_error(transport, result, "Failed to update session host");
907 log_warn("HOST_ANNOUNCEMENT failed from %s: %s", client_ip, asciichat_error_string(result));
908 return;
909 }
910
911 // TODO: Broadcast HOST_DESIGNATED to all participants in the session
912 // This requires iterating connected clients and sending to those in the same session
913 // For now, new participants will get host info when they join
914
915 log_info("Session host updated via HOST_ANNOUNCEMENT from %s", client_ip);
916}
917
918static void acds_on_host_lost(const acip_host_lost_t *host_lost, acip_transport_t *transport, const char *client_ip,
919 void *app_ctx) {
920 acds_server_t *server = (acds_server_t *)app_ctx;
921
922 log_info("HOST_LOST from %s: session=%02x%02x..., participant=%02x%02x..., last_host=%02x%02x..., reason=%u",
923 client_ip, host_lost->session_id[0], host_lost->session_id[1], host_lost->participant_id[0],
924 host_lost->participant_id[1], host_lost->last_host_id[0], host_lost->last_host_id[1],
925 host_lost->disconnect_reason);
926
927 // Start host migration (set in_migration flag in database for bookkeeping)
928 asciichat_error_t result = database_session_start_migration(server->db, host_lost->session_id);
929 if (result != ASCIICHAT_OK) {
930 acip_send_error(transport, result, "Failed to start host migration");
931 log_warn("HOST_LOST failed from %s: %s", client_ip, asciichat_error_string(result));
932 return;
933 }
934
935 // Track migration for timeout detection
936 // (Host already elected future host 5 minutes ago; participants will failover to pre-elected host)
937 migration_context_t *migration = find_or_create_migration(server, host_lost->session_id, true);
938 if (!migration) {
939 acip_send_error(transport, ERROR_MEMORY, "Failed to track migration");
940 log_warn("HOST_LOST: Failed to create migration context from %s", client_ip);
941 return;
942 }
943
944 log_info("Migration tracking started for session %02x%02x... (participant %02x%02x...)", host_lost->session_id[0],
945 host_lost->session_id[1], host_lost->participant_id[0], host_lost->participant_id[1]);
946
947 // NOTE: No candidate collection needed - future host was pre-elected 5 minutes ago.
948 // Participants already know who the new host is from the last FUTURE_HOST_ELECTED broadcast.
949 // They will instantly failover to the pre-elected host.
950 // ACDS just tracks migration timeout for cleanup.
951}
952
953// Global ACIP callback structure for ACDS
954static const acip_acds_callbacks_t g_acds_callbacks = {
955 .on_session_create = acds_on_session_create,
956 .on_session_lookup = acds_on_session_lookup,
957 .on_session_join = acds_on_session_join,
958 .on_session_leave = acds_on_session_leave,
959 .on_webrtc_sdp = acds_on_webrtc_sdp,
960 .on_webrtc_ice = acds_on_webrtc_ice,
961 .on_host_announcement = acds_on_host_announcement,
962 .on_host_lost = acds_on_host_lost,
963 .on_network_quality = acds_on_network_quality,
964 .app_ctx = NULL // Set dynamically to server instance
965};
966
967void *acds_client_handler(void *arg) {
969 if (!ctx) {
970 log_error("Client handler: NULL context");
971 return NULL;
972 }
973
974 acds_server_t *server = (acds_server_t *)ctx->user_data;
975 socket_t client_socket = ctx->client_socket;
976
977 // Get client IP for logging
978 char client_ip[INET6_ADDRSTRLEN] = {0};
979 tcp_client_context_get_ip(ctx, client_ip, sizeof(client_ip));
980
981 log_info("Client handler started for %s", client_ip);
982
983 // Register client in TCP server registry with allocated client data
985 if (!client_data) {
986 tcp_server_reject_client(client_socket, "Failed to allocate client data");
987 SAFE_FREE(ctx);
988 return NULL;
989 }
990 memset(client_data, 0, sizeof(*client_data));
991 client_data->joined_session = false;
992 client_data->handshake_complete = false;
993
994 // Initialize crypto handshake context with client IP name
995 char crypto_name[96];
996 SAFE_SNPRINTF(crypto_name, sizeof(crypto_name), "crypto_discovery_client_%s", client_ip);
997 asciichat_error_t handshake_result = crypto_handshake_init(crypto_name, &client_data->handshake_ctx, true);
998 if (handshake_result != ASCIICHAT_OK) {
999 log_error("Failed to initialize crypto handshake for client %s", client_ip);
1000 SAFE_FREE(client_data);
1001 tcp_server_reject_client(client_socket, "Failed to initialize crypto handshake");
1002 SAFE_FREE(ctx);
1003 return NULL;
1004 }
1005
1006 // Set server identity keys for handshake
1009 memcpy(client_data->handshake_ctx.server_public_key.key, server->identity_public, 32);
1010 memcpy(client_data->handshake_ctx.server_private_key.key.ed25519, server->identity_secret, 64);
1011
1012 if (tcp_server_add_client(server->tcp_server, client_socket, client_data) != ASCIICHAT_OK) {
1013 SAFE_FREE(client_data);
1014 tcp_server_reject_client(client_socket, "Failed to register client in registry");
1015 SAFE_FREE(ctx);
1016 return NULL;
1017 }
1018
1019 log_debug("Client %s registered (socket=%d, total=%zu)", client_ip, client_socket,
1021
1022 // Create transport for handshake and subsequent client communication
1023 acip_transport_t *transport = acip_tcp_transport_create("acds_tcp_client", client_socket, NULL);
1024 if (!transport) {
1025 log_error("Failed to create TCP transport for client %s", client_ip);
1026 tcp_server_remove_client(server->tcp_server, client_socket);
1027 SAFE_FREE(ctx);
1028 return NULL;
1029 }
1030
1031 // Perform crypto handshake using shared server_like implementation
1032 log_debug("Performing crypto handshake with client %s", client_ip);
1033 handshake_result = session_server_like_handshake(&client_data->handshake_ctx, transport);
1034 if (handshake_result != ASCIICHAT_OK) {
1035 log_warn("Crypto handshake failed for client %s", client_ip);
1036 acip_transport_destroy(transport);
1037 tcp_server_remove_client(server->tcp_server, client_socket);
1038 SAFE_FREE(ctx);
1039 return NULL;
1040 }
1041
1042 client_data->handshake_complete = true;
1043 log_info("Crypto handshake complete for client %s", client_ip);
1044
1045 // Store transport and registry ID in client data, and link back from transport
1046 client_data->transport = transport;
1047 client_data->registry_id = client_socket;
1048 transport->user_data = client_data;
1049
1050 // Main packet processing loop
1051 uint64_t last_packet_time_ns = time_get_ns();
1052 const uint64_t idle_disconnect_ns = 60ULL * NS_PER_SEC_INT; // Disconnect after 60s idle
1053
1054 while (atomic_load_bool(&server->tcp_server->running)) {
1055 packet_type_t packet_type;
1056 void *payload = NULL;
1057 size_t payload_size = 0;
1058
1059 // Receive packet (blocking with system timeout)
1060 int result = receive_packet(client_socket, &packet_type, &payload, &payload_size);
1061
1062 if (result >= 0) {
1063 last_packet_time_ns = time_get_ns();
1064 log_info("★ ACDS_CLIENT_HANDLER: Received packet type=%u from %s, payload_size=%zu", packet_type, client_ip,
1065 payload_size);
1066
1067 // Track network stats (TCP packets)
1068 // Packet size includes header (6 bytes: magic 4 + type 1 + length 2 + CRC 4)
1069 size_t total_packet_size = payload_size + 14; // 4 (magic) + 1 (type) + 2 (length) + 4 (CRC) + 3 (client ID)
1070 atomic_fetch_add_u64(&server->stats.bytes_received, total_packet_size);
1072 }
1073
1074 if (result < 0) {
1075 // Check error context to distinguish timeout from actual disconnect
1077 bool has_context = HAS_ERRNO(&err_ctx);
1078
1079 // Check if this is a timeout (non-fatal) or actual disconnect (fatal)
1080 asciichat_error_t error = GET_ERRNO();
1081 if (error == ERROR_NETWORK_TIMEOUT ||
1082 (error == ERROR_NETWORK && has_context && strstr(err_ctx.context_message, "timed out") != NULL)) {
1083 // Check if client has been idle too long (abrupt disconnect without FIN)
1084 uint64_t idle_ns = time_get_ns() - last_packet_time_ns;
1085 if (idle_ns >= idle_disconnect_ns) {
1086 log_info("Client %s: idle for %llu seconds, disconnecting", client_ip,
1087 (unsigned long long)(idle_ns / NS_PER_SEC_INT));
1088 if (payload) {
1089 buffer_pool_free(NULL, payload, payload_size);
1090 }
1091 break;
1092 }
1093 log_debug("Client %s: receive timeout, continuing to wait for packets", client_ip);
1094 if (payload) {
1095 buffer_pool_free(NULL, payload, payload_size);
1096 }
1097 continue;
1098 }
1099
1100 // Actual disconnect or fatal error
1101 log_info("Client %s disconnected", client_ip);
1102 if (payload) {
1103 buffer_pool_free(NULL, payload, payload_size);
1104 }
1105 break;
1106 }
1107
1108 log_debug("Received packet type 0x%02X from %s, length=%zu", packet_type, client_ip, payload_size);
1109
1110 // Multi-key session creation protocol: block non-PING/PONG/SESSION_CREATE messages
1111 if (client_data->in_multikey_session_create) {
1112 bool allowed = (packet_type == PACKET_TYPE_ACIP_SESSION_CREATE || packet_type == PACKET_TYPE_PING ||
1113 packet_type == PACKET_TYPE_PONG);
1114
1115 if (!allowed) {
1116 log_warn("Client %s sent packet type 0x%02X during multi-key session creation - only SESSION_CREATE/PING/PONG "
1117 "allowed",
1118 client_ip, packet_type);
1119
1120 // Send error response using persistent transport
1122 "Only SESSION_CREATE/PING/PONG allowed during multi-key session creation");
1123
1124 // Free payload and continue
1125 if (payload) {
1126 buffer_pool_free(NULL, payload, payload_size);
1127 }
1128 continue;
1129 }
1130 }
1131
1132 // O(1) ACIP array-based dispatch
1133 // Set server context for callbacks
1134 acip_acds_callbacks_t callbacks = g_acds_callbacks;
1135 callbacks.app_ctx = server;
1136
1137 asciichat_error_t dispatch_result =
1138 acip_handle_acds_packet(transport, packet_type, payload, payload_size, client_ip, &callbacks);
1139
1140 if (dispatch_result != ASCIICHAT_OK) {
1141 log_warn("ACIP handler failed for packet type 0x%02X from %s: %s", packet_type, client_ip,
1142 asciichat_error_string(dispatch_result));
1143 }
1144
1145 // Free payload (allocated by receive_packet via buffer_pool_alloc)
1146 if (payload) {
1147 buffer_pool_free(NULL, payload, payload_size);
1148 }
1149
1150 // Keep handler running after session_established
1151 // Handler continues to receive and relay WebRTC signaling packets (SDP, ICE)
1152 // Broadcasts don't go through this socket - they use tcp_server_foreach_client() to write directly
1153 }
1154
1155 // Cleanup - handler always cleans up since it runs until connection closes
1156 if (client_data->joined_session) {
1157 asciichat_error_t leave_result = acds_remove_client_from_session(server, client_data);
1158 if (leave_result != ASCIICHAT_OK) {
1159 log_warn("Could not clean up disconnected participant %02x%02x...: %s", client_data->participant_id[0],
1160 client_data->participant_id[1], asciichat_error_string(leave_result));
1161 }
1162 }
1163 tcp_server_remove_client(server->tcp_server, client_socket);
1164 log_debug("Client %s unregistered (total=%zu)", client_ip, tcp_server_get_client_count(server->tcp_server));
1165
1166 // Destroy transport before closing socket
1167 acip_transport_destroy(transport);
1168 socket_close(client_socket);
1169
1170 SAFE_FREE(ctx);
1171
1172 log_info("Client handler finished for %s", client_ip);
1173 return NULL;
1174}
1175
1176// =============================================================================
1177// WebSocket Client Handler
1178// =============================================================================
1179
1180static asciichat_error_t acds_receive_websocket_packet(acip_transport_t *transport, void **buffer, size_t *length,
1181 void **allocation) {
1182 asciichat_error_t result = acip_transport_recv(transport, buffer, length, allocation);
1183 if (result != ASCIICHAT_OK || !transport->crypto_ctx)
1184 return result;
1185 if (*length < sizeof(packet_header_t))
1186 return SET_ERRNO(ERROR_NETWORK_PROTOCOL, "Truncated encrypted discovery packet");
1187 const packet_header_t *header = (const packet_header_t *)*buffer;
1188 size_t ciphertext_len = NET_TO_HOST_U32(header->length);
1189 if (NET_TO_HOST_U16(header->type) != PACKET_TYPE_ENCRYPTED || ciphertext_len != *length - sizeof(packet_header_t)) {
1190 return SET_ERRNO(ERROR_NETWORK_PROTOCOL, "Plain WebSocket discovery packets must be encrypted");
1191 }
1192 uint8_t *plaintext = buffer_pool_alloc(NULL, ciphertext_len);
1193 if (!plaintext)
1194 return SET_ERRNO(ERROR_MEMORY, "Could not allocate decrypted discovery packet");
1195 size_t plaintext_len = 0;
1196 crypto_result_t decrypted = crypto_decrypt(transport->crypto_ctx, (const uint8_t *)*buffer + sizeof(*header),
1197 ciphertext_len, plaintext, ciphertext_len, &plaintext_len);
1198 if (decrypted != CRYPTO_OK || plaintext_len < sizeof(packet_header_t)) {
1199 buffer_pool_free(NULL, plaintext, ciphertext_len);
1200 return SET_ERRNO(ERROR_CRYPTO, "Failed to decrypt discovery packet");
1201 }
1202 if (*allocation)
1203 buffer_pool_free(NULL, *allocation, 0);
1204 *buffer = plaintext;
1205 *allocation = plaintext;
1206 *length = plaintext_len;
1207 return ASCIICHAT_OK;
1208}
1209
1212 if (!ctx) {
1213 log_error("WebSocket client handler: NULL context");
1214 return NULL;
1215 }
1216
1217 acds_server_t *server = (acds_server_t *)ctx->user_data;
1218 acip_transport_t *transport = ctx->transport;
1219 char *client_ip = ctx->client_ip;
1220
1221 log_info("WebSocket client handler started for %s", client_ip);
1222
1223 // Allocate client_data
1225 if (!client_data) {
1226 log_error("Failed to allocate client data for WebSocket client %s", client_ip);
1227 SAFE_FREE(ctx);
1228 return NULL;
1229 }
1230
1231 client_data->transport = transport;
1232 client_data->joined_session = false;
1233 client_data->handshake_complete = false;
1234 transport->user_data = client_data;
1235
1236 // Initialize crypto handshake context with client IP name
1237 char crypto_name[96];
1238 SAFE_SNPRINTF(crypto_name, sizeof(crypto_name), "crypto_discovery_websocket_%s", client_ip);
1239 asciichat_error_t hs_init_result = crypto_handshake_init(crypto_name, &client_data->handshake_ctx, true);
1240 if (hs_init_result != ASCIICHAT_OK) {
1241 log_error("Failed to initialize crypto handshake for WebSocket client %s", client_ip);
1242 SAFE_FREE(client_data);
1243 SAFE_FREE(ctx);
1244 return NULL;
1245 }
1246
1247 // Set server identity keys for handshake
1250 memcpy(client_data->handshake_ctx.server_public_key.key, server->identity_public, 32);
1251 memcpy(client_data->handshake_ctx.server_private_key.key.ed25519, server->identity_secret, 64);
1252
1253 // Generate synthetic socket ID for tcp_server registry
1254 static _Atomic int g_ws_synthetic_id = INT_MAX;
1255 int synthetic_id = atomic_fetch_sub(&g_ws_synthetic_id, 1);
1256 client_data->registry_id = synthetic_id;
1257
1258 // Register in tcp_server with synthetic ID (so callbacks can find client_data)
1259 if (tcp_server_add_client(server->tcp_server, synthetic_id, client_data) != ASCIICHAT_OK) {
1260 log_error("Failed to register WebSocket client %s in registry", client_ip);
1261 SAFE_FREE(client_data);
1262 SAFE_FREE(ctx);
1263 return NULL;
1264 }
1265
1266 log_debug("WebSocket client %s registered (synthetic_id=%d, total=%zu)", client_ip, synthetic_id,
1268
1269 // Determine if client authentication is required by checking server configuration
1270 bool auth_required = server->config.require_client_identity;
1271 ctx->auth_required = auth_required;
1272
1273 // Plain WebSocket signaling requires the ACIP crypto handshake. TLS can
1274 // replace application encryption when identity authentication is not needed.
1275 bool skip_handshake = ctx->is_secure && !auth_required;
1276
1277 if (skip_handshake) {
1278 client_data->handshake_complete = true;
1279 log_info("Secure WebSocket connection from %s - using TLS without custom crypto handshake", client_ip);
1280 } else {
1281 log_info("WebSocket connection from %s - proceeding with crypto handshake%s", client_ip,
1282 auth_required ? " (client authentication required)" : "");
1283
1284 // Perform crypto handshake using shared server_like implementation
1285 log_debug("Performing crypto handshake with WebSocket client %s", client_ip);
1286 asciichat_error_t hs_result = session_server_like_handshake(&client_data->handshake_ctx, transport);
1287 if (hs_result != ASCIICHAT_OK) {
1288 log_warn("Crypto handshake failed for WebSocket client %s", client_ip);
1289 tcp_server_remove_client(server->tcp_server, synthetic_id);
1290 SAFE_FREE(ctx);
1291 return NULL;
1292 }
1293
1294 client_data->handshake_complete = true;
1295 log_debug("Crypto handshake completed for WebSocket client %s", client_ip);
1296 transport->crypto_ctx = &client_data->handshake_ctx.crypto_ctx;
1297 }
1298
1299 // Main packet processing loop using transport recv
1300 while (atomic_load_bool(&server->tcp_server->running)) {
1301 void *recv_buffer = NULL;
1302 size_t recv_len = 0;
1303 void *alloc_buffer = NULL;
1304
1305 asciichat_error_t recv_result = acds_receive_websocket_packet(transport, &recv_buffer, &recv_len, &alloc_buffer);
1306 if (recv_result != ASCIICHAT_OK) {
1307 // Check if this is a timeout or disconnect
1308 if (recv_result == ERROR_NETWORK_TIMEOUT) {
1309 log_debug("WebSocket client %s: receive timeout, continuing to wait for packets", client_ip);
1310 if (alloc_buffer) {
1311 buffer_pool_free(NULL, alloc_buffer, 0);
1312 }
1313 continue;
1314 }
1315
1316 // Actual disconnect or fatal error
1317 log_info("WebSocket client %s disconnected", client_ip);
1318 if (alloc_buffer) {
1319 buffer_pool_free(NULL, alloc_buffer, 0);
1320 }
1321 break;
1322 }
1323
1324 // Parse packet header from received data
1325 if (recv_len < sizeof(packet_header_t)) {
1326 log_warn("WebSocket client %s: received packet too small (%zu bytes)", client_ip, recv_len);
1327 if (alloc_buffer) {
1328 buffer_pool_free(NULL, alloc_buffer, 0);
1329 }
1330 continue;
1331 }
1332
1333 const packet_header_t *header = (const packet_header_t *)recv_buffer;
1334 packet_type_t packet_type = (packet_type_t)NET_TO_HOST_U16(header->type);
1335 size_t payload_size = NET_TO_HOST_U32(header->length);
1336 const void *payload = (const uint8_t *)recv_buffer + sizeof(packet_header_t);
1337
1338 // Validate payload size
1339 if (recv_len < sizeof(packet_header_t) + payload_size) {
1340 log_warn("WebSocket client %s: packet size mismatch (expected %zu, got %zu)", client_ip,
1341 sizeof(packet_header_t) + payload_size, recv_len);
1342 if (alloc_buffer) {
1343 buffer_pool_free(NULL, alloc_buffer, 0);
1344 }
1345 continue;
1346 }
1347
1348 log_debug("Received packet type 0x%02X from WebSocket client %s, length=%zu", packet_type, client_ip, payload_size);
1349
1350 // Multi-key session creation protocol: block non-PING/PONG/SESSION_CREATE messages
1351 if (client_data->in_multikey_session_create) {
1352 bool allowed = (packet_type == PACKET_TYPE_ACIP_SESSION_CREATE || packet_type == PACKET_TYPE_PING ||
1353 packet_type == PACKET_TYPE_PONG);
1354
1355 if (!allowed) {
1356 log_warn("WebSocket client %s sent packet type 0x%02X during multi-key session creation - only "
1357 "SESSION_CREATE/PING/PONG allowed",
1358 client_ip, packet_type);
1360 "Only SESSION_CREATE/PING/PONG allowed during multi-key session creation");
1361 if (alloc_buffer) {
1362 buffer_pool_free(NULL, alloc_buffer, 0);
1363 }
1364 continue;
1365 }
1366 }
1367
1368 // O(1) ACIP array-based dispatch
1369 acip_acds_callbacks_t callbacks = g_acds_callbacks;
1370 callbacks.app_ctx = server;
1371
1372 asciichat_error_t dispatch_result =
1373 acip_handle_acds_packet(transport, packet_type, payload, payload_size, client_ip, &callbacks);
1374
1375 if (dispatch_result != ASCIICHAT_OK) {
1376 log_warn("ACIP handler failed for packet type 0x%02X from WebSocket client %s: %s", packet_type, client_ip,
1377 asciichat_error_string(dispatch_result));
1378 }
1379
1380 if (alloc_buffer) {
1381 buffer_pool_free(NULL, alloc_buffer, 0);
1382 }
1383 }
1384
1385 // Cleanup
1386 if (client_data->joined_session) {
1387 asciichat_error_t leave_result = acds_remove_client_from_session(server, client_data);
1388 if (leave_result != ASCIICHAT_OK) {
1389 log_warn("Could not clean up disconnected WebSocket participant %02x%02x...: %s", client_data->participant_id[0],
1390 client_data->participant_id[1], asciichat_error_string(leave_result));
1391 }
1392 }
1393 tcp_server_remove_client(server->tcp_server, synthetic_id);
1394 log_debug("WebSocket client %s unregistered (total=%zu)", client_ip, tcp_server_get_client_count(server->tcp_server));
1395
1396 // Transport is owned by WebSocket server, don't destroy it here
1397
1398 log_info("WebSocket client handler finished for %s", client_ip);
1399
1400 SAFE_FREE(ctx);
1401
1402 return NULL;
1403}
ascii-chat Discovery Service (ACDS) Protocol Message Formats
asciichat_error_t acds_verify_session_create(const uint8_t identity_pubkey[32], uint64_t timestamp, uint8_t capabilities, uint8_t max_participants, const uint8_t signature[64])
Verify SESSION_CREATE signature.
asciichat_error_t acds_verify_session_join(const uint8_t identity_pubkey[32], uint64_t timestamp, const char *session_string, const uint8_t signature[64])
Verify SESSION_JOIN signature.
bool acds_validate_timestamp(uint64_t timestamp_ms, uint32_t window_seconds)
Check if timestamp is within acceptable window.
ACIP client-side protocol library and ACDS client connection API.
asciichat_error_t acip_handle_acds_packet(acip_transport_t *transport, packet_type_t type, const void *payload, size_t payload_len, const char *client_ip, const acip_acds_callbacks_t *callbacks)
Handle incoming ACDS packet with O(1) dispatch.
ACIP Discovery Server (ACDS) packet handlers.
Application-level callbacks for library code.
#define APP_CALLBACK_VOID(callback_name)
uint64_t atomic_fetch_add_u64(atomic_t *a, uint64_t delta)
Atomically add to a uint64_t and return the previous value.
Definition atomic.c:248
void atomic_store_bool(atomic_t *a, bool value)
Atomically store a boolean value.
Definition atomic.c:177
bool atomic_load_bool(atomic_t *a)
Atomically load a boolean value.
Definition atomic.c:169
void atomic_store_u64(atomic_t *a, uint64_t value)
Atomically store a uint64_t value.
Definition atomic.c:241
🗃️ Lock-Free Unified Memory Buffer Pool with Lazy Allocation
asciichat_error_t thread_pool_spawn(void *pool, void *(*thread_func)(void *), void *thread_arg, int stop_id, const char *name)
Spawn a worker thread in the pool.
asciichat_error_t crypto_handshake_init(const char *name, crypto_handshake_context_t *ctx, bool is_server)
Initialize named crypto handshake context.
asciichat_error_t database_session_update_host(sqlite3 *db, const uint8_t session_id[16], const uint8_t host_participant_id[16], const char *host_address, uint16_t host_port, uint8_t connection_type)
Update session host (discovery mode)
asciichat_error_t database_session_clear_host(sqlite3 *db, const uint8_t session_id[16])
Clear session host (discovery mode - host migration)
asciichat_error_t database_session_lookup(sqlite3 *db, const char *session_string, const acds_config_t *config, acip_session_info_t *resp)
Lookup session by string.
asciichat_error_t database_session_join(sqlite3 *db, const acip_session_join_t *req, const acds_config_t *config, acip_session_joined_t *resp)
Join existing session.
asciichat_error_t database_session_start_migration(sqlite3 *db, const uint8_t session_id[16])
Start host migration (discovery mode)
asciichat_error_t database_session_leave(sqlite3 *db, const uint8_t session_id[16], const uint8_t participant_id[16])
Leave session.
void database_close(sqlite3 *db)
Close database.
session_entry_t * database_session_find_by_id(sqlite3 *db, const uint8_t session_id[16])
Find session by session_id.
asciichat_error_t database_session_create(sqlite3 *db, const acip_session_create_t *req, const acds_config_t *config, acip_session_created_t *resp)
Create new session.
asciichat_error_t database_init(const char *db_path, sqlite3 **db)
Initialize database and create schema.
void database_session_cleanup_expired(sqlite3 *db)
Clean up inactive sessions.
🔄 Network byte order conversion helpers
#define NET_TO_HOST_U16(val)
Definition endian.h:111
#define NET_TO_HOST_U32(val)
Definition endian.h:81
Network error handling utilities.
acip_session_join_t
Definition acds.h:403
acip_participant_joined_t
Definition acds.h:492
acip_session_joined_t
Definition acds.h:474
acip_session_lookup_t
Definition acds.h:304
acip_webrtc_ice_t
Definition acds.h:650
acip_host_lost_t
Definition acds.h:952
acip_host_announcement_t
Definition acds.h:910
acip_session_created_t
Definition acds.h:269
acip_webrtc_sdp_t
Definition acds.h:604
acip_participant_left_t
Definition acds.h:509
acip_session_create_t
Definition acds.h:221
acip_session_info_t
Definition acds.h:355
acip_session_leave_t
Definition acds.h:524
acip_nat_quality_t
Definition acds.h:891
@ SESSION_TYPE_DIRECT_TCP
Direct TCP connection to server IP:port (default)
Definition acds.h:145
void buffer_pool_free(buffer_pool_t *pool, const void *data, size_t size)
Free a buffer back to the pool (lock-free)
void * buffer_pool_alloc(buffer_pool_t *pool, size_t size)
Allocate a buffer from the pool (lock-free fast path)
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:414
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MALLOC(size, cast)
Definition common.h:264
#define SAFE_SNPRINTF(buffer, buffer_size,...)
Definition common.h:492
#define SAFE_CALLOC(count, size, cast)
Definition common.h:274
unsigned long long uint64_t
Definition common.h:59
unsigned char uint8_t
Definition common.h:56
crypto_result_t
Cryptographic operation result codes.
crypto_result_t crypto_decrypt(crypto_context_t *ctx, const uint8_t *ciphertext, size_t ciphertext_len, uint8_t *plaintext_out, size_t plaintext_out_size, size_t *plaintext_len_out)
Decrypt data using XSalsa20-Poly1305.
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
#define GET_ERRNO()
Get current error code (0 if no error)
#define HAS_ERRNO(var)
Check if an error occurred and get full context.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ERROR_INVALID_STATE
@ ERROR_NETWORK
Definition error_codes.h:77
@ ERROR_CRYPTO_VERIFICATION
@ ERROR_NETWORK_PROTOCOL
Definition error_codes.h:81
@ ERROR_MEMORY
Definition error_codes.h:56
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_RATE_LIMITED
Definition error_codes.h:85
@ ERROR_CRYPTO
Definition error_codes.h:96
@ ERROR_INVALID_PARAM
@ ERROR_NETWORK_TIMEOUT
Definition error_codes.h:80
uint8_t key[32]
Definition key_types.h:71
key_type_t type
Definition key_types.h:92
uint8_t ed25519[64]
Definition key_types.h:94
union private_key_t::@14 key
key_type_t type
Definition key_types.h:70
@ KEY_TYPE_ED25519
Definition key_types.h:52
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
#define log_info(...)
Log an INFO message.
Definition log/log.h:561
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
uint64_t time_get_ns(void)
Get current monotonic time in nanoseconds.
Definition util/time.c:108
#define NS_PER_SEC_INT
Definition time.h:157
#define NS_PER_MS_INT
Definition time.h:156
int receive_packet(socket_t sockfd, packet_type_t *type, void **data, size_t *len)
Receive a basic packet without encryption.
Definition packet.c:797
#define MAX_IDENTITY_KEYS
Maximum number of identity keys that can be loaded (for multi-key support)
packet_type_t
Network protocol packet type enumeration.
Definition packet.h:286
@ PACKET_TYPE_ACIP_PARTICIPANT_JOINED
Participant joined notification (ACDS -> Existing Participants)
Definition packet.h:469
@ PACKET_TYPE_PONG
Keepalive pong response.
Definition packet.h:385
@ PACKET_TYPE_ENCRYPTED
Encrypted packet (after handshake completion)
Definition packet.h:333
@ PACKET_TYPE_ACIP_SESSION_CREATE
Create new session (Client -> Discovery Server)
Definition packet.h:407
@ PACKET_TYPE_ACIP_PARTICIPANT_LEFT
Participant left notification (ACDS -> Remaining Participants)
Definition packet.h:471
@ PACKET_TYPE_ACIP_SESSION_CREATED
Session created response (Discovery Server -> Client)
Definition packet.h:409
@ PACKET_TYPE_PING
Keepalive ping packet.
Definition packet.h:383
int socket_close(socket_t sock)
Close a socket.
void platform_sleep_ms(unsigned int ms)
Sleep for a specified number of milliseconds.
stun_server_t
Definition stun.h:75
turn_server_t
Definition turn.h:101
int socket_t
Server-side handshake functions.
🌍 IP Address Parsing and Formatting Utilities
void session_entry_destroy(session_entry_t *entry)
Free a session entry and all its resources.
void tcp_server_reject_client(socket_t socket, const char *reason)
Reject client connection with reason.
asciichat_error_t tcp_server_remove_client(tcp_server_t *server, socket_t socket)
Remove client from registry.
const char * tcp_client_context_get_ip(const tcp_client_context_t *ctx, char *buf, size_t len)
Get formatted IP address from client context.
size_t tcp_server_get_client_count(tcp_server_t *server)
Get client count.
asciichat_error_t tcp_server_add_client(tcp_server_t *server, socket_t socket, void *client_data)
Add client to registry.
📝 Logging API with multiple log levels and terminal output control
🌐 Core network I/O operations with timeout support
asciichat_error_t rate_limiter_record(rate_limiter_t *limiter, const char *ip_address, rate_event_type_t event_type)
Record a rate limit event.
Definition rate_limit.c:145
void rate_limiter_destroy(rate_limiter_t *limiter)
Destroy rate limiter and free resources.
Definition rate_limit.c:116
void rate_limiter_set_sqlite_db(rate_limiter_t *limiter, void *db)
Set SQLite database handle for rate limiter.
Definition rate_limit.c:107
rate_limiter_t * rate_limiter_create_sqlite(const char *db_path)
Create SQLite-backed rate limiter.
Definition rate_limit.c:90
asciichat_error_t rate_limiter_check(rate_limiter_t *limiter, const char *ip_address, rate_event_type_t event_type, const rate_limit_config_t *config, bool *allowed)
Check if an event from an IP address should be rate limited.
Definition rate_limit.c:128
asciichat_error_t rate_limiter_prune(rate_limiter_t *limiter, uint32_t max_age_secs)
Clean up old rate limit events.
Definition rate_limit.c:161
🚦 Rate limiting API with pluggable backends
@ RATE_EVENT_SESSION_LOOKUP
Session lookup.
Definition rate_limit.h:49
@ RATE_EVENT_SESSION_JOIN
Session join.
Definition rate_limit.h:50
@ RATE_EVENT_SESSION_CREATE
Session creation.
Definition rate_limit.h:48
asciichat_error_t acip_send_session_joined(acip_transport_t *transport, const acip_session_joined_t *response)
Send SESSION_JOINED response packet.
Definition send.c:390
asciichat_error_t acip_send_error(acip_transport_t *transport, uint32_t error_code, const char *message)
Send error message packet.
Definition send.c:301
asciichat_error_t acip_send_session_info(acip_transport_t *transport, const acip_session_info_t *info)
Send SESSION_INFO response packet.
Definition send.c:382
asciichat_error_t packet_send_via_transport(acip_transport_t *transport, packet_type_t type, const void *payload, size_t payload_len, uint32_t client_id)
Send packet via transport with proper header (exported for generic wrappers)
Definition send.c:41
ACIP shared/bidirectional packet sending functions.
asciichat_error_t session_server_like_handshake(crypto_handshake_context_t *ctx, acip_transport_t *transport)
tcp_server_t * session_server_like_get_tcp_server(void)
Shared initialization, networking, and teardown for server-like modes.
asciichat_error_t signaling_relay_network_quality(sqlite3 *db, tcp_server_t *tcp_server, const acip_nat_quality_t *quality, size_t total_packet_len)
Relay NETWORK_QUALITY to session participants.
Definition signaling.c:270
asciichat_error_t signaling_broadcast(sqlite3 *db, tcp_server_t *tcp_server, const uint8_t session_id[16], packet_type_t packet_type, const void *packet, size_t packet_len, const uint8_t *exclude_participant_id)
Broadcast packet to all session participants.
Definition signaling.c:230
asciichat_error_t signaling_relay_ice(sqlite3 *db, tcp_server_t *tcp_server, const acip_webrtc_ice_t *ice, size_t total_packet_len)
Relay ICE candidate to recipient.
Definition signaling.c:187
asciichat_error_t signaling_relay_sdp(sqlite3 *db, tcp_server_t *tcp_server, const acip_webrtc_sdp_t *sdp, size_t total_packet_len)
Relay SDP offer/answer to recipient.
Definition signaling.c:139
Cross-platform socket interface for ascii-chat.
💾 SQLite rate limiting backend interface
uint8_t session_id[16]
void * acds_websocket_client_handler(void *arg)
WebSocket client handler (thread entry point)
void * acds_client_handler(void *arg)
Per-client connection handler (thread entry point)
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.
Per-client connection data.
bool handshake_complete
Whether crypto handshake has completed.
uint8_t session_id[16]
Session UUID (valid if joined_session)
uint8_t participant_id[16]
Participant UUID (valid if joined_session)
int registry_id
Socket or synthetic ID used in tcp_server registry.
crypto_handshake_context_t handshake_ctx
Handshake context for encrypted communication.
bool joined_session
Whether client has successfully joined a session.
acip_transport_t * transport
Persistent transport for this client connection.
bool in_multikey_session_create
True during multi-key SESSION_CREATE sequence.
Discovery server configuration.
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.
bool require_client_identity
Require clients to provide signed identity when joining sessions.
stun_server_t stun_servers[4]
STUN server configurations.
Discovery server state.
acds_config_t config
Runtime configuration.
atomic_t shutdown
Shutdown flag for worker threads.
uint8_t identity_public[32]
Ed25519 public key.
size_t num_active_migrations
Number of active migrations.
tcp_server_t * tcp_server
TCP server (owned by server_like)
uint8_t identity_secret[64]
Ed25519 secret key.
sqlite3 * db
SQLite database handle.
acds_stats_t stats
Network statistics for all connections.
struct rate_limiter_s * rate_limiter
SQLite-backed rate limiter.
migration_context_t active_migrations[32]
Slots for up to 32 concurrent migrations.
thread_pool_t * worker_pool
Thread pool for background workers.
atomic_t packets_received
Total packets received (TCP + WebSocket)
atomic_t packets_sent
Total packets sent (TCP + WebSocket)
atomic_t bytes_sent
Total bytes sent (TCP + WebSocket)
atomic_t bytes_received
Total bytes received (TCP + WebSocket)
time_t start_time
Server start time.
ACDS packet handler callbacks.
void * app_ctx
Application context (passed to all callbacks)
void(* on_session_create)(const acip_session_create_t *req, acip_transport_t *transport, const char *client_ip, void *app_ctx)
Called when client requests session creation.
Transport instance structure.
Definition transport.h:214
crypto_context_t * crypto_ctx
Optional encryption context.
Definition transport.h:216
void * user_data
Application-specific context (e.g., per-client data)
Definition transport.h:219
Error context structure.
char * context_message
Optional custom message (dynamically allocated, owned by system)
In-memory host migration context.
uint64_t migration_start_ns
When migration started (nanoseconds since sokol_time setup)
uint8_t session_id[16]
Session UUID.
Network packet header structure.
Definition packet.h:598
uint32_t length
Payload data length in bytes (0 for header-only packets)
Definition packet.h:604
uint16_t type
Packet type (packet_type_t enumeration)
Definition packet.h:602
Session entry data structure.
uint8_t initiator_id[16]
First participant who created/joined the session.
uint8_t host_participant_id[16]
Current host's participant_id (valid if host_established)
bool host_established
Whether a host has been designated (false = still negotiating)
uint8_t current_participants
Active participant count.
socket_t client_socket
Client connection socket.
void * user_data
User-provided data from config.
atomic_t running
Server running flag (set false to shutdown)
Context passed to client handler thread.
acip_transport_t * transport
ACIP transport for this client.
char client_ip[64]
Client IP address (for logging/rate limiting)
bool auth_required
True if client authentication is required.
void * user_data
User-provided data from server config.
bool is_secure
True if connection is using TLS (wss://)
void thread_pool_destroy(thread_pool_t *pool)
Destroy a thread pool.
thread_pool_t * thread_pool_create(int num_threads)
acip_transport_t * acip_tcp_transport_create(const char *name, socket_t sockfd, crypto_context_t *crypto_ctx)
Create TCP transport from existing socket.
void acip_transport_destroy(acip_transport_t *transport)
Destroy transport and free all resources.
⏱️ High-precision timing utilities using sokol_time.h and uthash
Transport abstraction layer for ACIP protocol.