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

Go to the source code of this file.

Macros

#define SELECT_SESSION_BASE
 

Functions

asciichat_error_t database_init (const char *db_path, sqlite3 **db)
 
void database_close (sqlite3 *db)
 
asciichat_error_t database_session_create (sqlite3 *db, const acip_session_create_t *req, const acds_config_t *config, acip_session_created_t *resp)
 
asciichat_error_t database_session_lookup (sqlite3 *db, const char *session_string, const acds_config_t *config, acip_session_info_t *resp)
 
asciichat_error_t database_session_join (sqlite3 *db, const acip_session_join_t *req, const acds_config_t *config, acip_session_joined_t *resp)
 
asciichat_error_t database_session_leave (sqlite3 *db, const uint8_t session_id[16], const uint8_t participant_id[16])
 
session_entry_t * database_session_find_by_id (sqlite3 *db, const uint8_t session_id[16])
 
session_entry_t * database_session_find_by_string (sqlite3 *db, const char *session_string)
 
void database_session_cleanup_expired (sqlite3 *db)
 
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)
 
asciichat_error_t database_session_clear_host (sqlite3 *db, const uint8_t session_id[16])
 
asciichat_error_t database_session_start_migration (sqlite3 *db, const uint8_t session_id[16])
 
bool database_session_is_migration_ready (sqlite3 *db, const uint8_t session_id[16], uint64_t migration_window_ms)
 
asciichat_error_t database_session_add_key (sqlite3 *db, const char *session_string, const uint8_t identity_pubkey[32], uint32_t key_version)
 
asciichat_error_t database_session_revoke_key (sqlite3 *db, const char *session_string, const uint8_t identity_pubkey[32])
 
bool database_session_verify_key (sqlite3 *db, const char *session_string, const uint8_t identity_pubkey[32])
 
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)
 

Macro Definition Documentation

◆ SELECT_SESSION_BASE

#define SELECT_SESSION_BASE
Value:
"SELECT session_string, session_id, host_pubkey, password_hash, " \
"max_participants, current_participants, capabilities, has_password, " \
"expose_ip_publicly, session_type, server_address, server_port, " \
"created_at, expires_at, last_activity_at, initiator_id, host_established, " \
"host_participant_id, host_address, host_port, host_connection_type, " \
"in_migration, migration_start_ns FROM sessions"

Definition at line 25 of file discovery/database.c.

110 {
111 randombytes_buf(uuid_out, 16);
112 uuid_out[6] = (uuid_out[6] & 0x0F) | 0x40; // Version 4
113 uuid_out[8] = (uuid_out[8] & 0x3F) | 0x80; // RFC4122 variant
114}
115
119static uint64_t database_get_current_time_ms(void) {
120 uint64_t current_time_ns = time_get_realtime_ns();
121 return time_ns_to_ms(current_time_ns);
122}
123
127static bool verify_password(const char *password, const char *hash) {
128 return crypto_pwhash_str_verify(hash, password, strlen(password)) == 0;
129}
130
137static session_entry_t *load_session_from_row(sqlite3_stmt *stmt) {
138 session_entry_t *session = SAFE_MALLOC(sizeof(session_entry_t), session_entry_t *);
139 if (!session) {
140 return NULL;
141 }
142 memset(session, 0, sizeof(*session));
143
144 // Column order from SELECT statements must match:
145 // 0: session_string, 1: session_id, 2: host_pubkey, 3: password_hash,
146 // 4: max_participants, 5: current_participants, 6: capabilities,
147 // 7: has_password, 8: expose_ip_publicly, 9: session_type,
148 // 10: server_address, 11: server_port, 12: created_at, 13: expires_at,
149 // 14: last_activity_at, 15: initiator_id, 16: host_established, 17: host_participant_id,
150 // 18: host_address, 19: host_port, 20: host_connection_type,
151 // 21: in_migration, 22: migration_start_ns
152
153 const char *str = (const char *)sqlite3_column_text(stmt, 0);
154 if (str) {
155 SAFE_STRNCPY(session->session_string, str, sizeof(session->session_string));
156 }
157
158 const void *blob = sqlite3_column_blob(stmt, 1);
159 if (blob) {
160 memcpy(session->session_id, blob, 16);
161 }
162
163 blob = sqlite3_column_blob(stmt, 2);
164 if (blob) {
165 memcpy(session->host_pubkey, blob, 32);
166 }
167
168 str = (const char *)sqlite3_column_text(stmt, 3);
169 if (str) {
170 SAFE_STRNCPY(session->password_hash, str, sizeof(session->password_hash));
171 }
172
173 session->max_participants = (uint8_t)sqlite3_column_int(stmt, 4);
174 session->current_participants = (uint8_t)sqlite3_column_int(stmt, 5);
175 session->capabilities = (uint8_t)sqlite3_column_int(stmt, 6);
176 session->has_password = sqlite3_column_int(stmt, 7) != 0;
177 session->expose_ip_publicly = sqlite3_column_int(stmt, 8) != 0;
178 session->session_type = (uint8_t)sqlite3_column_int(stmt, 9);
179
180 str = (const char *)sqlite3_column_text(stmt, 10);
181 if (str) {
182 SAFE_STRNCPY(session->server_address, str, sizeof(session->server_address));
183 }
184
185 session->server_port = (uint16_t)sqlite3_column_int(stmt, 11);
186 session->created_at = (uint64_t)sqlite3_column_int64(stmt, 12);
187 session->expires_at = (uint64_t)sqlite3_column_int64(stmt, 13);
188 session->last_activity_at = (uint64_t)sqlite3_column_int64(stmt, 14);
189
190 // Discovery mode host negotiation fields
191 blob = sqlite3_column_blob(stmt, 15);
192 if (blob) {
193 memcpy(session->initiator_id, blob, 16);
194 }
195
196 session->host_established = sqlite3_column_int(stmt, 16) != 0;
197
198 blob = sqlite3_column_blob(stmt, 17);
199 if (blob) {
200 memcpy(session->host_participant_id, blob, 16);
201 }
202
203 str = (const char *)sqlite3_column_text(stmt, 18);
204 if (str) {
205 SAFE_STRNCPY(session->host_address, str, sizeof(session->host_address));
206 }
207
208 session->host_port = (uint16_t)sqlite3_column_int(stmt, 19);
209 session->host_connection_type = (uint8_t)sqlite3_column_int(stmt, 20);
210
211 // Host migration state
212 session->in_migration = sqlite3_column_int(stmt, 21) != 0;
213 session->migration_start_ns = (uint64_t)sqlite3_column_int64(stmt, 22);
214
215 return session;
216}
217
221static void load_session_participants(sqlite3 *db, session_entry_t *session) {
222 static const char *sql = "SELECT participant_id, identity_pubkey, joined_at "
223 "FROM participants WHERE session_string = ?";
224
225 sqlite3_stmt *stmt = NULL;
226 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
227 return;
228 }
229
230 sqlite3_bind_text(stmt, 1, session->session_string, -1, SQLITE_STATIC);
231
232 size_t idx = 0;
233 while (sqlite3_step(stmt) == SQLITE_ROW && idx < MAX_PARTICIPANTS) {
234 participant_t *p = SAFE_MALLOC(sizeof(participant_t), participant_t *);
235 if (!p) {
236 break;
237 }
238 memset(p, 0, sizeof(*p));
239
240 const void *blob = sqlite3_column_blob(stmt, 0);
241 if (blob) {
242 memcpy(p->participant_id, blob, 16);
243 }
244
245 blob = sqlite3_column_blob(stmt, 1);
246 if (blob) {
247 memcpy(p->identity_pubkey, blob, 32);
248 }
249
250 p->joined_at = (uint64_t)sqlite3_column_int64(stmt, 2);
251
252 session->participants[idx++] = p;
253 }
254
255 sqlite3_finalize(stmt);
256}
257
258// ============================================================================
259// Database Lifecycle
260// ============================================================================
261
262asciichat_error_t database_init(const char *db_path, sqlite3 **db) {
263 if (!db_path || !db) {
264 return SET_ERRNO(ERROR_INVALID_PARAM, "db_path or db is NULL");
265 }
266
267 log_info("Opening database: %s", db_path);
268
269 int rc = sqlite3_open(db_path, db);
270 if (rc != SQLITE_OK) {
271 const char *err = sqlite3_errmsg(*db);
272 sqlite3_close(*db);
273 *db = NULL;
274 return SET_ERRNO(ERROR_CONFIG, "Failed to open database: %s", err);
275 }
276
277 // Enable Write-Ahead Logging for concurrent reads
278 char *err_msg = NULL;
279 rc = sqlite3_exec(*db, "PRAGMA journal_mode=WAL;", NULL, NULL, &err_msg);
280 if (rc != SQLITE_OK) {
281 log_warn("Failed to enable WAL mode: %s", err_msg ? err_msg : "unknown error");
282 sqlite3_free(err_msg);
283 }
284
285 // Enable foreign key constraints
286 rc = sqlite3_exec(*db, "PRAGMA foreign_keys=ON;", NULL, NULL, &err_msg);
287 if (rc != SQLITE_OK) {
288 log_error("Failed to enable foreign keys: %s", err_msg ? err_msg : "unknown error");
289 sqlite3_free(err_msg);
290 sqlite3_close(*db);
291 *db = NULL;
292 return SET_ERRNO(ERROR_CONFIG, "Failed to enable foreign keys");
293 }
294
295 // Create schema
296 rc = sqlite3_exec(*db, schema_sql, NULL, NULL, &err_msg);
297 if (rc != SQLITE_OK) {
298 log_error("Failed to create schema: %s", err_msg ? err_msg : "unknown error");
299 sqlite3_free(err_msg);
300 sqlite3_close(*db);
301 *db = NULL;
302 return SET_ERRNO(ERROR_CONFIG, "Failed to create database schema");
303 }
304
305 log_info("Database initialized successfully (SQLite as single source of truth)");
306 return ASCIICHAT_OK;
307}
308
309void database_close(sqlite3 *db) {
310 if (!db) {
311 return;
312 }
313 sqlite3_close(db);
314 log_debug("Database closed");
315}
316
317// ============================================================================
318// Session Operations
319// ============================================================================
320
321asciichat_error_t database_session_create(sqlite3 *db, const acip_session_create_t *req, const acds_config_t *config,
322 acip_session_created_t *resp) {
323 if (!db || !req || !config || !resp) {
324 return SET_ERRNO(ERROR_INVALID_PARAM, "db, req, config, or resp is NULL");
325 }
326
327 memset(resp, 0, sizeof(*resp));
328
329 // Generate or use reserved session string
330 char session_string[ACIP_MAX_SESSION_STRING_LEN] = {0};
331 if (req->reserved_string_len > 0) {
332 const char *reserved_str = (const char *)(req + 1);
333 size_t len = req->reserved_string_len < (ACIP_MAX_SESSION_STRING_LEN - 1) ? req->reserved_string_len
334 : (ACIP_MAX_SESSION_STRING_LEN - 1);
335 memcpy(session_string, reserved_str, len);
336 session_string[len] = '\0';
337
338 if (!is_session_string(session_string)) {
339 asciichat_error_context_t ctx;
340 if (HAS_ERRNO(&ctx)) {
341 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid session string: %s (%s)", session_string, ctx.context_message);
342 }
343 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid session string: %s", session_string);
344 }
345 } else {
346 asciichat_error_t result = acds_string_generate(session_string, sizeof(session_string));
347 if (result != ASCIICHAT_OK) {
348 return result;
349 }
350 }
351
352 // Generate session ID
353 uint8_t session_id[16];
354 generate_uuid(session_id);
355
356 // Set timestamps
357 uint64_t now = database_get_current_time_ms();
358 uint64_t expires_at = now + ACIP_SESSION_EXPIRATION_MS;
359
360 // Calculate max_participants
361 uint8_t max_participants =
362 req->max_participants > 0 && req->max_participants <= MAX_PARTICIPANTS ? req->max_participants : MAX_PARTICIPANTS;
363
364 // Insert into database
365 const char *sql = "INSERT INTO sessions "
366 "(session_string, session_id, host_pubkey, password_hash, max_participants, "
367 "current_participants, capabilities, has_password, expose_ip_publicly, "
368 "session_type, server_address, server_port, created_at, expires_at, last_activity_at) "
369 "VALUES (?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
370
371 sqlite3_stmt *stmt = NULL;
372 int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
373 if (rc != SQLITE_OK) {
374 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare session insert: %s", sqlite3_errmsg(db));
375 }
376
377 log_info("DATABASE_SESSION_CREATE: expose_ip_publicly=%d, server_address='%s' server_port=%u, session_type=%u, "
378 "has_password=%u",
379 req->expose_ip_publicly, req->server_address, req->server_port, req->session_type, req->has_password);
380 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
381 sqlite3_bind_blob(stmt, 2, session_id, 16, SQLITE_STATIC);
382 sqlite3_bind_blob(stmt, 3, req->identity_pubkey, 32, SQLITE_STATIC);
383 sqlite3_bind_text(stmt, 4, req->has_password ? (const char *)req->password_hash : NULL, -1, SQLITE_STATIC);
384 sqlite3_bind_int(stmt, 5, max_participants);
385 sqlite3_bind_int(stmt, 6, req->capabilities);
386 sqlite3_bind_int(stmt, 7, req->has_password ? 1 : 0);
387 sqlite3_bind_int(stmt, 8, req->expose_ip_publicly ? 1 : 0);
388 sqlite3_bind_int(stmt, 9, req->session_type);
389 sqlite3_bind_text(stmt, 10, req->server_address, -1, SQLITE_STATIC);
390 sqlite3_bind_int(stmt, 11, req->server_port);
391 sqlite3_bind_int64(stmt, 12, (sqlite3_int64)now);
392 sqlite3_bind_int64(stmt, 13, (sqlite3_int64)expires_at);
393 sqlite3_bind_int64(stmt, 14, (sqlite3_int64)now); // last_activity_at = created_at
394
395 rc = sqlite3_step(stmt);
396 sqlite3_finalize(stmt);
397
398 if (rc != SQLITE_DONE) {
399 if (rc == SQLITE_CONSTRAINT) {
400 return SET_ERRNO(ERROR_INVALID_STATE, "Session string already exists: %s", session_string);
401 }
402 return SET_ERRNO(ERROR_CONFIG, "Failed to insert session: %s", sqlite3_errmsg(db));
403 }
404
405 // Fill response
406 resp->session_string_len = (uint8_t)strlen(session_string);
407 SAFE_STRNCPY(resp->session_string, session_string, sizeof(resp->session_string));
408 memcpy(resp->session_id, session_id, 16);
409 resp->expires_at = expires_at;
410 resp->stun_count = config->stun_count;
411 resp->turn_count = config->turn_count;
412
413 log_info("Session created: %s (session_id=%02x%02x%02x%02x..., max_participants=%d, has_password=%d)", session_string,
414 session_id[0], session_id[1], session_id[2], session_id[3], max_participants, req->has_password ? 1 : 0);
415
416 return ASCIICHAT_OK;
417}
418
419asciichat_error_t database_session_lookup(sqlite3 *db, const char *session_string, const acds_config_t *config,
420 acip_session_info_t *resp) {
421 if (!db || !session_string || !config || !resp) {
422 return SET_ERRNO(ERROR_INVALID_PARAM, "db, session_string, config, or resp is NULL");
423 }
424
425 memset(resp, 0, sizeof(*resp));
426
427 const char *sql = "SELECT session_string, session_id, host_pubkey, password_hash, "
428 "max_participants, current_participants, capabilities, has_password, "
429 "expose_ip_publicly, session_type, server_address, server_port, "
430 "created_at, expires_at, last_activity_at FROM sessions WHERE session_string = ?";
431
432 sqlite3_stmt *stmt = NULL;
433 int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
434 if (rc != SQLITE_OK) {
435 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare session lookup: %s", sqlite3_errmsg(db));
436 }
437
438 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
439
440 rc = sqlite3_step(stmt);
441 if (rc != SQLITE_ROW) {
442 sqlite3_finalize(stmt);
443 resp->found = 0;
444 log_debug("Session lookup failed: %s (not found)", session_string);
445 return ASCIICHAT_OK;
446 }
447
448 // Load session data
449 resp->found = 1;
450
451 const void *blob = sqlite3_column_blob(stmt, 1);
452 if (blob) {
453 memcpy(resp->session_id, blob, 16);
454 }
455
456 blob = sqlite3_column_blob(stmt, 2);
457 if (blob) {
458 memcpy(resp->host_pubkey, blob, 32);
459 }
460
461 resp->max_participants = (uint8_t)sqlite3_column_int(stmt, 4);
462 resp->current_participants = (uint8_t)sqlite3_column_int(stmt, 5);
463 resp->capabilities = (uint8_t)sqlite3_column_int(stmt, 6);
464 resp->has_password = sqlite3_column_int(stmt, 7) != 0;
465 resp->session_type = (uint8_t)sqlite3_column_int(stmt, 9);
466 resp->created_at = (uint64_t)sqlite3_column_int64(stmt, 12);
467 resp->expires_at = (uint64_t)sqlite3_column_int64(stmt, 13);
468
469 // ACDS policy flags
470 resp->require_server_verify = config->require_server_verify ? 1 : 0;
471 resp->require_client_verify = config->require_client_verify ? 1 : 0;
472
473 sqlite3_finalize(stmt);
474
475 log_debug("Session lookup: %s (found, participants=%d/%d)", session_string, resp->current_participants,
476 resp->max_participants);
477
478 return ASCIICHAT_OK;
479}
480
481asciichat_error_t database_session_join(sqlite3 *db, const acip_session_join_t *req, const acds_config_t *config,
482 acip_session_joined_t *resp) {
483 if (!db || !req || !config || !resp) {
484 return SET_ERRNO(ERROR_INVALID_PARAM, "db, req, config, or resp is NULL");
485 }
486
487 memset(resp, 0, sizeof(*resp));
488 resp->success = 0;
489
490 // Extract session string
491 char session_string[ACIP_MAX_SESSION_STRING_LEN] = {0};
492 size_t len = req->session_string_len < (ACIP_MAX_SESSION_STRING_LEN - 1) ? req->session_string_len
493 : (ACIP_MAX_SESSION_STRING_LEN - 1);
494 memcpy(session_string, req->session_string, len);
495 session_string[len] = '\0';
496
497 // Find session
498 session_entry_t *session = database_session_find_by_string(db, session_string);
499 if (!session) {
500 resp->error_code = ACIP_ERROR_SESSION_NOT_FOUND;
501 SAFE_STRNCPY(resp->error_message, "Session not found", sizeof(resp->error_message));
502 log_warn("Session join failed: %s (not found)", session_string);
503 return ASCIICHAT_OK;
504 }
505
506 // Check if session is full
507 if (session->current_participants >= session->max_participants) {
508 session_entry_destroy(session);
509 resp->error_code = ACIP_ERROR_SESSION_FULL;
510 SAFE_STRNCPY(resp->error_message, "Session is full", sizeof(resp->error_message));
511 log_warn("Session join failed: %s (full)", session_string);
512 return ASCIICHAT_OK;
513 }
514
515 // Verify password if required
516 if (session->has_password && req->has_password) {
517 if (!verify_password(req->password, session->password_hash)) {
518 session_entry_destroy(session);
519 resp->error_code = ACIP_ERROR_INVALID_PASSWORD;
520 SAFE_STRNCPY(resp->error_message, "Invalid password", sizeof(resp->error_message));
521 log_warn("Session join failed: %s (invalid password)", session_string);
522 return ASCIICHAT_OK;
523 }
524 } else if (session->has_password && !req->has_password) {
525 session_entry_destroy(session);
526 resp->error_code = ACIP_ERROR_INVALID_PASSWORD;
527 SAFE_STRNCPY(resp->error_message, "Password required", sizeof(resp->error_message));
528 log_warn("Session join failed: %s (password required)", session_string);
529 return ASCIICHAT_OK;
530 }
531
532 // Generate participant ID
533 uint8_t participant_id[16];
534 generate_uuid(participant_id);
535 uint64_t now = database_get_current_time_ms();
536
537 // Begin transaction
538 char *err_msg = NULL;
539 int rc = sqlite3_exec(db, "BEGIN IMMEDIATE;", NULL, NULL, &err_msg);
540 if (rc != SQLITE_OK) {
541 session_entry_destroy(session);
542 log_error("Failed to begin transaction: %s", err_msg ? err_msg : "unknown");
543 sqlite3_free(err_msg);
544 return SET_ERRNO(ERROR_CONFIG, "Failed to begin transaction");
545 }
546
547 // Insert participant
548 const char *insert_sql = "INSERT INTO participants (participant_id, session_string, identity_pubkey, joined_at) "
549 "VALUES (?, ?, ?, ?)";
550 sqlite3_stmt *stmt = NULL;
551 rc = sqlite3_prepare_v2(db, insert_sql, -1, &stmt, NULL);
552 if (rc != SQLITE_OK) {
553 sqlite3_exec(db, "ROLLBACK;", NULL, NULL, NULL);
554 session_entry_destroy(session);
555 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare participant insert: %s", sqlite3_errmsg(db));
556 }
557
558 sqlite3_bind_blob(stmt, 1, participant_id, 16, SQLITE_STATIC);
559 sqlite3_bind_text(stmt, 2, session->session_string, -1, SQLITE_STATIC);
560 sqlite3_bind_blob(stmt, 3, req->identity_pubkey, 32, SQLITE_STATIC);
561 sqlite3_bind_int64(stmt, 4, (sqlite3_int64)now);
562
563 rc = sqlite3_step(stmt);
564 sqlite3_finalize(stmt);
565
566 if (rc != SQLITE_DONE) {
567 sqlite3_exec(db, "ROLLBACK;", NULL, NULL, NULL);
568 session_entry_destroy(session);
569 return SET_ERRNO(ERROR_CONFIG, "Failed to insert participant: %s", sqlite3_errmsg(db));
570 }
571
572 // Update participant count and last activity
573 const char *update_sql = "UPDATE sessions SET current_participants = current_participants + 1, "
574 "last_activity_at = ? WHERE session_string = ?";
575 rc = sqlite3_prepare_v2(db, update_sql, -1, &stmt, NULL);
576 if (rc == SQLITE_OK) {
577 sqlite3_bind_int64(stmt, 1, (sqlite3_int64)now);
578 sqlite3_bind_text(stmt, 2, session->session_string, -1, SQLITE_STATIC);
579 sqlite3_step(stmt);
580 sqlite3_finalize(stmt);
581 }
582
583 // Set initiator_id if this is the first participant (for discovery mode tiebreaker)
584 bool is_first_participant = (session->current_participants == 0);
585 bool is_zero_initiator = true;
586 for (int i = 0; i < 16; i++) {
587 if (session->initiator_id[i] != 0) {
588 is_zero_initiator = false;
589 break;
590 }
591 }
592
593 if (is_first_participant || is_zero_initiator) {
594 const char *initiator_sql = "UPDATE sessions SET initiator_id = ? WHERE session_string = ?";
595 rc = sqlite3_prepare_v2(db, initiator_sql, -1, &stmt, NULL);
596 if (rc == SQLITE_OK) {
597 sqlite3_bind_blob(stmt, 1, participant_id, 16, SQLITE_STATIC);
598 sqlite3_bind_text(stmt, 2, session->session_string, -1, SQLITE_STATIC);
599 sqlite3_step(stmt);
600 sqlite3_finalize(stmt);
601 memcpy(session->initiator_id, participant_id, 16);
602 log_debug("Set initiator_id to new participant %02x%02x...", participant_id[0], participant_id[1]);
603 }
604 }
605
606 // Commit transaction
607 rc = sqlite3_exec(db, "COMMIT;", NULL, NULL, &err_msg);
608 if (rc != SQLITE_OK) {
609 log_error("Failed to commit transaction: %s", err_msg ? err_msg : "unknown");
610 sqlite3_free(err_msg);
611 sqlite3_exec(db, "ROLLBACK;", NULL, NULL, NULL);
612 session_entry_destroy(session);
613 return SET_ERRNO(ERROR_CONFIG, "Failed to commit transaction");
614 }
615
616 // Fill response
617 resp->success = 1;
618 resp->error_code = ACIP_ERROR_NONE;
619 memcpy(resp->participant_id, participant_id, 16);
620 memcpy(resp->session_id, session->session_id, 16);
621
622 // Discovery mode host negotiation fields
623 memcpy(resp->initiator_id, session->initiator_id, 16);
624 resp->host_established = session->host_established ? 1 : 0;
625 if (session->host_established) {
626 memcpy(resp->host_id, session->host_participant_id, 16);
627 }
628 // peer_count = current_participants (excluding self) that need to negotiate
629 resp->peer_count = session->current_participants; // Will be incremented after this returns
630
631 log_debug("SESSION_JOIN response: session_id=%02x%02x%02x%02x..., participant_id=%02x%02x%02x%02x..., "
632 "initiator=%02x%02x..., host_established=%d, peer_count=%d",
633 resp->session_id[0], resp->session_id[1], resp->session_id[2], resp->session_id[3], resp->participant_id[0],
634 resp->participant_id[1], resp->participant_id[2], resp->participant_id[3], resp->initiator_id[0],
635 resp->initiator_id[1], resp->host_established, resp->peer_count);
636
637 // IP disclosure logic
638 bool reveal_ip = false;
639 log_info("DATABASE_SESSION_JOIN: has_password=%d, expose_ip_publicly=%d, server_address='%s'", session->has_password,
640 session->expose_ip_publicly, session->server_address);
641 if (session->has_password) {
642 reveal_ip = true; // Password was verified
643 } else if (session->expose_ip_publicly) {
644 reveal_ip = true; // Explicit opt-in
645 }
646 log_info("DATABASE_SESSION_JOIN: reveal_ip=%d", reveal_ip);
647
648 if (reveal_ip) {
649 SAFE_STRNCPY(resp->server_address, session->server_address, sizeof(resp->server_address));
650 resp->server_port = session->server_port;
651 resp->session_type = session->session_type;
652
653 // Generate TURN credentials for WebRTC sessions
654 if (session->session_type == SESSION_TYPE_WEBRTC && config->turn_secret[0] != '\0') {
655 turn_credentials_t turn_creds;
656 asciichat_error_t turn_result =
657 turn_generate_credentials(session_string, config->turn_secret, 86400, &turn_creds);
658 if (turn_result == ASCIICHAT_OK) {
659 SAFE_STRNCPY(resp->turn_username, turn_creds.username, sizeof(resp->turn_username));
660 SAFE_STRNCPY(resp->turn_password, turn_creds.password, sizeof(resp->turn_password));
661 log_debug("Generated TURN credentials for session %s", session_string);
662 }
663 }
664
665 log_info("Participant joined session %s (participants=%d/%d, server=%s:%d, type=%s)", session_string,
666 session->current_participants + 1, session->max_participants, resp->server_address, resp->server_port,
667 session->session_type == SESSION_TYPE_WEBRTC ? "WebRTC" : "DirectTCP");
668 } else {
669 log_info("Participant joined session %s (participants=%d/%d, IP WITHHELD)", session_string,
670 session->current_participants + 1, session->max_participants);
671 }
672
673 session_entry_destroy(session);
674 return ASCIICHAT_OK;
675}
676
677asciichat_error_t database_session_leave(sqlite3 *db, const uint8_t session_id[16], const uint8_t participant_id[16]) {
678 if (!db || !session_id || !participant_id) {
679 return SET_ERRNO(ERROR_INVALID_PARAM, "db, session_id, or participant_id is NULL");
680 }
681
682 // Look up session_string from session_id
683 char session_string[SESSION_STRING_BUFFER_SIZE] = {0};
684 const char *lookup_sql = "SELECT session_string FROM sessions WHERE session_id = ?";
685 sqlite3_stmt *stmt = NULL;
686 int rc = sqlite3_prepare_v2(db, lookup_sql, -1, &stmt, NULL);
687 if (rc != SQLITE_OK) {
688 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare session lookup: %s", sqlite3_errmsg(db));
689 }
690
691 sqlite3_bind_blob(stmt, 1, session_id, 16, SQLITE_STATIC);
692 rc = sqlite3_step(stmt);
693 if (rc != SQLITE_ROW) {
694 sqlite3_finalize(stmt);
695 return SET_ERRNO(ERROR_INVALID_STATE, "Session not found");
696 }
697
698 const char *str = (const char *)sqlite3_column_text(stmt, 0);
699 if (str) {
700 SAFE_STRNCPY(session_string, str, sizeof(session_string));
701 }
702 sqlite3_finalize(stmt);
703
704 if (session_string[0] == '\0') {
705 return SET_ERRNO(ERROR_INVALID_STATE, "Session string is empty");
706 }
707
708 // Begin transaction
709 char *err_msg = NULL;
710 rc = sqlite3_exec(db, "BEGIN IMMEDIATE;", NULL, NULL, &err_msg);
711 if (rc != SQLITE_OK) {
712 log_error("Failed to begin transaction: %s", err_msg ? err_msg : "unknown");
713 sqlite3_free(err_msg);
714 return SET_ERRNO(ERROR_CONFIG, "Failed to begin transaction");
715 }
716
717 // Delete participant
718 const char *del_sql = "DELETE FROM participants WHERE participant_id = ? AND session_string = ?";
719 rc = sqlite3_prepare_v2(db, del_sql, -1, &stmt, NULL);
720 if (rc != SQLITE_OK) {
721 sqlite3_exec(db, "ROLLBACK;", NULL, NULL, NULL);
722 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare participant delete: %s", sqlite3_errmsg(db));
723 }
724
725 sqlite3_bind_blob(stmt, 1, participant_id, 16, SQLITE_STATIC);
726 sqlite3_bind_text(stmt, 2, session_string, -1, SQLITE_STATIC);
727 rc = sqlite3_step(stmt);
728 sqlite3_finalize(stmt);
729
730 if (rc != SQLITE_DONE) {
731 sqlite3_exec(db, "ROLLBACK;", NULL, NULL, NULL);
732 return SET_ERRNO(ERROR_CONFIG, "Failed to delete participant: %s", sqlite3_errmsg(db));
733 }
734
735 int changes = sqlite3_changes(db);
736 if (changes == 0) {
737 sqlite3_exec(db, "ROLLBACK;", NULL, NULL, NULL);
738 return SET_ERRNO(ERROR_INVALID_STATE, "Participant not in session");
739 }
740
741 uint64_t now = database_get_current_time_ms();
742
743 // Decrement participant count and update last activity
744 const char *update_sql = "UPDATE sessions SET current_participants = current_participants - 1, "
745 "last_activity_at = ? WHERE session_string = ? AND current_participants > 0";
746 rc = sqlite3_prepare_v2(db, update_sql, -1, &stmt, NULL);
747 if (rc == SQLITE_OK) {
748 sqlite3_bind_int64(stmt, 1, (sqlite3_int64)now);
749 sqlite3_bind_text(stmt, 2, session_string, -1, SQLITE_STATIC);
750 sqlite3_step(stmt);
751 sqlite3_finalize(stmt);
752 }
753
754 // Check if session is now empty and delete if so
755 const char *check_sql = "SELECT current_participants FROM sessions WHERE session_string = ?";
756 rc = sqlite3_prepare_v2(db, check_sql, -1, &stmt, NULL);
757 if (rc == SQLITE_OK) {
758 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
759 if (sqlite3_step(stmt) == SQLITE_ROW) {
760 int count = sqlite3_column_int(stmt, 0);
761
762 if (count <= 0) {
763 log_info("Session %s has no participants, deleting", session_string);
764 sqlite3_finalize(stmt);
765
766 // Delete empty session
767 const char *del_session_sql = "DELETE FROM sessions WHERE session_string = ?";
768 rc = sqlite3_prepare_v2(db, del_session_sql, -1, &stmt, NULL);
769 if (rc == SQLITE_OK) {
770 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
771 sqlite3_step(stmt);
772 sqlite3_finalize(stmt);
773 }
774 stmt = NULL;
775 } else {
776 log_info("Participant left session %s (participants=%d remaining)", session_string, count);
777 }
778 }
779 if (stmt) {
780 sqlite3_finalize(stmt);
781 }
782 }
783
784 // Commit transaction
785 rc = sqlite3_exec(db, "COMMIT;", NULL, NULL, &err_msg);
786 if (rc != SQLITE_OK) {
787 log_error("Failed to commit transaction: %s", err_msg ? err_msg : "unknown");
788 sqlite3_free(err_msg);
789 sqlite3_exec(db, "ROLLBACK;", NULL, NULL, NULL);
790 return SET_ERRNO(ERROR_CONFIG, "Failed to commit transaction");
791 }
792
793 return ASCIICHAT_OK;
794}
795
796session_entry_t *database_session_find_by_id(sqlite3 *db, const uint8_t session_id[16]) {
797 if (!db || !session_id) {
798 return NULL;
799 }
800
801 static const char sql[] = SELECT_SESSION_BASE " WHERE session_id = ?";
802
803 sqlite3_stmt *stmt = NULL;
804 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
805 return NULL;
806 }
807
808 sqlite3_bind_blob(stmt, 1, session_id, 16, SQLITE_STATIC);
809
810 session_entry_t *session = NULL;
811 if (sqlite3_step(stmt) == SQLITE_ROW) {
812 session = load_session_from_row(stmt);
813 if (session) {
814 load_session_participants(db, session);
815 }
816 }
817
818 sqlite3_finalize(stmt);
819 return session;
820}
821
822session_entry_t *database_session_find_by_string(sqlite3 *db, const char *session_string) {
823 if (!db || !session_string) {
824 return NULL;
825 }
826
827 static const char sql[] = SELECT_SESSION_BASE " WHERE session_string = ?";
828
829 sqlite3_stmt *stmt = NULL;
830 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
831 return NULL;
832 }
833
834 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
835
836 session_entry_t *session = NULL;
837 if (sqlite3_step(stmt) == SQLITE_ROW) {
838 session = load_session_from_row(stmt);
839 if (session) {
840 load_session_participants(db, session);
841 }
842 }
843
844 sqlite3_finalize(stmt);
845 return session;
846}
847
848void database_session_cleanup_expired(sqlite3 *db) {
849 if (!db) {
850 return;
851 }
852
853 uint64_t now = database_get_current_time_ms();
854 // 3 hours in milliseconds
855 uint64_t inactivity_threshold = 3ULL * SEC_PER_HOUR * MS_PER_SEC_INT;
856 uint64_t cutoff_time = now - inactivity_threshold;
857
858 // Log sessions about to be deleted
859 const char *log_sql = "SELECT session_string, last_activity_at FROM sessions WHERE last_activity_at < ?";
860 sqlite3_stmt *stmt = NULL;
861 if (sqlite3_prepare_v2(db, log_sql, -1, &stmt, NULL) == SQLITE_OK) {
862 sqlite3_bind_int64(stmt, 1, (sqlite3_int64)cutoff_time);
863 while (sqlite3_step(stmt) == SQLITE_ROW) {
864 const char *session_string = (const char *)sqlite3_column_text(stmt, 0);
865 uint64_t last_activity = (uint64_t)sqlite3_column_int64(stmt, 1);
866 uint64_t inactive_ms = now - last_activity;
867 uint64_t inactive_hours = inactive_ms / (SEC_PER_HOUR * MS_PER_SEC_INT);
868 log_info("Session %s inactive for %lu hours, deleting", session_string ? session_string : "<unknown>",
869 inactive_hours);
870 }
871 sqlite3_finalize(stmt);
872 }
873
874 // Delete inactive sessions (CASCADE deletes participants)
875 const char *del_sql = "DELETE FROM sessions WHERE last_activity_at < ?";
876 if (sqlite3_prepare_v2(db, del_sql, -1, &stmt, NULL) == SQLITE_OK) {
877 sqlite3_bind_int64(stmt, 1, (sqlite3_int64)cutoff_time);
878 sqlite3_step(stmt);
879 int deleted = sqlite3_changes(db);
880 sqlite3_finalize(stmt);
881
882 if (deleted > 0) {
883 log_info("Cleaned up %d inactive sessions (>3 hours)", deleted);
884 }
885 }
886}
887
888asciichat_error_t database_session_update_host(sqlite3 *db, const uint8_t session_id[16],
889 const uint8_t host_participant_id[16], const char *host_address,
890 uint16_t host_port, uint8_t connection_type) {
891 if (!db || !session_id || !host_participant_id) {
892 return SET_ERRNO(ERROR_INVALID_PARAM, "db, session_id, or host_participant_id is NULL");
893 }
894
895 uint64_t now = database_get_current_time_ms();
896
897 const char *sql = "UPDATE sessions SET "
898 "host_established = 1, "
899 "host_participant_id = ?, "
900 "host_address = ?, "
901 "host_port = ?, "
902 "host_connection_type = ?, "
903 "last_activity_at = ? "
904 "WHERE session_id = ?";
905
906 sqlite3_stmt *stmt = NULL;
907 int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
908 if (rc != SQLITE_OK) {
909 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare host update: %s", sqlite3_errmsg(db));
910 }
911
912 sqlite3_bind_blob(stmt, 1, host_participant_id, 16, SQLITE_STATIC);
913 sqlite3_bind_text(stmt, 2, host_address ? host_address : "", -1, SQLITE_STATIC);
914 sqlite3_bind_int(stmt, 3, host_port);
915 sqlite3_bind_int(stmt, 4, connection_type);
916 sqlite3_bind_int64(stmt, 5, (sqlite3_int64)now);
917 sqlite3_bind_blob(stmt, 6, session_id, 16, SQLITE_STATIC);
918
919 rc = sqlite3_step(stmt);
920 sqlite3_finalize(stmt);
921
922 if (rc != SQLITE_DONE) {
923 return SET_ERRNO(ERROR_CONFIG, "Failed to update host: %s", sqlite3_errmsg(db));
924 }
925
926 int changes = sqlite3_changes(db);
927 if (changes == 0) {
928 return SET_ERRNO(ERROR_INVALID_STATE, "Session not found for host update");
929 }
930
931 log_info("Session host updated: participant=%02x%02x..., address=%s:%u, type=%d", host_participant_id[0],
932 host_participant_id[1], host_address ? host_address : "(none)", host_port, connection_type);
933
934 return ASCIICHAT_OK;
935}
936
937asciichat_error_t database_session_clear_host(sqlite3 *db, const uint8_t session_id[16]) {
938 if (!db || !session_id) {
939 return SET_ERRNO(ERROR_INVALID_PARAM, "db or session_id is NULL");
940 }
941
942 const char *sql = "UPDATE sessions SET "
943 "host_established = 0, "
944 "host_participant_id = NULL, "
945 "host_address = NULL, "
946 "host_port = 0, "
947 "host_connection_type = 0 "
948 "WHERE session_id = ?";
949
950 sqlite3_stmt *stmt = NULL;
951 int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
952 if (rc != SQLITE_OK) {
953 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare host clear: %s", sqlite3_errmsg(db));
954 }
955
956 sqlite3_bind_blob(stmt, 1, session_id, 16, SQLITE_STATIC);
957
958 rc = sqlite3_step(stmt);
959 sqlite3_finalize(stmt);
960
961 if (rc != SQLITE_DONE) {
962 return SET_ERRNO(ERROR_CONFIG, "Failed to clear host: %s", sqlite3_errmsg(db));
963 }
964
965 int changes = sqlite3_changes(db);
966 if (changes == 0) {
967 return SET_ERRNO(ERROR_INVALID_STATE, "Session not found for host clear");
968 }
969
970 log_info("Session host cleared (session=%02x%02x...) - ready for migration", session_id[0], session_id[1]);
971
972 return ASCIICHAT_OK;
973}
974
975asciichat_error_t database_session_start_migration(sqlite3 *db, const uint8_t session_id[16]) {
976 if (!db || !session_id) {
977 return SET_ERRNO(ERROR_INVALID_PARAM, "db or session_id is NULL");
978 }
979
980 uint64_t now_ns = time_get_realtime_ns();
981
982 const char *sql = "UPDATE sessions SET "
983 "in_migration = 1, "
984 "migration_start_ns = ? "
985 "WHERE session_id = ?";
986
987 sqlite3_stmt *stmt = NULL;
988 int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
989 if (rc != SQLITE_OK) {
990 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare migration start: %s", sqlite3_errmsg(db));
991 }
992
993 sqlite3_bind_int64(stmt, 1, (sqlite3_int64)now_ns);
994 sqlite3_bind_blob(stmt, 2, session_id, 16, SQLITE_STATIC);
995
996 rc = sqlite3_step(stmt);
997 sqlite3_finalize(stmt);
998
999 if (rc != SQLITE_DONE) {
1000 return SET_ERRNO(ERROR_CONFIG, "Failed to start migration: %s", sqlite3_errmsg(db));
1001 }
1002
1003 int changes = sqlite3_changes(db);
1004 if (changes == 0) {
1005 return SET_ERRNO(ERROR_INVALID_STATE, "Session not found for migration start");
1006 }
1007
1008 log_info("Session migration started (session=%02x%02x..., start_ns=%" PRIu64 ")", session_id[0], session_id[1],
1009 now_ns);
1010
1011 return ASCIICHAT_OK;
1012}
1013
1014bool database_session_is_migration_ready(sqlite3 *db, const uint8_t session_id[16], uint64_t migration_window_ms) {
1015 if (!db || !session_id) {
1016 return false;
1017 }
1018
1019 uint64_t now_ns = time_get_realtime_ns();
1020 const char *sql = "SELECT in_migration, migration_start_ns FROM sessions WHERE session_id = ?";
1021
1022 sqlite3_stmt *stmt = NULL;
1023 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
1024 return false;
1025 }
1026
1027 sqlite3_bind_blob(stmt, 1, session_id, 16, SQLITE_STATIC);
1028
1029 bool ready = false;
1030 if (sqlite3_step(stmt) == SQLITE_ROW) {
1031 int in_migration = sqlite3_column_int(stmt, 0);
1032 uint64_t migration_start_ns = (uint64_t)sqlite3_column_int64(stmt, 1);
1033
1034 if (in_migration) {
1035 uint64_t elapsed_ns = now_ns - migration_start_ns;
1036 uint64_t migration_window_ns = migration_window_ms * NS_PER_MS_INT;
1037 if (elapsed_ns >= migration_window_ns) {
1038 ready = true;
1039 log_debug("Migration window complete (session=%02x%02x..., elapsed=%lu ns, window=%lu ns)", session_id[0],
1040 session_id[1], elapsed_ns, migration_window_ns);
1041 }
1042 }
1043 }
1044
1045 sqlite3_finalize(stmt);
1046 return ready;
1047}
1048
1049// ============================================================================
1050// Multi-Key Management Functions
1051// ============================================================================
1052
1053asciichat_error_t database_session_add_key(sqlite3 *db, const char *session_string, const uint8_t identity_pubkey[32],
1054 uint32_t key_version) {
1055 if (!db || !session_string || !identity_pubkey) {
1056 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for add_key");
1057 }
1058
1059 const char *sql = "INSERT OR IGNORE INTO session_keys (session_string, identity_pubkey, key_version, added_at) "
1060 "VALUES (?, ?, ?, ?)";
1061
1062 sqlite3_stmt *stmt = NULL;
1063 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
1064 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare add_key statement: %s", sqlite3_errmsg(db));
1065 }
1066
1067 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
1068 sqlite3_bind_blob(stmt, 2, identity_pubkey, 32, SQLITE_STATIC);
1069 sqlite3_bind_int(stmt, 3, key_version);
1070 sqlite3_bind_int64(stmt, 4, database_get_current_time_ms());
1071
1072 int rc = sqlite3_step(stmt);
1073 sqlite3_finalize(stmt);
1074
1075 if (rc != SQLITE_DONE) {
1076 return SET_ERRNO(ERROR_CONFIG, "Failed to add session key: %s", sqlite3_errmsg(db));
1077 }
1078
1079 log_debug("Added key to session %s (version=%u)", session_string, key_version);
1080 return ASCIICHAT_OK;
1081}
1082
1083asciichat_error_t database_session_revoke_key(sqlite3 *db, const char *session_string,
1084 const uint8_t identity_pubkey[32]) {
1085 if (!db || !session_string || !identity_pubkey) {
1086 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for revoke_key");
1087 }
1088
1089 const char *sql = "UPDATE session_keys SET revoked = 1 WHERE session_string = ? AND identity_pubkey = ?";
1090
1091 sqlite3_stmt *stmt = NULL;
1092 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
1093 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare revoke_key statement: %s", sqlite3_errmsg(db));
1094 }
1095
1096 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
1097 sqlite3_bind_blob(stmt, 2, identity_pubkey, 32, SQLITE_STATIC);
1098
1099 int rc = sqlite3_step(stmt);
1100 sqlite3_finalize(stmt);
1101
1102 if (rc != SQLITE_DONE) {
1103 return SET_ERRNO(ERROR_CONFIG, "Failed to revoke session key: %s", sqlite3_errmsg(db));
1104 }
1105
1106 log_debug("Revoked key from session %s", session_string);
1107 return ASCIICHAT_OK;
1108}
1109
1110bool database_session_verify_key(sqlite3 *db, const char *session_string, const uint8_t identity_pubkey[32]) {
1111 if (!db || !session_string || !identity_pubkey) {
1112 return false;
1113 }
1114
1115 const char *sql = "SELECT 1 FROM session_keys WHERE session_string = ? AND identity_pubkey = ? AND revoked = 0";
1116
1117 sqlite3_stmt *stmt = NULL;
1118 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
1119 return false;
1120 }
1121
1122 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
1123 sqlite3_bind_blob(stmt, 2, identity_pubkey, 32, SQLITE_STATIC);
1124
1125 bool valid = (sqlite3_step(stmt) == SQLITE_ROW);
1126 sqlite3_finalize(stmt);
1127
1128 return valid;
1129}
1130
1131asciichat_error_t database_session_get_keys(sqlite3 *db, const char *session_string, uint8_t (*keys_out)[32],
1132 size_t max_keys, size_t *count_out) {
1133 if (!db || !session_string || !keys_out || !count_out) {
1134 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for get_keys");
1135 }
1136
1137 const char *sql = "SELECT identity_pubkey FROM session_keys WHERE session_string = ? AND revoked = 0 "
1138 "ORDER BY key_version ASC";
1139
1140 sqlite3_stmt *stmt = NULL;
1141 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
1142 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare get_keys statement: %s", sqlite3_errmsg(db));
1143 }
1144
1145 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
1146
1147 size_t count = 0;
1148 while (sqlite3_step(stmt) == SQLITE_ROW && count < max_keys) {
1149 const void *blob = sqlite3_column_blob(stmt, 0);
1150 if (blob && sqlite3_column_bytes(stmt, 0) == 32) {
1151 memcpy(keys_out[count], blob, 32);
1152 count++;
1153 }
1154 }
1155
1156 sqlite3_finalize(stmt);
1157 *count_out = count;
1158
1159 log_debug("Retrieved %zu keys for session %s", count, session_string);
1160 return ASCIICHAT_OK;
1161}
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)
asciichat_error_t database_session_clear_host(sqlite3 *db, const uint8_t session_id[16])
asciichat_error_t database_session_lookup(sqlite3 *db, const char *session_string, const acds_config_t *config, acip_session_info_t *resp)
asciichat_error_t database_session_add_key(sqlite3 *db, const char *session_string, const uint8_t identity_pubkey[32], uint32_t key_version)
asciichat_error_t database_session_join(sqlite3 *db, const acip_session_join_t *req, const acds_config_t *config, acip_session_joined_t *resp)
asciichat_error_t database_session_start_migration(sqlite3 *db, const uint8_t session_id[16])
asciichat_error_t database_session_leave(sqlite3 *db, const uint8_t session_id[16], const uint8_t participant_id[16])
void database_close(sqlite3 *db)
session_entry_t * database_session_find_by_id(sqlite3 *db, const uint8_t session_id[16])
#define SELECT_SESSION_BASE
session_entry_t * database_session_find_by_string(sqlite3 *db, const char *session_string)
bool database_session_is_migration_ready(sqlite3 *db, const uint8_t session_id[16], uint64_t migration_window_ms)
asciichat_error_t database_session_create(sqlite3 *db, const acip_session_create_t *req, const acds_config_t *config, acip_session_created_t *resp)
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)
asciichat_error_t database_init(const char *db_path, sqlite3 **db)
bool database_session_verify_key(sqlite3 *db, const char *session_string, const uint8_t identity_pubkey[32])
asciichat_error_t database_session_revoke_key(sqlite3 *db, const char *session_string, const uint8_t identity_pubkey[32])
void database_session_cleanup_expired(sqlite3 *db)
bool is_session_string(const char *str)
asciichat_error_t acds_string_generate(char *output, size_t output_size)
void session_entry_destroy(session_entry_t *entry)
Free a session entry and all its resources.
#define MAX_PARTICIPANTS
Definition session.h:33
uint8_t session_id[16]
uint8_t participant_id[16]
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.
asciichat_error_t turn_generate_credentials(const char *session_id, const char *secret, uint32_t validity_seconds, turn_credentials_t *out_credentials)
uint64_t time_get_realtime_ns(void)
Definition util/time.c:59

Function Documentation

◆ database_close()

void database_close ( sqlite3 *  db)

Definition at line 310 of file discovery/database.c.

310 {
311 if (!db) {
312 return;
313 }
314 sqlite3_close(db);
315 log_debug("Database closed");
316}

Referenced by acds_server_init(), and acds_server_shutdown().

◆ database_init()

asciichat_error_t database_init ( const char *  db_path,
sqlite3 **  db 
)

Definition at line 263 of file discovery/database.c.

263 {
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}

Referenced by acds_server_init().

◆ database_session_add_key()

asciichat_error_t database_session_add_key ( sqlite3 *  db,
const char *  session_string,
const uint8_t  identity_pubkey[32],
uint32_t  key_version 
)

Definition at line 1054 of file discovery/database.c.

1055 {
1056 if (!db || !session_string || !identity_pubkey) {
1057 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for add_key");
1058 }
1059
1060 const char *sql = "INSERT OR IGNORE INTO session_keys (session_string, identity_pubkey, key_version, added_at) "
1061 "VALUES (?, ?, ?, ?)";
1062
1063 sqlite3_stmt *stmt = NULL;
1064 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
1065 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare add_key statement: %s", sqlite3_errmsg(db));
1066 }
1067
1068 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
1069 sqlite3_bind_blob(stmt, 2, identity_pubkey, 32, SQLITE_STATIC);
1070 sqlite3_bind_int(stmt, 3, key_version);
1071 sqlite3_bind_int64(stmt, 4, database_get_current_time_ms());
1072
1073 int rc = sqlite3_step(stmt);
1074 sqlite3_finalize(stmt);
1075
1076 if (rc != SQLITE_DONE) {
1077 return SET_ERRNO(ERROR_CONFIG, "Failed to add session key: %s", sqlite3_errmsg(db));
1078 }
1079
1080 log_debug("Added key to session %s (version=%u)", session_string, key_version);
1081 return ASCIICHAT_OK;
1082}

◆ database_session_cleanup_expired()

void database_session_cleanup_expired ( sqlite3 *  db)

Definition at line 849 of file discovery/database.c.

849 {
850 if (!db) {
851 return;
852 }
853
854 uint64_t now = database_get_current_time_ms();
855 // 3 hours in milliseconds
856 uint64_t inactivity_threshold = 3ULL * SEC_PER_HOUR * MS_PER_SEC_INT;
857 uint64_t cutoff_time = now - inactivity_threshold;
858
859 // Log sessions about to be deleted
860 const char *log_sql = "SELECT session_string, last_activity_at FROM sessions WHERE last_activity_at < ?";
861 sqlite3_stmt *stmt = NULL;
862 if (sqlite3_prepare_v2(db, log_sql, -1, &stmt, NULL) == SQLITE_OK) {
863 sqlite3_bind_int64(stmt, 1, (sqlite3_int64)cutoff_time);
864 while (sqlite3_step(stmt) == SQLITE_ROW) {
865 const char *session_string = (const char *)sqlite3_column_text(stmt, 0);
866 uint64_t last_activity = (uint64_t)sqlite3_column_int64(stmt, 1);
867 uint64_t inactive_ms = now - last_activity;
868 uint64_t inactive_hours = inactive_ms / (SEC_PER_HOUR * MS_PER_SEC_INT);
869 log_info("Session %s inactive for %lu hours, deleting", session_string ? session_string : "<unknown>",
870 inactive_hours);
871 }
872 sqlite3_finalize(stmt);
873 }
874
875 // Delete inactive sessions (CASCADE deletes participants)
876 const char *del_sql = "DELETE FROM sessions WHERE last_activity_at < ?";
877 if (sqlite3_prepare_v2(db, del_sql, -1, &stmt, NULL) == SQLITE_OK) {
878 sqlite3_bind_int64(stmt, 1, (sqlite3_int64)cutoff_time);
879 sqlite3_step(stmt);
880 int deleted = sqlite3_changes(db);
881 sqlite3_finalize(stmt);
882
883 if (deleted > 0) {
884 log_info("Cleaned up %d inactive sessions (>3 hours)", deleted);
885 }
886 }
887}

◆ database_session_clear_host()

asciichat_error_t database_session_clear_host ( sqlite3 *  db,
const uint8_t  session_id[16] 
)

Definition at line 938 of file discovery/database.c.

938 {
939 if (!db || !session_id) {
940 return SET_ERRNO(ERROR_INVALID_PARAM, "db or session_id is NULL");
941 }
942
943 const char *sql = "UPDATE sessions SET "
944 "host_established = 0, "
945 "host_participant_id = NULL, "
946 "host_address = NULL, "
947 "host_port = 0, "
948 "host_connection_type = 0 "
949 "WHERE session_id = ?";
950
951 sqlite3_stmt *stmt = NULL;
952 int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
953 if (rc != SQLITE_OK) {
954 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare host clear: %s", sqlite3_errmsg(db));
955 }
956
957 sqlite3_bind_blob(stmt, 1, session_id, 16, SQLITE_STATIC);
958
959 rc = sqlite3_step(stmt);
960 sqlite3_finalize(stmt);
961
962 if (rc != SQLITE_DONE) {
963 return SET_ERRNO(ERROR_CONFIG, "Failed to clear host: %s", sqlite3_errmsg(db));
964 }
965
966 int changes = sqlite3_changes(db);
967 if (changes == 0) {
968 return SET_ERRNO(ERROR_INVALID_STATE, "Session not found for host clear");
969 }
970
971 log_info("Session host cleared (session=%02x%02x...) - ready for migration", session_id[0], session_id[1]);
972
973 return ASCIICHAT_OK;
974}

References session_id.

◆ database_session_create()

asciichat_error_t database_session_create ( sqlite3 *  db,
const acip_session_create_t *  req,
const acds_config_t config,
acip_session_created_t *  resp 
)

Definition at line 322 of file discovery/database.c.

323 {
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
335 : (ACIP_MAX_SESSION_STRING_LEN - 1);
336 memcpy(session_string, reserved_str, len);
337 session_string[len] = '\0';
338
339 if (!is_session_string(session_string)) {
340 asciichat_error_context_t ctx;
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
354 uint8_t session_id[16];
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}

References acds_string_generate(), is_session_string(), MAX_PARTICIPANTS, session_id, acds_config_t::stun_count, and acds_config_t::turn_count.

◆ database_session_find_by_id()

session_entry_t * database_session_find_by_id ( sqlite3 *  db,
const uint8_t  session_id[16] 
)

Definition at line 797 of file discovery/database.c.

797 {
798 if (!db || !session_id) {
799 return NULL;
800 }
801
802 static const char sql[] = SELECT_SESSION_BASE " WHERE session_id = ?";
803
804 sqlite3_stmt *stmt = NULL;
805 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
806 return NULL;
807 }
808
809 sqlite3_bind_blob(stmt, 1, session_id, 16, SQLITE_STATIC);
810
811 session_entry_t *session = NULL;
812 if (sqlite3_step(stmt) == SQLITE_ROW) {
813 session = load_session_from_row(stmt);
814 if (session) {
815 load_session_participants(db, session);
816 }
817 }
818
819 sqlite3_finalize(stmt);
820 return session;
821}

References SELECT_SESSION_BASE, and session_id.

Referenced by signaling_broadcast(), signaling_relay_ice(), and signaling_relay_sdp().

◆ database_session_find_by_string()

session_entry_t * database_session_find_by_string ( sqlite3 *  db,
const char *  session_string 
)

Definition at line 823 of file discovery/database.c.

823 {
824 if (!db || !session_string) {
825 return NULL;
826 }
827
828 static const char sql[] = SELECT_SESSION_BASE " WHERE session_string = ?";
829
830 sqlite3_stmt *stmt = NULL;
831 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
832 return NULL;
833 }
834
835 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
836
837 session_entry_t *session = NULL;
838 if (sqlite3_step(stmt) == SQLITE_ROW) {
839 session = load_session_from_row(stmt);
840 if (session) {
841 load_session_participants(db, session);
842 }
843 }
844
845 sqlite3_finalize(stmt);
846 return session;
847}

References SELECT_SESSION_BASE.

Referenced by database_session_join().

◆ database_session_get_keys()

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 
)

Definition at line 1132 of file discovery/database.c.

1133 {
1134 if (!db || !session_string || !keys_out || !count_out) {
1135 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for get_keys");
1136 }
1137
1138 const char *sql = "SELECT identity_pubkey FROM session_keys WHERE session_string = ? AND revoked = 0 "
1139 "ORDER BY key_version ASC";
1140
1141 sqlite3_stmt *stmt = NULL;
1142 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
1143 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare get_keys statement: %s", sqlite3_errmsg(db));
1144 }
1145
1146 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
1147
1148 size_t count = 0;
1149 while (sqlite3_step(stmt) == SQLITE_ROW && count < max_keys) {
1150 const void *blob = sqlite3_column_blob(stmt, 0);
1151 if (blob && sqlite3_column_bytes(stmt, 0) == 32) {
1152 memcpy(keys_out[count], blob, 32);
1153 count++;
1154 }
1155 }
1156
1157 sqlite3_finalize(stmt);
1158 *count_out = count;
1159
1160 log_debug("Retrieved %zu keys for session %s", count, session_string);
1161 return ASCIICHAT_OK;
1162}

◆ database_session_is_migration_ready()

bool database_session_is_migration_ready ( sqlite3 *  db,
const uint8_t  session_id[16],
uint64_t  migration_window_ms 
)

Definition at line 1015 of file discovery/database.c.

1015 {
1016 if (!db || !session_id) {
1017 return false;
1018 }
1019
1020 uint64_t now_ns = time_get_realtime_ns();
1021 const char *sql = "SELECT in_migration, migration_start_ns FROM sessions WHERE session_id = ?";
1022
1023 sqlite3_stmt *stmt = NULL;
1024 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
1025 return false;
1026 }
1027
1028 sqlite3_bind_blob(stmt, 1, session_id, 16, SQLITE_STATIC);
1029
1030 bool ready = false;
1031 if (sqlite3_step(stmt) == SQLITE_ROW) {
1032 int in_migration = sqlite3_column_int(stmt, 0);
1033 uint64_t migration_start_ns = (uint64_t)sqlite3_column_int64(stmt, 1);
1034
1035 if (in_migration) {
1036 uint64_t elapsed_ns = now_ns - migration_start_ns;
1037 uint64_t migration_window_ns = migration_window_ms * NS_PER_MS_INT;
1038 if (elapsed_ns >= migration_window_ns) {
1039 ready = true;
1040 log_debug("Migration window complete (session=%02x%02x..., elapsed=%lu ns, window=%lu ns)", session_id[0],
1041 session_id[1], elapsed_ns, migration_window_ns);
1042 }
1043 }
1044 }
1045
1046 sqlite3_finalize(stmt);
1047 return ready;
1048}

References session_id, and time_get_realtime_ns().

◆ database_session_join()

asciichat_error_t database_session_join ( sqlite3 *  db,
const acip_session_join_t *  req,
const acds_config_t config,
acip_session_joined_t *  resp 
)

Definition at line 482 of file discovery/database.c.

483 {
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
494 : (ACIP_MAX_SESSION_STRING_LEN - 1);
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
534 uint8_t participant_id[16];
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
632 log_debug("SESSION_JOIN response: session_id=%02x%02x%02x%02x..., participant_id=%02x%02x%02x%02x..., "
633 "initiator=%02x%02x..., host_established=%d, peer_count=%d",
634 resp->session_id[0], resp->session_id[1], resp->session_id[2], resp->session_id[3], resp->participant_id[0],
635 resp->participant_id[1], resp->participant_id[2], resp->participant_id[3], resp->initiator_id[0],
636 resp->initiator_id[1], resp->host_established, resp->peer_count);
637
638 // IP disclosure logic
639 bool reveal_ip = false;
640 log_info("DATABASE_SESSION_JOIN: has_password=%d, expose_ip_publicly=%d, server_address='%s'", session->has_password,
641 session->expose_ip_publicly, session->server_address);
642 if (session->has_password) {
643 reveal_ip = true; // Password was verified
644 } else if (session->expose_ip_publicly) {
645 reveal_ip = true; // Explicit opt-in
646 }
647 log_info("DATABASE_SESSION_JOIN: reveal_ip=%d", reveal_ip);
648
649 if (reveal_ip) {
650 SAFE_STRNCPY(resp->server_address, session->server_address, sizeof(resp->server_address));
651 resp->server_port = session->server_port;
652 resp->session_type = session->session_type;
653
654 // Generate TURN credentials for WebRTC sessions
655 if (session->session_type == SESSION_TYPE_WEBRTC && config->turn_secret[0] != '\0') {
656 turn_credentials_t turn_creds;
657 asciichat_error_t turn_result =
658 turn_generate_credentials(session_string, config->turn_secret, 86400, &turn_creds);
659 if (turn_result == ASCIICHAT_OK) {
660 SAFE_STRNCPY(resp->turn_username, turn_creds.username, sizeof(resp->turn_username));
661 SAFE_STRNCPY(resp->turn_password, turn_creds.password, sizeof(resp->turn_password));
662 log_debug("Generated TURN credentials for session %s", session_string);
663 }
664 }
665
666 log_info("Participant joined session %s (participants=%d/%d, server=%s:%d, type=%s)", session_string,
667 session->current_participants + 1, session->max_participants, resp->server_address, resp->server_port,
668 session->session_type == SESSION_TYPE_WEBRTC ? "WebRTC" : "DirectTCP");
669 } else {
670 log_info("Participant joined session %s (participants=%d/%d, IP WITHHELD)", session_string,
671 session->current_participants + 1, session->max_participants);
672 }
673
674 session_entry_destroy(session);
675 return ASCIICHAT_OK;
676}

References database_session_find_by_string(), participant_id, session_entry_destroy(), turn_generate_credentials(), and acds_config_t::turn_secret.

◆ database_session_leave()

asciichat_error_t database_session_leave ( sqlite3 *  db,
const uint8_t  session_id[16],
const uint8_t  participant_id[16] 
)

Definition at line 678 of file discovery/database.c.

678 {
679 if (!db || !session_id || !participant_id) {
680 return SET_ERRNO(ERROR_INVALID_PARAM, "db, session_id, or participant_id is NULL");
681 }
682
683 // Look up session_string from session_id
684 char session_string[SESSION_STRING_BUFFER_SIZE] = {0};
685 const char *lookup_sql = "SELECT session_string FROM sessions WHERE session_id = ?";
686 sqlite3_stmt *stmt = NULL;
687 int rc = sqlite3_prepare_v2(db, lookup_sql, -1, &stmt, NULL);
688 if (rc != SQLITE_OK) {
689 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare session lookup: %s", sqlite3_errmsg(db));
690 }
691
692 sqlite3_bind_blob(stmt, 1, session_id, 16, SQLITE_STATIC);
693 rc = sqlite3_step(stmt);
694 if (rc != SQLITE_ROW) {
695 sqlite3_finalize(stmt);
696 return SET_ERRNO(ERROR_INVALID_STATE, "Session not found");
697 }
698
699 const char *str = (const char *)sqlite3_column_text(stmt, 0);
700 if (str) {
701 SAFE_STRNCPY(session_string, str, sizeof(session_string));
702 }
703 sqlite3_finalize(stmt);
704
705 if (session_string[0] == '\0') {
706 return SET_ERRNO(ERROR_INVALID_STATE, "Session string is empty");
707 }
708
709 // Begin transaction
710 char *err_msg = NULL;
711 rc = sqlite3_exec(db, "BEGIN IMMEDIATE;", NULL, NULL, &err_msg);
712 if (rc != SQLITE_OK) {
713 log_error("Failed to begin transaction: %s", err_msg ? err_msg : "unknown");
714 sqlite3_free(err_msg);
715 return SET_ERRNO(ERROR_CONFIG, "Failed to begin transaction");
716 }
717
718 // Delete participant
719 const char *del_sql = "DELETE FROM participants WHERE participant_id = ? AND session_string = ?";
720 rc = sqlite3_prepare_v2(db, del_sql, -1, &stmt, NULL);
721 if (rc != SQLITE_OK) {
722 sqlite3_exec(db, "ROLLBACK;", NULL, NULL, NULL);
723 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare participant delete: %s", sqlite3_errmsg(db));
724 }
725
726 sqlite3_bind_blob(stmt, 1, participant_id, 16, SQLITE_STATIC);
727 sqlite3_bind_text(stmt, 2, session_string, -1, SQLITE_STATIC);
728 rc = sqlite3_step(stmt);
729 sqlite3_finalize(stmt);
730
731 if (rc != SQLITE_DONE) {
732 sqlite3_exec(db, "ROLLBACK;", NULL, NULL, NULL);
733 return SET_ERRNO(ERROR_CONFIG, "Failed to delete participant: %s", sqlite3_errmsg(db));
734 }
735
736 int changes = sqlite3_changes(db);
737 if (changes == 0) {
738 sqlite3_exec(db, "ROLLBACK;", NULL, NULL, NULL);
739 return SET_ERRNO(ERROR_INVALID_STATE, "Participant not in session");
740 }
741
742 uint64_t now = database_get_current_time_ms();
743
744 // Decrement participant count and update last activity
745 const char *update_sql = "UPDATE sessions SET current_participants = current_participants - 1, "
746 "last_activity_at = ? WHERE session_string = ? AND current_participants > 0";
747 rc = sqlite3_prepare_v2(db, update_sql, -1, &stmt, NULL);
748 if (rc == SQLITE_OK) {
749 sqlite3_bind_int64(stmt, 1, (sqlite3_int64)now);
750 sqlite3_bind_text(stmt, 2, session_string, -1, SQLITE_STATIC);
751 sqlite3_step(stmt);
752 sqlite3_finalize(stmt);
753 }
754
755 // Check if session is now empty and delete if so
756 const char *check_sql = "SELECT current_participants FROM sessions WHERE session_string = ?";
757 rc = sqlite3_prepare_v2(db, check_sql, -1, &stmt, NULL);
758 if (rc == SQLITE_OK) {
759 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
760 if (sqlite3_step(stmt) == SQLITE_ROW) {
761 int count = sqlite3_column_int(stmt, 0);
762
763 if (count <= 0) {
764 log_info("Session %s has no participants, deleting", session_string);
765 sqlite3_finalize(stmt);
766
767 // Delete empty session
768 const char *del_session_sql = "DELETE FROM sessions WHERE session_string = ?";
769 rc = sqlite3_prepare_v2(db, del_session_sql, -1, &stmt, NULL);
770 if (rc == SQLITE_OK) {
771 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
772 sqlite3_step(stmt);
773 sqlite3_finalize(stmt);
774 }
775 stmt = NULL;
776 } else {
777 log_info("Participant left session %s (participants=%d remaining)", session_string, count);
778 }
779 }
780 if (stmt) {
781 sqlite3_finalize(stmt);
782 }
783 }
784
785 // Commit transaction
786 rc = sqlite3_exec(db, "COMMIT;", NULL, NULL, &err_msg);
787 if (rc != SQLITE_OK) {
788 log_error("Failed to commit transaction: %s", err_msg ? err_msg : "unknown");
789 sqlite3_free(err_msg);
790 sqlite3_exec(db, "ROLLBACK;", NULL, NULL, NULL);
791 return SET_ERRNO(ERROR_CONFIG, "Failed to commit transaction");
792 }
793
794 return ASCIICHAT_OK;
795}

References participant_id, and session_id.

◆ database_session_lookup()

asciichat_error_t database_session_lookup ( sqlite3 *  db,
const char *  session_string,
const acds_config_t config,
acip_session_info_t *  resp 
)

Definition at line 420 of file discovery/database.c.

421 {
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}

References acds_config_t::require_client_verify, and acds_config_t::require_server_verify.

◆ database_session_revoke_key()

asciichat_error_t database_session_revoke_key ( sqlite3 *  db,
const char *  session_string,
const uint8_t  identity_pubkey[32] 
)

Definition at line 1084 of file discovery/database.c.

1085 {
1086 if (!db || !session_string || !identity_pubkey) {
1087 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for revoke_key");
1088 }
1089
1090 const char *sql = "UPDATE session_keys SET revoked = 1 WHERE session_string = ? AND identity_pubkey = ?";
1091
1092 sqlite3_stmt *stmt = NULL;
1093 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
1094 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare revoke_key statement: %s", sqlite3_errmsg(db));
1095 }
1096
1097 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
1098 sqlite3_bind_blob(stmt, 2, identity_pubkey, 32, SQLITE_STATIC);
1099
1100 int rc = sqlite3_step(stmt);
1101 sqlite3_finalize(stmt);
1102
1103 if (rc != SQLITE_DONE) {
1104 return SET_ERRNO(ERROR_CONFIG, "Failed to revoke session key: %s", sqlite3_errmsg(db));
1105 }
1106
1107 log_debug("Revoked key from session %s", session_string);
1108 return ASCIICHAT_OK;
1109}

◆ database_session_start_migration()

asciichat_error_t database_session_start_migration ( sqlite3 *  db,
const uint8_t  session_id[16] 
)

Definition at line 976 of file discovery/database.c.

976 {
977 if (!db || !session_id) {
978 return SET_ERRNO(ERROR_INVALID_PARAM, "db or session_id is NULL");
979 }
980
981 uint64_t now_ns = time_get_realtime_ns();
982
983 const char *sql = "UPDATE sessions SET "
984 "in_migration = 1, "
985 "migration_start_ns = ? "
986 "WHERE session_id = ?";
987
988 sqlite3_stmt *stmt = NULL;
989 int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
990 if (rc != SQLITE_OK) {
991 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare migration start: %s", sqlite3_errmsg(db));
992 }
993
994 sqlite3_bind_int64(stmt, 1, (sqlite3_int64)now_ns);
995 sqlite3_bind_blob(stmt, 2, session_id, 16, SQLITE_STATIC);
996
997 rc = sqlite3_step(stmt);
998 sqlite3_finalize(stmt);
999
1000 if (rc != SQLITE_DONE) {
1001 return SET_ERRNO(ERROR_CONFIG, "Failed to start migration: %s", sqlite3_errmsg(db));
1002 }
1003
1004 int changes = sqlite3_changes(db);
1005 if (changes == 0) {
1006 return SET_ERRNO(ERROR_INVALID_STATE, "Session not found for migration start");
1007 }
1008
1009 log_info("Session migration started (session=%02x%02x..., start_ns=%" PRIu64 ")", session_id[0], session_id[1],
1010 now_ns);
1011
1012 return ASCIICHAT_OK;
1013}

References session_id, and time_get_realtime_ns().

◆ database_session_update_host()

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 
)

Definition at line 889 of file discovery/database.c.

891 {
892 if (!db || !session_id || !host_participant_id) {
893 return SET_ERRNO(ERROR_INVALID_PARAM, "db, session_id, or host_participant_id is NULL");
894 }
895
896 uint64_t now = database_get_current_time_ms();
897
898 const char *sql = "UPDATE sessions SET "
899 "host_established = 1, "
900 "host_participant_id = ?, "
901 "host_address = ?, "
902 "host_port = ?, "
903 "host_connection_type = ?, "
904 "last_activity_at = ? "
905 "WHERE session_id = ?";
906
907 sqlite3_stmt *stmt = NULL;
908 int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
909 if (rc != SQLITE_OK) {
910 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare host update: %s", sqlite3_errmsg(db));
911 }
912
913 sqlite3_bind_blob(stmt, 1, host_participant_id, 16, SQLITE_STATIC);
914 sqlite3_bind_text(stmt, 2, host_address ? host_address : "", -1, SQLITE_STATIC);
915 sqlite3_bind_int(stmt, 3, host_port);
916 sqlite3_bind_int(stmt, 4, connection_type);
917 sqlite3_bind_int64(stmt, 5, (sqlite3_int64)now);
918 sqlite3_bind_blob(stmt, 6, session_id, 16, SQLITE_STATIC);
919
920 rc = sqlite3_step(stmt);
921 sqlite3_finalize(stmt);
922
923 if (rc != SQLITE_DONE) {
924 return SET_ERRNO(ERROR_CONFIG, "Failed to update host: %s", sqlite3_errmsg(db));
925 }
926
927 int changes = sqlite3_changes(db);
928 if (changes == 0) {
929 return SET_ERRNO(ERROR_INVALID_STATE, "Session not found for host update");
930 }
931
932 log_info("Session host updated: participant=%02x%02x..., address=%s:%u, type=%d", host_participant_id[0],
933 host_participant_id[1], host_address ? host_address : "(none)", host_port, connection_type);
934
935 return ASCIICHAT_OK;
936}

References session_id.

◆ database_session_verify_key()

bool database_session_verify_key ( sqlite3 *  db,
const char *  session_string,
const uint8_t  identity_pubkey[32] 
)

Definition at line 1111 of file discovery/database.c.

1111 {
1112 if (!db || !session_string || !identity_pubkey) {
1113 return false;
1114 }
1115
1116 const char *sql = "SELECT 1 FROM session_keys WHERE session_string = ? AND identity_pubkey = ? AND revoked = 0";
1117
1118 sqlite3_stmt *stmt = NULL;
1119 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
1120 return false;
1121 }
1122
1123 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
1124 sqlite3_bind_blob(stmt, 2, identity_pubkey, 32, SQLITE_STATIC);
1125
1126 bool valid = (sqlite3_step(stmt) == SQLITE_ROW);
1127 sqlite3_finalize(stmt);
1128
1129 return valid;
1130}

References valid.