ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
discovery/database.c
Go to the documentation of this file.
1
12#include <ascii-chat/log/log.h>
15#include <string.h>
16#include <time.h>
17#include <inttypes.h>
18#include <sodium.h>
19
20// ============================================================================
21// SQL Query Constants (Defined Once - Compile-time string concatenation)
22// ============================================================================
23
24// Base SELECT for all session fields
25#define SELECT_SESSION_BASE \
26 "SELECT session_string, session_id, host_pubkey, password_hash, " \
27 "max_participants, current_participants, capabilities, has_password, " \
28 "expose_ip_publicly, session_type, server_address, server_port, " \
29 "created_at, expires_at, last_activity_at, initiator_id, host_established, " \
30 "host_participant_id, host_address, host_port, host_connection_type, " \
31 "in_migration, migration_start_ns FROM sessions"
32
33// ============================================================================
34// SQL schema for creating tables
35// ============================================================================
36
37static const char *schema_sql =
38 // Sessions table (session_string is primary key for uniqueness guarantee)
39 "CREATE TABLE IF NOT EXISTS sessions ("
40 " session_string TEXT PRIMARY KEY," // Prevents duplicate session strings at DB level
41 " session_id BLOB UNIQUE NOT NULL," // Kept for compatibility, still unique
42 " host_pubkey BLOB NOT NULL,"
43 " password_hash TEXT,"
44 " max_participants INTEGER DEFAULT 4,"
45 " current_participants INTEGER DEFAULT 0,"
46 " capabilities INTEGER DEFAULT 3," // video + audio
47 " has_password INTEGER DEFAULT 0,"
48 " expose_ip_publicly INTEGER DEFAULT 0,"
49 " session_type INTEGER DEFAULT 0,"
50 " server_address TEXT,"
51 " server_port INTEGER DEFAULT 0,"
52 " created_at INTEGER NOT NULL,"
53 " expires_at INTEGER NOT NULL,"
54 " last_activity_at INTEGER NOT NULL,"
55 // Discovery mode host negotiation fields
56 " initiator_id BLOB," // First participant (for tiebreaker)
57 " host_established INTEGER DEFAULT 0," // 0=negotiating, 1=host designated
58 " host_participant_id BLOB," // Current host's participant_id
59 " host_address TEXT," // Host's reachable address
60 " host_port INTEGER DEFAULT 0," // Host's port
61 " host_connection_type INTEGER DEFAULT 0," // How to reach host
62 // Host migration state
63 " in_migration INTEGER DEFAULT 0," // 0=not migrating, 1=collecting HOST_LOST packets
64 " migration_start_ns INTEGER DEFAULT 0" // When migration started in nanoseconds
65 ");"
66
67 // Participants table
68 "CREATE TABLE IF NOT EXISTS participants ("
69 " participant_id BLOB PRIMARY KEY,"
70 " session_string TEXT NOT NULL,"
71 " identity_pubkey BLOB NOT NULL,"
72 " joined_at INTEGER NOT NULL,"
73 " FOREIGN KEY (session_string) REFERENCES sessions(session_string) ON DELETE CASCADE"
74 ");"
75
76 // Session keys table (multi-key support with versioning)
77 "CREATE TABLE IF NOT EXISTS session_keys ("
78 " id INTEGER PRIMARY KEY AUTOINCREMENT,"
79 " session_string TEXT NOT NULL,"
80 " identity_pubkey BLOB NOT NULL," // Ed25519 public key (32 bytes)
81 " key_version INTEGER NOT NULL," // Versioning for key rotation
82 " added_at INTEGER NOT NULL," // When key was added (Unix ms)
83 " revoked INTEGER DEFAULT 0," // 0=active, 1=revoked
84 " FOREIGN KEY (session_string) REFERENCES sessions(session_string) ON DELETE CASCADE,"
85 " UNIQUE(session_string, identity_pubkey)" // Prevent duplicate keys per session
86 ");"
87
88 // Rate limiting events
89 "CREATE TABLE IF NOT EXISTS rate_events ("
90 " id INTEGER PRIMARY KEY AUTOINCREMENT,"
91 " ip_address TEXT NOT NULL,"
92 " event_type TEXT NOT NULL,"
93 " timestamp INTEGER NOT NULL"
94 ");"
95
96 // Indexes for efficient queries
97 "CREATE INDEX IF NOT EXISTS idx_sessions_id ON sessions(session_id);"
98 "CREATE INDEX IF NOT EXISTS idx_sessions_activity ON sessions(last_activity_at);"
99 "CREATE INDEX IF NOT EXISTS idx_participants_session ON participants(session_string);"
100 "CREATE INDEX IF NOT EXISTS idx_session_keys_session ON session_keys(session_string);"
101 "CREATE INDEX IF NOT EXISTS idx_session_keys_active ON session_keys(session_string, revoked);"
102 "CREATE INDEX IF NOT EXISTS idx_rate_events ON rate_events(ip_address, event_type, timestamp);";
103
104// ============================================================================
105// Helper Functions
106// ============================================================================
107
111static void generate_uuid(uint8_t uuid_out[16]) {
112 randombytes_buf(uuid_out, 16);
113 uuid_out[6] = (uuid_out[6] & 0x0F) | 0x40; // Version 4
114 uuid_out[8] = (uuid_out[8] & 0x3F) | 0x80; // RFC4122 variant
115}
116
120static uint64_t database_get_current_time_ms(void) {
121 uint64_t current_time_ns = time_get_realtime_ns();
122 return time_ns_to_ms(current_time_ns);
123}
124
128static bool verify_password(const char *password, const char *hash) {
129 return crypto_pwhash_str_verify(hash, password, strlen(password)) == 0;
130}
131
138static session_entry_t *load_session_from_row(sqlite3_stmt *stmt) {
140 if (!session) {
141 return NULL;
142 }
143 memset(session, 0, sizeof(*session));
144
145 // Column order from SELECT statements must match:
146 // 0: session_string, 1: session_id, 2: host_pubkey, 3: password_hash,
147 // 4: max_participants, 5: current_participants, 6: capabilities,
148 // 7: has_password, 8: expose_ip_publicly, 9: session_type,
149 // 10: server_address, 11: server_port, 12: created_at, 13: expires_at,
150 // 14: last_activity_at, 15: initiator_id, 16: host_established, 17: host_participant_id,
151 // 18: host_address, 19: host_port, 20: host_connection_type,
152 // 21: in_migration, 22: migration_start_ns
153
154 const char *str = (const char *)sqlite3_column_text(stmt, 0);
155 if (str) {
156 SAFE_STRNCPY(session->session_string, str, sizeof(session->session_string));
157 }
158
159 const void *blob = sqlite3_column_blob(stmt, 1);
160 if (blob) {
161 memcpy(session->session_id, blob, 16);
162 }
163
164 blob = sqlite3_column_blob(stmt, 2);
165 if (blob) {
166 memcpy(session->host_pubkey, blob, 32);
167 }
168
169 str = (const char *)sqlite3_column_text(stmt, 3);
170 if (str) {
171 SAFE_STRNCPY(session->password_hash, str, sizeof(session->password_hash));
172 }
173
174 session->max_participants = (uint8_t)sqlite3_column_int(stmt, 4);
175 session->current_participants = (uint8_t)sqlite3_column_int(stmt, 5);
176 session->capabilities = (uint8_t)sqlite3_column_int(stmt, 6);
177 session->has_password = sqlite3_column_int(stmt, 7) != 0;
178 session->expose_ip_publicly = sqlite3_column_int(stmt, 8) != 0;
179 session->session_type = (uint8_t)sqlite3_column_int(stmt, 9);
180
181 str = (const char *)sqlite3_column_text(stmt, 10);
182 if (str) {
183 SAFE_STRNCPY(session->server_address, str, sizeof(session->server_address));
184 }
185
186 session->server_port = (uint16_t)sqlite3_column_int(stmt, 11);
187 session->created_at = (uint64_t)sqlite3_column_int64(stmt, 12);
188 session->expires_at = (uint64_t)sqlite3_column_int64(stmt, 13);
189 session->last_activity_at = (uint64_t)sqlite3_column_int64(stmt, 14);
190
191 // Discovery mode host negotiation fields
192 blob = sqlite3_column_blob(stmt, 15);
193 if (blob) {
194 memcpy(session->initiator_id, blob, 16);
195 }
196
197 session->host_established = sqlite3_column_int(stmt, 16) != 0;
198
199 blob = sqlite3_column_blob(stmt, 17);
200 if (blob) {
201 memcpy(session->host_participant_id, blob, 16);
202 }
203
204 str = (const char *)sqlite3_column_text(stmt, 18);
205 if (str) {
206 SAFE_STRNCPY(session->host_address, str, sizeof(session->host_address));
207 }
208
209 session->host_port = (uint16_t)sqlite3_column_int(stmt, 19);
210 session->host_connection_type = (uint8_t)sqlite3_column_int(stmt, 20);
211
212 // Host migration state
213 session->in_migration = sqlite3_column_int(stmt, 21) != 0;
214 session->migration_start_ns = (uint64_t)sqlite3_column_int64(stmt, 22);
215
216 return session;
217}
218
222static void load_session_participants(sqlite3 *db, session_entry_t *session) {
223 static const char *sql = "SELECT participant_id, identity_pubkey, joined_at "
224 "FROM participants WHERE session_string = ?";
225
226 sqlite3_stmt *stmt = NULL;
227 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
228 return;
229 }
230
231 sqlite3_bind_text(stmt, 1, session->session_string, -1, SQLITE_STATIC);
232
233 size_t idx = 0;
234 while (sqlite3_step(stmt) == SQLITE_ROW && idx < MAX_PARTICIPANTS) {
236 if (!p) {
237 break;
238 }
239 memset(p, 0, sizeof(*p));
240
241 const void *blob = sqlite3_column_blob(stmt, 0);
242 if (blob) {
243 memcpy(p->participant_id, blob, 16);
244 }
245
246 blob = sqlite3_column_blob(stmt, 1);
247 if (blob) {
248 memcpy(p->identity_pubkey, blob, 32);
249 }
250
251 p->joined_at = (uint64_t)sqlite3_column_int64(stmt, 2);
252
253 session->participants[idx++] = p;
254 }
255
256 sqlite3_finalize(stmt);
257}
258
259// ============================================================================
260// Database Lifecycle
261// ============================================================================
262
263asciichat_error_t database_init(const char *db_path, sqlite3 **db) {
264 if (!db_path || !db) {
265 return SET_ERRNO(ERROR_INVALID_PARAM, "db_path or db is NULL");
266 }
267
268 log_info("Opening database: %s", db_path);
269
270 int rc = sqlite3_open(db_path, db);
271 if (rc != SQLITE_OK) {
272 const char *err = sqlite3_errmsg(*db);
273 sqlite3_close(*db);
274 *db = NULL;
275 return SET_ERRNO(ERROR_CONFIG, "Failed to open database: %s", err);
276 }
277
278 // Enable Write-Ahead Logging for concurrent reads
279 char *err_msg = NULL;
280 rc = sqlite3_exec(*db, "PRAGMA journal_mode=WAL;", NULL, NULL, &err_msg);
281 if (rc != SQLITE_OK) {
282 log_warn("Failed to enable WAL mode: %s", err_msg ? err_msg : "unknown error");
283 sqlite3_free(err_msg);
284 }
285
286 // Enable foreign key constraints
287 rc = sqlite3_exec(*db, "PRAGMA foreign_keys=ON;", NULL, NULL, &err_msg);
288 if (rc != SQLITE_OK) {
289 log_error("Failed to enable foreign keys: %s", err_msg ? err_msg : "unknown error");
290 sqlite3_free(err_msg);
291 sqlite3_close(*db);
292 *db = NULL;
293 return SET_ERRNO(ERROR_CONFIG, "Failed to enable foreign keys");
294 }
295
296 // Create schema
297 rc = sqlite3_exec(*db, schema_sql, NULL, NULL, &err_msg);
298 if (rc != SQLITE_OK) {
299 log_error("Failed to create schema: %s", err_msg ? err_msg : "unknown error");
300 sqlite3_free(err_msg);
301 sqlite3_close(*db);
302 *db = NULL;
303 return SET_ERRNO(ERROR_CONFIG, "Failed to create database schema");
304 }
305
306 log_info("Database initialized successfully (SQLite as single source of truth)");
307 return ASCIICHAT_OK;
308}
309
310void database_close(sqlite3 *db) {
311 if (!db) {
312 return;
313 }
314 sqlite3_close(db);
315 log_debug("Database closed");
316}
317
318// ============================================================================
319// Session Operations
320// ============================================================================
321
324 if (!db || !req || !config || !resp) {
325 return SET_ERRNO(ERROR_INVALID_PARAM, "db, req, config, or resp is NULL");
326 }
327
328 memset(resp, 0, sizeof(*resp));
329
330 // Generate or use reserved session string
331 char session_string[ACIP_MAX_SESSION_STRING_LEN] = {0};
332 if (req->reserved_string_len > 0) {
333 const char *reserved_str = (const char *)(req + 1);
334 size_t len = req->reserved_string_len < (ACIP_MAX_SESSION_STRING_LEN - 1) ? req->reserved_string_len
336 memcpy(session_string, reserved_str, len);
337 session_string[len] = '\0';
338
339 if (!is_session_string(session_string)) {
341 if (HAS_ERRNO(&ctx)) {
342 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid session string: %s (%s)", session_string, ctx.context_message);
343 }
344 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid session string: %s", session_string);
345 }
346 } else {
347 asciichat_error_t result = acds_string_generate(session_string, sizeof(session_string));
348 if (result != ASCIICHAT_OK) {
349 return result;
350 }
351 }
352
353 // Generate session ID
355 generate_uuid(session_id);
356
357 // Set timestamps
358 uint64_t now = database_get_current_time_ms();
359 uint64_t expires_at = now + ACIP_SESSION_EXPIRATION_MS;
360
361 // Calculate max_participants
362 uint8_t max_participants =
363 req->max_participants > 0 && req->max_participants <= MAX_PARTICIPANTS ? req->max_participants : MAX_PARTICIPANTS;
364
365 // Insert into database
366 const char *sql = "INSERT INTO sessions "
367 "(session_string, session_id, host_pubkey, password_hash, max_participants, "
368 "current_participants, capabilities, has_password, expose_ip_publicly, "
369 "session_type, server_address, server_port, created_at, expires_at, last_activity_at) "
370 "VALUES (?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
371
372 sqlite3_stmt *stmt = NULL;
373 int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
374 if (rc != SQLITE_OK) {
375 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare session insert: %s", sqlite3_errmsg(db));
376 }
377
378 log_info("DATABASE_SESSION_CREATE: expose_ip_publicly=%d, server_address='%s' server_port=%u, session_type=%u, "
379 "has_password=%u",
380 req->expose_ip_publicly, req->server_address, req->server_port, req->session_type, req->has_password);
381 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
382 sqlite3_bind_blob(stmt, 2, session_id, 16, SQLITE_STATIC);
383 sqlite3_bind_blob(stmt, 3, req->identity_pubkey, 32, SQLITE_STATIC);
384 sqlite3_bind_text(stmt, 4, req->has_password ? (const char *)req->password_hash : NULL, -1, SQLITE_STATIC);
385 sqlite3_bind_int(stmt, 5, max_participants);
386 sqlite3_bind_int(stmt, 6, req->capabilities);
387 sqlite3_bind_int(stmt, 7, req->has_password ? 1 : 0);
388 sqlite3_bind_int(stmt, 8, req->expose_ip_publicly ? 1 : 0);
389 sqlite3_bind_int(stmt, 9, req->session_type);
390 sqlite3_bind_text(stmt, 10, req->server_address, -1, SQLITE_STATIC);
391 sqlite3_bind_int(stmt, 11, req->server_port);
392 sqlite3_bind_int64(stmt, 12, (sqlite3_int64)now);
393 sqlite3_bind_int64(stmt, 13, (sqlite3_int64)expires_at);
394 sqlite3_bind_int64(stmt, 14, (sqlite3_int64)now); // last_activity_at = created_at
395
396 rc = sqlite3_step(stmt);
397 sqlite3_finalize(stmt);
398
399 if (rc != SQLITE_DONE) {
400 if (rc == SQLITE_CONSTRAINT) {
401 return SET_ERRNO(ERROR_INVALID_STATE, "Session string already exists: %s", session_string);
402 }
403 return SET_ERRNO(ERROR_CONFIG, "Failed to insert session: %s", sqlite3_errmsg(db));
404 }
405
406 // Fill response
407 resp->session_string_len = (uint8_t)strlen(session_string);
408 SAFE_STRNCPY(resp->session_string, session_string, sizeof(resp->session_string));
409 memcpy(resp->session_id, session_id, 16);
410 resp->expires_at = expires_at;
411 resp->stun_count = config->stun_count;
412 resp->turn_count = config->turn_count;
413
414 log_info("Session created: %s (session_id=%02x%02x%02x%02x..., max_participants=%d, has_password=%d)", session_string,
415 session_id[0], session_id[1], session_id[2], session_id[3], max_participants, req->has_password ? 1 : 0);
416
417 return ASCIICHAT_OK;
418}
419
420asciichat_error_t database_session_lookup(sqlite3 *db, const char *session_string, const acds_config_t *config,
421 acip_session_info_t *resp) {
422 if (!db || !session_string || !config || !resp) {
423 return SET_ERRNO(ERROR_INVALID_PARAM, "db, session_string, config, or resp is NULL");
424 }
425
426 memset(resp, 0, sizeof(*resp));
427
428 const char *sql = "SELECT session_string, session_id, host_pubkey, password_hash, "
429 "max_participants, current_participants, capabilities, has_password, "
430 "expose_ip_publicly, session_type, server_address, server_port, "
431 "created_at, expires_at, last_activity_at FROM sessions WHERE session_string = ?";
432
433 sqlite3_stmt *stmt = NULL;
434 int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
435 if (rc != SQLITE_OK) {
436 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare session lookup: %s", sqlite3_errmsg(db));
437 }
438
439 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
440
441 rc = sqlite3_step(stmt);
442 if (rc != SQLITE_ROW) {
443 sqlite3_finalize(stmt);
444 resp->found = 0;
445 log_debug("Session lookup failed: %s (not found)", session_string);
446 return ASCIICHAT_OK;
447 }
448
449 // Load session data
450 resp->found = 1;
451
452 const void *blob = sqlite3_column_blob(stmt, 1);
453 if (blob) {
454 memcpy(resp->session_id, blob, 16);
455 }
456
457 blob = sqlite3_column_blob(stmt, 2);
458 if (blob) {
459 memcpy(resp->host_pubkey, blob, 32);
460 }
461
462 resp->max_participants = (uint8_t)sqlite3_column_int(stmt, 4);
463 resp->current_participants = (uint8_t)sqlite3_column_int(stmt, 5);
464 resp->capabilities = (uint8_t)sqlite3_column_int(stmt, 6);
465 resp->has_password = sqlite3_column_int(stmt, 7) != 0;
466 resp->session_type = (uint8_t)sqlite3_column_int(stmt, 9);
467 resp->created_at = (uint64_t)sqlite3_column_int64(stmt, 12);
468 resp->expires_at = (uint64_t)sqlite3_column_int64(stmt, 13);
469
470 // ACDS policy flags
471 resp->require_server_verify = config->require_server_verify ? 1 : 0;
472 resp->require_client_verify = config->require_client_verify ? 1 : 0;
473
474 sqlite3_finalize(stmt);
475
476 log_debug("Session lookup: %s (found, participants=%d/%d)", session_string, resp->current_participants,
477 resp->max_participants);
478
479 return ASCIICHAT_OK;
480}
481
483 acip_session_joined_t *resp) {
484 if (!db || !req || !config || !resp) {
485 return SET_ERRNO(ERROR_INVALID_PARAM, "db, req, config, or resp is NULL");
486 }
487
488 memset(resp, 0, sizeof(*resp));
489 resp->success = 0;
490
491 // Extract session string
492 char session_string[ACIP_MAX_SESSION_STRING_LEN] = {0};
493 size_t len = req->session_string_len < (ACIP_MAX_SESSION_STRING_LEN - 1) ? req->session_string_len
495 memcpy(session_string, req->session_string, len);
496 session_string[len] = '\0';
497
498 // Find session
499 session_entry_t *session = database_session_find_by_string(db, session_string);
500 if (!session) {
501 resp->error_code = ACIP_ERROR_SESSION_NOT_FOUND;
502 SAFE_STRNCPY(resp->error_message, "Session not found", sizeof(resp->error_message));
503 log_warn("Session join failed: %s (not found)", session_string);
504 return ASCIICHAT_OK;
505 }
506
507 // Check if session is full
508 if (session->current_participants >= session->max_participants) {
509 session_entry_destroy(session);
510 resp->error_code = ACIP_ERROR_SESSION_FULL;
511 SAFE_STRNCPY(resp->error_message, "Session is full", sizeof(resp->error_message));
512 log_warn("Session join failed: %s (full)", session_string);
513 return ASCIICHAT_OK;
514 }
515
516 // Verify password if required
517 if (session->has_password && req->has_password) {
518 if (!verify_password(req->password, session->password_hash)) {
519 session_entry_destroy(session);
520 resp->error_code = ACIP_ERROR_INVALID_PASSWORD;
521 SAFE_STRNCPY(resp->error_message, "Invalid password", sizeof(resp->error_message));
522 log_warn("Session join failed: %s (invalid password)", session_string);
523 return ASCIICHAT_OK;
524 }
525 } else if (session->has_password && !req->has_password) {
526 session_entry_destroy(session);
527 resp->error_code = ACIP_ERROR_INVALID_PASSWORD;
528 SAFE_STRNCPY(resp->error_message, "Password required", sizeof(resp->error_message));
529 log_warn("Session join failed: %s (password required)", session_string);
530 return ASCIICHAT_OK;
531 }
532
533 // Generate participant ID
535 generate_uuid(participant_id);
536 uint64_t now = database_get_current_time_ms();
537
538 // Begin transaction
539 char *err_msg = NULL;
540 int rc = sqlite3_exec(db, "BEGIN IMMEDIATE;", NULL, NULL, &err_msg);
541 if (rc != SQLITE_OK) {
542 session_entry_destroy(session);
543 log_error("Failed to begin transaction: %s", err_msg ? err_msg : "unknown");
544 sqlite3_free(err_msg);
545 return SET_ERRNO(ERROR_CONFIG, "Failed to begin transaction");
546 }
547
548 // Insert participant
549 const char *insert_sql = "INSERT INTO participants (participant_id, session_string, identity_pubkey, joined_at) "
550 "VALUES (?, ?, ?, ?)";
551 sqlite3_stmt *stmt = NULL;
552 rc = sqlite3_prepare_v2(db, insert_sql, -1, &stmt, NULL);
553 if (rc != SQLITE_OK) {
554 sqlite3_exec(db, "ROLLBACK;", NULL, NULL, NULL);
555 session_entry_destroy(session);
556 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare participant insert: %s", sqlite3_errmsg(db));
557 }
558
559 sqlite3_bind_blob(stmt, 1, participant_id, 16, SQLITE_STATIC);
560 sqlite3_bind_text(stmt, 2, session->session_string, -1, SQLITE_STATIC);
561 sqlite3_bind_blob(stmt, 3, req->identity_pubkey, 32, SQLITE_STATIC);
562 sqlite3_bind_int64(stmt, 4, (sqlite3_int64)now);
563
564 rc = sqlite3_step(stmt);
565 sqlite3_finalize(stmt);
566
567 if (rc != SQLITE_DONE) {
568 sqlite3_exec(db, "ROLLBACK;", NULL, NULL, NULL);
569 session_entry_destroy(session);
570 return SET_ERRNO(ERROR_CONFIG, "Failed to insert participant: %s", sqlite3_errmsg(db));
571 }
572
573 // Update participant count and last activity
574 const char *update_sql = "UPDATE sessions SET current_participants = current_participants + 1, "
575 "last_activity_at = ? WHERE session_string = ?";
576 rc = sqlite3_prepare_v2(db, update_sql, -1, &stmt, NULL);
577 if (rc == SQLITE_OK) {
578 sqlite3_bind_int64(stmt, 1, (sqlite3_int64)now);
579 sqlite3_bind_text(stmt, 2, session->session_string, -1, SQLITE_STATIC);
580 sqlite3_step(stmt);
581 sqlite3_finalize(stmt);
582 }
583
584 // Set initiator_id if this is the first participant (for discovery mode tiebreaker)
585 bool is_first_participant = (session->current_participants == 0);
586 bool is_zero_initiator = true;
587 for (int i = 0; i < 16; i++) {
588 if (session->initiator_id[i] != 0) {
589 is_zero_initiator = false;
590 break;
591 }
592 }
593
594 if (is_first_participant || is_zero_initiator) {
595 const char *initiator_sql = "UPDATE sessions SET initiator_id = ? WHERE session_string = ?";
596 rc = sqlite3_prepare_v2(db, initiator_sql, -1, &stmt, NULL);
597 if (rc == SQLITE_OK) {
598 sqlite3_bind_blob(stmt, 1, participant_id, 16, SQLITE_STATIC);
599 sqlite3_bind_text(stmt, 2, session->session_string, -1, SQLITE_STATIC);
600 sqlite3_step(stmt);
601 sqlite3_finalize(stmt);
602 memcpy(session->initiator_id, participant_id, 16);
603 log_debug("Set initiator_id to new participant %02x%02x...", participant_id[0], participant_id[1]);
604 }
605 }
606
607 // Commit transaction
608 rc = sqlite3_exec(db, "COMMIT;", NULL, NULL, &err_msg);
609 if (rc != SQLITE_OK) {
610 log_error("Failed to commit transaction: %s", err_msg ? err_msg : "unknown");
611 sqlite3_free(err_msg);
612 sqlite3_exec(db, "ROLLBACK;", NULL, NULL, NULL);
613 session_entry_destroy(session);
614 return SET_ERRNO(ERROR_CONFIG, "Failed to commit transaction");
615 }
616
617 // Fill response
618 resp->success = 1;
619 resp->error_code = ACIP_ERROR_NONE;
620 memcpy(resp->participant_id, participant_id, 16);
621 memcpy(resp->session_id, session->session_id, 16);
622
623 // Discovery mode host negotiation fields
624 memcpy(resp->initiator_id, session->initiator_id, 16);
625 resp->host_established = session->host_established ? 1 : 0;
626 if (session->host_established) {
627 memcpy(resp->host_id, session->host_participant_id, 16);
628 }
629 // peer_count = current_participants (excluding self) that need to negotiate
630 resp->peer_count = session->current_participants; // Will be incremented after this returns
631 // The transport type is not private server-address data. WebRTC clients need
632 // it to choose the signaling path even while the server IP remains hidden.
633 resp->session_type = session->session_type;
634
635 log_debug("SESSION_JOIN response: session_id=%02x%02x%02x%02x..., participant_id=%02x%02x%02x%02x..., "
636 "initiator=%02x%02x..., host_established=%d, peer_count=%d",
637 resp->session_id[0], resp->session_id[1], resp->session_id[2], resp->session_id[3], resp->participant_id[0],
638 resp->participant_id[1], resp->participant_id[2], resp->participant_id[3], resp->initiator_id[0],
639 resp->initiator_id[1], resp->host_established, resp->peer_count);
640
641 // IP disclosure logic
642 log_info("DATABASE_SESSION_JOIN: has_password=%d, expose_ip_publicly=%d, server_address='%s'", session->has_password,
643 session->expose_ip_publicly, session->server_address);
644 bool reveal_ip = session->has_password || session->expose_ip_publicly;
645 log_info("DATABASE_SESSION_JOIN: reveal_ip=%d", reveal_ip);
646
647 if (reveal_ip) {
648 SAFE_STRNCPY(resp->server_address, session->server_address, sizeof(resp->server_address));
649 resp->server_port = session->server_port;
650 // Generate TURN credentials for WebRTC sessions
651 if (session->session_type == SESSION_TYPE_WEBRTC && config->turn_secret[0] != '\0') {
652 turn_credentials_t turn_creds;
653 asciichat_error_t turn_result =
654 turn_generate_credentials(session_string, config->turn_secret, 86400, &turn_creds);
655 if (turn_result == ASCIICHAT_OK) {
656 SAFE_STRNCPY(resp->turn_username, turn_creds.username, sizeof(resp->turn_username));
657 SAFE_STRNCPY(resp->turn_password, turn_creds.password, sizeof(resp->turn_password));
658 log_debug("Generated TURN credentials for session %s", session_string);
659 }
660 }
661
662 log_info("Participant joined session %s (participants=%d/%d, server=%s:%d, type=%s)", session_string,
663 session->current_participants + 1, session->max_participants, resp->server_address, resp->server_port,
664 session->session_type == SESSION_TYPE_WEBRTC ? "WebRTC" : "DirectTCP");
665 } else {
666 log_info("Participant joined session %s (participants=%d/%d, IP WITHHELD)", session_string,
667 session->current_participants + 1, session->max_participants);
668 }
669
670 session_entry_destroy(session);
671 return ASCIICHAT_OK;
672}
673
675 if (!db || !session_id || !participant_id) {
676 return SET_ERRNO(ERROR_INVALID_PARAM, "db, session_id, or participant_id is NULL");
677 }
678
679 // Look up session_string from session_id
680 char session_string[SESSION_STRING_BUFFER_SIZE] = {0};
681 const char *lookup_sql = "SELECT session_string FROM sessions WHERE session_id = ?";
682 sqlite3_stmt *stmt = NULL;
683 int rc = sqlite3_prepare_v2(db, lookup_sql, -1, &stmt, NULL);
684 if (rc != SQLITE_OK) {
685 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare session lookup: %s", sqlite3_errmsg(db));
686 }
687
688 sqlite3_bind_blob(stmt, 1, session_id, 16, SQLITE_STATIC);
689 rc = sqlite3_step(stmt);
690 if (rc != SQLITE_ROW) {
691 sqlite3_finalize(stmt);
692 return SET_ERRNO(ERROR_INVALID_STATE, "Session not found");
693 }
694
695 const char *str = (const char *)sqlite3_column_text(stmt, 0);
696 if (str) {
697 SAFE_STRNCPY(session_string, str, sizeof(session_string));
698 }
699 sqlite3_finalize(stmt);
700
701 if (session_string[0] == '\0') {
702 return SET_ERRNO(ERROR_INVALID_STATE, "Session string is empty");
703 }
704
705 // Begin transaction
706 char *err_msg = NULL;
707 rc = sqlite3_exec(db, "BEGIN IMMEDIATE;", NULL, NULL, &err_msg);
708 if (rc != SQLITE_OK) {
709 log_error("Failed to begin transaction: %s", err_msg ? err_msg : "unknown");
710 sqlite3_free(err_msg);
711 return SET_ERRNO(ERROR_CONFIG, "Failed to begin transaction");
712 }
713
714 // Delete participant
715 const char *del_sql = "DELETE FROM participants WHERE participant_id = ? AND session_string = ?";
716 rc = sqlite3_prepare_v2(db, del_sql, -1, &stmt, NULL);
717 if (rc != SQLITE_OK) {
718 sqlite3_exec(db, "ROLLBACK;", NULL, NULL, NULL);
719 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare participant delete: %s", sqlite3_errmsg(db));
720 }
721
722 sqlite3_bind_blob(stmt, 1, participant_id, 16, SQLITE_STATIC);
723 sqlite3_bind_text(stmt, 2, session_string, -1, SQLITE_STATIC);
724 rc = sqlite3_step(stmt);
725 sqlite3_finalize(stmt);
726
727 if (rc != SQLITE_DONE) {
728 sqlite3_exec(db, "ROLLBACK;", NULL, NULL, NULL);
729 return SET_ERRNO(ERROR_CONFIG, "Failed to delete participant: %s", sqlite3_errmsg(db));
730 }
731
732 int changes = sqlite3_changes(db);
733 if (changes == 0) {
734 sqlite3_exec(db, "ROLLBACK;", NULL, NULL, NULL);
735 return SET_ERRNO(ERROR_INVALID_STATE, "Participant not in session");
736 }
737
738 uint64_t now = database_get_current_time_ms();
739
740 // Decrement participant count and update last activity
741 const char *update_sql = "UPDATE sessions SET current_participants = current_participants - 1, "
742 "last_activity_at = ? WHERE session_string = ? AND current_participants > 0";
743 rc = sqlite3_prepare_v2(db, update_sql, -1, &stmt, NULL);
744 if (rc == SQLITE_OK) {
745 sqlite3_bind_int64(stmt, 1, (sqlite3_int64)now);
746 sqlite3_bind_text(stmt, 2, session_string, -1, SQLITE_STATIC);
747 sqlite3_step(stmt);
748 sqlite3_finalize(stmt);
749 }
750
751 // Check if session is now empty and delete if so
752 const char *check_sql = "SELECT current_participants FROM sessions WHERE session_string = ?";
753 rc = sqlite3_prepare_v2(db, check_sql, -1, &stmt, NULL);
754 if (rc == SQLITE_OK) {
755 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
756 if (sqlite3_step(stmt) == SQLITE_ROW) {
757 int count = sqlite3_column_int(stmt, 0);
758
759 if (count <= 0) {
760 log_info("Session %s has no participants, deleting", session_string);
761 sqlite3_finalize(stmt);
762
763 // Delete empty session
764 const char *del_session_sql = "DELETE FROM sessions WHERE session_string = ?";
765 rc = sqlite3_prepare_v2(db, del_session_sql, -1, &stmt, NULL);
766 if (rc == SQLITE_OK) {
767 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
768 sqlite3_step(stmt);
769 sqlite3_finalize(stmt);
770 }
771 stmt = NULL;
772 } else {
773 log_info("Participant left session %s (participants=%d remaining)", session_string, count);
774 }
775 }
776 if (stmt) {
777 sqlite3_finalize(stmt);
778 }
779 }
780
781 // Commit transaction
782 rc = sqlite3_exec(db, "COMMIT;", NULL, NULL, &err_msg);
783 if (rc != SQLITE_OK) {
784 log_error("Failed to commit transaction: %s", err_msg ? err_msg : "unknown");
785 sqlite3_free(err_msg);
786 sqlite3_exec(db, "ROLLBACK;", NULL, NULL, NULL);
787 return SET_ERRNO(ERROR_CONFIG, "Failed to commit transaction");
788 }
789
790 return ASCIICHAT_OK;
791}
792
794 if (!db || !session_id) {
795 return NULL;
796 }
797
798 static const char sql[] = SELECT_SESSION_BASE " WHERE session_id = ?";
799
800 sqlite3_stmt *stmt = NULL;
801 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
802 return NULL;
803 }
804
805 sqlite3_bind_blob(stmt, 1, session_id, 16, SQLITE_STATIC);
806
807 session_entry_t *session = NULL;
808 if (sqlite3_step(stmt) == SQLITE_ROW) {
809 session = load_session_from_row(stmt);
810 if (session) {
811 load_session_participants(db, session);
812 }
813 }
814
815 sqlite3_finalize(stmt);
816 return session;
817}
818
819session_entry_t *database_session_find_by_string(sqlite3 *db, const char *session_string) {
820 if (!db || !session_string) {
821 return NULL;
822 }
823
824 static const char sql[] = SELECT_SESSION_BASE " WHERE session_string = ?";
825
826 sqlite3_stmt *stmt = NULL;
827 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
828 return NULL;
829 }
830
831 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
832
833 session_entry_t *session = NULL;
834 if (sqlite3_step(stmt) == SQLITE_ROW) {
835 session = load_session_from_row(stmt);
836 if (session) {
837 load_session_participants(db, session);
838 }
839 }
840
841 sqlite3_finalize(stmt);
842 return session;
843}
844
846 if (!db) {
847 return;
848 }
849
850 uint64_t now = database_get_current_time_ms();
851 // 3 hours in milliseconds
852 uint64_t inactivity_threshold = 3ULL * SEC_PER_HOUR * MS_PER_SEC_INT;
853 uint64_t cutoff_time = now - inactivity_threshold;
854
855 // Log sessions about to be deleted
856 const char *log_sql = "SELECT session_string, last_activity_at FROM sessions WHERE last_activity_at < ?";
857 sqlite3_stmt *stmt = NULL;
858 if (sqlite3_prepare_v2(db, log_sql, -1, &stmt, NULL) == SQLITE_OK) {
859 sqlite3_bind_int64(stmt, 1, (sqlite3_int64)cutoff_time);
860 while (sqlite3_step(stmt) == SQLITE_ROW) {
861 const char *session_string = (const char *)sqlite3_column_text(stmt, 0);
862 uint64_t last_activity = (uint64_t)sqlite3_column_int64(stmt, 1);
863 uint64_t inactive_ms = now - last_activity;
864 uint64_t inactive_hours = inactive_ms / (SEC_PER_HOUR * MS_PER_SEC_INT);
865 log_info("Session %s inactive for %lu hours, deleting", session_string ? session_string : "<unknown>",
866 inactive_hours);
867 }
868 sqlite3_finalize(stmt);
869 }
870
871 // Delete inactive sessions (CASCADE deletes participants)
872 const char *del_sql = "DELETE FROM sessions WHERE last_activity_at < ?";
873 if (sqlite3_prepare_v2(db, del_sql, -1, &stmt, NULL) == SQLITE_OK) {
874 sqlite3_bind_int64(stmt, 1, (sqlite3_int64)cutoff_time);
875 sqlite3_step(stmt);
876 int deleted = sqlite3_changes(db);
877 sqlite3_finalize(stmt);
878
879 if (deleted > 0) {
880 log_info("Cleaned up %d inactive sessions (>3 hours)", deleted);
881 }
882 }
883}
884
886 const uint8_t host_participant_id[16], const char *host_address,
887 uint16_t host_port, uint8_t connection_type) {
888 if (!db || !session_id || !host_participant_id) {
889 return SET_ERRNO(ERROR_INVALID_PARAM, "db, session_id, or host_participant_id is NULL");
890 }
891
892 uint64_t now = database_get_current_time_ms();
893
894 const char *sql = "UPDATE sessions SET "
895 "host_established = 1, "
896 "host_participant_id = ?, "
897 "host_address = ?, "
898 "host_port = ?, "
899 "host_connection_type = ?, "
900 "last_activity_at = ? "
901 "WHERE session_id = ?";
902
903 sqlite3_stmt *stmt = NULL;
904 int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
905 if (rc != SQLITE_OK) {
906 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare host update: %s", sqlite3_errmsg(db));
907 }
908
909 sqlite3_bind_blob(stmt, 1, host_participant_id, 16, SQLITE_STATIC);
910 sqlite3_bind_text(stmt, 2, host_address ? host_address : "", -1, SQLITE_STATIC);
911 sqlite3_bind_int(stmt, 3, host_port);
912 sqlite3_bind_int(stmt, 4, connection_type);
913 sqlite3_bind_int64(stmt, 5, (sqlite3_int64)now);
914 sqlite3_bind_blob(stmt, 6, session_id, 16, SQLITE_STATIC);
915
916 rc = sqlite3_step(stmt);
917 sqlite3_finalize(stmt);
918
919 if (rc != SQLITE_DONE) {
920 return SET_ERRNO(ERROR_CONFIG, "Failed to update host: %s", sqlite3_errmsg(db));
921 }
922
923 int changes = sqlite3_changes(db);
924 if (changes == 0) {
925 return SET_ERRNO(ERROR_INVALID_STATE, "Session not found for host update");
926 }
927
928 log_info("Session host updated: participant=%02x%02x..., address=%s:%u, type=%d", host_participant_id[0],
929 host_participant_id[1], host_address ? host_address : "(none)", host_port, connection_type);
930
931 return ASCIICHAT_OK;
932}
933
935 if (!db || !session_id) {
936 return SET_ERRNO(ERROR_INVALID_PARAM, "db or session_id is NULL");
937 }
938
939 const char *sql = "UPDATE sessions SET "
940 "host_established = 0, "
941 "host_participant_id = NULL, "
942 "host_address = NULL, "
943 "host_port = 0, "
944 "host_connection_type = 0 "
945 "WHERE session_id = ?";
946
947 sqlite3_stmt *stmt = NULL;
948 int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
949 if (rc != SQLITE_OK) {
950 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare host clear: %s", sqlite3_errmsg(db));
951 }
952
953 sqlite3_bind_blob(stmt, 1, session_id, 16, SQLITE_STATIC);
954
955 rc = sqlite3_step(stmt);
956 sqlite3_finalize(stmt);
957
958 if (rc != SQLITE_DONE) {
959 return SET_ERRNO(ERROR_CONFIG, "Failed to clear host: %s", sqlite3_errmsg(db));
960 }
961
962 int changes = sqlite3_changes(db);
963 if (changes == 0) {
964 return SET_ERRNO(ERROR_INVALID_STATE, "Session not found for host clear");
965 }
966
967 log_info("Session host cleared (session=%02x%02x...) - ready for migration", session_id[0], session_id[1]);
968
969 return ASCIICHAT_OK;
970}
971
973 if (!db || !session_id) {
974 return SET_ERRNO(ERROR_INVALID_PARAM, "db or session_id is NULL");
975 }
976
978
979 const char *sql = "UPDATE sessions SET "
980 "in_migration = 1, "
981 "migration_start_ns = ? "
982 "WHERE session_id = ?";
983
984 sqlite3_stmt *stmt = NULL;
985 int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
986 if (rc != SQLITE_OK) {
987 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare migration start: %s", sqlite3_errmsg(db));
988 }
989
990 sqlite3_bind_int64(stmt, 1, (sqlite3_int64)now_ns);
991 sqlite3_bind_blob(stmt, 2, session_id, 16, SQLITE_STATIC);
992
993 rc = sqlite3_step(stmt);
994 sqlite3_finalize(stmt);
995
996 if (rc != SQLITE_DONE) {
997 return SET_ERRNO(ERROR_CONFIG, "Failed to start migration: %s", sqlite3_errmsg(db));
998 }
999
1000 int changes = sqlite3_changes(db);
1001 if (changes == 0) {
1002 return SET_ERRNO(ERROR_INVALID_STATE, "Session not found for migration start");
1003 }
1004
1005 log_info("Session migration started (session=%02x%02x..., start_ns=%" PRIu64 ")", session_id[0], session_id[1],
1006 now_ns);
1007
1008 return ASCIICHAT_OK;
1009}
1010
1011bool database_session_is_migration_ready(sqlite3 *db, const uint8_t session_id[16], uint64_t migration_window_ms) {
1012 if (!db || !session_id) {
1013 return false;
1014 }
1015
1016 uint64_t now_ns = time_get_realtime_ns();
1017 const char *sql = "SELECT in_migration, migration_start_ns FROM sessions WHERE session_id = ?";
1018
1019 sqlite3_stmt *stmt = NULL;
1020 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
1021 return false;
1022 }
1023
1024 sqlite3_bind_blob(stmt, 1, session_id, 16, SQLITE_STATIC);
1025
1026 bool ready = false;
1027 if (sqlite3_step(stmt) == SQLITE_ROW) {
1028 int in_migration = sqlite3_column_int(stmt, 0);
1029 uint64_t migration_start_ns = (uint64_t)sqlite3_column_int64(stmt, 1);
1030
1031 if (in_migration) {
1032 uint64_t elapsed_ns = now_ns - migration_start_ns;
1033 uint64_t migration_window_ns = migration_window_ms * NS_PER_MS_INT;
1034 if (elapsed_ns >= migration_window_ns) {
1035 ready = true;
1036 log_debug("Migration window complete (session=%02x%02x..., elapsed=%lu ns, window=%lu ns)", session_id[0],
1037 session_id[1], elapsed_ns, migration_window_ns);
1038 }
1039 }
1040 }
1041
1042 sqlite3_finalize(stmt);
1043 return ready;
1044}
1045
1046// ============================================================================
1047// Multi-Key Management Functions
1048// ============================================================================
1049
1050asciichat_error_t database_session_add_key(sqlite3 *db, const char *session_string, const uint8_t identity_pubkey[32],
1051 uint32_t key_version) {
1052 if (!db || !session_string || !identity_pubkey) {
1053 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for add_key");
1054 }
1055
1056 const char *sql = "INSERT OR IGNORE INTO session_keys (session_string, identity_pubkey, key_version, added_at) "
1057 "VALUES (?, ?, ?, ?)";
1058
1059 sqlite3_stmt *stmt = NULL;
1060 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
1061 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare add_key statement: %s", sqlite3_errmsg(db));
1062 }
1063
1064 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
1065 sqlite3_bind_blob(stmt, 2, identity_pubkey, 32, SQLITE_STATIC);
1066 sqlite3_bind_int(stmt, 3, key_version);
1067 sqlite3_bind_int64(stmt, 4, database_get_current_time_ms());
1068
1069 int rc = sqlite3_step(stmt);
1070 sqlite3_finalize(stmt);
1071
1072 if (rc != SQLITE_DONE) {
1073 return SET_ERRNO(ERROR_CONFIG, "Failed to add session key: %s", sqlite3_errmsg(db));
1074 }
1075
1076 log_debug("Added key to session %s (version=%u)", session_string, key_version);
1077 return ASCIICHAT_OK;
1078}
1079
1080asciichat_error_t database_session_revoke_key(sqlite3 *db, const char *session_string,
1081 const uint8_t identity_pubkey[32]) {
1082 if (!db || !session_string || !identity_pubkey) {
1083 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for revoke_key");
1084 }
1085
1086 const char *sql = "UPDATE session_keys SET revoked = 1 WHERE session_string = ? AND identity_pubkey = ?";
1087
1088 sqlite3_stmt *stmt = NULL;
1089 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
1090 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare revoke_key statement: %s", sqlite3_errmsg(db));
1091 }
1092
1093 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
1094 sqlite3_bind_blob(stmt, 2, identity_pubkey, 32, SQLITE_STATIC);
1095
1096 int rc = sqlite3_step(stmt);
1097 sqlite3_finalize(stmt);
1098
1099 if (rc != SQLITE_DONE) {
1100 return SET_ERRNO(ERROR_CONFIG, "Failed to revoke session key: %s", sqlite3_errmsg(db));
1101 }
1102
1103 log_debug("Revoked key from session %s", session_string);
1104 return ASCIICHAT_OK;
1105}
1106
1107bool database_session_verify_key(sqlite3 *db, const char *session_string, const uint8_t identity_pubkey[32]) {
1108 if (!db || !session_string || !identity_pubkey) {
1109 return false;
1110 }
1111
1112 const char *sql = "SELECT 1 FROM session_keys WHERE session_string = ? AND identity_pubkey = ? AND revoked = 0";
1113
1114 sqlite3_stmt *stmt = NULL;
1115 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
1116 return false;
1117 }
1118
1119 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
1120 sqlite3_bind_blob(stmt, 2, identity_pubkey, 32, SQLITE_STATIC);
1121
1122 bool valid = (sqlite3_step(stmt) == SQLITE_ROW);
1123 sqlite3_finalize(stmt);
1124
1125 return valid;
1126}
1127
1128asciichat_error_t database_session_get_keys(sqlite3 *db, const char *session_string, uint8_t (*keys_out)[32],
1129 size_t max_keys, size_t *count_out) {
1130 if (!db || !session_string || !keys_out || !count_out) {
1131 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for get_keys");
1132 }
1133
1134 const char *sql = "SELECT identity_pubkey FROM session_keys WHERE session_string = ? AND revoked = 0 "
1135 "ORDER BY key_version ASC";
1136
1137 sqlite3_stmt *stmt = NULL;
1138 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
1139 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare get_keys statement: %s", sqlite3_errmsg(db));
1140 }
1141
1142 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
1143
1144 size_t count = 0;
1145 while (sqlite3_step(stmt) == SQLITE_ROW && count < max_keys) {
1146 const void *blob = sqlite3_column_blob(stmt, 0);
1147 if (blob && sqlite3_column_bytes(stmt, 0) == 32) {
1148 memcpy(keys_out[count], blob, 32);
1149 count++;
1150 }
1151 }
1152
1153 sqlite3_finalize(stmt);
1154 *count_out = count;
1155
1156 log_debug("Retrieved %zu keys for session %s", count, session_string);
1157 return ASCIICHAT_OK;
1158}
1159
1160// ============================================================================
1161// Statistics Queries
1162// ============================================================================
1163
1164asciichat_error_t database_stats_session_count(sqlite3 *db, int64_t *count_out) {
1165 if (!db || !count_out) {
1166 return SET_ERRNO(ERROR_INVALID_PARAM, "db or count_out is NULL");
1167 }
1168
1169 const char *query = "SELECT COUNT(*) FROM sessions WHERE expires_at > ?";
1170 sqlite3_stmt *stmt;
1171 int rc = sqlite3_prepare_v2(db, query, -1, &stmt, NULL);
1172 if (rc != SQLITE_OK) {
1173 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare statement: %s", sqlite3_errmsg(db));
1174 }
1175
1176 time_t now = time(NULL);
1177 sqlite3_bind_int64(stmt, 1, (sqlite3_int64)now);
1178
1179 int64_t count = 0;
1180 rc = sqlite3_step(stmt);
1181 if (rc == SQLITE_ROW) {
1182 count = sqlite3_column_int64(stmt, 0);
1183 } else if (rc != SQLITE_DONE) {
1184 sqlite3_finalize(stmt);
1185 return SET_ERRNO(ERROR_CONFIG, "Failed to execute query: %s", sqlite3_errmsg(db));
1186 }
1187
1188 sqlite3_finalize(stmt);
1189 *count_out = count;
1190
1191 log_debug("Active session count: %lld", (long long)count);
1192 return ASCIICHAT_OK;
1193}
1194
1195asciichat_error_t database_stats_unique_clients(sqlite3 *db, int64_t *count_out) {
1196 if (!db || !count_out) {
1197 return SET_ERRNO(ERROR_INVALID_PARAM, "db or count_out is NULL");
1198 }
1199
1200 const char *query = "SELECT COUNT(DISTINCT participant_id) FROM participants";
1201 sqlite3_stmt *stmt;
1202 int rc = sqlite3_prepare_v2(db, query, -1, &stmt, NULL);
1203 if (rc != SQLITE_OK) {
1204 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare statement: %s", sqlite3_errmsg(db));
1205 }
1206
1207 int64_t count = 0;
1208 rc = sqlite3_step(stmt);
1209 if (rc == SQLITE_ROW) {
1210 count = sqlite3_column_int64(stmt, 0);
1211 } else if (rc != SQLITE_DONE) {
1212 sqlite3_finalize(stmt);
1213 return SET_ERRNO(ERROR_CONFIG, "Failed to execute query: %s", sqlite3_errmsg(db));
1214 }
1215
1216 sqlite3_finalize(stmt);
1217 *count_out = count;
1218
1219 log_debug("Unique clients count: %lld", (long long)count);
1220 return ASCIICHAT_OK;
1221}
bool valid
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_stats_unique_clients(sqlite3 *db, int64_t *count_out)
Get total number of unique clients/participants.
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_add_key(sqlite3 *db, const char *session_string, const uint8_t identity_pubkey[32], uint32_t key_version)
Add a key to a session.
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.
asciichat_error_t database_stats_session_count(sqlite3 *db, int64_t *count_out)
Get total number of active sessions.
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.
#define SELECT_SESSION_BASE
session_entry_t * database_session_find_by_string(sqlite3 *db, const char *session_string)
Find session by session_string.
bool database_session_is_migration_ready(sqlite3 *db, const uint8_t session_id[16], uint64_t migration_window_ms)
Check if migration window has completed.
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_session_get_keys(sqlite3 *db, const char *session_string, uint8_t(*keys_out)[32], size_t max_keys, size_t *count_out)
Get all active keys for a session.
asciichat_error_t database_init(const char *db_path, sqlite3 **db)
Initialize database and create schema.
bool database_session_verify_key(sqlite3 *db, const char *session_string, const uint8_t identity_pubkey[32])
Check if a key is valid for a session (exists and not revoked)
asciichat_error_t database_session_revoke_key(sqlite3 *db, const char *session_string, const uint8_t identity_pubkey[32])
Revoke a key from a session.
void database_session_cleanup_expired(sqlite3 *db)
Clean up inactive sessions.
bool is_session_string(const char *str)
Check if a string is a valid session string.
asciichat_error_t acds_string_generate(char *output, size_t output_size)
Generate random session string.
acip_session_join_t
Definition acds.h:403
acip_session_joined_t
Definition acds.h:474
#define ACIP_MAX_SESSION_STRING_LEN
Maximum session string length (e.g., "swift-river-mountain" = 20 chars)
Definition acds.h:1085
acip_session_created_t
Definition acds.h:269
acip_session_create_t
Definition acds.h:221
acip_session_info_t
Definition acds.h:355
#define ACIP_SESSION_EXPIRATION_MS
Session maximum lifetime (24 hours in milliseconds) Note: Sessions are actually cleaned up after 3 ho...
Definition acds.h:1091
@ ACIP_ERROR_INVALID_PASSWORD
Password verification failed.
Definition acds.h:1068
@ ACIP_ERROR_SESSION_NOT_FOUND
Session does not exist.
Definition acds.h:1066
@ ACIP_ERROR_SESSION_FULL
Session has reached max participants.
Definition acds.h:1067
@ ACIP_ERROR_NONE
No error (success)
Definition acds.h:1065
@ SESSION_TYPE_WEBRTC
WebRTC P2P mesh with STUN/TURN relay.
Definition acds.h:146
unsigned short uint16_t
Definition common.h:57
unsigned int uint32_t
Definition common.h:58
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:414
#define SAFE_MALLOC(size, cast)
Definition common.h:264
unsigned long long uint64_t
Definition common.h:59
unsigned char uint8_t
Definition common.h:56
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
#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
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_CONFIG
Definition error_codes.h:57
@ ERROR_INVALID_PARAM
#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
#define SEC_PER_HOUR
Definition time.h:168
uint64_t time_get_realtime_ns(void)
Get current wall-clock (real) time in nanoseconds.
Definition util/time.c:119
#define NS_PER_MS_INT
Definition time.h:156
#define MS_PER_SEC_INT
Definition time.h:164
void session_entry_destroy(session_entry_t *entry)
Free a session entry and all its resources.
📝 Logging API with multiple log levels and terminal output control
uint8_t session_id[16]
uint8_t participant_id[16]
#define MAX_PARTICIPANTS
#define SESSION_STRING_BUFFER_SIZE
Discovery server configuration.
bool require_server_verify
ACDS policy: require servers to verify client identity during handshake.
uint8_t turn_count
Number of configured TURN servers (0-4)
char turn_secret[256]
Shared secret for TURN credential generation (HMAC-SHA1)
uint8_t stun_count
Number of configured STUN servers (0-4)
bool require_client_verify
ACDS policy: require clients to verify server identity during handshake.
Error context structure.
char * context_message
Optional custom message (dynamically allocated, owned by system)
Participant in a session.
uint8_t identity_pubkey[32]
Ed25519 public key.
uint64_t joined_at
Unix timestamp (ms)
Session entry data structure.
bool expose_ip_publicly
Allow IP disclosure without verification (explicit opt-in via –acds-expose-ip)
bool has_password
Password protection flag.
char server_address[64]
IPv4/IPv6 address or hostname.
uint8_t max_participants
1-8
uint8_t host_pubkey[32]
Host's Ed25519 key.
participant_t * participants[16]
Participant array (in-memory only)
uint8_t initiator_id[16]
First participant who created/joined the session.
uint16_t host_port
Host's port (valid if host_established)
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)
bool in_migration
Currently collecting HOST_LOST packets.
char session_string[(38+1)]
e.g., "affectionate-acquaintance-acquaintance"
uint8_t session_id[16]
UUID.
char password_hash[128]
Argon2id hash (if has_password)
uint64_t created_at
Unix timestamp (ms) - when session was created.
char host_address[64]
Host's reachable address (valid if host_established)
uint64_t migration_start_ns
When migration started in nanoseconds (for collection window timeout)
uint64_t expires_at
Unix timestamp (ms) - absolute max lifetime (created_at + 24h)
uint8_t capabilities
bit 0: video, bit 1: audio
uint8_t session_type
acds_session_type_t: 0=DIRECT_TCP, 1=WEBRTC
uint64_t last_activity_at
Unix timestamp (ms) - updated on join/leave/host updates (cleanup uses this)
uint16_t server_port
Port number for client connection.
uint8_t current_participants
Active participant count.
uint8_t host_connection_type
acip_connection_type_t: how to reach host
TURN server credentials (username + password)
⏱️ High-precision timing utilities using sokol_time.h and uthash
asciichat_error_t turn_generate_credentials(const char *session_id, const char *secret, uint32_t validity_seconds, turn_credentials_t *out_credentials)
Generate time-limited TURN credentials.
TURN server credential generation for WebRTC.