110 {
111 randombytes_buf(uuid_out, 16);
112 uuid_out[6] = (uuid_out[6] & 0x0F) | 0x40;
113 uuid_out[8] = (uuid_out[8] & 0x3F) | 0x80;
114}
115
119static uint64_t database_get_current_time_ms(void) {
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
145
146
147
148
149
150
151
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
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
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;
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
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
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
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
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
310 if (!db) {
311 return;
312 }
313 sqlite3_close(db);
314 log_debug("Database closed");
315}
316
317
318
319
320
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
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
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 {
347 if (result != ASCIICHAT_OK) {
348 return result;
349 }
350 }
351
352
355
356
357 uint64_t now = database_get_current_time_ms();
358 uint64_t expires_at = now + ACIP_SESSION_EXPIRATION_MS;
359
360
361 uint8_t max_participants =
363
364
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);
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
406 resp->session_string_len = (uint8_t)strlen(session_string);
407 SAFE_STRNCPY(resp->session_string, session_string, sizeof(resp->session_string));
409 resp->expires_at = expires_at;
412
413 log_info("Session created: %s (session_id=%02x%02x%02x%02x..., max_participants=%d, has_password=%d)", session_string,
415
416 return ASCIICHAT_OK;
417}
418
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
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
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
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
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
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
507 if (session->current_participants >= session->max_participants) {
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
516 if (session->has_password && req->has_password) {
517 if (!verify_password(req->password, session->password_hash)) {
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) {
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
535 uint64_t now = database_get_current_time_ms();
536
537
538 char *err_msg = NULL;
539 int rc = sqlite3_exec(db, "BEGIN IMMEDIATE;", NULL, NULL, &err_msg);
540 if (rc != SQLITE_OK) {
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
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);
555 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare participant insert: %s", sqlite3_errmsg(db));
556 }
557
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);
569 return SET_ERRNO(ERROR_CONFIG, "Failed to insert participant: %s", sqlite3_errmsg(db));
570 }
571
572
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
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) {
598 sqlite3_bind_text(stmt, 2, session->session_string, -1, SQLITE_STATIC);
599 sqlite3_step(stmt);
600 sqlite3_finalize(stmt);
603 }
604 }
605
606
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);
613 return SET_ERRNO(ERROR_CONFIG, "Failed to commit transaction");
614 }
615
616
617 resp->success = 1;
618 resp->error_code = ACIP_ERROR_NONE;
620 memcpy(resp->session_id, session->session_id, 16);
621
622
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
629 resp->peer_count = session->current_participants;
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
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;
643 } else if (session->expose_ip_publicly) {
644 reveal_ip = true;
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
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 =
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
674 return ASCIICHAT_OK;
675}
676
679 return SET_ERRNO(ERROR_INVALID_PARAM, "db, session_id, or participant_id is NULL");
680 }
681
682
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
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
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
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
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
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
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
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
798 return NULL;
799 }
800
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
823 if (!db || !session_string) {
824 return NULL;
825 }
826
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
849 if (!db) {
850 return;
851 }
852
853 uint64_t now = database_get_current_time_ms();
854
855 uint64_t inactivity_threshold = 3ULL * SEC_PER_HOUR * MS_PER_SEC_INT;
856 uint64_t cutoff_time = now - inactivity_threshold;
857
858
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
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
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
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
977 return SET_ERRNO(ERROR_INVALID_PARAM, "db or session_id is NULL");
978 }
979
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
1016 return false;
1017 }
1018
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
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
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
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
1129}
1130
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}
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)