10#include <ascii-chat/discovery/database.h>
11#include <ascii-chat/discovery/strings.h>
12#include <ascii-chat/log/logging.h>
13#include <ascii-chat/network/webrtc/turn_credentials.h>
14#include <ascii-chat/util/time.h>
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"
37static const char *schema_sql =
39 "CREATE TABLE IF NOT EXISTS sessions ("
40 " session_string TEXT PRIMARY KEY,"
41 " session_id BLOB UNIQUE NOT NULL,"
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,"
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,"
57 " host_established INTEGER DEFAULT 0,"
58 " host_participant_id BLOB,"
60 " host_port INTEGER DEFAULT 0,"
61 " host_connection_type INTEGER DEFAULT 0,"
63 " in_migration INTEGER DEFAULT 0,"
64 " migration_start_ns INTEGER DEFAULT 0"
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"
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,"
81 " key_version INTEGER NOT NULL,"
82 " added_at INTEGER NOT NULL,"
83 " revoked INTEGER DEFAULT 0,"
84 " FOREIGN KEY (session_string) REFERENCES sessions(session_string) ON DELETE CASCADE,"
85 " UNIQUE(session_string, identity_pubkey)"
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"
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);";
111static void generate_uuid(uint8_t uuid_out[16]) {
112 randombytes_buf(uuid_out, 16);
113 uuid_out[6] = (uuid_out[6] & 0x0F) | 0x40;
114 uuid_out[8] = (uuid_out[8] & 0x3F) | 0x80;
120static uint64_t database_get_current_time_ms(
void) {
122 return time_ns_to_ms(current_time_ns);
128static bool verify_password(
const char *password,
const char *hash) {
129 return crypto_pwhash_str_verify(hash, password, strlen(password)) == 0;
138static session_entry_t *load_session_from_row(sqlite3_stmt *stmt) {
139 session_entry_t *session = SAFE_MALLOC(
sizeof(session_entry_t), session_entry_t *);
143 memset(session, 0,
sizeof(*session));
154 const char *str = (
const char *)sqlite3_column_text(stmt, 0);
156 SAFE_STRNCPY(session->session_string, str,
sizeof(session->session_string));
159 const void *blob = sqlite3_column_blob(stmt, 1);
161 memcpy(session->session_id, blob, 16);
164 blob = sqlite3_column_blob(stmt, 2);
166 memcpy(session->host_pubkey, blob, 32);
169 str = (
const char *)sqlite3_column_text(stmt, 3);
171 SAFE_STRNCPY(session->password_hash, str,
sizeof(session->password_hash));
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);
181 str = (
const char *)sqlite3_column_text(stmt, 10);
183 SAFE_STRNCPY(session->server_address, str,
sizeof(session->server_address));
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);
192 blob = sqlite3_column_blob(stmt, 15);
194 memcpy(session->initiator_id, blob, 16);
197 session->host_established = sqlite3_column_int(stmt, 16) != 0;
199 blob = sqlite3_column_blob(stmt, 17);
201 memcpy(session->host_participant_id, blob, 16);
204 str = (
const char *)sqlite3_column_text(stmt, 18);
206 SAFE_STRNCPY(session->host_address, str,
sizeof(session->host_address));
209 session->host_port = (uint16_t)sqlite3_column_int(stmt, 19);
210 session->host_connection_type = (uint8_t)sqlite3_column_int(stmt, 20);
213 session->in_migration = sqlite3_column_int(stmt, 21) != 0;
214 session->migration_start_ns = (uint64_t)sqlite3_column_int64(stmt, 22);
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 = ?";
226 sqlite3_stmt *stmt = NULL;
227 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
231 sqlite3_bind_text(stmt, 1, session->session_string, -1, SQLITE_STATIC);
235 participant_t *p = SAFE_MALLOC(
sizeof(participant_t), participant_t *);
239 memset(p, 0,
sizeof(*p));
241 const void *blob = sqlite3_column_blob(stmt, 0);
243 memcpy(p->participant_id, blob, 16);
246 blob = sqlite3_column_blob(stmt, 1);
248 memcpy(p->identity_pubkey, blob, 32);
251 p->joined_at = (uint64_t)sqlite3_column_int64(stmt, 2);
253 session->participants[idx++] = p;
256 sqlite3_finalize(stmt);
264 if (!db_path || !db) {
265 return SET_ERRNO(ERROR_INVALID_PARAM,
"db_path or db is NULL");
268 log_info(
"Opening database: %s", db_path);
270 int rc = sqlite3_open(db_path, db);
271 if (rc != SQLITE_OK) {
272 const char *err = sqlite3_errmsg(*db);
275 return SET_ERRNO(ERROR_CONFIG,
"Failed to open database: %s", err);
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);
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);
293 return SET_ERRNO(ERROR_CONFIG,
"Failed to enable foreign keys");
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);
303 return SET_ERRNO(ERROR_CONFIG,
"Failed to create database schema");
306 log_info(
"Database initialized successfully (SQLite as single source of truth)");
315 log_debug(
"Database closed");
323 acip_session_created_t *resp) {
324 if (!db || !req || !config || !resp) {
325 return SET_ERRNO(ERROR_INVALID_PARAM,
"db, req, config, or resp is NULL");
328 memset(resp, 0,
sizeof(*resp));
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';
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);
344 return SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid session string: %s", session_string);
348 if (result != ASCIICHAT_OK) {
358 uint64_t now = database_get_current_time_ms();
359 uint64_t expires_at = now + ACIP_SESSION_EXPIRATION_MS;
362 uint8_t max_participants =
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, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
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));
378 log_info(
"DATABASE_SESSION_CREATE: expose_ip_publicly=%d, server_address='%s' server_port=%u, session_type=%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);
396 rc = sqlite3_step(stmt);
397 sqlite3_finalize(stmt);
399 if (rc != SQLITE_DONE) {
400 if (rc == SQLITE_CONSTRAINT) {
401 return SET_ERRNO(ERROR_INVALID_STATE,
"Session string already exists: %s", session_string);
403 return SET_ERRNO(ERROR_CONFIG,
"Failed to insert session: %s", sqlite3_errmsg(db));
407 resp->session_string_len = (uint8_t)strlen(session_string);
408 SAFE_STRNCPY(resp->session_string, session_string,
sizeof(resp->session_string));
410 resp->expires_at = expires_at;
414 log_info(
"Session created: %s (session_id=%02x%02x%02x%02x..., max_participants=%d, has_password=%d)", session_string,
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");
426 memset(resp, 0,
sizeof(*resp));
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 = ?";
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));
439 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
441 rc = sqlite3_step(stmt);
442 if (rc != SQLITE_ROW) {
443 sqlite3_finalize(stmt);
445 log_debug(
"Session lookup failed: %s (not found)", session_string);
452 const void *blob = sqlite3_column_blob(stmt, 1);
454 memcpy(resp->session_id, blob, 16);
457 blob = sqlite3_column_blob(stmt, 2);
459 memcpy(resp->host_pubkey, blob, 32);
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);
474 sqlite3_finalize(stmt);
476 log_debug(
"Session lookup: %s (found, participants=%d/%d)", session_string, resp->current_participants,
477 resp->max_participants);
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");
488 memset(resp, 0,
sizeof(*resp));
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';
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);
508 if (session->current_participants >= session->max_participants) {
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);
517 if (session->has_password && req->has_password) {
518 if (!verify_password(req->password, session->password_hash)) {
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);
525 }
else if (session->has_password && !req->has_password) {
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);
536 uint64_t now = database_get_current_time_ms();
539 char *err_msg = NULL;
540 int rc = sqlite3_exec(db,
"BEGIN IMMEDIATE;", NULL, NULL, &err_msg);
541 if (rc != SQLITE_OK) {
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");
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);
556 return SET_ERRNO(ERROR_CONFIG,
"Failed to prepare participant insert: %s", sqlite3_errmsg(db));
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);
564 rc = sqlite3_step(stmt);
565 sqlite3_finalize(stmt);
567 if (rc != SQLITE_DONE) {
568 sqlite3_exec(db,
"ROLLBACK;", NULL, NULL, NULL);
570 return SET_ERRNO(ERROR_CONFIG,
"Failed to insert participant: %s", sqlite3_errmsg(db));
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);
581 sqlite3_finalize(stmt);
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;
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) {
599 sqlite3_bind_text(stmt, 2, session->session_string, -1, SQLITE_STATIC);
601 sqlite3_finalize(stmt);
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);
614 return SET_ERRNO(ERROR_CONFIG,
"Failed to commit transaction");
619 resp->error_code = ACIP_ERROR_NONE;
621 memcpy(resp->session_id, session->session_id, 16);
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);
630 resp->peer_count = session->current_participants;
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);
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) {
644 }
else if (session->expose_ip_publicly) {
647 log_info(
"DATABASE_SESSION_JOIN: reveal_ip=%d", 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;
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 =
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);
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");
670 log_info(
"Participant joined session %s (participants=%d/%d, IP WITHHELD)", session_string,
671 session->current_participants + 1, session->max_participants);
680 return SET_ERRNO(ERROR_INVALID_PARAM,
"db, session_id, or participant_id is NULL");
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));
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");
699 const char *str = (
const char *)sqlite3_column_text(stmt, 0);
701 SAFE_STRNCPY(session_string, str,
sizeof(session_string));
703 sqlite3_finalize(stmt);
705 if (session_string[0] ==
'\0') {
706 return SET_ERRNO(ERROR_INVALID_STATE,
"Session string is empty");
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");
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));
727 sqlite3_bind_text(stmt, 2, session_string, -1, SQLITE_STATIC);
728 rc = sqlite3_step(stmt);
729 sqlite3_finalize(stmt);
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));
736 int changes = sqlite3_changes(db);
738 sqlite3_exec(db,
"ROLLBACK;", NULL, NULL, NULL);
739 return SET_ERRNO(ERROR_INVALID_STATE,
"Participant not in session");
742 uint64_t now = database_get_current_time_ms();
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);
752 sqlite3_finalize(stmt);
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);
764 log_info(
"Session %s has no participants, deleting", session_string);
765 sqlite3_finalize(stmt);
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);
773 sqlite3_finalize(stmt);
777 log_info(
"Participant left session %s (participants=%d remaining)", session_string, count);
781 sqlite3_finalize(stmt);
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");
804 sqlite3_stmt *stmt = NULL;
805 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
809 sqlite3_bind_blob(stmt, 1,
session_id, 16, SQLITE_STATIC);
811 session_entry_t *session = NULL;
812 if (sqlite3_step(stmt) == SQLITE_ROW) {
813 session = load_session_from_row(stmt);
815 load_session_participants(db, session);
819 sqlite3_finalize(stmt);
824 if (!db || !session_string) {
830 sqlite3_stmt *stmt = NULL;
831 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
835 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
837 session_entry_t *session = NULL;
838 if (sqlite3_step(stmt) == SQLITE_ROW) {
839 session = load_session_from_row(stmt);
841 load_session_participants(db, session);
845 sqlite3_finalize(stmt);
854 uint64_t now = database_get_current_time_ms();
856 uint64_t inactivity_threshold = 3ULL * SEC_PER_HOUR * MS_PER_SEC_INT;
857 uint64_t cutoff_time = now - inactivity_threshold;
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>",
872 sqlite3_finalize(stmt);
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);
880 int deleted = sqlite3_changes(db);
881 sqlite3_finalize(stmt);
884 log_info(
"Cleaned up %d inactive sessions (>3 hours)", deleted);
890 const uint8_t host_participant_id[16],
const char *host_address,
891 uint16_t host_port, uint8_t connection_type) {
892 if (!db || !
session_id || !host_participant_id) {
893 return SET_ERRNO(ERROR_INVALID_PARAM,
"db, session_id, or host_participant_id is NULL");
896 uint64_t now = database_get_current_time_ms();
898 const char *sql =
"UPDATE sessions SET "
899 "host_established = 1, "
900 "host_participant_id = ?, "
903 "host_connection_type = ?, "
904 "last_activity_at = ? "
905 "WHERE session_id = ?";
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));
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);
920 rc = sqlite3_step(stmt);
921 sqlite3_finalize(stmt);
923 if (rc != SQLITE_DONE) {
924 return SET_ERRNO(ERROR_CONFIG,
"Failed to update host: %s", sqlite3_errmsg(db));
927 int changes = sqlite3_changes(db);
929 return SET_ERRNO(ERROR_INVALID_STATE,
"Session not found for host update");
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);
940 return SET_ERRNO(ERROR_INVALID_PARAM,
"db or session_id is NULL");
943 const char *sql =
"UPDATE sessions SET "
944 "host_established = 0, "
945 "host_participant_id = NULL, "
946 "host_address = NULL, "
948 "host_connection_type = 0 "
949 "WHERE session_id = ?";
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));
957 sqlite3_bind_blob(stmt, 1,
session_id, 16, SQLITE_STATIC);
959 rc = sqlite3_step(stmt);
960 sqlite3_finalize(stmt);
962 if (rc != SQLITE_DONE) {
963 return SET_ERRNO(ERROR_CONFIG,
"Failed to clear host: %s", sqlite3_errmsg(db));
966 int changes = sqlite3_changes(db);
968 return SET_ERRNO(ERROR_INVALID_STATE,
"Session not found for host clear");
971 log_info(
"Session host cleared (session=%02x%02x...) - ready for migration",
session_id[0],
session_id[1]);
978 return SET_ERRNO(ERROR_INVALID_PARAM,
"db or session_id is NULL");
983 const char *sql =
"UPDATE sessions SET "
985 "migration_start_ns = ? "
986 "WHERE session_id = ?";
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));
994 sqlite3_bind_int64(stmt, 1, (sqlite3_int64)now_ns);
995 sqlite3_bind_blob(stmt, 2,
session_id, 16, SQLITE_STATIC);
997 rc = sqlite3_step(stmt);
998 sqlite3_finalize(stmt);
1000 if (rc != SQLITE_DONE) {
1001 return SET_ERRNO(ERROR_CONFIG,
"Failed to start migration: %s", sqlite3_errmsg(db));
1004 int changes = sqlite3_changes(db);
1006 return SET_ERRNO(ERROR_INVALID_STATE,
"Session not found for migration start");
1009 log_info(
"Session migration started (session=%02x%02x..., start_ns=%" PRIu64
")",
session_id[0],
session_id[1],
1012 return ASCIICHAT_OK;
1021 const char *sql =
"SELECT in_migration, migration_start_ns FROM sessions WHERE session_id = ?";
1023 sqlite3_stmt *stmt = NULL;
1024 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
1028 sqlite3_bind_blob(stmt, 1,
session_id, 16, SQLITE_STATIC);
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);
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) {
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);
1046 sqlite3_finalize(stmt);
1055 uint32_t key_version) {
1056 if (!db || !session_string || !identity_pubkey) {
1057 return SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid parameters for add_key");
1060 const char *sql =
"INSERT OR IGNORE INTO session_keys (session_string, identity_pubkey, key_version, added_at) "
1061 "VALUES (?, ?, ?, ?)";
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));
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());
1073 int rc = sqlite3_step(stmt);
1074 sqlite3_finalize(stmt);
1076 if (rc != SQLITE_DONE) {
1077 return SET_ERRNO(ERROR_CONFIG,
"Failed to add session key: %s", sqlite3_errmsg(db));
1080 log_debug(
"Added key to session %s (version=%u)", session_string, key_version);
1081 return ASCIICHAT_OK;
1085 const uint8_t identity_pubkey[32]) {
1086 if (!db || !session_string || !identity_pubkey) {
1087 return SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid parameters for revoke_key");
1090 const char *sql =
"UPDATE session_keys SET revoked = 1 WHERE session_string = ? AND identity_pubkey = ?";
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));
1097 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
1098 sqlite3_bind_blob(stmt, 2, identity_pubkey, 32, SQLITE_STATIC);
1100 int rc = sqlite3_step(stmt);
1101 sqlite3_finalize(stmt);
1103 if (rc != SQLITE_DONE) {
1104 return SET_ERRNO(ERROR_CONFIG,
"Failed to revoke session key: %s", sqlite3_errmsg(db));
1107 log_debug(
"Revoked key from session %s", session_string);
1108 return ASCIICHAT_OK;
1112 if (!db || !session_string || !identity_pubkey) {
1116 const char *sql =
"SELECT 1 FROM session_keys WHERE session_string = ? AND identity_pubkey = ? AND revoked = 0";
1118 sqlite3_stmt *stmt = NULL;
1119 if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
1123 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
1124 sqlite3_bind_blob(stmt, 2, identity_pubkey, 32, SQLITE_STATIC);
1126 bool valid = (sqlite3_step(stmt) == SQLITE_ROW);
1127 sqlite3_finalize(stmt);
1133 size_t max_keys,
size_t *count_out) {
1134 if (!db || !session_string || !keys_out || !count_out) {
1135 return SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid parameters for get_keys");
1138 const char *sql =
"SELECT identity_pubkey FROM session_keys WHERE session_string = ? AND revoked = 0 "
1139 "ORDER BY key_version ASC";
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));
1146 sqlite3_bind_text(stmt, 1, session_string, -1, SQLITE_STATIC);
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);
1157 sqlite3_finalize(stmt);
1160 log_debug(
"Retrieved %zu keys for session %s", count, session_string);
1161 return ASCIICHAT_OK;
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.
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)