ascii-chat 0.6.0
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
discovery.c
Go to the documentation of this file.
1
17#include "network/mdns/discovery_tui.h" // For discovery_tui_server_t struct only
19#include "platform/thread.h"
20#include "platform/mutex.h"
21#include "log/logging.h"
22#include "mdns.h"
23#include <string.h>
24#include <ctype.h>
25#include <stdlib.h>
26#include <time.h>
27
28// ============================================================================
29// Thread Coordination
30// ============================================================================
31
41
42// ============================================================================
43// Utility Functions
44// ============================================================================
45
46void pubkey_to_hex(const uint8_t pubkey[32], char hex_out[65]) {
47 if (!pubkey || !hex_out)
48 return;
49 for (int i = 0; i < 32; i++) {
50 sprintf(hex_out + (i * 2), "%02x", pubkey[i]);
51 }
52 hex_out[64] = '\0';
53}
54
55asciichat_error_t hex_to_pubkey(const char *hex_str, uint8_t pubkey_out[32]) {
56 if (!hex_str || !pubkey_out) {
57 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid hex_str or pubkey_out pointer");
59 }
60
61 if (strlen(hex_str) != 64) {
62 SET_ERRNO(ERROR_INVALID_PARAM, "Hex string must be exactly 64 characters");
64 }
65
66 for (int i = 0; i < 32; i++) {
67 char hex_byte[3];
68 hex_byte[0] = hex_str[i * 2];
69 hex_byte[1] = hex_str[i * 2 + 1];
70 hex_byte[2] = '\0';
71
72 if (!isxdigit(hex_byte[0]) || !isxdigit(hex_byte[1])) {
73 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid hex character in string");
75 }
76
77 pubkey_out[i] = (uint8_t)strtol(hex_byte, NULL, 16);
78 }
79
80 return ASCIICHAT_OK;
81}
82
83bool is_session_string(const char *str) {
84 if (!str || strlen(str) == 0)
85 return false;
86
87 // Parse as word-word-word where each word is 3-64 alphanumerics
88 // Hyphens are separators between words
89 // No leading/trailing hyphens, no consecutive hyphens
90 size_t len = strlen(str);
91
92 // Check length bounds (minimum: "a-a-a" = 5, maximum: 64 chars per word * 3 + 2 hyphens = 194)
93 if (len < 5 || len > 194) {
94 return false;
95 }
96
97 // Must not start or end with hyphen
98 if (str[0] == '-' || str[len - 1] == '-') {
99 return false;
100 }
101
102 // Count words separated by hyphens
103 int word_count = 1; // Start with 1 (not 0) to account for first word
104 int current_word_len = 0;
105 bool last_was_hyphen = false;
106
107 for (size_t i = 0; i < len; i++) {
108 char c = str[i];
109
110 if (isalnum(c)) {
111 current_word_len++;
112
113 // Individual word must be 3-64 characters
114 if (current_word_len > 64) {
115 return false;
116 }
117 last_was_hyphen = false;
118 } else if (c == '-') {
119 // Hyphen marks end of current word
120 if (last_was_hyphen) {
121 // Consecutive hyphens
122 return false;
123 }
124
125 // Current word must have been at least 3 chars
126 if (current_word_len < 3) {
127 return false;
128 }
129
130 word_count++;
131 current_word_len = 0;
132 last_was_hyphen = true;
133 } else {
134 // Invalid character
135 return false;
136 }
137 }
138
139 // Final word must be 3-64 characters
140 if (current_word_len < 3 || current_word_len > 64) {
141 return false;
142 }
143
144 // Must have exactly 3 words
145 return word_count == 3;
146}
147
148// ============================================================================
149// TXT Record Parsing
150// ============================================================================
151
164static asciichat_error_t parse_txt_records(const char *txt, char *session_string_out, char *host_pubkey_out) {
165 if (!txt || !session_string_out || !host_pubkey_out) {
166 return ERROR_INVALID_PARAM;
167 }
168
169 memset(session_string_out, 0, 49);
170 memset(host_pubkey_out, 0, 65);
171
172 const char *ptr = txt;
173 while (*ptr && ptr < txt + 512) {
174 // Skip whitespace
175 while (*ptr && isspace(*ptr))
176 ptr++;
177 if (!*ptr)
178 break;
179
180 // Parse key=value pair
181 const char *key_start = ptr;
182 while (*ptr && *ptr != '=' && !isspace(*ptr))
183 ptr++;
184 size_t key_len = ptr - key_start;
185
186 // Skip '='
187 if (*ptr != '=') {
188 ptr++;
189 continue;
190 }
191 ptr++;
192
193 // Parse value until whitespace or end
194 const char *value_start = ptr;
195 while (*ptr && !isspace(*ptr))
196 ptr++;
197 size_t value_len = ptr - value_start;
198
199 // Check key and extract value
200 if (key_len == strlen("session_string") && strncmp(key_start, "session_string", key_len) == 0) {
201 if (value_len < 49) {
202 strncpy(session_string_out, value_start, value_len);
203 session_string_out[value_len] = '\0';
204 }
205 } else if (key_len == strlen("host_pubkey") && strncmp(key_start, "host_pubkey", key_len) == 0) {
206 if (value_len == 64) {
207 strncpy(host_pubkey_out, value_start, 64);
208 host_pubkey_out[64] = '\0';
209 }
210 }
211
212 // Move to next pair
213 ptr++;
214 }
215
216 if (session_string_out[0] && host_pubkey_out[0]) {
217 return ASCIICHAT_OK;
218 }
219
220 return ERROR_NOT_FOUND;
221}
222
223// ============================================================================
224// mDNS Query Implementation (Core Module)
225// ============================================================================
226
238
242static int64_t discovery_get_time_ms(void) {
243 struct timespec ts;
244 clock_gettime(CLOCK_MONOTONIC, &ts);
245 return (int64_t)ts.tv_sec * 1000 + (int64_t)ts.tv_nsec / 1000000;
246}
247
252static void discovery_mdns_callback(const asciichat_mdns_discovery_t *discovery, void *user_data) {
253 mdns_query_state_t *state = (mdns_query_state_t *)user_data;
254 if (!state || !discovery) {
255 return;
256 }
257
258 // Check if we've exceeded capacity
259 if (state->count >= state->capacity) {
260 log_warn("mDNS: Reached maximum server capacity (%d)", state->capacity);
261 return;
262 }
263
264 // Check if timeout exceeded
265 int64_t elapsed = discovery_get_time_ms() - state->start_time_ms;
266 if (elapsed > state->timeout_ms) {
267 state->query_complete = true;
268 return;
269 }
270
271 // Only accept services of the right type
272 if (strstr(discovery->type, "_ascii-chat._tcp") == NULL) {
273 return;
274 }
275
276 // Check if we already have this server (avoid duplicates)
277 for (int i = 0; i < state->count; i++) {
278 if (strcmp(state->servers[i].name, discovery->name) == 0 && state->servers[i].port == discovery->port) {
279 // Update TTL if newer
280 if (discovery->ttl > state->servers[i].ttl) {
281 state->servers[i].ttl = discovery->ttl;
282 }
283 return; // Already have this server
284 }
285 }
286
287 // Add new server to our array
288 discovery_tui_server_t *server = &state->servers[state->count];
289 memset(server, 0, sizeof(discovery_tui_server_t));
290
291 // Copy service information
292 SAFE_STRNCPY(server->name, discovery->name, sizeof(server->name));
293 SAFE_STRNCPY(server->ipv4, discovery->ipv4, sizeof(server->ipv4));
294 SAFE_STRNCPY(server->ipv6, discovery->ipv6, sizeof(server->ipv6));
295 server->port = discovery->port;
296 server->ttl = discovery->ttl;
297
298 // Prefer IPv4 address as the primary address, fall back to hostname
299 if (discovery->ipv4[0] != '\0') {
300 SAFE_STRNCPY(server->address, discovery->ipv4, sizeof(server->address));
301 } else if (discovery->host[0] != '\0') {
302 SAFE_STRNCPY(server->address, discovery->host, sizeof(server->address));
303 } else if (discovery->ipv6[0] != '\0') {
304 SAFE_STRNCPY(server->address, discovery->ipv6, sizeof(server->address));
305 }
306
307 state->count++;
308 log_debug("mDNS: Found server '%s' at %s:%u", discovery->name, server->address, discovery->port);
309}
310
320discovery_tui_server_t *discovery_mdns_query(int timeout_ms, int max_servers, bool quiet, int *out_count) {
321 if (!out_count) {
322 SET_ERRNO(ERROR_INVALID_PARAM, "out_count pointer is NULL");
323 return NULL;
324 }
325
326 *out_count = 0;
327
328 // Apply defaults
329 if (timeout_ms <= 0) {
330 timeout_ms = 2000;
331 }
332 if (max_servers <= 0) {
333 max_servers = 20;
334 }
335
336 // Allocate state for collecting servers
337 mdns_query_state_t state;
338 memset(&state, 0, sizeof(state));
339 state.capacity = max_servers;
340 state.timeout_ms = timeout_ms;
341 state.start_time_ms = discovery_get_time_ms();
342
343 // Allocate server array
345 if (!state.servers) {
346 SET_ERRNO(ERROR_MEMORY, "Failed to allocate mDNS discovery server array");
347 return NULL;
348 }
349 memset(state.servers, 0, state.capacity * sizeof(discovery_tui_server_t));
350
351 if (!quiet) {
352 log_info("mDNS: Searching for ASCII-Chat servers on local network (timeout: %dms)", state.timeout_ms);
353 printf("🔍 Searching for ASCII-Chat servers on LAN...\n");
354 }
355
356 // Initialize mDNS
358 if (!mdns) {
359 log_warn("mDNS: Failed to initialize mDNS - discovery unavailable");
360 SAFE_FREE(state.servers);
361 return NULL;
362 }
363
364 // Start mDNS query for _ascii-chat._tcp services
365 asciichat_error_t query_result =
366 asciichat_mdns_query(mdns, "_ascii-chat._tcp.local", discovery_mdns_callback, &state);
367
368 if (query_result != ASCIICHAT_OK) {
369 log_info("mDNS: Query failed - no servers found via service discovery");
371 SAFE_FREE(state.servers);
372 return NULL;
373 }
374
375 // Poll for responses until timeout
376 int64_t deadline = state.start_time_ms + state.timeout_ms;
377 while (!state.query_complete && discovery_get_time_ms() < deadline) {
378 int poll_timeout = (int)(deadline - discovery_get_time_ms());
379 if (poll_timeout < 0) {
380 poll_timeout = 0;
381 }
382 if (poll_timeout > 100) {
383 poll_timeout = 100; // Check every 100ms
384 }
385 asciichat_mdns_update(mdns, poll_timeout);
386 }
387
388 // Cleanup mDNS
390
391 if (!quiet) {
392 if (state.count > 0) {
393 printf("✅ Found %d ASCII-Chat server%s on LAN\n", state.count, state.count == 1 ? "" : "s");
394 log_info("mDNS: Found %d server(s)", state.count);
395 } else {
396 printf("❌ No ASCII-Chat servers found on LAN\n");
397 log_info("mDNS: No servers found");
398 }
399 }
400
401 *out_count = state.count;
402 return state.servers;
403}
404
409 SAFE_FREE(servers);
410}
411
412// ============================================================================
413// mDNS Discovery Thread
414// ============================================================================
415
423
425static void *mdns_thread_fn(void *arg) {
427 if (!ctx)
428 return NULL;
429
430 // Call the public mDNS query function in this module
431 int discovered_count = 0;
432 discovery_tui_server_t *discovered_servers =
433 discovery_mdns_query((int)(ctx->timeout_ms > 0 ? ctx->timeout_ms : 2000), 20, true, &discovered_count);
434
435 if (!discovered_servers || discovered_count == 0) {
436 log_debug("mDNS: No servers found (this is normal if no servers are on LAN)");
437 if (discovered_servers) {
438 discovery_mdns_free(discovered_servers);
439 }
440 mutex_lock(&ctx->state->lock);
441 ctx->state->mdns_done = true;
442 cond_signal(&ctx->state->signal);
443 mutex_unlock(&ctx->state->lock);
444 SAFE_FREE(ctx);
445 return NULL;
446 }
447
448 // Search for server matching our session string
449 for (int i = 0; i < discovered_count; i++) {
450 discovery_tui_server_t *server = &discovered_servers[i];
451
452 if (strcmp(server->name, ctx->session_string) == 0) {
453 // Found a match!
454 mutex_lock(&ctx->state->lock);
455 {
456 if (!ctx->state->found) {
457 ctx->state->found = true;
458 ctx->state->result->success = true;
459 ctx->state->result->source = DISCOVERY_SOURCE_MDNS;
460 ctx->state->result->server_port = server->port;
461
462 const char *best_addr = (server->ipv4[0] != '\0') ? server->ipv4 : server->ipv6;
463 strncpy(ctx->state->result->server_address, best_addr, sizeof(ctx->state->result->server_address) - 1);
464 ctx->state->result->server_address[sizeof(ctx->state->result->server_address) - 1] = '\0';
465
466 strncpy(ctx->state->result->mdns_service_name, server->name,
467 sizeof(ctx->state->result->mdns_service_name) - 1);
468 ctx->state->result->mdns_service_name[sizeof(ctx->state->result->mdns_service_name) - 1] = '\0';
469
470 log_info("mDNS: Found session '%s' at %s:%d", ctx->session_string, ctx->state->result->server_address,
471 ctx->state->result->server_port);
472 cond_signal(&ctx->state->signal);
473 }
474 }
475 mutex_unlock(&ctx->state->lock);
476 break;
477 }
478 }
479
480 discovery_mdns_free(discovered_servers);
481
482 mutex_lock(&ctx->state->lock);
483 ctx->state->mdns_done = true;
484 cond_signal(&ctx->state->signal);
485 mutex_unlock(&ctx->state->lock);
486 SAFE_FREE(ctx);
487 return NULL;
488}
489
490// ============================================================================
491// ACDS Discovery Thread
492// ============================================================================
493
500
502static void *acds_thread_fn(void *arg) {
504 if (!ctx)
505 return NULL;
506
507 acds_client_t client;
508 memset(&client, 0, sizeof(client));
509
510 acds_client_config_t client_config;
511 memset(&client_config, 0, sizeof(client_config));
512
513 strncpy(client_config.server_address, ctx->config->acds_server, sizeof(client_config.server_address) - 1);
514 client_config.server_port = ctx->config->acds_port;
515 client_config.timeout_ms = ctx->config->acds_timeout_ms;
516
517 // Connect to ACDS server
518 asciichat_error_t conn_err = acds_client_connect(&client, &client_config);
519 if (conn_err != ASCIICHAT_OK) {
520 log_debug("ACDS: Failed to connect to %s:%d", ctx->config->acds_server, ctx->config->acds_port);
521 mutex_lock(&ctx->state->lock);
522 ctx->state->acds_done = true;
523 cond_signal(&ctx->state->signal);
524 mutex_unlock(&ctx->state->lock);
525 SAFE_FREE(ctx);
526 return NULL;
527 }
528
529 // Look up session
530 acds_session_lookup_result_t lookup_result;
531 memset(&lookup_result, 0, sizeof(lookup_result));
532
533 asciichat_error_t lookup_err = acds_session_lookup(&client, ctx->session_string, &lookup_result);
534 if (lookup_err != ASCIICHAT_OK) {
535 log_debug("ACDS: Session lookup failed for '%s'", ctx->session_string);
536 acds_client_disconnect(&client);
537 mutex_lock(&ctx->state->lock);
538 ctx->state->acds_done = true;
539 cond_signal(&ctx->state->signal);
540 mutex_unlock(&ctx->state->lock);
541 SAFE_FREE(ctx);
542 return NULL;
543 }
544
545 if (!lookup_result.found) {
546 log_debug("ACDS: Session '%s' not found", ctx->session_string);
547 acds_client_disconnect(&client);
548 mutex_lock(&ctx->state->lock);
549 ctx->state->acds_done = true;
550 cond_signal(&ctx->state->signal);
551 mutex_unlock(&ctx->state->lock);
552 SAFE_FREE(ctx);
553 return NULL;
554 }
555
556 // Verify pubkey if provided
557 if (ctx->config->expected_pubkey) {
558 if (memcmp(lookup_result.host_pubkey, ctx->config->expected_pubkey, 32) != 0) {
559 log_warn("ACDS: Session found but pubkey mismatch (MITM?)");
560 acds_client_disconnect(&client);
561 mutex_lock(&ctx->state->lock);
562 ctx->state->acds_done = true;
563 cond_signal(&ctx->state->signal);
564 mutex_unlock(&ctx->state->lock);
565 SAFE_FREE(ctx);
566 return NULL;
567 }
568 }
569
570 // Join session to get server connection details
571 acds_session_join_params_t join_params;
572 memset(&join_params, 0, sizeof(join_params));
573 join_params.session_string = ctx->session_string;
574
575 if (ctx->config->client_pubkey) {
576 memcpy(join_params.identity_pubkey, ctx->config->client_pubkey, 32);
577 }
578 if (ctx->config->client_seckey) {
579 memcpy(join_params.identity_seckey, ctx->config->client_seckey, 64);
580 }
581 if (ctx->config->password) {
582 join_params.has_password = true;
583 strncpy(join_params.password, ctx->config->password, sizeof(join_params.password) - 1);
584 }
585
586 acds_session_join_result_t join_result;
587 memset(&join_result, 0, sizeof(join_result));
588
589 asciichat_error_t join_err = acds_session_join(&client, &join_params, &join_result);
590 acds_client_disconnect(&client);
591
592 if (join_err != ASCIICHAT_OK || !join_result.success) {
593 log_debug("ACDS: Session join failed: %s", join_result.error_message);
594 mutex_lock(&ctx->state->lock);
595 ctx->state->acds_done = true;
596 cond_signal(&ctx->state->signal);
597 mutex_unlock(&ctx->state->lock);
598 SAFE_FREE(ctx);
599 return NULL;
600 }
601
602 // Found it! Populate result
603 mutex_lock(&ctx->state->lock);
604 {
605 if (!ctx->state->found) {
606 ctx->state->found = true;
607 ctx->state->result->success = true;
608 ctx->state->result->source = DISCOVERY_SOURCE_ACDS;
609
610 memcpy(ctx->state->result->host_pubkey, lookup_result.host_pubkey, 32);
611 memcpy(ctx->state->result->session_id, join_result.session_id, 16);
612 memcpy(ctx->state->result->participant_id, join_result.participant_id, 16);
613
614 strncpy(ctx->state->result->server_address, join_result.server_address,
615 sizeof(ctx->state->result->server_address) - 1);
616 ctx->state->result->server_port = join_result.server_port;
617
618 log_info("ACDS: Found session '%s' at %s:%d", ctx->session_string, ctx->state->result->server_address,
619 ctx->state->result->server_port);
620 cond_signal(&ctx->state->signal);
621 }
622 }
623 mutex_unlock(&ctx->state->lock);
624
625 SAFE_FREE(ctx);
626 return NULL;
627}
628
629// ============================================================================
630// Main Discovery Function
631// ============================================================================
632
634 if (!config)
635 return;
636
637 memset(config, 0, sizeof(*config));
638
639 // Check if debug or release build
640#ifdef NDEBUG
641 // Release: use internet ACDS
642 strncpy(config->acds_server, "discovery.ascii-chat.com", sizeof(config->acds_server) - 1);
643#else
644 // Debug: use local ACDS
645 strncpy(config->acds_server, "127.0.0.1", sizeof(config->acds_server) - 1);
646#endif
647
648 config->acds_port = 27225;
649 config->mdns_timeout_ms = 2000;
650 config->acds_timeout_ms = 5000;
651 config->insecure_mode = false;
652 config->expected_pubkey = NULL;
653}
654
655asciichat_error_t discover_session_parallel(const char *session_string, const discovery_config_t *config,
656 discovery_result_t *result) {
657 if (!session_string || !config || !result) {
658 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters to discover_session_parallel");
659 return ERROR_INVALID_PARAM;
660 }
661
662 // Validate session string format
663 if (!is_session_string(session_string)) {
664 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid session string format");
665 return ERROR_INVALID_PARAM;
666 }
667
668 memset(result, 0, sizeof(*result));
669 log_info("Discovery: Looking up session '%s'", session_string);
670
671 // Initialize thread state
673 memset(&state, 0, sizeof(state));
674 state.result = result;
675 mutex_init(&state.lock);
676 cond_init(&state.signal);
677
678 // Determine which discovery methods to use
679 bool use_mdns = true;
680 bool use_acds = config->expected_pubkey != NULL || config->insecure_mode;
681
682 if (!use_acds) {
683 log_debug("Discovery: mDNS-only mode (no --server-key and no --acds-insecure)");
684 }
685
686 // Spawn mDNS thread
687 asciichat_thread_t mdns_thread;
688 asciichat_thread_init(&mdns_thread);
689
690 if (use_mdns) {
691 mdns_thread_context_t *mdns_ctx = SAFE_MALLOC(sizeof(*mdns_ctx), mdns_thread_context_t *);
692 if (!mdns_ctx) {
693 SET_ERRNO(ERROR_MEMORY, "Failed to allocate mDNS context");
694 mutex_destroy(&state.lock);
695 cond_destroy(&state.signal);
696 return ERROR_MEMORY;
697 }
698
699 mdns_ctx->session_string = session_string;
700 mdns_ctx->state = &state;
701 mdns_ctx->expected_pubkey = config->expected_pubkey;
702
703 int thread_err = asciichat_thread_create(&mdns_thread, mdns_thread_fn, mdns_ctx);
704 if (thread_err != 0) {
705 log_warn("Discovery: Failed to spawn mDNS thread");
706 SAFE_FREE(mdns_ctx);
707 use_mdns = false;
708 }
709 }
710
711 // Spawn ACDS thread
712 asciichat_thread_t acds_thread;
713 asciichat_thread_init(&acds_thread);
714
715 if (use_acds) {
716 acds_thread_context_t *acds_ctx = SAFE_MALLOC(sizeof(*acds_ctx), acds_thread_context_t *);
717 if (!acds_ctx) {
718 SET_ERRNO(ERROR_MEMORY, "Failed to allocate ACDS context");
719 if (use_mdns && asciichat_thread_is_initialized(&mdns_thread)) {
720 asciichat_thread_join(&mdns_thread, NULL);
721 }
722 mutex_destroy(&state.lock);
723 cond_destroy(&state.signal);
724 return ERROR_MEMORY;
725 }
726
727 acds_ctx->session_string = session_string;
728 acds_ctx->state = &state;
729 acds_ctx->config = config;
730
731 int thread_err = asciichat_thread_create(&acds_thread, acds_thread_fn, acds_ctx);
732 if (thread_err != 0) {
733 log_warn("Discovery: Failed to spawn ACDS thread");
734 SAFE_FREE(acds_ctx);
735 use_acds = false;
736 }
737 }
738
739 // Wait for result (mDNS timeout 2s + ACDS timeout 5s max)
740 uint32_t wait_timeout_ms = config->acds_timeout_ms + 1000;
741 mutex_lock(&state.lock);
742 {
743 uint32_t elapsed_ms = 0;
744 while (!state.found && elapsed_ms < wait_timeout_ms) {
745 // Check if both threads are done
746 if (state.mdns_done && state.acds_done) {
747 break;
748 }
749
750 // Wait with timeout
751 cond_timedwait(&state.signal, &state.lock, 500);
752 elapsed_ms += 500;
753 }
754 }
755 mutex_unlock(&state.lock);
756
757 // Join threads
758 if (use_mdns && asciichat_thread_is_initialized(&mdns_thread)) {
759 asciichat_thread_join(&mdns_thread, NULL);
760 }
761 if (use_acds && asciichat_thread_is_initialized(&acds_thread)) {
762 asciichat_thread_join(&acds_thread, NULL);
763 }
764
765 // Cleanup
766 mutex_destroy(&state.lock);
767 cond_destroy(&state.signal);
768
769 // Check result
770 if (!result->success) {
771 SET_ERRNO(ERROR_NOT_FOUND, "Session '%s' not found (mDNS/ACDS timeout)", session_string);
772 return ERROR_NOT_FOUND;
773 }
774
775 log_info("Discovery: Session '%s' discovered via %s", session_string,
776 result->source == DISCOVERY_SOURCE_MDNS ? "mDNS" : "ACDS");
777
778 return ASCIICHAT_OK;
779}
asciichat_error_t acds_session_lookup(acds_client_t *client, const char *session_string, acds_session_lookup_result_t *result)
Look up session by string.
asciichat_error_t acds_session_join(acds_client_t *client, const acds_session_join_params_t *params, acds_session_join_result_t *result)
Join an existing session.
asciichat_error_t acds_client_connect(acds_client_t *client, const acds_client_config_t *config)
Connect to ACDS server.
Definition acds_client.c:53
void acds_client_disconnect(acds_client_t *client)
Disconnect from ACDS server.
void pubkey_to_hex(const uint8_t pubkey[32], char hex_out[65])
Convert Ed25519 public key to hex string.
Definition discovery.c:46
asciichat_error_t hex_to_pubkey(const char *hex_str, uint8_t pubkey_out[32])
Convert hex string to Ed25519 public key.
Definition discovery.c:55
bool is_session_string(const char *str)
Check if a string matches session string pattern.
Definition discovery.c:83
void discovery_mdns_free(discovery_tui_server_t *servers)
Free memory from mDNS discovery results.
Definition discovery.c:408
void discovery_config_init_defaults(discovery_config_t *config)
Initialize discovery config with defaults.
Definition discovery.c:633
discovery_tui_server_t * discovery_mdns_query(int timeout_ms, int max_servers, bool quiet, int *out_count)
Public mDNS query function used by both parallel discovery and TUI wrapper.
Definition discovery.c:320
asciichat_error_t discover_session_parallel(const char *session_string, const discovery_config_t *config, discovery_result_t *result)
Look up session in parallel on mDNS and ACDS.
Definition discovery.c:655
Parallel mDNS and ACDS session discovery.
TUI-based service discovery for ascii-chat client.
unsigned int uint32_t
Definition common.h:58
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:358
#define SAFE_FREE(ptr)
Definition common.h:320
#define SAFE_MALLOC(size, cast)
Definition common.h:208
unsigned char uint8_t
Definition common.h:56
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:46
@ ERROR_NOT_FOUND
@ ERROR_MEMORY
Definition error_codes.h:53
@ ASCIICHAT_OK
Definition error_codes.h:48
@ ERROR_INVALID_PARAM
#define log_warn(...)
Log a WARN message.
#define log_info(...)
Log an INFO message.
#define log_debug(...)
Log a DEBUG message.
int mutex_init(mutex_t *mutex)
Initialize a mutex.
int cond_init(cond_t *cond)
Initialize a condition variable.
int cond_timedwait(cond_t *cond, mutex_t *mutex, int timeout_ms)
Wait on a condition variable with timeout.
void asciichat_thread_init(asciichat_thread_t *thread)
Initialize a thread handle to an uninitialized state.
#define mutex_lock(mutex)
Lock a mutex (with debug tracking in debug builds)
Definition mutex.h:140
pthread_mutex_t mutex_t
Mutex type (POSIX: pthread_mutex_t)
Definition mutex.h:38
int cond_signal(cond_t *cond)
Signal a condition variable (wake one waiting thread)
bool asciichat_thread_is_initialized(asciichat_thread_t *thread)
Check if a thread handle has been initialized.
pthread_cond_t cond_t
Condition variable type (POSIX: pthread_cond_t)
Definition cond.h:38
int asciichat_thread_join(asciichat_thread_t *thread, void **retval)
Wait for a thread to complete (blocking)
#define mutex_unlock(mutex)
Unlock a mutex (with debug tracking in debug builds)
Definition mutex.h:175
pthread_t asciichat_thread_t
Thread handle type (POSIX: pthread_t)
int asciichat_thread_create(asciichat_thread_t *thread, void *(*func)(void *), void *arg)
Create a new thread.
int cond_destroy(cond_t *cond)
Destroy a condition variable.
int mutex_destroy(mutex_t *mutex)
Destroy a mutex.
📝 Logging API with multiple log levels and terminal output control
asciichat_error_t asciichat_mdns_update(asciichat_mdns_t *mdns, int timeout_ms)
Process pending mDNS events (must be called regularly)
Definition mdns.c:255
void asciichat_mdns_shutdown(asciichat_mdns_t *mdns)
Shutdown mDNS context and cleanup.
Definition mdns.c:61
asciichat_mdns_t * asciichat_mdns_init(void)
Initialize mDNS context.
Definition mdns.c:30
asciichat_error_t asciichat_mdns_query(asciichat_mdns_t *mdns, const char *service_type, asciichat_mdns_discovery_callback_fn callback, void *user_data)
Query for services on the local network.
Definition mdns.c:219
Cross-platform mutex interface for ascii-chat.
🧵 Cross-platform thread interface for ascii-chat
ACDS client connection configuration.
Definition acds_client.h:44
char server_address[256]
ACDS server address (e.g., "discovery.ascii.chat" or "127.0.0.1")
Definition acds_client.h:45
uint32_t timeout_ms
Connection timeout in milliseconds.
Definition acds_client.h:47
uint16_t server_port
ACDS server port (default: 27225)
Definition acds_client.h:46
ACDS client connection handle.
Definition acds_client.h:53
Session join parameters.
uint8_t identity_pubkey[32]
Participant's Ed25519 public key.
bool has_password
Password provided.
char password[128]
Password (if has_password)
const char * session_string
Session to join.
uint8_t identity_seckey[64]
Ed25519 secret key (for signing)
Session join result.
char error_message[129]
Error message (if !success, null-terminated)
uint8_t participant_id[16]
Participant UUID (if success)
bool success
Join succeeded.
char server_address[65]
Server IP/hostname (if success, null-terminated)
uint16_t server_port
Server port (if success)
uint8_t session_id[16]
Session UUID (if success)
Session lookup result.
bool found
Session exists.
uint8_t host_pubkey[32]
Host's Ed25519 public key.
discovery_thread_state_t * state
Definition discovery.c:497
const discovery_config_t * config
Definition discovery.c:498
const char * session_string
Definition discovery.c:496
Discovered service information.
Definition mdns.h:82
Internal mDNS context structure.
Definition mdns.c:18
Session discovery configuration.
Definition discovery.h:111
const uint8_t * client_seckey
Client's Ed25519 secret key (can be NULL)
Definition discovery.h:126
uint32_t acds_timeout_ms
ACDS lookup timeout (default: 5000ms)
Definition discovery.h:122
uint32_t mdns_timeout_ms
mDNS search timeout (default: 2000ms)
Definition discovery.h:121
const char * password
Optional session password (NULL if none)
Definition discovery.h:127
uint16_t acds_port
ACDS server port (default: 27225)
Definition discovery.h:118
char acds_server[256]
ACDS server address (e.g., "localhost" or "acds.ascii-chat.com")
Definition discovery.h:117
bool insecure_mode
Allow no verification (–acds-insecure flag)
Definition discovery.h:114
const uint8_t * expected_pubkey
Expected server pubkey (NULL = no verification)
Definition discovery.h:113
const uint8_t * client_pubkey
Client's Ed25519 public key (can be NULL)
Definition discovery.h:125
Result from session discovery.
Definition discovery.h:85
char mdns_service_name[256]
mDNS service instance name (e.g., "swift-river-mountain")
Definition discovery.h:96
uint16_t server_port
Server port (typically 27224)
Definition discovery.h:89
uint8_t host_pubkey[32]
Ed25519 public key of discovered server.
Definition discovery.h:87
uint8_t participant_id[16]
Assigned participant ID (from SESSION_JOIN)
Definition discovery.h:101
uint8_t session_id[16]
ACDS session UUID.
Definition discovery.h:100
char server_address[256]
Server IP or hostname.
Definition discovery.h:88
bool success
Discovery succeeded.
Definition discovery.h:86
enum discovery_result_t::@3 source
Which discovery method found the server.
discovery_result_t * result
Definition discovery.c:36
Discovered server information from mDNS.
char name[256]
Service instance name (e.g., "swift-river-canyon")
uint16_t port
Server port number.
char ipv6[46]
IPv6 address (if available)
uint32_t ttl
TTL remaining (seconds)
char ipv4[16]
IPv4 address (if available)
char address[256]
Server address (IPv4, IPv6, or hostname)
Internal state for collecting discovered services.
Definition discovery.c:230
int count
Number of servers discovered so far.
Definition discovery.c:232
int64_t start_time_ms
When discovery started (for timeout)
Definition discovery.c:234
bool query_complete
Set when discovery completes.
Definition discovery.c:236
int capacity
Allocated capacity.
Definition discovery.c:233
int timeout_ms
Discovery timeout in milliseconds.
Definition discovery.c:235
discovery_tui_server_t * servers
Array of discovered servers.
Definition discovery.c:231
discovery_thread_state_t * state
Definition discovery.c:419
const uint8_t * expected_pubkey
Definition discovery.c:420
const char * session_string
Definition discovery.c:418
⏱️ High-precision timing utilities using sokol_time.h and uthash