#define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include // Include nostr_core_lib for Nostr functionality #include "../nostr_core_lib/cjson/cJSON.h" #include "../nostr_core_lib/nostr_core/nostr_core.h" #include "../nostr_core_lib/nostr_core/nip013.h" // NIP-13: Proof of Work #include "config.h" // Configuration management system #include "sql_schema.h" // Embedded database schema #include "websockets.h" // WebSocket protocol implementation #include "subscriptions.h" // Subscription management system // Forward declarations for unified request validator int nostr_validate_unified_request(const char* json_string, size_t json_length); int ginxsom_request_validator_init(const char* db_path, const char* app_name); void ginxsom_request_validator_cleanup(void); // Forward declarations for NIP-42 functions from request_validator.c int nostr_nip42_generate_challenge(char *challenge_buffer, size_t buffer_size); int nostr_nip42_verify_auth_event(cJSON *event, const char *challenge_id, const char *relay_url, int time_tolerance_seconds); // Color constants for logging #define RED "\033[31m" #define GREEN "\033[32m" #define YELLOW "\033[33m" #define BLUE "\033[34m" #define BOLD "\033[1m" #define RESET "\033[0m" // Global state sqlite3* g_db = NULL; // Non-static so config.c can access it int g_server_running = 1; // Non-static so websockets.c can access it struct lws_context *ws_context = NULL; // Non-static so websockets.c can access it // NIP-11 relay information structure struct relay_info { char name[RELAY_NAME_MAX_LENGTH]; char description[RELAY_DESCRIPTION_MAX_LENGTH]; char banner[RELAY_URL_MAX_LENGTH]; char icon[RELAY_URL_MAX_LENGTH]; char pubkey[RELAY_PUBKEY_MAX_LENGTH]; char contact[RELAY_CONTACT_MAX_LENGTH]; char software[RELAY_URL_MAX_LENGTH]; char version[64]; char privacy_policy[RELAY_URL_MAX_LENGTH]; char terms_of_service[RELAY_URL_MAX_LENGTH]; cJSON* supported_nips; // Array of supported NIP numbers cJSON* limitation; // Server limitations object cJSON* retention; // Event retention policies array cJSON* relay_countries; // Array of country codes cJSON* language_tags; // Array of language tags cJSON* tags; // Array of content tags char posting_policy[RELAY_URL_MAX_LENGTH]; cJSON* fees; // Payment fee structure char payments_url[RELAY_URL_MAX_LENGTH]; }; // Global relay information instance moved to unified cache // static struct relay_info g_relay_info = {0}; // REMOVED - now in g_unified_cache.relay_info // NIP-40 Expiration configuration (now in nip040.c) extern struct expiration_config g_expiration_config; // Global subscription manager instance (defined in websockets.c) extern subscription_manager_t g_subscription_manager; ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // DATA STRUCTURES ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // Forward declarations for logging functions void log_info(const char* message); void log_success(const char* message); void log_error(const char* message); void log_warning(const char* message); // Forward declaration for subscription manager configuration void update_subscription_manager_config(void); // Forward declarations for subscription database logging void log_subscription_created(const subscription_t* sub); void log_subscription_closed(const char* sub_id, const char* client_ip, const char* reason); void log_subscription_disconnected(const char* client_ip); void log_event_broadcast(const char* event_id, const char* sub_id, const char* client_ip); void update_subscription_events_sent(const char* sub_id, int events_sent); // Forward declarations for NIP-01 event handling const char* extract_d_tag_value(cJSON* tags); int check_and_handle_replaceable_event(int kind, const char* pubkey, long created_at); int check_and_handle_addressable_event(int kind, const char* pubkey, const char* d_tag_value, long created_at); int handle_event_message(cJSON* event, char* error_message, size_t error_size); // Forward declaration for unified validation int nostr_validate_unified_request(const char* json_string, size_t json_length); // Forward declaration for admin event processing (kind 23456) int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size, struct lws* wsi); // Forward declaration for enhanced admin event authorization int is_authorized_admin_event(cJSON* event, char* error_message, size_t error_size); // Forward declaration for NOTICE message support void send_notice_message(struct lws* wsi, const char* message); // Forward declarations for NIP-42 authentication functions void send_nip42_auth_challenge(struct lws* wsi, struct per_session_data* pss); void handle_nip42_auth_signed_event(struct lws* wsi, struct per_session_data* pss, cJSON* auth_event); void handle_nip42_auth_challenge_response(struct lws* wsi, struct per_session_data* pss, const char* challenge); // Forward declarations for NIP-09 deletion request handling int handle_deletion_request(cJSON* event, char* error_message, size_t error_size); int delete_events_by_id(const char* requester_pubkey, cJSON* event_ids); int delete_events_by_address(const char* requester_pubkey, cJSON* addresses, long deletion_timestamp); int mark_event_as_deleted(const char* event_id, const char* deletion_event_id, const char* reason); // Forward declaration for database functions int store_event(cJSON* event); // Forward declarations for NIP-11 relay information handling void init_relay_info(); void cleanup_relay_info(); cJSON* generate_relay_info_json(); int handle_nip11_http_request(struct lws* wsi, const char* accept_header); // Forward declaration for WebSocket relay server int start_websocket_relay(int port_override, int strict_port); // Forward declarations for NIP-13 PoW handling (now in nip013.c) void init_pow_config(); int validate_event_pow(cJSON* event, char* error_message, size_t error_size); // Forward declarations for NIP-40 expiration handling (now in nip040.c) void init_expiration_config(); long extract_expiration_timestamp(cJSON* tags); int is_event_expired(cJSON* event, time_t current_time); int validate_event_expiration(cJSON* event, char* error_message, size_t error_size); ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // LOGGING FUNCTIONS ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // Helper function to get current timestamp string static void get_timestamp_string(char* buffer, size_t buffer_size) { time_t now = time(NULL); struct tm* local_time = localtime(&now); strftime(buffer, buffer_size, "%Y-%m-%d %H:%M:%S", local_time); } // Logging functions void log_info(const char* message) { char timestamp[32]; get_timestamp_string(timestamp, sizeof(timestamp)); printf("[%s] [INFO] %s\n", timestamp, message); fflush(stdout); } void log_success(const char* message) { char timestamp[32]; get_timestamp_string(timestamp, sizeof(timestamp)); printf("[%s] [SUCCESS] %s\n", timestamp, message); fflush(stdout); } void log_error(const char* message) { char timestamp[32]; get_timestamp_string(timestamp, sizeof(timestamp)); printf("[%s] [ERROR] %s\n", timestamp, message); fflush(stdout); } void log_warning(const char* message) { char timestamp[32]; get_timestamp_string(timestamp, sizeof(timestamp)); printf("[%s] [WARNING] %s\n", timestamp, message); fflush(stdout); } // Update subscription manager configuration from config system void update_subscription_manager_config(void) { g_subscription_manager.max_subscriptions_per_client = get_config_int("max_subscriptions_per_client", MAX_SUBSCRIPTIONS_PER_CLIENT); g_subscription_manager.max_total_subscriptions = get_config_int("max_total_subscriptions", MAX_TOTAL_SUBSCRIPTIONS); char config_msg[256]; snprintf(config_msg, sizeof(config_msg), "Subscription limits: max_per_client=%d, max_total=%d", g_subscription_manager.max_subscriptions_per_client, g_subscription_manager.max_total_subscriptions); log_info(config_msg); } // Signal handler for graceful shutdown void signal_handler(int sig) { if (sig == SIGINT || sig == SIGTERM) { log_info("Received shutdown signal"); g_server_running = 0; } } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // NOTICE MESSAGE SUPPORT ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // Send NOTICE message to client (NIP-01) void send_notice_message(struct lws* wsi, const char* message) { if (!wsi || !message) return; cJSON* notice_msg = cJSON_CreateArray(); cJSON_AddItemToArray(notice_msg, cJSON_CreateString("NOTICE")); cJSON_AddItemToArray(notice_msg, cJSON_CreateString(message)); char* msg_str = cJSON_Print(notice_msg); if (msg_str) { size_t msg_len = strlen(msg_str); unsigned char* buf = malloc(LWS_PRE + msg_len); if (buf) { memcpy(buf + LWS_PRE, msg_str, msg_len); lws_write(wsi, buf + LWS_PRE, msg_len, LWS_WRITE_TEXT); free(buf); } free(msg_str); } cJSON_Delete(notice_msg); } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // DATABASE FUNCTIONS ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // Initialize database connection and schema int init_database(const char* database_path_override) { // Priority 1: Command line database path override const char* db_path = database_path_override; // Priority 2: Configuration system (if available) if (!db_path) { db_path = get_config_value("database_path"); } // Priority 3: Default path if (!db_path) { db_path = DEFAULT_DATABASE_PATH; } int rc = sqlite3_open(db_path, &g_db); if (rc != SQLITE_OK) { log_error("Cannot open database"); return -1; } char success_msg[256]; snprintf(success_msg, sizeof(success_msg), "Database connection established: %s", db_path); log_success(success_msg); // Check if database is already initialized by looking for the events table const char* check_sql = "SELECT name FROM sqlite_master WHERE type='table' AND name='events'"; sqlite3_stmt* check_stmt; rc = sqlite3_prepare_v2(g_db, check_sql, -1, &check_stmt, NULL); if (rc == SQLITE_OK) { int has_events_table = (sqlite3_step(check_stmt) == SQLITE_ROW); sqlite3_finalize(check_stmt); if (has_events_table) { log_info("Database schema already exists, checking version"); // Check existing schema version and migrate if needed const char* version_sql = "SELECT value FROM schema_info WHERE key = 'version'"; sqlite3_stmt* version_stmt; const char* db_version = NULL; int needs_migration = 0; if (sqlite3_prepare_v2(g_db, version_sql, -1, &version_stmt, NULL) == SQLITE_OK) { if (sqlite3_step(version_stmt) == SQLITE_ROW) { db_version = (char*)sqlite3_column_text(version_stmt, 0); char version_msg[256]; snprintf(version_msg, sizeof(version_msg), "Existing database schema version: %s", db_version ? db_version : "unknown"); log_info(version_msg); // Check if migration is needed if (!db_version || strcmp(db_version, "5") == 0) { needs_migration = 1; log_info("Database migration needed: v5 -> v6 (adding auth_rules table)"); } else if (strcmp(db_version, "6") == 0) { log_info("Database is already at current schema version v6"); } else if (strcmp(db_version, EMBEDDED_SCHEMA_VERSION) == 0) { log_info("Database is at current schema version"); } else { char warning_msg[256]; snprintf(warning_msg, sizeof(warning_msg), "Unknown database schema version: %s", db_version); log_warning(warning_msg); } } else { log_info("Database exists but no version information found, assuming migration needed"); needs_migration = 1; } sqlite3_finalize(version_stmt); } else { log_info("Cannot read schema version, assuming migration needed"); needs_migration = 1; } // Perform migration if needed if (needs_migration) { log_info("Performing database schema migration to v6"); // Check if auth_rules table already exists const char* check_auth_rules_sql = "SELECT name FROM sqlite_master WHERE type='table' AND name='auth_rules'"; sqlite3_stmt* check_stmt; int has_auth_rules = 0; if (sqlite3_prepare_v2(g_db, check_auth_rules_sql, -1, &check_stmt, NULL) == SQLITE_OK) { has_auth_rules = (sqlite3_step(check_stmt) == SQLITE_ROW); sqlite3_finalize(check_stmt); } if (!has_auth_rules) { // Add auth_rules table const char* create_auth_rules_sql = "CREATE TABLE IF NOT EXISTS auth_rules (" " id INTEGER PRIMARY KEY AUTOINCREMENT," " rule_type TEXT NOT NULL," // 'pubkey_whitelist', 'pubkey_blacklist', 'hash_blacklist' " operation TEXT NOT NULL," // 'event', 'event_kind_1', etc. " rule_target TEXT NOT NULL," // pubkey, hash, or other identifier " enabled INTEGER DEFAULT 1," // 0 = disabled, 1 = enabled " priority INTEGER DEFAULT 1000," // Lower numbers = higher priority " description TEXT," // Optional description " created_at INTEGER DEFAULT (strftime('%s', 'now'))," " UNIQUE(rule_type, operation, rule_target)" ");"; char* error_msg = NULL; int rc = sqlite3_exec(g_db, create_auth_rules_sql, NULL, NULL, &error_msg); if (rc != SQLITE_OK) { char error_log[512]; snprintf(error_log, sizeof(error_log), "Failed to create auth_rules table: %s", error_msg ? error_msg : "unknown error"); log_error(error_log); if (error_msg) sqlite3_free(error_msg); return -1; } log_success("Created auth_rules table"); } else { log_info("auth_rules table already exists, skipping creation"); } // Update schema version to v6 const char* update_version_sql = "INSERT OR REPLACE INTO schema_info (key, value, updated_at) " "VALUES ('version', '6', strftime('%s', 'now'))"; char* error_msg = NULL; int rc = sqlite3_exec(g_db, update_version_sql, NULL, NULL, &error_msg); if (rc != SQLITE_OK) { char error_log[512]; snprintf(error_log, sizeof(error_log), "Failed to update schema version: %s", error_msg ? error_msg : "unknown error"); log_error(error_log); if (error_msg) sqlite3_free(error_msg); return -1; } log_success("Database migration to v6 completed successfully"); } } else { // Initialize database schema using embedded SQL log_info("Initializing database schema from embedded SQL"); // Execute the embedded schema SQL char* error_msg = NULL; rc = sqlite3_exec(g_db, EMBEDDED_SCHEMA_SQL, NULL, NULL, &error_msg); if (rc != SQLITE_OK) { char error_log[512]; snprintf(error_log, sizeof(error_log), "Failed to initialize database schema: %s", error_msg ? error_msg : "unknown error"); log_error(error_log); if (error_msg) { sqlite3_free(error_msg); } return -1; } log_success("Database schema initialized successfully"); // Log schema version information char version_msg[256]; snprintf(version_msg, sizeof(version_msg), "Database schema version: %s", EMBEDDED_SCHEMA_VERSION); log_info(version_msg); } } else { log_error("Failed to check existing database schema"); return -1; } return 0; } // Close database connection void close_database() { if (g_db) { sqlite3_close(g_db); g_db = NULL; log_info("Database connection closed"); } } // Event type classification typedef enum { EVENT_TYPE_REGULAR, EVENT_TYPE_REPLACEABLE, EVENT_TYPE_EPHEMERAL, EVENT_TYPE_ADDRESSABLE, EVENT_TYPE_UNKNOWN } event_type_t; event_type_t classify_event_kind(int kind) { if ((kind >= 1000 && kind < 10000) || (kind >= 4 && kind < 45) || kind == 1 || kind == 2) { return EVENT_TYPE_REGULAR; } if ((kind >= 10000 && kind < 20000) || kind == 0 || kind == 3) { return EVENT_TYPE_REPLACEABLE; } if (kind >= 20000 && kind < 30000) { return EVENT_TYPE_EPHEMERAL; } if (kind >= 30000 && kind < 40000) { return EVENT_TYPE_ADDRESSABLE; } return EVENT_TYPE_UNKNOWN; } const char* event_type_to_string(event_type_t type) { switch (type) { case EVENT_TYPE_REGULAR: return "regular"; case EVENT_TYPE_REPLACEABLE: return "replaceable"; case EVENT_TYPE_EPHEMERAL: return "ephemeral"; case EVENT_TYPE_ADDRESSABLE: return "addressable"; default: return "unknown"; } } // Helper function to extract d tag value from tags array const char* extract_d_tag_value(cJSON* tags) { if (!tags || !cJSON_IsArray(tags)) { return NULL; } cJSON* tag = NULL; cJSON_ArrayForEach(tag, tags) { if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) { cJSON* tag_name = cJSON_GetArrayItem(tag, 0); cJSON* tag_value = cJSON_GetArrayItem(tag, 1); if (cJSON_IsString(tag_name) && cJSON_IsString(tag_value)) { const char* name = cJSON_GetStringValue(tag_name); if (name && strcmp(name, "d") == 0) { return cJSON_GetStringValue(tag_value); } } } } return NULL; } // Check and handle replaceable events according to NIP-01 int check_and_handle_replaceable_event(int kind, const char* pubkey, long created_at) { if (!g_db || !pubkey) return 0; const char* sql = "SELECT created_at FROM events WHERE kind = ? AND pubkey = ? ORDER BY created_at DESC LIMIT 1"; sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { return 0; // Allow storage on DB error } sqlite3_bind_int(stmt, 1, kind); sqlite3_bind_text(stmt, 2, pubkey, -1, SQLITE_STATIC); int result = 0; if (sqlite3_step(stmt) == SQLITE_ROW) { long existing_created_at = sqlite3_column_int64(stmt, 0); if (created_at <= existing_created_at) { result = -1; // Older or same timestamp, reject } else { // Delete older versions const char* delete_sql = "DELETE FROM events WHERE kind = ? AND pubkey = ? AND created_at < ?"; sqlite3_stmt* delete_stmt; if (sqlite3_prepare_v2(g_db, delete_sql, -1, &delete_stmt, NULL) == SQLITE_OK) { sqlite3_bind_int(delete_stmt, 1, kind); sqlite3_bind_text(delete_stmt, 2, pubkey, -1, SQLITE_STATIC); sqlite3_bind_int64(delete_stmt, 3, created_at); sqlite3_step(delete_stmt); sqlite3_finalize(delete_stmt); } } } sqlite3_finalize(stmt); return result; } // Check and handle addressable events according to NIP-01 int check_and_handle_addressable_event(int kind, const char* pubkey, const char* d_tag_value, long created_at) { if (!g_db || !pubkey) return 0; // If no d tag, treat as regular replaceable if (!d_tag_value) { return check_and_handle_replaceable_event(kind, pubkey, created_at); } const char* sql = "SELECT created_at FROM events WHERE kind = ? AND pubkey = ? AND json_extract(tags, '$[*][1]') = ? " "AND json_extract(tags, '$[*][0]') = 'd' ORDER BY created_at DESC LIMIT 1"; sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { return 0; // Allow storage on DB error } sqlite3_bind_int(stmt, 1, kind); sqlite3_bind_text(stmt, 2, pubkey, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 3, d_tag_value, -1, SQLITE_STATIC); int result = 0; if (sqlite3_step(stmt) == SQLITE_ROW) { long existing_created_at = sqlite3_column_int64(stmt, 0); if (created_at <= existing_created_at) { result = -1; // Older or same timestamp, reject } else { // Delete older versions with same kind, pubkey, and d tag const char* delete_sql = "DELETE FROM events WHERE kind = ? AND pubkey = ? AND created_at < ? " "AND json_extract(tags, '$[*][1]') = ? AND json_extract(tags, '$[*][0]') = 'd'"; sqlite3_stmt* delete_stmt; if (sqlite3_prepare_v2(g_db, delete_sql, -1, &delete_stmt, NULL) == SQLITE_OK) { sqlite3_bind_int(delete_stmt, 1, kind); sqlite3_bind_text(delete_stmt, 2, pubkey, -1, SQLITE_STATIC); sqlite3_bind_int64(delete_stmt, 3, created_at); sqlite3_bind_text(delete_stmt, 4, d_tag_value, -1, SQLITE_STATIC); sqlite3_step(delete_stmt); sqlite3_finalize(delete_stmt); } } } sqlite3_finalize(stmt); return result; } // Store event in database int store_event(cJSON* event) { if (!g_db || !event) { return -1; } // Extract event fields cJSON* id = cJSON_GetObjectItem(event, "id"); cJSON* pubkey = cJSON_GetObjectItem(event, "pubkey"); cJSON* created_at = cJSON_GetObjectItem(event, "created_at"); cJSON* kind = cJSON_GetObjectItem(event, "kind"); cJSON* content = cJSON_GetObjectItem(event, "content"); cJSON* sig = cJSON_GetObjectItem(event, "sig"); cJSON* tags = cJSON_GetObjectItem(event, "tags"); if (!id || !pubkey || !created_at || !kind || !content || !sig) { log_error("Invalid event - missing required fields"); return -1; } // Classify event type event_type_t type = classify_event_kind((int)cJSON_GetNumberValue(kind)); // Serialize tags to JSON (use empty array if no tags) char* tags_json = NULL; if (tags && cJSON_IsArray(tags)) { tags_json = cJSON_Print(tags); } else { tags_json = strdup("[]"); } if (!tags_json) { log_error("Failed to serialize tags to JSON"); return -1; } // Prepare SQL statement for event insertion const char* sql = "INSERT INTO events (id, pubkey, created_at, kind, event_type, content, sig, tags) " "VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { log_error("Failed to prepare event insert statement"); free(tags_json); return -1; } // Bind parameters sqlite3_bind_text(stmt, 1, cJSON_GetStringValue(id), -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, cJSON_GetStringValue(pubkey), -1, SQLITE_STATIC); sqlite3_bind_int64(stmt, 3, (sqlite3_int64)cJSON_GetNumberValue(created_at)); sqlite3_bind_int(stmt, 4, (int)cJSON_GetNumberValue(kind)); sqlite3_bind_text(stmt, 5, event_type_to_string(type), -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 6, cJSON_GetStringValue(content), -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 7, cJSON_GetStringValue(sig), -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 8, tags_json, -1, SQLITE_TRANSIENT); // Execute statement rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { if (rc == SQLITE_CONSTRAINT) { log_warning("Event already exists in database"); free(tags_json); return 0; // Not an error, just duplicate } char error_msg[256]; snprintf(error_msg, sizeof(error_msg), "Failed to insert event: %s", sqlite3_errmsg(g_db)); log_error(error_msg); free(tags_json); return -1; } free(tags_json); log_success("Event stored in database"); return 0; } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // EVENT STORAGE AND RETRIEVAL ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// cJSON* retrieve_event(const char* event_id) { if (!g_db || !event_id) { return NULL; } const char* sql = "SELECT id, pubkey, created_at, kind, content, sig, tags FROM events WHERE id = ?"; sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { return NULL; } sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_STATIC); cJSON* event = NULL; if (sqlite3_step(stmt) == SQLITE_ROW) { event = cJSON_CreateObject(); cJSON_AddStringToObject(event, "id", (char*)sqlite3_column_text(stmt, 0)); cJSON_AddStringToObject(event, "pubkey", (char*)sqlite3_column_text(stmt, 1)); cJSON_AddNumberToObject(event, "created_at", sqlite3_column_int64(stmt, 2)); cJSON_AddNumberToObject(event, "kind", sqlite3_column_int(stmt, 3)); cJSON_AddStringToObject(event, "content", (char*)sqlite3_column_text(stmt, 4)); cJSON_AddStringToObject(event, "sig", (char*)sqlite3_column_text(stmt, 5)); // Parse tags JSON const char* tags_json = (char*)sqlite3_column_text(stmt, 6); if (tags_json) { cJSON* tags = cJSON_Parse(tags_json); if (tags) { cJSON_AddItemToObject(event, "tags", tags); } else { cJSON_AddItemToObject(event, "tags", cJSON_CreateArray()); } } else { cJSON_AddItemToObject(event, "tags", cJSON_CreateArray()); } } sqlite3_finalize(stmt); return event; } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // SUBSCRIPTION HANDLERS ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, struct per_session_data *pss) { log_info("Handling REQ message for persistent subscription"); if (!cJSON_IsArray(filters)) { log_error("REQ filters is not an array"); return 0; } // Check for kind 33334 configuration event requests BEFORE creating subscription int config_events_sent = 0; int has_config_request = 0; // Check if any filter requests kind 33334 (configuration events) for (int i = 0; i < cJSON_GetArraySize(filters); i++) { cJSON* filter = cJSON_GetArrayItem(filters, i); if (filter && cJSON_IsObject(filter)) { if (req_filter_requests_config_events(filter)) { has_config_request = 1; // Generate synthetic config event for this subscription cJSON* filters_array = cJSON_CreateArray(); cJSON_AddItemToArray(filters_array, cJSON_Duplicate(filter, 1)); cJSON* event_msg = generate_synthetic_config_event_for_subscription(sub_id, filters_array); if (event_msg) { char* msg_str = cJSON_Print(event_msg); if (msg_str) { size_t msg_len = strlen(msg_str); unsigned char* buf = malloc(LWS_PRE + msg_len); if (buf) { memcpy(buf + LWS_PRE, msg_str, msg_len); lws_write(wsi, buf + LWS_PRE, msg_len, LWS_WRITE_TEXT); config_events_sent++; free(buf); } free(msg_str); } cJSON_Delete(event_msg); } cJSON_Delete(filters_array); char debug_msg[256]; snprintf(debug_msg, sizeof(debug_msg), "Generated %d synthetic config events for subscription %s", config_events_sent, sub_id); log_info(debug_msg); break; // Only generate once per subscription } } } // If only config events were requested, we can return early after sending EOSE // But still create the subscription for future config updates // Check session subscription limits if (pss && pss->subscription_count >= g_subscription_manager.max_subscriptions_per_client) { log_error("Maximum subscriptions per client exceeded"); // Send CLOSED notice cJSON* closed_msg = cJSON_CreateArray(); cJSON_AddItemToArray(closed_msg, cJSON_CreateString("CLOSED")); cJSON_AddItemToArray(closed_msg, cJSON_CreateString(sub_id)); cJSON_AddItemToArray(closed_msg, cJSON_CreateString("error: too many subscriptions")); char* closed_str = cJSON_Print(closed_msg); if (closed_str) { size_t closed_len = strlen(closed_str); unsigned char* buf = malloc(LWS_PRE + closed_len); if (buf) { memcpy(buf + LWS_PRE, closed_str, closed_len); lws_write(wsi, buf + LWS_PRE, closed_len, LWS_WRITE_TEXT); free(buf); } free(closed_str); } cJSON_Delete(closed_msg); return has_config_request ? config_events_sent : 0; } // Create persistent subscription subscription_t* subscription = create_subscription(sub_id, wsi, filters, pss ? pss->client_ip : "unknown"); if (!subscription) { log_error("Failed to create subscription"); return has_config_request ? config_events_sent : 0; } // Add to global manager if (add_subscription_to_manager(subscription) != 0) { log_error("Failed to add subscription to global manager"); free_subscription(subscription); // Send CLOSED notice cJSON* closed_msg = cJSON_CreateArray(); cJSON_AddItemToArray(closed_msg, cJSON_CreateString("CLOSED")); cJSON_AddItemToArray(closed_msg, cJSON_CreateString(sub_id)); cJSON_AddItemToArray(closed_msg, cJSON_CreateString("error: subscription limit reached")); char* closed_str = cJSON_Print(closed_msg); if (closed_str) { size_t closed_len = strlen(closed_str); unsigned char* buf = malloc(LWS_PRE + closed_len); if (buf) { memcpy(buf + LWS_PRE, closed_str, closed_len); lws_write(wsi, buf + LWS_PRE, closed_len, LWS_WRITE_TEXT); free(buf); } free(closed_str); } cJSON_Delete(closed_msg); return has_config_request ? config_events_sent : 0; } // Add to session's subscription list (if session data available) if (pss) { pthread_mutex_lock(&pss->session_lock); subscription->session_next = pss->subscriptions; pss->subscriptions = subscription; pss->subscription_count++; pthread_mutex_unlock(&pss->session_lock); } int events_sent = config_events_sent; // Start with synthetic config events // Process each filter in the array for (int i = 0; i < cJSON_GetArraySize(filters); i++) { cJSON* filter = cJSON_GetArrayItem(filters, i); if (!filter || !cJSON_IsObject(filter)) { log_warning("Invalid filter object"); continue; } // Build SQL query based on filter - exclude ephemeral events (kinds 20000-29999) from historical queries char sql[1024] = "SELECT id, pubkey, created_at, kind, content, sig, tags FROM events WHERE 1=1 AND kind < 20000"; char* sql_ptr = sql + strlen(sql); int remaining = sizeof(sql) - strlen(sql); // Note: Expiration filtering will be done at application level // after retrieving events to ensure compatibility with all SQLite versions // Handle kinds filter cJSON* kinds = cJSON_GetObjectItem(filter, "kinds"); if (kinds && cJSON_IsArray(kinds)) { int kind_count = cJSON_GetArraySize(kinds); if (kind_count > 0) { snprintf(sql_ptr, remaining, " AND kind IN ("); sql_ptr += strlen(sql_ptr); remaining = sizeof(sql) - strlen(sql); for (int k = 0; k < kind_count; k++) { cJSON* kind = cJSON_GetArrayItem(kinds, k); if (cJSON_IsNumber(kind)) { if (k > 0) { snprintf(sql_ptr, remaining, ","); sql_ptr++; remaining--; } snprintf(sql_ptr, remaining, "%d", (int)cJSON_GetNumberValue(kind)); sql_ptr += strlen(sql_ptr); remaining = sizeof(sql) - strlen(sql); } } snprintf(sql_ptr, remaining, ")"); sql_ptr += strlen(sql_ptr); remaining = sizeof(sql) - strlen(sql); } } // Handle authors filter cJSON* authors = cJSON_GetObjectItem(filter, "authors"); if (authors && cJSON_IsArray(authors)) { int author_count = cJSON_GetArraySize(authors); if (author_count > 0) { snprintf(sql_ptr, remaining, " AND pubkey IN ("); sql_ptr += strlen(sql_ptr); remaining = sizeof(sql) - strlen(sql); for (int a = 0; a < author_count; a++) { cJSON* author = cJSON_GetArrayItem(authors, a); if (cJSON_IsString(author)) { if (a > 0) { snprintf(sql_ptr, remaining, ","); sql_ptr++; remaining--; } snprintf(sql_ptr, remaining, "'%s'", cJSON_GetStringValue(author)); sql_ptr += strlen(sql_ptr); remaining = sizeof(sql) - strlen(sql); } } snprintf(sql_ptr, remaining, ")"); sql_ptr += strlen(sql_ptr); remaining = sizeof(sql) - strlen(sql); } } // Handle ids filter cJSON* ids = cJSON_GetObjectItem(filter, "ids"); if (ids && cJSON_IsArray(ids)) { int id_count = cJSON_GetArraySize(ids); if (id_count > 0) { snprintf(sql_ptr, remaining, " AND id IN ("); sql_ptr += strlen(sql_ptr); remaining = sizeof(sql) - strlen(sql); for (int i = 0; i < id_count; i++) { cJSON* id = cJSON_GetArrayItem(ids, i); if (cJSON_IsString(id)) { if (i > 0) { snprintf(sql_ptr, remaining, ","); sql_ptr++; remaining--; } snprintf(sql_ptr, remaining, "'%s'", cJSON_GetStringValue(id)); sql_ptr += strlen(sql_ptr); remaining = sizeof(sql) - strlen(sql); } } snprintf(sql_ptr, remaining, ")"); sql_ptr += strlen(sql_ptr); remaining = sizeof(sql) - strlen(sql); } } // Handle since filter cJSON* since = cJSON_GetObjectItem(filter, "since"); if (since && cJSON_IsNumber(since)) { snprintf(sql_ptr, remaining, " AND created_at >= %ld", (long)cJSON_GetNumberValue(since)); sql_ptr += strlen(sql_ptr); remaining = sizeof(sql) - strlen(sql); } // Handle until filter cJSON* until = cJSON_GetObjectItem(filter, "until"); if (until && cJSON_IsNumber(until)) { snprintf(sql_ptr, remaining, " AND created_at <= %ld", (long)cJSON_GetNumberValue(until)); sql_ptr += strlen(sql_ptr); remaining = sizeof(sql) - strlen(sql); } // Add ordering and limit snprintf(sql_ptr, remaining, " ORDER BY created_at DESC"); sql_ptr += strlen(sql_ptr); remaining = sizeof(sql) - strlen(sql); // Handle limit filter cJSON* limit = cJSON_GetObjectItem(filter, "limit"); if (limit && cJSON_IsNumber(limit)) { int limit_val = (int)cJSON_GetNumberValue(limit); if (limit_val > 0 && limit_val <= 5000) { snprintf(sql_ptr, remaining, " LIMIT %d", limit_val); } } else { // Default limit to prevent excessive queries snprintf(sql_ptr, remaining, " LIMIT 500"); } // Debug: Log the SQL query being executed char debug_msg[1280]; snprintf(debug_msg, sizeof(debug_msg), "Executing SQL: %s", sql); log_info(debug_msg); // Execute query and send events sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { char error_msg[256]; snprintf(error_msg, sizeof(error_msg), "Failed to prepare subscription query: %s", sqlite3_errmsg(g_db)); log_error(error_msg); continue; } int row_count = 0; while (sqlite3_step(stmt) == SQLITE_ROW) { row_count++; // Build event JSON cJSON* event = cJSON_CreateObject(); cJSON_AddStringToObject(event, "id", (char*)sqlite3_column_text(stmt, 0)); cJSON_AddStringToObject(event, "pubkey", (char*)sqlite3_column_text(stmt, 1)); cJSON_AddNumberToObject(event, "created_at", sqlite3_column_int64(stmt, 2)); cJSON_AddNumberToObject(event, "kind", sqlite3_column_int(stmt, 3)); cJSON_AddStringToObject(event, "content", (char*)sqlite3_column_text(stmt, 4)); cJSON_AddStringToObject(event, "sig", (char*)sqlite3_column_text(stmt, 5)); // Parse tags JSON const char* tags_json = (char*)sqlite3_column_text(stmt, 6); cJSON* tags = NULL; if (tags_json) { tags = cJSON_Parse(tags_json); } if (!tags) { tags = cJSON_CreateArray(); } cJSON_AddItemToObject(event, "tags", tags); // Check expiration filtering (NIP-40) at application level pthread_mutex_lock(&g_unified_cache.cache_lock); int expiration_enabled = g_unified_cache.expiration_config.enabled; int filter_responses = g_unified_cache.expiration_config.filter_responses; pthread_mutex_unlock(&g_unified_cache.cache_lock); if (expiration_enabled && filter_responses) { time_t current_time = time(NULL); if (is_event_expired(event, current_time)) { // Skip this expired event cJSON* event_id_obj = cJSON_GetObjectItem(event, "id"); const char* event_id = event_id_obj ? cJSON_GetStringValue(event_id_obj) : "unknown"; char debug_msg[256]; snprintf(debug_msg, sizeof(debug_msg), "Filtering expired event from subscription: %.16s", event_id); log_info(debug_msg); cJSON_Delete(event); continue; } } // Send EVENT message cJSON* event_msg = cJSON_CreateArray(); cJSON_AddItemToArray(event_msg, cJSON_CreateString("EVENT")); cJSON_AddItemToArray(event_msg, cJSON_CreateString(sub_id)); cJSON_AddItemToArray(event_msg, event); char* msg_str = cJSON_Print(event_msg); if (msg_str) { size_t msg_len = strlen(msg_str); unsigned char* buf = malloc(LWS_PRE + msg_len); if (buf) { memcpy(buf + LWS_PRE, msg_str, msg_len); lws_write(wsi, buf + LWS_PRE, msg_len, LWS_WRITE_TEXT); free(buf); } free(msg_str); } cJSON_Delete(event_msg); events_sent++; } char row_debug[128]; snprintf(row_debug, sizeof(row_debug), "Query returned %d rows", row_count); log_info(row_debug); sqlite3_finalize(stmt); } char events_debug[128]; snprintf(events_debug, sizeof(events_debug), "Total events sent: %d", events_sent); log_info(events_debug); return events_sent; } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // ADMIN EVENT AUTHORIZATION ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // Enhanced admin event authorization function int is_authorized_admin_event(cJSON* event, char* error_buffer, size_t error_buffer_size) { if (!event || !error_buffer) { if (error_buffer && error_buffer_size > 0) { snprintf(error_buffer, error_buffer_size, "Invalid parameters for admin authorization"); } return -1; } // Step 1: Verify event kind is admin type cJSON *kind_json = cJSON_GetObjectItem(event, "kind"); if (!kind_json || !cJSON_IsNumber(kind_json)) { snprintf(error_buffer, error_buffer_size, "Missing or invalid event kind"); return -1; } int event_kind = kind_json->valueint; if (event_kind != 23456) { snprintf(error_buffer, error_buffer_size, "Event kind %d is not an admin event type", event_kind); return -1; } // Step 2: Check if event targets this relay (look for 'p' tag with our relay pubkey) cJSON *tags = cJSON_GetObjectItem(event, "tags"); if (!tags || !cJSON_IsArray(tags)) { // No tags array - treat as regular event for different relay log_info("Admin event has no tags array - treating as event for different relay"); snprintf(error_buffer, error_buffer_size, "Admin event not targeting this relay (no tags)"); return -1; } int targets_this_relay = 0; cJSON *tag; cJSON_ArrayForEach(tag, tags) { if (cJSON_IsArray(tag)) { cJSON *tag_name = cJSON_GetArrayItem(tag, 0); cJSON *tag_value = cJSON_GetArrayItem(tag, 1); if (tag_name && cJSON_IsString(tag_name) && tag_value && cJSON_IsString(tag_value) && strcmp(tag_name->valuestring, "p") == 0) { // Compare with our relay pubkey const char* relay_pubkey = get_config_value("relay_pubkey"); if (relay_pubkey && strcmp(tag_value->valuestring, relay_pubkey) == 0) { targets_this_relay = 1; break; } } } } if (!targets_this_relay) { // Admin event for different relay - not an error, just not for us log_info("Admin event targets different relay - treating as regular event"); snprintf(error_buffer, error_buffer_size, "Admin event not targeting this relay"); return -1; } // Step 3: Verify admin signature authorization cJSON *pubkey_json = cJSON_GetObjectItem(event, "pubkey"); if (!pubkey_json || !cJSON_IsString(pubkey_json)) { log_warning("Unauthorized admin event attempt: missing or invalid pubkey"); snprintf(error_buffer, error_buffer_size, "Unauthorized admin event attempt: missing pubkey"); return -1; } // Get admin pubkey from configuration const char* admin_pubkey = get_config_value("admin_pubkey"); if (!admin_pubkey || strlen(admin_pubkey) == 0) { log_warning("Unauthorized admin event attempt: no admin pubkey configured"); snprintf(error_buffer, error_buffer_size, "Unauthorized admin event attempt: no admin configured"); return -1; } // Compare pubkeys if (strcmp(pubkey_json->valuestring, admin_pubkey) != 0) { log_warning("Unauthorized admin event attempt: pubkey mismatch"); char warning_msg[256]; snprintf(warning_msg, sizeof(warning_msg), "Unauthorized admin event attempt from pubkey: %.32s...", pubkey_json->valuestring); log_warning(warning_msg); snprintf(error_buffer, error_buffer_size, "Unauthorized admin event attempt: invalid admin pubkey"); return -1; } // Step 4: Verify event signature if (nostr_verify_event_signature(event) != 0) { log_warning("Unauthorized admin event attempt: invalid signature"); snprintf(error_buffer, error_buffer_size, "Unauthorized admin event attempt: signature verification failed"); return -1; } // All checks passed - authorized admin event log_info("Admin event authorization successful"); return 0; } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // MAIN PROGRAM ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // Print usage information void print_usage(const char* program_name) { printf("Usage: %s [OPTIONS]\n", program_name); printf("\n"); printf("C Nostr Relay Server - Event-Based Configuration\n"); printf("\n"); printf("Options:\n"); printf(" -h, --help Show this help message\n"); printf(" -v, --version Show version information\n"); printf(" -p, --port PORT Override relay port (first-time startup only)\n"); printf(" --strict-port Fail if exact port is unavailable (no port increment)\n"); printf(" -a, --admin-pubkey HEX Override admin public key (64-char hex)\n"); printf(" -r, --relay-privkey HEX Override relay private key (64-char hex)\n"); printf("\n"); printf("Configuration:\n"); printf(" This relay uses event-based configuration stored in the database.\n"); printf(" On first startup, keys are automatically generated and printed once.\n"); printf(" Command line options like --port only apply during first-time setup.\n"); printf(" After initial setup, all configuration is managed via database events.\n"); printf(" Database file: .db (created automatically)\n"); printf("\n"); printf("Port Binding:\n"); printf(" Default: Try up to 10 consecutive ports if requested port is busy\n"); printf(" --strict-port: Fail immediately if exact requested port is unavailable\n"); printf("\n"); printf("Examples:\n"); printf(" %s # Start relay (auto-configure on first run)\n", program_name); printf(" %s -p 8080 # First-time setup with port 8080\n", program_name); printf(" %s --port 9000 # First-time setup with port 9000\n", program_name); printf(" %s --strict-port # Fail if default port 8888 is unavailable\n", program_name); printf(" %s -p 8080 --strict-port # Fail if port 8080 is unavailable\n", program_name); printf(" %s --help # Show this help\n", program_name); printf(" %s --version # Show version info\n", program_name); printf("\n"); } // Print version information void print_version() { printf("C Nostr Relay Server v1.0.0\n"); printf("Event-based configuration system\n"); printf("Built with nostr_core_lib integration\n"); printf("\n"); } int main(int argc, char* argv[]) { // Initialize CLI options structure cli_options_t cli_options = { .port_override = -1, // -1 = not set .admin_pubkey_override = {0}, // Empty string = not set .relay_privkey_override = {0}, // Empty string = not set .strict_port = 0 // 0 = allow port increment (default) }; // Parse command line arguments for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { print_usage(argv[0]); return 0; } else if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--version") == 0) { print_version(); return 0; } else if (strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "--port") == 0) { // Port override option if (i + 1 >= argc) { log_error("Port option requires a value. Use --help for usage information."); print_usage(argv[0]); return 1; } // Parse port number char* endptr; long port = strtol(argv[i + 1], &endptr, 10); if (endptr == argv[i + 1] || *endptr != '\0' || port < 1 || port > 65535) { log_error("Invalid port number. Port must be between 1 and 65535."); print_usage(argv[0]); return 1; } cli_options.port_override = (int)port; i++; // Skip the port argument char port_msg[128]; snprintf(port_msg, sizeof(port_msg), "Port override specified: %d", cli_options.port_override); log_info(port_msg); } else if (strcmp(argv[i], "-a") == 0 || strcmp(argv[i], "--admin-pubkey") == 0) { // Admin public key override option if (i + 1 >= argc) { log_error("Admin pubkey option requires a value. Use --help for usage information."); print_usage(argv[0]); return 1; } // Validate public key format (must be 64 hex characters) if (strlen(argv[i + 1]) != 64) { log_error("Invalid admin public key length. Must be exactly 64 hex characters."); print_usage(argv[0]); return 1; } // Validate hex format for (int j = 0; j < 64; j++) { char c = argv[i + 1][j]; if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { log_error("Invalid admin public key format. Must contain only hex characters (0-9, a-f, A-F)."); print_usage(argv[0]); return 1; } } strncpy(cli_options.admin_pubkey_override, argv[i + 1], sizeof(cli_options.admin_pubkey_override) - 1); cli_options.admin_pubkey_override[sizeof(cli_options.admin_pubkey_override) - 1] = '\0'; i++; // Skip the key argument log_info("Admin public key override specified"); } else if (strcmp(argv[i], "-r") == 0 || strcmp(argv[i], "--relay-privkey") == 0) { // Relay private key override option if (i + 1 >= argc) { log_error("Relay privkey option requires a value. Use --help for usage information."); print_usage(argv[0]); return 1; } // Validate private key format (must be 64 hex characters) if (strlen(argv[i + 1]) != 64) { log_error("Invalid relay private key length. Must be exactly 64 hex characters."); print_usage(argv[0]); return 1; } // Validate hex format for (int j = 0; j < 64; j++) { char c = argv[i + 1][j]; if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { log_error("Invalid relay private key format. Must contain only hex characters (0-9, a-f, A-F)."); print_usage(argv[0]); return 1; } } strncpy(cli_options.relay_privkey_override, argv[i + 1], sizeof(cli_options.relay_privkey_override) - 1); cli_options.relay_privkey_override[sizeof(cli_options.relay_privkey_override) - 1] = '\0'; i++; // Skip the key argument log_info("Relay private key override specified"); } else if (strcmp(argv[i], "--strict-port") == 0) { // Strict port mode option cli_options.strict_port = 1; log_info("Strict port mode enabled - will fail if exact port is unavailable"); } else { log_error("Unknown argument. Use --help for usage information."); print_usage(argv[0]); return 1; } } // Set up signal handlers signal(SIGINT, signal_handler); signal(SIGTERM, signal_handler); printf(BLUE BOLD "=== C Nostr Relay Server ===" RESET "\n"); // Initialize nostr library FIRST (required for key generation and event creation) if (nostr_init() != 0) { log_error("Failed to initialize nostr library"); return 1; } // Check if this is first-time startup or existing relay if (is_first_time_startup()) { log_info("First-time startup detected"); // Initialize event-based configuration system if (init_configuration_system(NULL, NULL) != 0) { log_error("Failed to initialize event-based configuration system"); nostr_cleanup(); return 1; } // Run first-time startup sequence (generates keys, sets up database path, but doesn't store private key yet) if (first_time_startup_sequence(&cli_options) != 0) { log_error("Failed to complete first-time startup sequence"); cleanup_configuration_system(); nostr_cleanup(); return 1; } // Initialize database with the generated relay pubkey if (init_database(g_database_path) != 0) { log_error("Failed to initialize database after first-time setup"); cleanup_configuration_system(); nostr_cleanup(); return 1; } // Now that database is available, store the relay private key securely const char* relay_privkey = get_temp_relay_private_key(); if (relay_privkey) { if (store_relay_private_key(relay_privkey) != 0) { log_error("Failed to store relay private key securely after database initialization"); cleanup_configuration_system(); nostr_cleanup(); return 1; } log_success("Relay private key stored securely in database"); } else { log_error("Relay private key not available from first-time startup"); cleanup_configuration_system(); nostr_cleanup(); return 1; } // Handle configuration setup after database is initialized if (cli_options.admin_pubkey_override && strlen(cli_options.admin_pubkey_override) == 64) { // Admin pubkey provided - populate config table directly log_info("Populating config table for admin pubkey override after database initialization"); // Populate default config values in table if (populate_default_config_values() != 0) { log_error("Failed to populate default config values"); cleanup_configuration_system(); nostr_cleanup(); close_database(); return 1; } // Add pubkeys to config table if (add_pubkeys_to_config_table() != 0) { log_error("Failed to add pubkeys to config table"); cleanup_configuration_system(); nostr_cleanup(); close_database(); return 1; } log_success("Configuration populated directly in config table after database initialization"); } else { // Admin private key available - retry storing initial config event if (retry_store_initial_config_event() != 0) { log_warning("Failed to store initial config event - will retry later"); } } // Now store the pubkeys in config table since database is available const char* admin_pubkey = get_admin_pubkey_cached(); const char* relay_pubkey_from_cache = get_relay_pubkey_cached(); if (admin_pubkey && strlen(admin_pubkey) == 64) { set_config_value_in_table("admin_pubkey", admin_pubkey, "string", "Administrator public key", "authentication", 0); log_success("Admin pubkey stored in config table for first-time startup"); } if (relay_pubkey_from_cache && strlen(relay_pubkey_from_cache) == 64) { set_config_value_in_table("relay_pubkey", relay_pubkey_from_cache, "string", "Relay public key", "relay", 0); log_success("Relay pubkey stored in config table for first-time startup"); } } else { log_info("Existing relay detected"); // Find existing database file char** existing_files = find_existing_db_files(); if (!existing_files || !existing_files[0]) { log_error("No existing relay database found"); nostr_cleanup(); return 1; } // Extract relay pubkey from filename char* relay_pubkey = extract_pubkey_from_filename(existing_files[0]); if (!relay_pubkey) { log_error("Failed to extract relay pubkey from database filename"); // Free the files array for (int i = 0; existing_files[i]; i++) { free(existing_files[i]); } free(existing_files); nostr_cleanup(); return 1; } // Initialize event-based configuration system if (init_configuration_system(NULL, NULL) != 0) { log_error("Failed to initialize event-based configuration system"); free(relay_pubkey); for (int i = 0; existing_files[i]; i++) { free(existing_files[i]); } free(existing_files); nostr_cleanup(); return 1; } // Setup existing relay (sets database path and loads config) if (startup_existing_relay(relay_pubkey) != 0) { log_error("Failed to setup existing relay"); cleanup_configuration_system(); free(relay_pubkey); for (int i = 0; existing_files[i]; i++) { free(existing_files[i]); } free(existing_files); nostr_cleanup(); return 1; } // Initialize database with existing database path if (init_database(g_database_path) != 0) { log_error("Failed to initialize existing database"); cleanup_configuration_system(); free(relay_pubkey); for (int i = 0; existing_files[i]; i++) { free(existing_files[i]); } free(existing_files); nostr_cleanup(); return 1; } // Load configuration from database cJSON* config_event = load_config_event_from_database(relay_pubkey); if (config_event) { if (apply_configuration_from_event(config_event) != 0) { log_warning("Failed to apply configuration from database"); } else { log_success("Configuration loaded from database"); // Extract admin pubkey from the config event and store in config table for unified cache access cJSON* pubkey_obj = cJSON_GetObjectItem(config_event, "pubkey"); const char* admin_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : NULL; // Store both admin and relay pubkeys in config table for unified cache if (admin_pubkey && strlen(admin_pubkey) == 64) { set_config_value_in_table("admin_pubkey", admin_pubkey, "string", "Administrator public key", "authentication", 0); log_info("Admin pubkey stored in config table for existing relay"); } if (relay_pubkey && strlen(relay_pubkey) == 64) { set_config_value_in_table("relay_pubkey", relay_pubkey, "string", "Relay public key", "relay", 0); log_info("Relay pubkey stored in config table for existing relay"); } } cJSON_Delete(config_event); } else { log_warning("No configuration event found in existing database"); } // Free memory free(relay_pubkey); for (int i = 0; existing_files[i]; i++) { free(existing_files[i]); } free(existing_files); } // Verify database is now available if (!g_db) { log_error("Database not available after initialization"); cleanup_configuration_system(); nostr_cleanup(); return 1; } // Configuration system is now fully initialized with event-based approach // All configuration is loaded from database events // Initialize unified request validator system if (ginxsom_request_validator_init(g_database_path, "c-relay") != 0) { log_error("Failed to initialize unified request validator"); cleanup_configuration_system(); nostr_cleanup(); close_database(); return 1; } log_success("Unified request validator initialized"); // Initialize NIP-11 relay information init_relay_info(); // Initialize NIP-13 PoW configuration init_pow_config(); // Initialize NIP-40 expiration configuration init_expiration_config(); // Update subscription manager configuration update_subscription_manager_config(); log_info("Starting relay server..."); // Start WebSocket Nostr relay server (port from configuration) int result = start_websocket_relay(-1, cli_options.strict_port); // Let config system determine port, pass strict_port flag // Cleanup cleanup_relay_info(); ginxsom_request_validator_cleanup(); cleanup_configuration_system(); nostr_cleanup(); close_database(); if (result == 0) { log_success("Server shutdown complete"); } else { log_error("Server shutdown with errors"); } return result; }