#define _GNU_SOURCE #include "config.h" #include "default_config_event.h" #include "../nostr_core_lib/nostr_core/nostr_core.h" #include #include #include #include #include #include #include #include #include // External database connection (from main.c) extern sqlite3* g_db; // Global unified configuration cache instance unified_config_cache_t g_unified_cache = { .cache_lock = PTHREAD_MUTEX_INITIALIZER, .cache_valid = 0, .cache_expires = 0 }; char g_database_path[512] = {0}; // ================================ // NEW ADMIN API STRUCTURES // ================================ // Migration state management typedef enum { MIGRATION_NOT_NEEDED, MIGRATION_NEEDED, MIGRATION_IN_PROGRESS, MIGRATION_COMPLETED, MIGRATION_FAILED } migration_state_t; typedef struct { migration_state_t state; int event_config_count; int table_config_count; int migration_errors; time_t migration_started; time_t migration_completed; char error_message[512]; } migration_status_t; static migration_status_t g_migration_status = {0}; // Configuration source type typedef enum { CONFIG_SOURCE_EVENT, // Current event-based system CONFIG_SOURCE_TABLE, // New table-based system CONFIG_SOURCE_HYBRID // During migration } config_source_t; // Logging functions (defined in main.c) extern void log_info(const char* message); extern void log_success(const char* message); extern void log_warning(const char* message); extern void log_error(const char* message); // Forward declarations for new admin API functions int populate_default_config_values(void); int process_admin_config_event(cJSON* event, char* error_message, size_t error_size); void invalidate_config_cache(void); int add_auth_rule_from_config(const char* rule_type, const char* pattern_type, const char* pattern_value, const char* action); int remove_auth_rule_from_config(const char* rule_type, const char* pattern_type, const char* pattern_value); int is_config_table_ready(void); int migrate_config_from_events_to_table(void); int populate_config_table_from_event(const cJSON* event); int handle_config_query_unified(cJSON* event, const char* query_type, char* error_message, size_t error_size, struct lws* wsi); int handle_config_set_unified(cJSON* event, const char* config_key, const char* config_value, char* error_message, size_t error_size, struct lws* wsi); // Forward declarations for tag parsing utilities const char* get_first_tag_name(cJSON* event); const char* get_tag_value(cJSON* event, const char* tag_name, int value_index); int parse_auth_query_parameters(cJSON* event, char** query_type, char** pattern_value); int handle_config_update_unified(cJSON* event, char* error_message, size_t error_size, struct lws* wsi); // Current configuration cache static cJSON* g_current_config = NULL; // Cache for initial configuration event (before database is initialized) static cJSON* g_pending_config_event = NULL; // Temporary storage for relay private key during first-time setup static char g_temp_relay_privkey[65] = {0}; // ================================ // UNIFIED CACHE MANAGEMENT FUNCTIONS // ================================ // Get cache timeout from environment variable or default (similar to request_validator) static int get_cache_timeout(void) { char *no_cache = getenv("GINX_NO_CACHE"); char *cache_timeout = getenv("GINX_CACHE_TIMEOUT"); if (no_cache && strcmp(no_cache, "1") == 0) { return 0; // No caching } if (cache_timeout) { int timeout = atoi(cache_timeout); return (timeout >= 0) ? timeout : 300; // Use provided value or default } return 300; // Default 5 minutes } // Helper function to safely return dynamically allocated string from static buffer static char* safe_strdup_from_static(const char* static_str) { if (!static_str) return NULL; return strdup(static_str); } // Force cache refresh - invalidates current cache void force_config_cache_refresh(void) { pthread_mutex_lock(&g_unified_cache.cache_lock); g_unified_cache.cache_valid = 0; g_unified_cache.cache_expires = 0; pthread_mutex_unlock(&g_unified_cache.cache_lock); log_info("Configuration cache forcibly invalidated"); } // Update specific cache value without full refresh int update_cache_value(const char* key, const char* value) { if (!key || !value) { return -1; } pthread_mutex_lock(&g_unified_cache.cache_lock); // Update specific cache fields if (strcmp(key, "admin_pubkey") == 0) { strncpy(g_unified_cache.admin_pubkey, value, sizeof(g_unified_cache.admin_pubkey) - 1); g_unified_cache.admin_pubkey[sizeof(g_unified_cache.admin_pubkey) - 1] = '\0'; } else if (strcmp(key, "relay_pubkey") == 0) { strncpy(g_unified_cache.relay_pubkey, value, sizeof(g_unified_cache.relay_pubkey) - 1); g_unified_cache.relay_pubkey[sizeof(g_unified_cache.relay_pubkey) - 1] = '\0'; } else if (strcmp(key, "auth_required") == 0) { g_unified_cache.auth_required = (strcmp(value, "true") == 0) ? 1 : 0; } else if (strcmp(key, "admin_enabled") == 0) { g_unified_cache.admin_enabled = (strcmp(value, "true") == 0) ? 1 : 0; } else if (strcmp(key, "max_file_size") == 0) { g_unified_cache.max_file_size = atol(value); } else if (strcmp(key, "nip42_mode") == 0) { if (strcmp(value, "disabled") == 0) { g_unified_cache.nip42_mode = 0; } else if (strcmp(value, "required") == 0) { g_unified_cache.nip42_mode = 2; } else { g_unified_cache.nip42_mode = 1; // Optional/enabled } } else if (strcmp(key, "nip42_challenge_timeout") == 0) { g_unified_cache.nip42_challenge_timeout = atoi(value); } else if (strcmp(key, "nip42_time_tolerance") == 0) { g_unified_cache.nip42_time_tolerance = atoi(value); } else { // For NIP-11 relay info fields, update the cache buffers if (strcmp(key, "relay_name") == 0) { strncpy(g_unified_cache.relay_info.name, value, sizeof(g_unified_cache.relay_info.name) - 1); g_unified_cache.relay_info.name[sizeof(g_unified_cache.relay_info.name) - 1] = '\0'; } else if (strcmp(key, "relay_description") == 0) { strncpy(g_unified_cache.relay_info.description, value, sizeof(g_unified_cache.relay_info.description) - 1); g_unified_cache.relay_info.description[sizeof(g_unified_cache.relay_info.description) - 1] = '\0'; } else if (strcmp(key, "relay_contact") == 0) { strncpy(g_unified_cache.relay_info.contact, value, sizeof(g_unified_cache.relay_info.contact) - 1); g_unified_cache.relay_info.contact[sizeof(g_unified_cache.relay_info.contact) - 1] = '\0'; } else if (strcmp(key, "relay_software") == 0) { strncpy(g_unified_cache.relay_info.software, value, sizeof(g_unified_cache.relay_info.software) - 1); g_unified_cache.relay_info.software[sizeof(g_unified_cache.relay_info.software) - 1] = '\0'; } else if (strcmp(key, "relay_version") == 0) { strncpy(g_unified_cache.relay_info.version, value, sizeof(g_unified_cache.relay_info.version) - 1); g_unified_cache.relay_info.version[sizeof(g_unified_cache.relay_info.version) - 1] = '\0'; } else if (strcmp(key, "supported_nips") == 0) { strncpy(g_unified_cache.relay_info.supported_nips_str, value, sizeof(g_unified_cache.relay_info.supported_nips_str) - 1); g_unified_cache.relay_info.supported_nips_str[sizeof(g_unified_cache.relay_info.supported_nips_str) - 1] = '\0'; } else if (strcmp(key, "language_tags") == 0) { strncpy(g_unified_cache.relay_info.language_tags_str, value, sizeof(g_unified_cache.relay_info.language_tags_str) - 1); g_unified_cache.relay_info.language_tags_str[sizeof(g_unified_cache.relay_info.language_tags_str) - 1] = '\0'; } else if (strcmp(key, "relay_countries") == 0) { strncpy(g_unified_cache.relay_info.relay_countries_str, value, sizeof(g_unified_cache.relay_info.relay_countries_str) - 1); g_unified_cache.relay_info.relay_countries_str[sizeof(g_unified_cache.relay_info.relay_countries_str) - 1] = '\0'; } else if (strcmp(key, "posting_policy") == 0) { strncpy(g_unified_cache.relay_info.posting_policy, value, sizeof(g_unified_cache.relay_info.posting_policy) - 1); g_unified_cache.relay_info.posting_policy[sizeof(g_unified_cache.relay_info.posting_policy) - 1] = '\0'; } else if (strcmp(key, "payments_url") == 0) { strncpy(g_unified_cache.relay_info.payments_url, value, sizeof(g_unified_cache.relay_info.payments_url) - 1); g_unified_cache.relay_info.payments_url[sizeof(g_unified_cache.relay_info.payments_url) - 1] = '\0'; } } // Reset cache expiration to extend validity int cache_timeout = get_cache_timeout(); g_unified_cache.cache_expires = time(NULL) + cache_timeout; pthread_mutex_unlock(&g_unified_cache.cache_lock); log_info("Updated specific cache value"); printf(" Key: %s\n", key); return 0; } // Refresh unified cache from database static int refresh_unified_cache_from_table(void) { if (!g_db) { log_error("Database not available for cache refresh"); return -1; } // Clear cache memset(&g_unified_cache, 0, sizeof(g_unified_cache)); g_unified_cache.cache_lock = (pthread_mutex_t)PTHREAD_MUTEX_INITIALIZER; // Load critical config values from table const char* admin_pubkey = get_config_value_from_table("admin_pubkey"); if (admin_pubkey) { strncpy(g_unified_cache.admin_pubkey, admin_pubkey, sizeof(g_unified_cache.admin_pubkey) - 1); g_unified_cache.admin_pubkey[sizeof(g_unified_cache.admin_pubkey) - 1] = '\0'; } const char* relay_pubkey = get_config_value_from_table("relay_pubkey"); if (relay_pubkey) { strncpy(g_unified_cache.relay_pubkey, relay_pubkey, sizeof(g_unified_cache.relay_pubkey) - 1); g_unified_cache.relay_pubkey[sizeof(g_unified_cache.relay_pubkey) - 1] = '\0'; } // Load auth-related config const char* auth_required = get_config_value_from_table("auth_required"); g_unified_cache.auth_required = (auth_required && strcmp(auth_required, "true") == 0) ? 1 : 0; const char* admin_enabled = get_config_value_from_table("admin_enabled"); g_unified_cache.admin_enabled = (admin_enabled && strcmp(admin_enabled, "true") == 0) ? 1 : 0; const char* max_file_size = get_config_value_from_table("max_file_size"); g_unified_cache.max_file_size = max_file_size ? atol(max_file_size) : 104857600; // 100MB default const char* nip42_mode = get_config_value_from_table("nip42_mode"); if (nip42_mode) { if (strcmp(nip42_mode, "disabled") == 0) { g_unified_cache.nip42_mode = 0; } else if (strcmp(nip42_mode, "required") == 0) { g_unified_cache.nip42_mode = 2; } else { g_unified_cache.nip42_mode = 1; // Optional/enabled } } else { g_unified_cache.nip42_mode = 1; // Default to optional/enabled } const char* challenge_timeout = get_config_value_from_table("nip42_challenge_timeout"); g_unified_cache.nip42_challenge_timeout = challenge_timeout ? atoi(challenge_timeout) : 600; const char* time_tolerance = get_config_value_from_table("nip42_time_tolerance"); g_unified_cache.nip42_time_tolerance = time_tolerance ? atoi(time_tolerance) : 300; // Set cache expiration int cache_timeout = get_cache_timeout(); g_unified_cache.cache_expires = time(NULL) + cache_timeout; g_unified_cache.cache_valid = 1; log_info("Unified configuration cache refreshed from database"); return 0; } // Get admin pubkey from cache (with automatic refresh) const char* get_admin_pubkey_cached(void) { pthread_mutex_lock(&g_unified_cache.cache_lock); // Check cache validity if (!g_unified_cache.cache_valid || time(NULL) > g_unified_cache.cache_expires) { refresh_unified_cache_from_table(); } const char* result = g_unified_cache.admin_pubkey[0] ? g_unified_cache.admin_pubkey : NULL; pthread_mutex_unlock(&g_unified_cache.cache_lock); return result; } // Get relay pubkey from cache (with automatic refresh) const char* get_relay_pubkey_cached(void) { pthread_mutex_lock(&g_unified_cache.cache_lock); // Check cache validity if (!g_unified_cache.cache_valid || time(NULL) > g_unified_cache.cache_expires) { refresh_unified_cache_from_table(); } const char* result = g_unified_cache.relay_pubkey[0] ? g_unified_cache.relay_pubkey : NULL; pthread_mutex_unlock(&g_unified_cache.cache_lock); return result; } // ================================ // UTILITY FUNCTIONS // ================================ char** find_existing_db_files(void) { DIR *dir; struct dirent *entry; char **files = NULL; int count = 0; dir = opendir("."); if (dir == NULL) { return NULL; } // Count .db files while ((entry = readdir(dir)) != NULL) { if (strstr(entry->d_name, ".db") != NULL) { count++; } } rewinddir(dir); if (count == 0) { closedir(dir); return NULL; } // Allocate array for filenames files = malloc((count + 1) * sizeof(char*)); if (!files) { closedir(dir); return NULL; } // Store filenames int i = 0; while ((entry = readdir(dir)) != NULL && i < count) { if (strstr(entry->d_name, ".db") != NULL) { files[i] = malloc(strlen(entry->d_name) + 1); if (files[i]) { strcpy(files[i], entry->d_name); i++; } } } files[i] = NULL; // Null terminate closedir(dir); return files; } char* extract_pubkey_from_filename(const char* filename) { if (!filename) return NULL; // Find .db extension const char* dot = strstr(filename, ".db"); if (!dot) return NULL; // Calculate pubkey length size_t pubkey_len = dot - filename; if (pubkey_len != 64) return NULL; // Invalid pubkey length // Allocate and copy pubkey char* pubkey = malloc(pubkey_len + 1); if (!pubkey) return NULL; strncpy(pubkey, filename, pubkey_len); pubkey[pubkey_len] = '\0'; return pubkey; } char* get_database_name_from_relay_pubkey(const char* relay_pubkey) { if (!relay_pubkey || strlen(relay_pubkey) != 64) { return NULL; } char* db_name = malloc(strlen(relay_pubkey) + 4); // +4 for ".db\0" if (!db_name) return NULL; sprintf(db_name, "%s.db", relay_pubkey); return db_name; } // ================================ // DATABASE FUNCTIONS // ================================ int create_database_with_relay_pubkey(const char* relay_pubkey) { char* db_name = get_database_name_from_relay_pubkey(relay_pubkey); if (!db_name) { log_error("Failed to generate database name"); return -1; } strncpy(g_database_path, db_name, sizeof(g_database_path) - 1); g_database_path[sizeof(g_database_path) - 1] = '\0'; log_info("Creating database with relay pubkey"); printf(" Database: %s\n", db_name); free(db_name); return 0; } // ================================ // CONFIGURATION EVENT FUNCTIONS // ================================ int store_config_event_in_database(const cJSON* event) { if (!event || !g_db) { return -1; } // Get event fields cJSON* id_obj = cJSON_GetObjectItem(event, "id"); cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey"); cJSON* created_at_obj = cJSON_GetObjectItem(event, "created_at"); cJSON* kind_obj = cJSON_GetObjectItem(event, "kind"); cJSON* content_obj = cJSON_GetObjectItem(event, "content"); cJSON* sig_obj = cJSON_GetObjectItem(event, "sig"); cJSON* tags_obj = cJSON_GetObjectItem(event, "tags"); if (!id_obj || !pubkey_obj || !created_at_obj || !kind_obj || !content_obj || !sig_obj || !tags_obj) { return -1; } // Convert tags to JSON string char* tags_str = cJSON_Print(tags_obj); if (!tags_str) { return -1; } // Insert or replace the configuration event const char* sql = "INSERT OR REPLACE 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 configuration event insert"); free(tags_str); return -1; } sqlite3_bind_text(stmt, 1, cJSON_GetStringValue(id_obj), -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, cJSON_GetStringValue(pubkey_obj), -1, SQLITE_STATIC); sqlite3_bind_int64(stmt, 3, (sqlite3_int64)cJSON_GetNumberValue(created_at_obj)); sqlite3_bind_int(stmt, 4, (int)cJSON_GetNumberValue(kind_obj)); sqlite3_bind_text(stmt, 5, "regular", -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 6, cJSON_GetStringValue(content_obj), -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 7, cJSON_GetStringValue(sig_obj), -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 8, tags_str, -1, SQLITE_TRANSIENT); rc = sqlite3_step(stmt); sqlite3_finalize(stmt); free(tags_str); if (rc == SQLITE_DONE) { log_success("Configuration event stored in database"); return 0; } else { log_error("Failed to store configuration event"); return -1; } } cJSON* load_config_event_from_database(const char* relay_pubkey) { if (!g_db || !relay_pubkey) { return NULL; } // Configuration is now managed through config table, not events log_info("Configuration events are no longer stored in events table"); return NULL; } // ================================ // CONFIGURATION ACCESS FUNCTIONS // ================================ const char* get_config_value(const char* key) { if (!key) { return NULL; } // Special fast path for frequently accessed keys via unified cache if (strcmp(key, "admin_pubkey") == 0) { const char* cached_value = get_admin_pubkey_cached(); return safe_strdup_from_static(cached_value); } if (strcmp(key, "relay_pubkey") == 0) { const char* cached_value = get_relay_pubkey_cached(); return safe_strdup_from_static(cached_value); } // For other keys, try config table first const char* table_value = get_config_value_from_table(key); if (table_value) { return table_value; } // Fallback to legacy event-based config for backward compatibility // Use unified cache buffer instead of static buffer if (!g_current_config) { return NULL; } // Look for key in current configuration tags cJSON* tags = cJSON_GetObjectItem(g_current_config, "tags"); if (!tags || !cJSON_IsArray(tags)) { return NULL; } pthread_mutex_lock(&g_unified_cache.cache_lock); cJSON* tag = NULL; cJSON_ArrayForEach(tag, tags) { if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) { cJSON* tag_key = cJSON_GetArrayItem(tag, 0); cJSON* tag_value = cJSON_GetArrayItem(tag, 1); if (tag_key && tag_value && cJSON_IsString(tag_key) && cJSON_IsString(tag_value)) { if (strcmp(cJSON_GetStringValue(tag_key), key) == 0) { strncpy(g_unified_cache.temp_buffer, cJSON_GetStringValue(tag_value), sizeof(g_unified_cache.temp_buffer) - 1); g_unified_cache.temp_buffer[sizeof(g_unified_cache.temp_buffer) - 1] = '\0'; const char* result = safe_strdup_from_static(g_unified_cache.temp_buffer); pthread_mutex_unlock(&g_unified_cache.cache_lock); return result; } } } } pthread_mutex_unlock(&g_unified_cache.cache_lock); return NULL; } int get_config_int(const char* key, int default_value) { const char* str_value = get_config_value(key); if (!str_value) { return default_value; } char* endptr; long val = strtol(str_value, &endptr, 10); if (endptr == str_value || *endptr != '\0') { // Free the dynamically allocated string free((char*)str_value); return default_value; } // Free the dynamically allocated string free((char*)str_value); return (int)val; } int get_config_bool(const char* key, int default_value) { const char* str_value = get_config_value(key); if (!str_value) { return default_value; } int result; if (strcasecmp(str_value, "true") == 0 || strcasecmp(str_value, "yes") == 0 || strcasecmp(str_value, "1") == 0) { result = 1; } else if (strcasecmp(str_value, "false") == 0 || strcasecmp(str_value, "no") == 0 || strcasecmp(str_value, "0") == 0) { result = 0; } else { result = default_value; } // Free the dynamically allocated string free((char*)str_value); return result; } // ================================ // NIP-42 KIND-SPECIFIC AUTHENTICATION // ================================ // Parse comma-separated kind list into array of integers // Returns number of kinds parsed, or -1 on error int parse_auth_required_kinds(const char* kinds_str, int* kinds_array, int max_kinds) { if (!kinds_str || !kinds_array || max_kinds <= 0) { return -1; } // Empty string means no kinds require auth if (strlen(kinds_str) == 0) { return 0; } char* str_copy = strdup(kinds_str); if (!str_copy) { return -1; } int count = 0; char* token = strtok(str_copy, ","); while (token && count < max_kinds) { // Trim whitespace while (*token == ' ' || *token == '\t') token++; char* end = token + strlen(token) - 1; while (end > token && (*end == ' ' || *end == '\t')) end--; *(end + 1) = '\0'; // Convert to integer char* endptr; long kind = strtol(token, &endptr, 10); if (endptr != token && *endptr == '\0' && kind >= 0 && kind <= 65535) { kinds_array[count] = (int)kind; count++; } token = strtok(NULL, ","); } free(str_copy); return count; } // Check if a specific event kind requires NIP-42 authentication int is_nip42_auth_required_for_kind(int event_kind) { const char* kinds_str = get_config_value("nip42_auth_required_kinds"); if (!kinds_str) { return 0; // No authentication required if setting is missing } // Parse the kinds list int required_kinds[100]; // Support up to 100 different kinds int count = parse_auth_required_kinds(kinds_str, required_kinds, 100); if (count < 0) { return 0; // Parse error, default to no auth required } // Check if event_kind is in the list for (int i = 0; i < count; i++) { if (required_kinds[i] == event_kind) { return 1; // Authentication required for this kind } } return 0; // Authentication not required for this kind } // Get NIP-42 global authentication requirement int is_nip42_auth_globally_required(void) { return get_config_bool("nip42_auth_required", 0); } // ================================ // FIRST-TIME STARTUP FUNCTIONS // ================================ int is_first_time_startup(void) { char** existing_files = find_existing_db_files(); if (existing_files) { // Free the array for (int i = 0; existing_files[i]; i++) { free(existing_files[i]); } free(existing_files); return 0; // Not first time } return 1; // First time } // ================================ // COMPATIBILITY FUNCTIONS // ================================ int init_configuration_system(const char* config_dir_override, const char* config_file_override) { // Suppress unused parameter warnings for compatibility function (void)config_dir_override; (void)config_file_override; log_info("Initializing event-based configuration system..."); // Initialize unified cache with proper structure initialization pthread_mutex_lock(&g_unified_cache.cache_lock); // Clear the entire cache structure memset(&g_unified_cache, 0, sizeof(g_unified_cache)); // Reinitialize the mutex after memset g_unified_cache.cache_lock = (pthread_mutex_t)PTHREAD_MUTEX_INITIALIZER; // Initialize basic cache state g_unified_cache.cache_valid = 0; g_unified_cache.cache_expires = 0; // Initialize relay_info structure with default values strncpy(g_unified_cache.relay_info.software, "https://git.laantungir.net/laantungir/c-relay.git", sizeof(g_unified_cache.relay_info.software) - 1); strncpy(g_unified_cache.relay_info.version, "0.2.0", sizeof(g_unified_cache.relay_info.version) - 1); // Initialize pow_config structure with defaults g_unified_cache.pow_config.enabled = 1; g_unified_cache.pow_config.min_pow_difficulty = 0; g_unified_cache.pow_config.validation_flags = 1; // NOSTR_POW_VALIDATE_BASIC g_unified_cache.pow_config.require_nonce_tag = 0; g_unified_cache.pow_config.reject_lower_targets = 0; g_unified_cache.pow_config.strict_format = 0; g_unified_cache.pow_config.anti_spam_mode = 0; // Initialize expiration_config structure with defaults g_unified_cache.expiration_config.enabled = 1; g_unified_cache.expiration_config.strict_mode = 1; g_unified_cache.expiration_config.filter_responses = 1; g_unified_cache.expiration_config.delete_expired = 0; g_unified_cache.expiration_config.grace_period = 1; pthread_mutex_unlock(&g_unified_cache.cache_lock); log_success("Event-based configuration system initialized with unified cache structures"); return 0; } void cleanup_configuration_system(void) { log_info("Cleaning up configuration system..."); if (g_current_config) { cJSON_Delete(g_current_config); g_current_config = NULL; } if (g_pending_config_event) { cJSON_Delete(g_pending_config_event); g_pending_config_event = NULL; } // Clear unified cache with proper cleanup of JSON objects pthread_mutex_lock(&g_unified_cache.cache_lock); // Clean up relay_info JSON objects if they exist if (g_unified_cache.relay_info.supported_nips) { cJSON_Delete(g_unified_cache.relay_info.supported_nips); } if (g_unified_cache.relay_info.limitation) { cJSON_Delete(g_unified_cache.relay_info.limitation); } if (g_unified_cache.relay_info.retention) { cJSON_Delete(g_unified_cache.relay_info.retention); } if (g_unified_cache.relay_info.relay_countries) { cJSON_Delete(g_unified_cache.relay_info.relay_countries); } if (g_unified_cache.relay_info.language_tags) { cJSON_Delete(g_unified_cache.relay_info.language_tags); } if (g_unified_cache.relay_info.tags) { cJSON_Delete(g_unified_cache.relay_info.tags); } if (g_unified_cache.relay_info.fees) { cJSON_Delete(g_unified_cache.relay_info.fees); } // Clear the entire cache structure memset(&g_unified_cache, 0, sizeof(g_unified_cache)); g_unified_cache.cache_lock = (pthread_mutex_t)PTHREAD_MUTEX_INITIALIZER; pthread_mutex_unlock(&g_unified_cache.cache_lock); log_success("Configuration system cleaned up with proper JSON cleanup"); } int set_database_config(const char* key, const char* value, const char* changed_by) { // Suppress unused parameter warnings for compatibility function (void)key; (void)value; (void)changed_by; // Temporary compatibility function - does nothing for now // In the new system, configuration is only updated via events log_warning("set_database_config called - not supported in event-based config"); return 0; } // ================================ // KEY GENERATION FUNCTIONS // ================================ // Helper function to generate random private key int generate_random_private_key_bytes(unsigned char* privkey_bytes) { if (!privkey_bytes) { return -1; } FILE* urandom = fopen("/dev/urandom", "rb"); if (!urandom) { log_error("Failed to open /dev/urandom for key generation"); return -1; } if (fread(privkey_bytes, 1, 32, urandom) != 32) { log_error("Failed to read random bytes for private key"); fclose(urandom); return -1; } fclose(urandom); // Verify the private key is valid using nostr_core_lib if (nostr_ec_private_key_verify(privkey_bytes) != NOSTR_SUCCESS) { log_error("Generated private key failed validation"); return -1; } return 0; } // ================================ // SECURE RELAY PRIVATE KEY STORAGE // ================================ int store_relay_private_key(const char* relay_privkey_hex) { if (!relay_privkey_hex) { log_error("Invalid relay private key for storage"); return -1; } // Validate private key format (must be 64 hex characters) if (strlen(relay_privkey_hex) != 64) { log_error("Invalid relay private key length (must be 64 hex characters)"); return -1; } // Validate hex format for (int i = 0; i < 64; i++) { char c = relay_privkey_hex[i]; if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { log_error("Invalid relay private key format (must be hex characters only)"); return -1; } } if (!g_db) { log_error("Database not available for relay private key storage"); return -1; } const char* sql = "INSERT OR REPLACE INTO relay_seckey (private_key_hex) VALUES (?)"; sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { log_error("Failed to prepare relay private key storage query"); return -1; } sqlite3_bind_text(stmt, 1, relay_privkey_hex, -1, SQLITE_STATIC); rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc == SQLITE_DONE) { log_success("Relay private key stored securely in database"); return 0; } else { log_error("Failed to store relay private key in database"); return -1; } } char* get_relay_private_key(void) { if (!g_db) { log_error("Database not available for relay private key retrieval"); return NULL; } const char* sql = "SELECT private_key_hex FROM relay_seckey"; sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { log_error("Failed to prepare relay private key retrieval query"); return NULL; } char* private_key = NULL; if (sqlite3_step(stmt) == SQLITE_ROW) { const char* key_from_db = (const char*)sqlite3_column_text(stmt, 0); if (key_from_db && strlen(key_from_db) == 64) { private_key = malloc(65); // 64 chars + null terminator if (private_key) { strcpy(private_key, key_from_db); } } } sqlite3_finalize(stmt); if (!private_key) { log_error("Relay private key not found in secure storage"); } return private_key; } const char* get_temp_relay_private_key(void) { if (strlen(g_temp_relay_privkey) == 64) { return g_temp_relay_privkey; } return NULL; } // ================================ // DEFAULT CONFIG EVENT CREATION // ================================ cJSON* create_default_config_event(const unsigned char* admin_privkey_bytes, const char* relay_privkey_hex, const char* relay_pubkey_hex, const cli_options_t* cli_options) { if (!admin_privkey_bytes || !relay_privkey_hex || !relay_pubkey_hex) { log_error("Invalid parameters for creating default config event"); return NULL; } log_info("Creating default configuration event..."); // Create tags array with default configuration values cJSON* tags = cJSON_CreateArray(); if (!tags) { log_error("Failed to create tags array"); return NULL; } // Add d tag with relay pubkey cJSON* d_tag = cJSON_CreateArray(); cJSON_AddItemToArray(d_tag, cJSON_CreateString("d")); cJSON_AddItemToArray(d_tag, cJSON_CreateString(relay_pubkey_hex)); cJSON_AddItemToArray(tags, d_tag); // Add relay keys cJSON* relay_pubkey_tag = cJSON_CreateArray(); cJSON_AddItemToArray(relay_pubkey_tag, cJSON_CreateString("relay_pubkey")); cJSON_AddItemToArray(relay_pubkey_tag, cJSON_CreateString(relay_pubkey_hex)); cJSON_AddItemToArray(tags, relay_pubkey_tag); // Note: relay_privkey is now stored securely in relay_seckey table // It is no longer included in the public configuration event // Add all default configuration values with command line overrides for (size_t i = 0; i < DEFAULT_CONFIG_COUNT; i++) { cJSON* tag = cJSON_CreateArray(); cJSON_AddItemToArray(tag, cJSON_CreateString(DEFAULT_CONFIG_VALUES[i].key)); // Check for command line overrides const char* value = DEFAULT_CONFIG_VALUES[i].value; if (cli_options) { // Override relay_port if specified on command line if (cli_options->port_override > 0 && strcmp(DEFAULT_CONFIG_VALUES[i].key, "relay_port") == 0) { char port_str[16]; snprintf(port_str, sizeof(port_str), "%d", cli_options->port_override); cJSON_AddItemToArray(tag, cJSON_CreateString(port_str)); log_info("Using command line port override in configuration event"); printf(" Port: %d (overriding default %s)\n", cli_options->port_override, DEFAULT_CONFIG_VALUES[i].value); } else { cJSON_AddItemToArray(tag, cJSON_CreateString(value)); } } else { cJSON_AddItemToArray(tag, cJSON_CreateString(value)); } cJSON_AddItemToArray(tags, tag); } // Create and sign event using nostr_core_lib cJSON* event = nostr_create_and_sign_event( 33334, // kind "C Nostr Relay Configuration", // content tags, // tags admin_privkey_bytes, // private key bytes for signing time(NULL) // created_at timestamp ); cJSON_Delete(tags); // Clean up tags as they were duplicated in nostr_create_and_sign_event if (!event) { log_error("Failed to create and sign configuration event"); return NULL; } // Log success information cJSON* id_obj = cJSON_GetObjectItem(event, "id"); cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey"); if (id_obj && pubkey_obj) { log_success("Default configuration event created successfully"); printf(" Event ID: %s\n", cJSON_GetStringValue(id_obj)); printf(" Admin Public Key: %s\n", cJSON_GetStringValue(pubkey_obj)); } return event; } // ================================ // IMPLEMENTED FUNCTIONS // ================================ int first_time_startup_sequence(const cli_options_t* cli_options) { log_info("Starting first-time startup sequence..."); // 1. Generate or use provided admin keypair unsigned char admin_privkey_bytes[32]; char admin_privkey[65], admin_pubkey[65]; int generated_admin_key = 0; // Track if we generated a new admin key if (cli_options && strlen(cli_options->admin_pubkey_override) == 64) { // Use provided admin public key directly - skip private key generation entirely log_info("Using provided admin public key override - skipping private key generation"); strncpy(admin_pubkey, cli_options->admin_pubkey_override, sizeof(admin_pubkey) - 1); admin_pubkey[sizeof(admin_pubkey) - 1] = '\0'; // Validate the public key format (must be 64 hex characters) for (int i = 0; i < 64; i++) { char c = admin_pubkey[i]; 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"); return -1; } } // Skip private key generation - we only need the pubkey for admin verification // Set a dummy private key that will never be used (not displayed or stored) memset(admin_privkey_bytes, 0, 32); // Zero out for security memset(admin_privkey, 0, sizeof(admin_privkey)); // Zero out the hex string generated_admin_key = 0; // Did not generate a new key } else { // Generate random admin keypair using /dev/urandom + nostr_core_lib log_info("Generating random admin keypair"); if (generate_random_private_key_bytes(admin_privkey_bytes) != 0) { log_error("Failed to generate admin private key"); return -1; } nostr_bytes_to_hex(admin_privkey_bytes, 32, admin_privkey); // Derive public key from private key unsigned char admin_pubkey_bytes[32]; if (nostr_ec_public_key_from_private_key(admin_privkey_bytes, admin_pubkey_bytes) != NOSTR_SUCCESS) { log_error("Failed to derive admin public key"); return -1; } nostr_bytes_to_hex(admin_pubkey_bytes, 32, admin_pubkey); generated_admin_key = 1; // Generated a new key } // 2. Generate or use provided relay keypair unsigned char relay_privkey_bytes[32]; char relay_privkey[65], relay_pubkey[65]; if (cli_options && strlen(cli_options->relay_privkey_override) == 64) { // Use provided relay private key log_info("Using provided relay private key override"); strncpy(relay_privkey, cli_options->relay_privkey_override, sizeof(relay_privkey) - 1); relay_privkey[sizeof(relay_privkey) - 1] = '\0'; // Convert hex string to bytes if (nostr_hex_to_bytes(relay_privkey, relay_privkey_bytes, 32) != NOSTR_SUCCESS) { log_error("Failed to convert relay private key hex to bytes"); return -1; } // Validate the private key if (nostr_ec_private_key_verify(relay_privkey_bytes) != NOSTR_SUCCESS) { log_error("Provided relay private key is invalid"); return -1; } } else { // Generate random relay keypair using /dev/urandom + nostr_core_lib if (generate_random_private_key_bytes(relay_privkey_bytes) != 0) { log_error("Failed to generate relay private key"); return -1; } nostr_bytes_to_hex(relay_privkey_bytes, 32, relay_privkey); } unsigned char relay_pubkey_bytes[32]; if (nostr_ec_public_key_from_private_key(relay_privkey_bytes, relay_pubkey_bytes) != NOSTR_SUCCESS) { log_error("Failed to derive relay public key"); return -1; } nostr_bytes_to_hex(relay_pubkey_bytes, 32, relay_pubkey); // 3. Store keys in unified cache (will be added to database after init) pthread_mutex_lock(&g_unified_cache.cache_lock); strncpy(g_unified_cache.admin_pubkey, admin_pubkey, sizeof(g_unified_cache.admin_pubkey) - 1); g_unified_cache.admin_pubkey[sizeof(g_unified_cache.admin_pubkey) - 1] = '\0'; strncpy(g_unified_cache.relay_pubkey, relay_pubkey, sizeof(g_unified_cache.relay_pubkey) - 1); g_unified_cache.relay_pubkey[sizeof(g_unified_cache.relay_pubkey) - 1] = '\0'; pthread_mutex_unlock(&g_unified_cache.cache_lock); // 4. Create database with relay pubkey name if (create_database_with_relay_pubkey(relay_pubkey) != 0) { log_error("Failed to create database with relay pubkey"); return -1; } // 5. Store relay private key in temporary storage for later secure storage strncpy(g_temp_relay_privkey, relay_privkey, sizeof(g_temp_relay_privkey) - 1); g_temp_relay_privkey[sizeof(g_temp_relay_privkey) - 1] = '\0'; log_info("Relay private key cached for secure storage after database initialization"); // 6. Handle configuration setup - defaults will be populated after database initialization log_info("Configuration setup prepared - defaults will be populated after database initialization"); // CLI overrides will be applied after database initialization in main.c // This prevents "g_db is NULL" errors during first-time startup // 10. Print admin private key for user to save (only if we generated a new key) if (generated_admin_key) { printf("\n"); printf("=================================================================\n"); printf("IMPORTANT: SAVE THIS ADMIN PRIVATE KEY SECURELY!\n"); printf("=================================================================\n"); printf("Admin Private Key: %s\n", admin_privkey); printf("Admin Public Key: %s\n", admin_pubkey); printf("Relay Public Key: %s\n", relay_pubkey); printf("\nDatabase: %s\n", g_database_path); printf("\nThis admin private key is needed to update configuration!\n"); printf("Store it safely - it will not be displayed again.\n"); printf("=================================================================\n"); printf("\n"); } else { printf("\n"); printf("=================================================================\n"); printf("RELAY STARTUP COMPLETE\n"); printf("=================================================================\n"); printf("Using provided admin public key for authentication\n"); printf("Admin Public Key: %s\n", admin_pubkey); printf("Relay Public Key: %s\n", relay_pubkey); printf("\nDatabase: %s\n", g_database_path); printf("=================================================================\n"); printf("\n"); } log_success("First-time startup sequence completed"); return 0; } int startup_existing_relay(const char* relay_pubkey) { if (!relay_pubkey) { log_error("Invalid relay pubkey for existing relay startup"); return -1; } log_info("Starting existing relay..."); printf(" Relay pubkey: %s\n", relay_pubkey); // Store relay pubkey in unified cache pthread_mutex_lock(&g_unified_cache.cache_lock); strncpy(g_unified_cache.relay_pubkey, relay_pubkey, sizeof(g_unified_cache.relay_pubkey) - 1); g_unified_cache.relay_pubkey[sizeof(g_unified_cache.relay_pubkey) - 1] = '\0'; pthread_mutex_unlock(&g_unified_cache.cache_lock); // Set database path char* db_name = get_database_name_from_relay_pubkey(relay_pubkey); if (!db_name) { log_error("Failed to generate database name"); return -1; } strncpy(g_database_path, db_name, sizeof(g_database_path) - 1); g_database_path[sizeof(g_database_path) - 1] = '\0'; free(db_name); // Ensure default configuration values are populated (for any missing keys) if (populate_default_config_values() != 0) { log_warning("Failed to populate default config values for existing relay - continuing"); } // Configuration will be migrated from events to table after database initialization log_info("Configuration migration will be performed after database is available"); // Load configuration event from database (after database is initialized) // This will be done in apply_configuration_from_database() log_success("Existing relay startup prepared"); return 0; } // ================================ // CONFIGURATION FIELD VALIDATION // ================================ // Validation helper functions static int is_valid_port(const char* port_str) { if (!port_str) return 0; char* endptr; long port = strtol(port_str, &endptr, 10); // Must be valid number and in valid port range return (endptr != port_str && *endptr == '\0' && port >= 1 && port <= 65535); } static int is_valid_boolean(const char* bool_str) { if (!bool_str) return 0; return (strcasecmp(bool_str, "true") == 0 || strcasecmp(bool_str, "false") == 0 || strcasecmp(bool_str, "yes") == 0 || strcasecmp(bool_str, "no") == 0 || strcasecmp(bool_str, "1") == 0 || strcasecmp(bool_str, "0") == 0); } static int is_valid_positive_integer(const char* int_str) { if (!int_str) return 0; char* endptr; long val = strtol(int_str, &endptr, 10); return (endptr != int_str && *endptr == '\0' && val >= 0); } static int is_valid_non_negative_integer(const char* int_str) { if (!int_str) return 0; char* endptr; long val = strtol(int_str, &endptr, 10); return (endptr != int_str && *endptr == '\0' && val >= 0); } static int is_valid_string_length(const char* str, size_t max_length) { if (!str) return 1; // NULL strings are valid (use defaults) return strlen(str) <= max_length; } static int is_valid_pow_mode(const char* mode_str) { if (!mode_str) return 0; return (strcasecmp(mode_str, "basic") == 0 || strcasecmp(mode_str, "strict") == 0 || strcasecmp(mode_str, "disabled") == 0); } static int is_valid_hex_key(const char* key_str) { if (!key_str) return 0; // Must be exactly 64 hex characters if (strlen(key_str) != 64) return 0; // Must contain only hex characters for (int i = 0; i < 64; i++) { char c = key_str[i]; if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { return 0; } } return 1; } // Main validation function for configuration fields static int validate_config_field(const char* key, const char* value, char* error_msg, size_t error_size) { if (!key || !value) { snprintf(error_msg, error_size, "key or value is NULL"); return -1; } // Port validation if (strcmp(key, "relay_port") == 0) { if (!is_valid_port(value)) { snprintf(error_msg, error_size, "invalid port number '%s' (must be 1-65535)", value); return -1; } return 0; } // Connection limits if (strcmp(key, "max_connections") == 0) { if (!is_valid_positive_integer(value)) { snprintf(error_msg, error_size, "invalid max_connections '%s' (must be positive integer)", value); return -1; } int val = atoi(value); if (val < 1 || val > 10000) { snprintf(error_msg, error_size, "max_connections '%s' out of range (1-10000)", value); return -1; } return 0; } // Boolean fields if (strcmp(key, "auth_enabled") == 0 || strcmp(key, "nip40_expiration_enabled") == 0 || strcmp(key, "nip40_expiration_strict") == 0 || strcmp(key, "nip40_expiration_filter") == 0) { if (!is_valid_boolean(value)) { snprintf(error_msg, error_size, "invalid boolean value '%s' for %s", value, key); return -1; } return 0; } // String length validation if (strcmp(key, "relay_description") == 0) { if (!is_valid_string_length(value, RELAY_DESCRIPTION_MAX_LENGTH)) { snprintf(error_msg, error_size, "relay_description too long (max %d characters)", RELAY_DESCRIPTION_MAX_LENGTH); return -1; } return 0; } if (strcmp(key, "relay_contact") == 0) { if (!is_valid_string_length(value, RELAY_CONTACT_MAX_LENGTH)) { snprintf(error_msg, error_size, "relay_contact too long (max %d characters)", RELAY_CONTACT_MAX_LENGTH); return -1; } return 0; } if (strcmp(key, "relay_software") == 0 || strcmp(key, "relay_version") == 0) { if (!is_valid_string_length(value, 256)) { snprintf(error_msg, error_size, "%s too long (max 256 characters)", key); return -1; } return 0; } // PoW difficulty validation if (strcmp(key, "pow_min_difficulty") == 0) { if (!is_valid_non_negative_integer(value)) { snprintf(error_msg, error_size, "invalid pow_min_difficulty '%s' (must be non-negative integer)", value); return -1; } int val = atoi(value); if (val > 32) { // 32 is practically impossible snprintf(error_msg, error_size, "pow_min_difficulty '%s' too high (max 32)", value); return -1; } return 0; } // PoW mode validation if (strcmp(key, "pow_mode") == 0) { if (!is_valid_pow_mode(value)) { snprintf(error_msg, error_size, "invalid pow_mode '%s' (must be basic, strict, or disabled)", value); return -1; } return 0; } // Time-based validation if (strcmp(key, "nip40_expiration_grace_period") == 0) { if (!is_valid_non_negative_integer(value)) { snprintf(error_msg, error_size, "invalid grace period '%s' (must be non-negative integer)", value); return -1; } int val = atoi(value); if (val > 86400) { // Max 1 day snprintf(error_msg, error_size, "grace period '%s' too long (max 86400 seconds)", value); return -1; } return 0; } // Subscription limits if (strcmp(key, "max_subscriptions_per_client") == 0) { if (!is_valid_positive_integer(value)) { snprintf(error_msg, error_size, "invalid max_subscriptions_per_client '%s'", value); return -1; } int val = atoi(value); if (val < 1 || val > 1000) { snprintf(error_msg, error_size, "max_subscriptions_per_client '%s' out of range (1-1000)", value); return -1; } return 0; } if (strcmp(key, "max_total_subscriptions") == 0) { if (!is_valid_positive_integer(value)) { snprintf(error_msg, error_size, "invalid max_total_subscriptions '%s'", value); return -1; } int val = atoi(value); if (val < 1 || val > 100000) { snprintf(error_msg, error_size, "max_total_subscriptions '%s' out of range (1-100000)", value); return -1; } return 0; } if (strcmp(key, "max_filters_per_subscription") == 0) { if (!is_valid_positive_integer(value)) { snprintf(error_msg, error_size, "invalid max_filters_per_subscription '%s'", value); return -1; } int val = atoi(value); if (val < 1 || val > 100) { snprintf(error_msg, error_size, "max_filters_per_subscription '%s' out of range (1-100)", value); return -1; } return 0; } // Event limits if (strcmp(key, "max_event_tags") == 0) { if (!is_valid_positive_integer(value)) { snprintf(error_msg, error_size, "invalid max_event_tags '%s'", value); return -1; } int val = atoi(value); if (val < 1 || val > 1000) { snprintf(error_msg, error_size, "max_event_tags '%s' out of range (1-1000)", value); return -1; } return 0; } if (strcmp(key, "max_content_length") == 0) { if (!is_valid_positive_integer(value)) { snprintf(error_msg, error_size, "invalid max_content_length '%s'", value); return -1; } int val = atoi(value); if (val < 100 || val > 1048576) { // 1MB max snprintf(error_msg, error_size, "max_content_length '%s' out of range (100-1048576)", value); return -1; } return 0; } if (strcmp(key, "max_message_length") == 0) { if (!is_valid_positive_integer(value)) { snprintf(error_msg, error_size, "invalid max_message_length '%s'", value); return -1; } int val = atoi(value); if (val < 1024 || val > 1048576) { // 1KB to 1MB snprintf(error_msg, error_size, "max_message_length '%s' out of range (1024-1048576)", value); return -1; } return 0; } // Performance limits if (strcmp(key, "default_limit") == 0 || strcmp(key, "max_limit") == 0) { if (!is_valid_positive_integer(value)) { snprintf(error_msg, error_size, "invalid %s '%s'", key, value); return -1; } int val = atoi(value); if (val < 1 || val > 50000) { snprintf(error_msg, error_size, "%s '%s' out of range (1-50000)", key, value); return -1; } return 0; } // Key validation for relay keys if (strcmp(key, "relay_pubkey") == 0 || strcmp(key, "relay_privkey") == 0) { if (!is_valid_hex_key(value)) { snprintf(error_msg, error_size, "invalid %s format (must be 64-character hex)", key); return -1; } return 0; } // Special validation for d tag (relay identifier) if (strcmp(key, "d") == 0) { if (!is_valid_hex_key(value)) { snprintf(error_msg, error_size, "invalid relay identifier 'd' format (must be 64-character hex)"); return -1; } return 0; } // NIP-42 Authentication fields if (strcmp(key, "nip42_auth_required") == 0) { if (!is_valid_boolean(value)) { snprintf(error_msg, error_size, "invalid boolean value '%s' for nip42_auth_required", value); return -1; } return 0; } if (strcmp(key, "nip42_auth_required_kinds") == 0) { // Validate comma-separated list of kind numbers if (!value || strlen(value) == 0) { return 0; // Empty list is valid } char* value_copy = strdup(value); if (!value_copy) { snprintf(error_msg, error_size, "memory allocation failed for kind validation"); return -1; } char* token = strtok(value_copy, ","); while (token) { // Trim whitespace while (*token == ' ' || *token == '\t') token++; char* end = token + strlen(token) - 1; while (end > token && (*end == ' ' || *end == '\t')) end--; *(end + 1) = '\0'; // Validate each kind number if (!is_valid_non_negative_integer(token)) { free(value_copy); snprintf(error_msg, error_size, "invalid kind number '%s' in nip42_auth_required_kinds", token); return -1; } int kind = atoi(token); if (kind < 0 || kind > 65535) { free(value_copy); snprintf(error_msg, error_size, "kind number '%s' out of range (0-65535)", token); return -1; } token = strtok(NULL, ","); } free(value_copy); return 0; } if (strcmp(key, "nip42_challenge_expiration") == 0) { if (!is_valid_positive_integer(value)) { snprintf(error_msg, error_size, "invalid nip42_challenge_expiration '%s' (must be positive integer)", value); return -1; } int val = atoi(value); if (val < 60 || val > 3600) { // 1 minute to 1 hour snprintf(error_msg, error_size, "nip42_challenge_expiration '%s' out of range (60-3600 seconds)", value); return -1; } return 0; } if (strcmp(key, "nip42_challenge_window") == 0) { if (!is_valid_positive_integer(value)) { snprintf(error_msg, error_size, "invalid nip42_challenge_window '%s' (must be positive integer)", value); return -1; } int val = atoi(value); if (val < 300 || val > 7200) { // 5 minutes to 2 hours snprintf(error_msg, error_size, "nip42_challenge_window '%s' out of range (300-7200 seconds)", value); return -1; } return 0; } if (strcmp(key, "nip42_max_auth_events") == 0) { if (!is_valid_positive_integer(value)) { snprintf(error_msg, error_size, "invalid nip42_max_auth_events '%s' (must be positive integer)", value); return -1; } int val = atoi(value); if (val < 10 || val > 10000) { // 10 to 10,000 auth events snprintf(error_msg, error_size, "nip42_max_auth_events '%s' out of range (10-10000)", value); return -1; } return 0; } // Unknown field - log warning but allow log_warning("Unknown configuration field"); printf(" Field: %s = %s\n", key, value); return 0; } // Validate all fields in a configuration event static int validate_configuration_event_fields(const cJSON* event, char* error_msg, size_t error_size) { if (!event) { snprintf(error_msg, error_size, "null configuration event"); return -1; } log_info("Validating configuration event fields..."); cJSON* tags = cJSON_GetObjectItem(event, "tags"); if (!tags || !cJSON_IsArray(tags)) { snprintf(error_msg, error_size, "missing or invalid tags array"); return -1; } int validated_fields = 0; int validation_errors = 0; char field_error[512]; cJSON* tag = NULL; cJSON_ArrayForEach(tag, tags) { if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) { cJSON* tag_key = cJSON_GetArrayItem(tag, 0); cJSON* tag_value = cJSON_GetArrayItem(tag, 1); if (tag_key && tag_value && cJSON_IsString(tag_key) && cJSON_IsString(tag_value)) { const char* key = cJSON_GetStringValue(tag_key); const char* value = cJSON_GetStringValue(tag_value); if (validate_config_field(key, value, field_error, sizeof(field_error)) != 0) { // Safely truncate the error message if needed size_t prefix_len = strlen("field validation failed: "); size_t available_space = error_size > prefix_len ? error_size - prefix_len - 1 : 0; if (available_space > 0) { snprintf(error_msg, error_size, "field validation failed: %.*s", (int)available_space, field_error); } else { strncpy(error_msg, "field validation failed", error_size - 1); error_msg[error_size - 1] = '\0'; } log_error("Configuration field validation failed"); printf(" Field: %s = %s\n", key, value); printf(" Error: %s\n", field_error); validation_errors++; return -1; // Stop on first error } else { validated_fields++; } } } } if (validation_errors > 0) { char summary[256]; snprintf(summary, sizeof(summary), "%d configuration fields failed validation", validation_errors); log_error(summary); return -1; } char success_msg[256]; snprintf(success_msg, sizeof(success_msg), "%d configuration fields validated successfully", validated_fields); log_success(success_msg); return 0; } int process_configuration_event(const cJSON* event) { if (!event) { log_error("Invalid configuration event"); return -1; } log_info("Processing configuration event..."); // Validate event structure cJSON* kind_obj = cJSON_GetObjectItem(event, "kind"); cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey"); if (!kind_obj || cJSON_GetNumberValue(kind_obj) != 33334) { log_error("Invalid event kind for configuration"); return -1; } if (!pubkey_obj) { log_error("Missing pubkey in configuration event"); return -1; } // Verify it's from the admin const char* event_pubkey = cJSON_GetStringValue(pubkey_obj); const char* admin_pubkey = get_admin_pubkey_cached(); if (admin_pubkey && strlen(admin_pubkey) > 0) { if (strcmp(event_pubkey, admin_pubkey) != 0) { log_error("Configuration event not from authorized admin"); return -1; } } // Comprehensive event validation using nostr_core_lib log_info("Validating configuration event structure and signature..."); // First validate the event structure (fields, format, etc.) if (nostr_validate_event_structure((cJSON*)event) != NOSTR_SUCCESS) { log_error("Configuration event has invalid structure"); return -1; } // Then validate the cryptographic signature if (nostr_verify_event_signature((cJSON*)event) != NOSTR_SUCCESS) { log_error("Configuration event has invalid signature"); return -1; } log_success("Configuration event structure and signature validated successfully"); // NEW: Validate configuration field values char validation_error[512]; if (validate_configuration_event_fields(event, validation_error, sizeof(validation_error)) != 0) { log_error("Configuration field validation failed"); printf(" Validation error: %s\n", validation_error); return -1; } // Store in database if (store_config_event_in_database(event) != 0) { log_error("Failed to store configuration event"); return -1; } // Apply configuration if (apply_configuration_from_event(event) != 0) { log_error("Failed to apply configuration from event"); return -1; } log_success("Configuration event processed successfully with field validation"); return 0; } // ================================ // RUNTIME CONFIGURATION HANDLERS // ================================ // External functions and globals from main.c that need to be updated extern void update_subscription_manager_config(void); extern void init_pow_config(void); extern void init_expiration_config(void); extern void init_relay_info(void); // Compare configuration values between old and new config static const char* get_config_value_from_event(const cJSON* event, const char* key) { if (!event || !key) return NULL; cJSON* tags = cJSON_GetObjectItem(event, "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_key = cJSON_GetArrayItem(tag, 0); cJSON* tag_value = cJSON_GetArrayItem(tag, 1); if (tag_key && tag_value && cJSON_IsString(tag_key) && cJSON_IsString(tag_value)) { if (strcmp(cJSON_GetStringValue(tag_key), key) == 0) { return cJSON_GetStringValue(tag_value); } } } } return NULL; } // Check if a configuration value has changed static int config_value_changed(const cJSON* old_config, const cJSON* new_config, const char* key) { const char* old_value = get_config_value_from_event(old_config, key); const char* new_value = get_config_value_from_event(new_config, key); // Both NULL - no change if (!old_value && !new_value) return 0; // One is NULL, other isn't - changed if (!old_value || !new_value) return 1; // Compare string values return strcmp(old_value, new_value) != 0; } // Apply runtime configuration changes by calling appropriate handlers int apply_runtime_config_handlers(const cJSON* old_config, const cJSON* new_config) { if (!new_config) return 0; int handlers_applied = 0; log_info("Checking for runtime configuration changes..."); // Subscription Manager Configuration if (config_value_changed(old_config, new_config, "max_subscriptions_per_client") || config_value_changed(old_config, new_config, "max_total_subscriptions")) { log_info("Subscription limits changed - updating subscription manager"); update_subscription_manager_config(); handlers_applied++; } // NIP-13 Proof of Work Configuration if (config_value_changed(old_config, new_config, "pow_min_difficulty") || config_value_changed(old_config, new_config, "pow_mode")) { log_info("PoW configuration changed - reinitializing PoW system"); init_pow_config(); handlers_applied++; } // NIP-40 Expiration Configuration if (config_value_changed(old_config, new_config, "nip40_expiration_enabled") || config_value_changed(old_config, new_config, "nip40_expiration_strict") || config_value_changed(old_config, new_config, "nip40_expiration_filter") || config_value_changed(old_config, new_config, "nip40_expiration_grace_period")) { log_info("Expiration configuration changed - reinitializing expiration system"); init_expiration_config(); handlers_applied++; } // NIP-11 Relay Information if (config_value_changed(old_config, new_config, "relay_description") || config_value_changed(old_config, new_config, "relay_contact") || config_value_changed(old_config, new_config, "relay_software") || config_value_changed(old_config, new_config, "relay_version") || config_value_changed(old_config, new_config, "max_message_length") || config_value_changed(old_config, new_config, "max_event_tags") || config_value_changed(old_config, new_config, "max_content_length")) { log_info("Relay information changed - reinitializing relay info"); init_relay_info(); handlers_applied++; } // Log configuration changes for audit if (handlers_applied > 0) { char audit_msg[512]; snprintf(audit_msg, sizeof(audit_msg), "Configuration updated via admin event - %d system components reinitialized", handlers_applied); log_success(audit_msg); } else { log_info("No runtime configuration changes detected"); } return handlers_applied; } int apply_configuration_from_event(const cJSON* event) { if (!event) { log_error("Invalid event for configuration application"); return -1; } log_info("Applying configuration from event..."); // Store previous config for comparison cJSON* old_config = g_current_config; // Update cached configuration g_current_config = cJSON_Duplicate(event, 1); // Extract admin pubkey if not already in cache cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey"); if (pubkey_obj) { const char* event_pubkey = cJSON_GetStringValue(pubkey_obj); const char* cached_admin_pubkey = get_admin_pubkey_cached(); if (!cached_admin_pubkey || strlen(cached_admin_pubkey) == 0) { // Update cache with admin pubkey from event pthread_mutex_lock(&g_unified_cache.cache_lock); strncpy(g_unified_cache.admin_pubkey, event_pubkey, sizeof(g_unified_cache.admin_pubkey) - 1); g_unified_cache.admin_pubkey[sizeof(g_unified_cache.admin_pubkey) - 1] = '\0'; pthread_mutex_unlock(&g_unified_cache.cache_lock); } } // Apply runtime configuration changes int handlers_applied = apply_runtime_config_handlers(old_config, g_current_config); // Clean up old config if (old_config) { cJSON_Delete(old_config); } char success_msg[256]; snprintf(success_msg, sizeof(success_msg), "Configuration applied from event (%d handlers executed)", handlers_applied); log_success(success_msg); return 0; } // ================================ // REAL-TIME EVENT HANDLER (called from main.c) // ================================ // Handle configuration events received via WebSocket int handle_configuration_event(cJSON* event, char* error_message, size_t error_size) { if (!event) { snprintf(error_message, error_size, "invalid: null configuration event"); return -1; } log_info("Handling configuration event from WebSocket"); // Use existing process_configuration_event function if (process_configuration_event(event) == 0) { // Success error_message[0] = '\0'; // Empty error message indicates success return 0; } else { // Failed to process snprintf(error_message, error_size, "error: failed to process configuration event"); return -1; } } // ================================ // NEW ADMIN API IMPLEMENTATION // ================================ // ================================ // CONFIG TABLE MANAGEMENT FUNCTIONS // ================================ // Note: Config table is now created via embedded schema in sql_schema.h // Get value from config table const char* get_config_value_from_table(const char* key) { if (!g_db || !key) return NULL; const char* sql = "SELECT value FROM config WHERE key = ?"; 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, key, -1, SQLITE_STATIC); const char* result = NULL; if (sqlite3_step(stmt) == SQLITE_ROW) { const char* value = (char*)sqlite3_column_text(stmt, 0); if (value) { // For NIP-11 fields, store in cache buffers but return dynamically allocated strings for consistency if (strcmp(key, "relay_name") == 0) { pthread_mutex_lock(&g_unified_cache.cache_lock); strncpy(g_unified_cache.relay_info.name, value, sizeof(g_unified_cache.relay_info.name) - 1); g_unified_cache.relay_info.name[sizeof(g_unified_cache.relay_info.name) - 1] = '\0'; result = strdup(value); // Return dynamically allocated copy pthread_mutex_unlock(&g_unified_cache.cache_lock); } else if (strcmp(key, "relay_description") == 0) { pthread_mutex_lock(&g_unified_cache.cache_lock); strncpy(g_unified_cache.relay_info.description, value, sizeof(g_unified_cache.relay_info.description) - 1); g_unified_cache.relay_info.description[sizeof(g_unified_cache.relay_info.description) - 1] = '\0'; result = strdup(value); // Return dynamically allocated copy pthread_mutex_unlock(&g_unified_cache.cache_lock); } else if (strcmp(key, "relay_contact") == 0) { pthread_mutex_lock(&g_unified_cache.cache_lock); strncpy(g_unified_cache.relay_info.contact, value, sizeof(g_unified_cache.relay_info.contact) - 1); g_unified_cache.relay_info.contact[sizeof(g_unified_cache.relay_info.contact) - 1] = '\0'; result = strdup(value); // Return dynamically allocated copy pthread_mutex_unlock(&g_unified_cache.cache_lock); } else if (strcmp(key, "relay_software") == 0) { pthread_mutex_lock(&g_unified_cache.cache_lock); strncpy(g_unified_cache.relay_info.software, value, sizeof(g_unified_cache.relay_info.software) - 1); g_unified_cache.relay_info.software[sizeof(g_unified_cache.relay_info.software) - 1] = '\0'; result = strdup(value); // Return dynamically allocated copy pthread_mutex_unlock(&g_unified_cache.cache_lock); } else if (strcmp(key, "relay_version") == 0) { pthread_mutex_lock(&g_unified_cache.cache_lock); strncpy(g_unified_cache.relay_info.version, value, sizeof(g_unified_cache.relay_info.version) - 1); g_unified_cache.relay_info.version[sizeof(g_unified_cache.relay_info.version) - 1] = '\0'; result = strdup(value); // Return dynamically allocated copy pthread_mutex_unlock(&g_unified_cache.cache_lock); } else if (strcmp(key, "supported_nips") == 0) { pthread_mutex_lock(&g_unified_cache.cache_lock); strncpy(g_unified_cache.relay_info.supported_nips_str, value, sizeof(g_unified_cache.relay_info.supported_nips_str) - 1); g_unified_cache.relay_info.supported_nips_str[sizeof(g_unified_cache.relay_info.supported_nips_str) - 1] = '\0'; result = strdup(value); // Return dynamically allocated copy pthread_mutex_unlock(&g_unified_cache.cache_lock); } else if (strcmp(key, "language_tags") == 0) { pthread_mutex_lock(&g_unified_cache.cache_lock); strncpy(g_unified_cache.relay_info.language_tags_str, value, sizeof(g_unified_cache.relay_info.language_tags_str) - 1); g_unified_cache.relay_info.language_tags_str[sizeof(g_unified_cache.relay_info.language_tags_str) - 1] = '\0'; result = strdup(value); // Return dynamically allocated copy pthread_mutex_unlock(&g_unified_cache.cache_lock); } else if (strcmp(key, "relay_countries") == 0) { pthread_mutex_lock(&g_unified_cache.cache_lock); strncpy(g_unified_cache.relay_info.relay_countries_str, value, sizeof(g_unified_cache.relay_info.relay_countries_str) - 1); g_unified_cache.relay_info.relay_countries_str[sizeof(g_unified_cache.relay_info.relay_countries_str) - 1] = '\0'; result = strdup(value); // Return dynamically allocated copy pthread_mutex_unlock(&g_unified_cache.cache_lock); } else if (strcmp(key, "posting_policy") == 0) { pthread_mutex_lock(&g_unified_cache.cache_lock); strncpy(g_unified_cache.relay_info.posting_policy, value, sizeof(g_unified_cache.relay_info.posting_policy) - 1); g_unified_cache.relay_info.posting_policy[sizeof(g_unified_cache.relay_info.posting_policy) - 1] = '\0'; result = strdup(value); // Return dynamically allocated copy pthread_mutex_unlock(&g_unified_cache.cache_lock); } else if (strcmp(key, "payments_url") == 0) { pthread_mutex_lock(&g_unified_cache.cache_lock); strncpy(g_unified_cache.relay_info.payments_url, value, sizeof(g_unified_cache.relay_info.payments_url) - 1); g_unified_cache.relay_info.payments_url[sizeof(g_unified_cache.relay_info.payments_url) - 1] = '\0'; result = strdup(value); // Return dynamically allocated copy pthread_mutex_unlock(&g_unified_cache.cache_lock); } else { // For other keys, return a dynamically allocated string to prevent buffer reuse result = strdup(value); } } } sqlite3_finalize(stmt); return result; } // Set value in config table int set_config_value_in_table(const char* key, const char* value, const char* data_type, const char* description, const char* category, int requires_restart) { if (!g_db || !key || !value || !data_type) { return -1; } const char* sql = "INSERT OR REPLACE INTO config (key, value, data_type, description, category, requires_restart) " "VALUES (?, ?, ?, ?, ?, ?)"; sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { return -1; } sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, value, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 3, data_type, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 4, description ? description : "", -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 5, category ? category : "general", -1, SQLITE_STATIC); sqlite3_bind_int(stmt, 6, requires_restart); rc = sqlite3_step(stmt); sqlite3_finalize(stmt); return (rc == SQLITE_DONE) ? 0 : -1; } // Update config in table (simpler version of set_config_value_in_table) int update_config_in_table(const char* key, const char* value) { if (!g_db || !key || !value) { return -1; } const char* sql = "UPDATE config SET value = ?, updated_at = strftime('%s', 'now') WHERE key = ?"; sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { return -1; } sqlite3_bind_text(stmt, 1, value, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, key, -1, SQLITE_STATIC); rc = sqlite3_step(stmt); sqlite3_finalize(stmt); return (rc == SQLITE_DONE) ? 0 : -1; } // Populate default config values int populate_default_config_values(void) { log_info("Populating default configuration values in table..."); // Add all default configuration values to the table for (size_t i = 0; i < DEFAULT_CONFIG_COUNT; i++) { const char* key = DEFAULT_CONFIG_VALUES[i].key; const char* value = DEFAULT_CONFIG_VALUES[i].value; // Determine data type const char* data_type = "string"; if (strcmp(key, "relay_port") == 0 || strcmp(key, "max_connections") == 0 || strcmp(key, "pow_min_difficulty") == 0 || strcmp(key, "max_subscriptions_per_client") == 0 || strcmp(key, "max_total_subscriptions") == 0 || strcmp(key, "max_filters_per_subscription") == 0 || strcmp(key, "max_event_tags") == 0 || strcmp(key, "max_content_length") == 0 || strcmp(key, "max_message_length") == 0 || strcmp(key, "default_limit") == 0 || strcmp(key, "max_limit") == 0 || strcmp(key, "nip42_challenge_expiration") == 0 || strcmp(key, "nip40_expiration_grace_period") == 0) { data_type = "integer"; } else if (strcmp(key, "auth_enabled") == 0 || strcmp(key, "nip40_expiration_enabled") == 0 || strcmp(key, "nip40_expiration_strict") == 0 || strcmp(key, "nip40_expiration_filter") == 0 || strcmp(key, "nip42_auth_required") == 0) { data_type = "boolean"; } // Set category const char* category = "general"; if (strstr(key, "relay_")) { category = "relay"; } else if (strstr(key, "nip40_")) { category = "expiration"; } else if (strstr(key, "nip42_") || strstr(key, "auth_")) { category = "authentication"; } else if (strstr(key, "pow_")) { category = "proof_of_work"; } else if (strstr(key, "max_")) { category = "limits"; } // Determine if requires restart (0 = dynamic, 1 = restart required) int requires_restart = 0; // Restart required configs if (strcmp(key, "relay_port") == 0 || strcmp(key, "max_connections") == 0 || strcmp(key, "auth_enabled") == 0 || strcmp(key, "nip42_auth_required") == 0 || strcmp(key, "nip42_auth_required_kinds") == 0 || strcmp(key, "nip42_challenge_timeout") == 0 || strcmp(key, "database_path") == 0) { requires_restart = 1; } if (set_config_value_in_table(key, value, data_type, NULL, category, requires_restart) != 0) { char error_msg[256]; snprintf(error_msg, sizeof(error_msg), "Failed to set default config: %s = %s", key, value); log_error(error_msg); } } log_success("Default configuration values populated with restart requirements"); return 0; } // Add dynamically generated pubkeys to config table int add_pubkeys_to_config_table(void) { if (!g_db) { log_error("Database not available for pubkey storage"); return -1; } log_info("Adding dynamically generated pubkeys to config table..."); // Get the pubkeys directly from unified cache (not through cached accessors to avoid circular dependency) pthread_mutex_lock(&g_unified_cache.cache_lock); const char* admin_pubkey = g_unified_cache.admin_pubkey[0] ? g_unified_cache.admin_pubkey : NULL; const char* relay_pubkey = g_unified_cache.relay_pubkey[0] ? g_unified_cache.relay_pubkey : NULL; pthread_mutex_unlock(&g_unified_cache.cache_lock); if (!admin_pubkey || strlen(admin_pubkey) != 64) { log_error("Admin pubkey not available or invalid for config table storage"); return -1; } if (!relay_pubkey || strlen(relay_pubkey) != 64) { log_error("Relay pubkey not available or invalid for config table storage"); return -1; } // Store admin pubkey in config table if (set_config_value_in_table("admin_pubkey", admin_pubkey, "string", "Administrator public key", "authentication", 0) != 0) { log_error("Failed to store admin_pubkey in config table"); return -1; } // Store relay pubkey in config table if (set_config_value_in_table("relay_pubkey", relay_pubkey, "string", "Relay public key", "relay", 0) != 0) { log_error("Failed to store relay_pubkey in config table"); return -1; } log_success("Dynamically generated pubkeys added to config table"); printf(" Admin pubkey: %s\n", admin_pubkey); printf(" Relay pubkey: %s\n", relay_pubkey); return 0; } // ================================ // ADMIN EVENT PROCESSING FUNCTIONS // ================================ // Forward declaration for admin authorization function from main.c extern int is_authorized_admin_event(cJSON* event); // Process admin events (updated for Kind 23456) int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size, struct lws* wsi) { log_info("DEBUG: Entering process_admin_event_in_config()"); cJSON* kind_obj = cJSON_GetObjectItem(event, "kind"); if (!kind_obj || !cJSON_IsNumber(kind_obj)) { log_error("DEBUG: Missing or invalid kind in admin event"); snprintf(error_message, error_size, "invalid: missing or invalid kind"); return -1; } int kind = (int)cJSON_GetNumberValue(kind_obj); log_info("DEBUG: Processing admin event"); printf(" Event kind: %d\n", kind); // Extract and log event details for debugging cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey"); cJSON* content_obj = cJSON_GetObjectItem(event, "content"); cJSON* tags_obj = cJSON_GetObjectItem(event, "tags"); const char* event_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : "unknown"; const char* event_content = content_obj ? cJSON_GetStringValue(content_obj) : "unknown"; log_info("DEBUG: Event details"); printf(" Pubkey: %.16s...\n", event_pubkey ? event_pubkey : "null"); printf(" Content length: %zu\n", event_content ? strlen(event_content) : 0); printf(" Has tags: %s\n", tags_obj ? "yes" : "no"); if (tags_obj && cJSON_IsArray(tags_obj)) { printf(" Tags count: %d\n", cJSON_GetArraySize(tags_obj)); } // DEFENSE-IN-DEPTH: Use comprehensive admin authorization validation log_info("DEBUG: Checking admin authorization"); if (!is_authorized_admin_event(event)) { // Log the unauthorized attempt for security monitoring char log_msg[256]; snprintf(log_msg, sizeof(log_msg), "DEBUG: Unauthorized admin event attempt in config processing - pubkey: %.16s...", event_pubkey ? event_pubkey : "null"); log_warning(log_msg); snprintf(error_message, error_size, "auth-required: not authorized admin"); return -1; } // Log successful admin authorization for audit trail log_info("DEBUG: Admin event authorized successfully in config processing"); // Route to appropriate handler based on kind log_info("DEBUG: Routing to kind-specific handler"); switch (kind) { case 23456: // New ephemeral auth rules management log_info("DEBUG: Routing to process_admin_auth_event (kind 23456)"); return process_admin_auth_event(event, error_message, error_size, wsi); default: log_error("DEBUG: Unsupported admin event kind"); printf(" Unsupported kind: %d\n", kind); snprintf(error_message, error_size, "invalid: unsupported admin event kind %d", kind); return -1; } } // Handle legacy Kind 33334 configuration management events int process_admin_config_event(cJSON* event, char* error_message, size_t error_size) { cJSON* kind_obj = cJSON_GetObjectItem(event, "kind"); int kind = kind_obj ? (int)cJSON_GetNumberValue(kind_obj) : 0; log_info("Processing admin configuration event"); printf(" Kind: %d\n", kind); // Parse tags to find query commands according to API specification cJSON* tags_obj = cJSON_GetObjectItem(event, "tags"); if (tags_obj && cJSON_IsArray(tags_obj)) { cJSON* tag = NULL; cJSON_ArrayForEach(tag, tags_obj) { if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) { continue; } cJSON* tag_name = cJSON_GetArrayItem(tag, 0); cJSON* tag_value = cJSON_GetArrayItem(tag, 1); if (!cJSON_IsString(tag_name) || !cJSON_IsString(tag_value)) { continue; } const char* tag_key = cJSON_GetStringValue(tag_name); const char* tag_val = cJSON_GetStringValue(tag_value); // Handle config_query commands per API spec if (strcmp(tag_key, "config_query") == 0) { printf(" Config Query: %s\n", tag_val); // Config queries are not implemented for legacy kind 33334 snprintf(error_message, error_size, "config queries not supported for legacy kind 33334"); return -1; } } } // Handle configuration updates (set action) - parse remaining tags for config updates if (!tags_obj || !cJSON_IsArray(tags_obj)) { snprintf(error_message, error_size, "invalid: configuration event must have tags"); return -1; } // Begin transaction for atomic config updates int rc = sqlite3_exec(g_db, "BEGIN IMMEDIATE TRANSACTION", NULL, NULL, NULL); if (rc != SQLITE_OK) { snprintf(error_message, error_size, "failed to begin config transaction"); return -1; } int updates_applied = 0; // Process each tag as a configuration parameter cJSON* config_tag = NULL; cJSON_ArrayForEach(config_tag, tags_obj) { if (!cJSON_IsArray(config_tag) || cJSON_GetArraySize(config_tag) < 2) { continue; } cJSON* tag_name = cJSON_GetArrayItem(config_tag, 0); cJSON* tag_value = cJSON_GetArrayItem(config_tag, 1); if (!cJSON_IsString(tag_name) || !cJSON_IsString(tag_value)) { continue; } const char* key = cJSON_GetStringValue(tag_name); const char* value = cJSON_GetStringValue(tag_value); // Skip query commands and system commands - only process config updates if (strcmp(key, "config_query") == 0) { continue; } // Update configuration in table if (update_config_in_table(key, value) == 0) { updates_applied++; } } if (updates_applied > 0) { sqlite3_exec(g_db, "COMMIT", NULL, NULL, NULL); invalidate_config_cache(); char success_msg[256]; snprintf(success_msg, sizeof(success_msg), "Applied %d configuration updates", updates_applied); log_success(success_msg); } else { sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL); snprintf(error_message, error_size, "no valid configuration parameters found"); return -1; } return 0; } // Handle Kind 23456 auth rules management int process_admin_auth_event(cJSON* event, char* error_message, size_t error_size, struct lws* wsi) { log_info("DEBUG: Entering process_admin_auth_event()"); cJSON* kind_obj = cJSON_GetObjectItem(event, "kind"); int kind = kind_obj ? (int)cJSON_GetNumberValue(kind_obj) : 0; log_info("DEBUG: Processing admin auth rule event through unified handler"); printf(" Kind: %d\n", kind); // Extract and log additional event details for debugging cJSON* content_obj = cJSON_GetObjectItem(event, "content"); cJSON* tags_obj = cJSON_GetObjectItem(event, "tags"); const char* event_content = content_obj ? cJSON_GetStringValue(content_obj) : "unknown"; log_info("DEBUG: Auth event details"); printf(" Content length: %zu\n", event_content ? strlen(event_content) : 0); printf(" Has tags: %s\n", tags_obj ? "yes" : "no"); if (tags_obj && cJSON_IsArray(tags_obj)) { printf(" Tags count: %d\n", cJSON_GetArraySize(tags_obj)); } // Route all Kind 23456 events through the unified handler if (kind == 23456) { log_info("DEBUG: Routing Kind 23456 to unified handler"); return handle_kind_23456_unified(event, error_message, error_size, wsi); } log_error("DEBUG: Unsupported auth event kind in process_admin_auth_event"); printf(" Unsupported kind: %d\n", kind); snprintf(error_message, error_size, "invalid: unsupported auth event kind %d", kind); return -1; } // ================================ // AUTH RULES MANAGEMENT FUNCTIONS // ================================ // Add auth rule from configuration int add_auth_rule_from_config(const char* rule_type, const char* pattern_type, const char* pattern_value, const char* action) { if (!g_db || !rule_type || !pattern_type || !pattern_value || !action) { return -1; } const char* sql = "INSERT INTO auth_rules (rule_type, pattern_type, pattern_value, action) " "VALUES (?, ?, ?, ?)"; sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { return -1; } sqlite3_bind_text(stmt, 1, rule_type, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, pattern_type, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 3, pattern_value, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 4, action, -1, SQLITE_STATIC); rc = sqlite3_step(stmt); sqlite3_finalize(stmt); return (rc == SQLITE_DONE) ? 0 : -1; } // Remove auth rule from configuration int remove_auth_rule_from_config(const char* rule_type, const char* pattern_type, const char* pattern_value) { if (!g_db || !rule_type || !pattern_type || !pattern_value) { return -1; } const char* sql = "DELETE FROM auth_rules WHERE rule_type = ? AND pattern_type = ? AND pattern_value = ?"; sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { return -1; } sqlite3_bind_text(stmt, 1, rule_type, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, pattern_type, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 3, pattern_value, -1, SQLITE_STATIC); rc = sqlite3_step(stmt); sqlite3_finalize(stmt); return (rc == SQLITE_DONE) ? 0 : -1; } // ================================ // UNIFIED TAG PARSING UTILITIES // ================================ // Get the first tag name from an event const char* get_first_tag_name(cJSON* event) { if (!event) return NULL; cJSON* tags_obj = cJSON_GetObjectItem(event, "tags"); if (!tags_obj || !cJSON_IsArray(tags_obj)) return NULL; cJSON* first_tag = cJSON_GetArrayItem(tags_obj, 0); if (!first_tag || !cJSON_IsArray(first_tag) || cJSON_GetArraySize(first_tag) < 1) { return NULL; } cJSON* tag_name = cJSON_GetArrayItem(first_tag, 0); if (!tag_name || !cJSON_IsString(tag_name)) return NULL; return cJSON_GetStringValue(tag_name); } // Get tag value at specified index const char* get_tag_value(cJSON* event, const char* tag_name, int value_index) { if (!event || !tag_name) return NULL; cJSON* tags_obj = cJSON_GetObjectItem(event, "tags"); if (!tags_obj || !cJSON_IsArray(tags_obj)) return NULL; cJSON* tag = NULL; cJSON_ArrayForEach(tag, tags_obj) { if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) <= value_index) { continue; } cJSON* tag_key = cJSON_GetArrayItem(tag, 0); if (!tag_key || !cJSON_IsString(tag_key)) continue; if (strcmp(cJSON_GetStringValue(tag_key), tag_name) == 0) { cJSON* tag_value = cJSON_GetArrayItem(tag, value_index); if (tag_value && cJSON_IsString(tag_value)) { return cJSON_GetStringValue(tag_value); } } } return NULL; } // Parse auth query parameters from event tags int parse_auth_query_parameters(cJSON* event, char** query_type, char** pattern_value) { if (!event || !query_type) return -1; *query_type = NULL; if (pattern_value) *pattern_value = NULL; const char* query_val = get_tag_value(event, "auth_query", 1); if (query_val) { *query_type = strdup(query_val); // For pattern queries, get the pattern value from the same tag if (strcmp(query_val, "pattern") == 0 && pattern_value) { const char* pattern_val = get_tag_value(event, "auth_query", 2); if (pattern_val) { *pattern_value = strdup(pattern_val); } } return 0; } return -1; } // ================================ // ADMIN RESPONSE EVENT SYSTEM // ================================ // Create signed kind 23457 admin response event cJSON* create_admin_response_event(const char* encrypted_content, const char* recipient_pubkey) { if (!encrypted_content || !recipient_pubkey) { log_error("Invalid parameters for admin response event creation"); return NULL; } log_info("Creating signed kind 23457 admin response event"); printf(" Recipient pubkey: %.16s...\n", recipient_pubkey); printf(" Encrypted content length: %zu\n", strlen(encrypted_content)); // Get relay private key for signing char* relay_privkey = get_relay_private_key(); if (!relay_privkey) { log_error("Relay private key not available for admin response signing"); return NULL; } // Convert relay private key from hex to bytes unsigned char relay_privkey_bytes[32]; if (nostr_hex_to_bytes(relay_privkey, relay_privkey_bytes, 32) != NOSTR_SUCCESS) { log_error("Failed to convert relay private key from hex for admin response"); free(relay_privkey); return NULL; } // Clean up private key string immediately free(relay_privkey); // Create tags array for kind 23457 event cJSON* tags = cJSON_CreateArray(); if (!tags) { log_error("Failed to create tags array for admin response event"); memset(relay_privkey_bytes, 0, 32); return NULL; } // Add p tag with recipient pubkey (admin who sent the query) cJSON* p_tag = cJSON_CreateArray(); cJSON_AddItemToArray(p_tag, cJSON_CreateString("p")); cJSON_AddItemToArray(p_tag, cJSON_CreateString(recipient_pubkey)); cJSON_AddItemToArray(tags, p_tag); // Create and sign the kind 23457 event using nostr_core_lib cJSON* response_event = nostr_create_and_sign_event( 23457, // kind: admin response encrypted_content, // content: NIP-44 encrypted response tags, // tags: p tag with recipient relay_privkey_bytes, // private key bytes for signing time(NULL) // created_at timestamp ); // Clean up private key bytes immediately after use memset(relay_privkey_bytes, 0, 32); cJSON_Delete(tags); // Clean up tags as they were duplicated in nostr_create_and_sign_event if (!response_event) { log_error("Failed to create and sign kind 23457 admin response event"); return NULL; } // Log success information cJSON* id_obj = cJSON_GetObjectItem(response_event, "id"); cJSON* pubkey_obj = cJSON_GetObjectItem(response_event, "pubkey"); if (id_obj && pubkey_obj) { log_success("Kind 23457 admin response event created and signed successfully"); printf(" Event ID: %s\n", cJSON_GetStringValue(id_obj)); printf(" Relay pubkey: %.16s...\n", cJSON_GetStringValue(pubkey_obj)); } return response_event; } // Encrypt admin response content using NIP-44 char* encrypt_admin_response_content(const cJSON* response_data, const char* recipient_pubkey) { if (!response_data || !recipient_pubkey) { log_error("Invalid parameters for admin response encryption"); return NULL; } log_info("Encrypting admin response content with NIP-44"); printf(" Recipient pubkey: %.16s...\n", recipient_pubkey); // Convert response data to JSON string char* response_json = cJSON_Print(response_data); if (!response_json) { log_error("Failed to serialize response data for encryption"); return NULL; } log_info("Response data serialized for encryption"); printf(" JSON length: %zu\n", strlen(response_json)); printf(" JSON preview: %.100s%s\n", response_json, strlen(response_json) > 100 ? "..." : ""); // Get relay private key for encryption char* relay_privkey = get_relay_private_key(); if (!relay_privkey) { log_error("Relay private key not available for admin response encryption"); free(response_json); return NULL; } // Convert relay private key from hex to bytes unsigned char relay_privkey_bytes[32]; if (nostr_hex_to_bytes(relay_privkey, relay_privkey_bytes, 32) != NOSTR_SUCCESS) { log_error("Failed to convert relay private key from hex for encryption"); free(relay_privkey); free(response_json); return NULL; } // Clean up private key string immediately free(relay_privkey); // Convert recipient public key from hex to bytes unsigned char recipient_pubkey_bytes[32]; if (nostr_hex_to_bytes(recipient_pubkey, recipient_pubkey_bytes, 32) != NOSTR_SUCCESS) { log_error("Failed to convert recipient public key from hex for encryption"); memset(relay_privkey_bytes, 0, 32); free(response_json); return NULL; } // Perform NIP-44 encryption (relay as sender, admin as recipient) char encrypted_content[8192]; // Buffer for encrypted content int encrypt_result = nostr_nip44_encrypt(relay_privkey_bytes, recipient_pubkey_bytes, response_json, encrypted_content, sizeof(encrypted_content)); // Clean up sensitive data immediately after use memset(relay_privkey_bytes, 0, 32); free(response_json); if (encrypt_result != NOSTR_SUCCESS) { log_error("NIP-44 encryption failed for admin response"); printf(" Encryption result code: %d\n", encrypt_result); return NULL; } log_success("Admin response content encrypted successfully with NIP-44"); printf(" Encrypted content length: %zu\n", strlen(encrypted_content)); printf(" Encrypted preview: %.50s...\n", encrypted_content); // Return encrypted content as allocated string return strdup(encrypted_content); } // Send admin response event using relay's standard event distribution system int send_admin_response_event(const cJSON* response_data, const char* recipient_pubkey, struct lws* wsi) { // Suppress unused parameter warning (void)wsi; if (!response_data || !recipient_pubkey) { log_error("Invalid parameters for admin response event transmission"); return -1; } log_info("Sending admin response as signed kind 23457 event through relay distribution system"); printf(" Recipient pubkey: %.16s...\n", recipient_pubkey); // Step 1: Encrypt response data using NIP-44 char* encrypted_content = encrypt_admin_response_content(response_data, recipient_pubkey); if (!encrypted_content) { log_error("Failed to encrypt admin response content"); return -1; } // Step 2: Create signed kind 23457 event cJSON* response_event = create_admin_response_event(encrypted_content, recipient_pubkey); free(encrypted_content); // Clean up encrypted content after use if (!response_event) { log_error("Failed to create admin response event"); return -1; } log_info("Admin response event created successfully"); cJSON* id_obj = cJSON_GetObjectItem(response_event, "id"); if (id_obj) { printf(" Event ID: %s\n", cJSON_GetStringValue(id_obj)); } // Step 3: Store event in database for persistence extern int store_event(cJSON* event); if (store_event(response_event) != 0) { log_warning("Failed to store admin response event in database (continuing with broadcast)"); } else { log_info("Admin response event stored in database successfully"); } // Step 4: Broadcast event to all matching subscriptions using relay's standard system extern int broadcast_event_to_subscriptions(cJSON* event); int broadcast_count = broadcast_event_to_subscriptions(response_event); if (broadcast_count >= 0) { log_success("Admin response event distributed through relay subscription system"); printf(" Event kind: 23457 (admin response)\n"); printf(" Subscriptions notified: %d\n", broadcast_count); // Clean up and return success - event creation succeeded regardless of broadcast count cJSON_Delete(response_event); return 0; } else { log_error("Failed to broadcast admin response event to subscriptions"); cJSON_Delete(response_event); return -1; } } // ================================ // WEBSOCKET RESPONSE SYSTEM (LEGACY) // ================================ // Map query command types to proper response types for frontend routing static const char* map_auth_query_type_to_response(const char* query_type) { if (!query_type) return "auth_rules_unknown"; if (strcmp(query_type, "all") == 0) { return "auth_rules_all"; } else if (strcmp(query_type, "whitelist") == 0) { return "auth_rules_whitelist"; } else if (strcmp(query_type, "blacklist") == 0) { return "auth_rules_blacklist"; } else if (strcmp(query_type, "pattern") == 0) { return "auth_rules_pattern"; } else { return "auth_rules_unknown"; } } // Map config query command types to proper response types for frontend routing static const char* map_config_query_type_to_response(const char* query_type) { if (!query_type) return "config_unknown"; if (strcmp(query_type, "all") == 0) { return "config_all"; } else if (strcmp(query_type, "category") == 0) { return "config_category"; } else if (strcmp(query_type, "key") == 0) { return "config_key"; } else { return "config_unknown"; } } // Build standardized query response cJSON* build_query_response(const char* query_type, cJSON* results_array, int total_count) { if (!query_type || !results_array) return NULL; cJSON* response = cJSON_CreateObject(); if (!response) return NULL; cJSON_AddStringToObject(response, "query_type", query_type); cJSON_AddNumberToObject(response, "total_results", total_count); cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL)); cJSON_AddItemToObject(response, "data", cJSON_Duplicate(results_array, 1)); return response; } // ================================ // UNIFIED KIND 23456 HANDLER // ================================ // Single unified handler for all Kind 23456 requests int handle_kind_23456_unified(cJSON* event, char* error_message, size_t error_size, struct lws* wsi) { // Suppress unused parameter warning (void)wsi; if (!event) { log_error("DEBUG: Null event passed to handle_kind_23456_unified"); snprintf(error_message, error_size, "invalid: null event"); return -1; } log_info("DEBUG: Processing Kind 23456 event through unified handler"); // Check if content is encrypted (NIP-44) cJSON* content_obj = cJSON_GetObjectItem(event, "content"); if (!content_obj || !cJSON_IsString(content_obj)) { log_error("DEBUG: Missing or invalid content in Kind 23456 event"); snprintf(error_message, error_size, "invalid: missing or invalid content"); return -1; } const char* content = cJSON_GetStringValue(content_obj); log_info("DEBUG: Event content analysis"); printf(" Content length: %zu\n", content ? strlen(content) : 0); printf(" Content preview: %.50s%s\n", content ? content : "null", (content && strlen(content) > 50) ? "..." : ""); cJSON* decrypted_content = NULL; // Check if content looks like NIP-44 encrypted content (base64 string, not JSON) if (content && strlen(content) > 10 && content[0] != '[' && content[0] != '{') { log_info("DEBUG: Detected NIP-44 encrypted content, attempting decryption"); printf(" Content appears to be base64 encrypted\n"); // Get relay private key for decryption log_info("DEBUG: Retrieving relay private key for decryption"); char* relay_privkey = get_relay_private_key(); if (!relay_privkey) { log_error("DEBUG: Relay private key not available for decryption"); snprintf(error_message, error_size, "error: relay private key not available for decryption"); return -1; } log_info("DEBUG: Relay private key retrieved successfully"); printf(" Relay privkey length: %zu\n", strlen(relay_privkey)); // Get sender's pubkey from the event for NIP-44 decryption cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey"); if (!pubkey_obj || !cJSON_IsString(pubkey_obj)) { log_error("DEBUG: Missing sender pubkey in event"); free(relay_privkey); snprintf(error_message, error_size, "invalid: missing sender pubkey in event"); return -1; } const char* sender_pubkey = cJSON_GetStringValue(pubkey_obj); if (!sender_pubkey || strlen(sender_pubkey) != 64) { log_error("DEBUG: Invalid sender pubkey format"); printf(" Sender pubkey: %s\n", sender_pubkey ? sender_pubkey : "null"); printf(" Sender pubkey length: %zu\n", sender_pubkey ? strlen(sender_pubkey) : 0); free(relay_privkey); snprintf(error_message, error_size, "invalid: invalid sender pubkey format"); return -1; } log_info("DEBUG: Sender pubkey validated"); printf(" Sender pubkey: %.16s...\n", sender_pubkey); // Convert relay private key from hex to bytes log_info("DEBUG: Converting relay private key from hex to bytes"); unsigned char relay_privkey_bytes[32]; if (nostr_hex_to_bytes(relay_privkey, relay_privkey_bytes, 32) != NOSTR_SUCCESS) { log_error("DEBUG: Failed to convert relay private key from hex"); free(relay_privkey); snprintf(error_message, error_size, "error: failed to convert relay private key"); return -1; } log_info("DEBUG: Relay private key converted successfully"); // Convert sender public key from hex to bytes log_info("DEBUG: Converting sender public key from hex to bytes"); unsigned char sender_pubkey_bytes[32]; if (nostr_hex_to_bytes(sender_pubkey, sender_pubkey_bytes, 32) != NOSTR_SUCCESS) { log_error("DEBUG: Failed to convert sender public key from hex"); free(relay_privkey); snprintf(error_message, error_size, "error: failed to convert sender public key"); return -1; } log_info("DEBUG: Sender public key converted successfully"); // Perform NIP-44 decryption (relay as recipient, admin as sender) log_info("DEBUG: Performing NIP-44 decryption"); printf(" Encrypted content length: %zu\n", strlen(content)); char decrypted_text[4096]; // Buffer for decrypted content int decrypt_result = nostr_nip44_decrypt(relay_privkey_bytes, sender_pubkey_bytes, content, decrypted_text, sizeof(decrypted_text)); // Clean up private key immediately after use memset(relay_privkey_bytes, 0, 32); free(relay_privkey); if (decrypt_result != NOSTR_SUCCESS) { log_error("DEBUG: NIP-44 decryption failed"); printf(" Decryption result code: %d\n", decrypt_result); snprintf(error_message, error_size, "error: NIP-44 decryption failed"); return -1; } log_info("DEBUG: NIP-44 decryption successful"); printf(" Decrypted content: %s\n", decrypted_text); printf(" Decrypted length: %zu\n", strlen(decrypted_text)); // Parse decrypted content as JSON array log_info("DEBUG: Parsing decrypted content as JSON"); decrypted_content = cJSON_Parse(decrypted_text); if (!decrypted_content || !cJSON_IsArray(decrypted_content)) { log_error("DEBUG: Decrypted content is not valid JSON array"); printf(" Decrypted content type: %s\n", decrypted_content ? (cJSON_IsArray(decrypted_content) ? "array" : "other") : "null"); snprintf(error_message, error_size, "error: decrypted content is not valid JSON array"); return -1; } log_info("DEBUG: Decrypted content parsed successfully as JSON array"); printf(" Array size: %d\n", cJSON_GetArraySize(decrypted_content)); // Replace event content with decrypted command array for processing log_info("DEBUG: Replacing event content with decrypted marker"); cJSON_DeleteItemFromObject(event, "content"); cJSON_AddStringToObject(event, "content", "decrypted"); // Create synthetic tags from decrypted command array log_info("DEBUG: Creating synthetic tags from decrypted command array"); cJSON* tags_obj = cJSON_GetObjectItem(event, "tags"); if (!tags_obj) { log_info("DEBUG: No existing tags, creating new tags array"); tags_obj = cJSON_CreateArray(); cJSON_AddItemToObject(event, "tags", tags_obj); } else { log_info("DEBUG: Using existing tags array"); printf(" Existing tags count: %d\n", cJSON_GetArraySize(tags_obj)); } // Add decrypted command as first tag if (cJSON_GetArraySize(decrypted_content) > 0) { log_info("DEBUG: Adding decrypted command as synthetic tag"); cJSON* first_item = cJSON_GetArrayItem(decrypted_content, 0); if (cJSON_IsString(first_item)) { const char* command_name = cJSON_GetStringValue(first_item); log_info("DEBUG: Creating command tag"); printf(" Command: %s\n", command_name ? command_name : "null"); cJSON* command_tag = cJSON_CreateArray(); cJSON_AddItemToArray(command_tag, cJSON_Duplicate(first_item, 1)); // Add remaining items as tag values for (int i = 1; i < cJSON_GetArraySize(decrypted_content); i++) { cJSON* item = cJSON_GetArrayItem(decrypted_content, i); if (item) { if (cJSON_IsString(item)) { printf(" Arg %d: %s\n", i, cJSON_GetStringValue(item)); } else { printf(" Arg %d: (non-string)\n", i); } cJSON_AddItemToArray(command_tag, cJSON_Duplicate(item, 1)); } } // Insert at beginning of tags array cJSON_InsertItemInArray(tags_obj, 0, command_tag); log_info("DEBUG: Synthetic command tag created and inserted"); printf(" Final tag array size: %d\n", cJSON_GetArraySize(tags_obj)); } else { log_error("DEBUG: First item in decrypted array is not a string"); } } else { log_error("DEBUG: Decrypted array is empty"); } cJSON_Delete(decrypted_content); } else { log_info("DEBUG: Content does not appear to be NIP-44 encrypted"); printf(" Content starts with: %c\n", content ? content[0] : '?'); printf(" Content length: %zu\n", content ? strlen(content) : 0); } // Parse first tag to determine action type (now from decrypted content if applicable) log_info("DEBUG: Parsing first tag to determine action type"); const char* action_type = get_first_tag_name(event); if (!action_type) { log_error("DEBUG: Missing or invalid first tag after processing"); cJSON* tags_obj = cJSON_GetObjectItem(event, "tags"); if (tags_obj && cJSON_IsArray(tags_obj)) { printf(" Tags array size: %d\n", cJSON_GetArraySize(tags_obj)); } else { printf(" No tags array found\n"); } snprintf(error_message, error_size, "invalid: missing or invalid first tag"); return -1; } log_info("DEBUG: Action type determined"); printf(" Action type: %s\n", action_type); // Route to appropriate handler based on action type log_info("DEBUG: Routing to action-specific handler"); if (strcmp(action_type, "auth_query") == 0) { log_info("DEBUG: Routing to auth_query handler"); const char* query_type = get_tag_value(event, action_type, 1); if (!query_type) { log_error("DEBUG: Missing auth_query type parameter"); snprintf(error_message, error_size, "invalid: missing auth_query type"); return -1; } printf(" Query type: %s\n", query_type); return handle_auth_query_unified(event, query_type, error_message, error_size, wsi); } else if (strcmp(action_type, "config_query") == 0) { log_info("DEBUG: Routing to config_query handler"); const char* query_type = get_tag_value(event, action_type, 1); if (!query_type) { log_error("DEBUG: Missing config_query type parameter"); snprintf(error_message, error_size, "invalid: missing config_query type"); return -1; } printf(" Query type: %s\n", query_type); return handle_config_query_unified(event, query_type, error_message, error_size, wsi); } else if (strcmp(action_type, "config_set") == 0) { log_info("DEBUG: Routing to config_set handler"); const char* config_key = get_tag_value(event, action_type, 1); const char* config_value = get_tag_value(event, action_type, 2); if (!config_key || !config_value) { log_error("DEBUG: Missing config_set parameters"); snprintf(error_message, error_size, "invalid: missing config_set key or value"); return -1; } printf(" Key: %s, Value: %s\n", config_key, config_value); return handle_config_set_unified(event, config_key, config_value, error_message, error_size, wsi); } else if (strcmp(action_type, "config_update") == 0) { log_info("DEBUG: Routing to config_update handler"); return handle_config_update_unified(event, error_message, error_size, wsi); } else if (strcmp(action_type, "system_command") == 0) { log_info("DEBUG: Routing to system_command handler"); const char* command = get_tag_value(event, action_type, 1); if (!command) { log_error("DEBUG: Missing system_command type parameter"); snprintf(error_message, error_size, "invalid: missing system_command type"); return -1; } printf(" Command: %s\n", command); return handle_system_command_unified(event, command, error_message, error_size, wsi); } else if (strcmp(action_type, "whitelist") == 0 || strcmp(action_type, "blacklist") == 0) { log_info("DEBUG: Routing to auth rule modification handler"); printf(" Rule type: %s\n", action_type); // Handle auth rule modifications (existing logic from process_admin_auth_event) return handle_auth_rule_modification_unified(event, error_message, error_size, wsi); } else { log_error("DEBUG: Unknown Kind 23456 action type"); printf(" Unknown action: %s\n", action_type); snprintf(error_message, error_size, "invalid: unknown Kind 23456 action type '%s'", action_type); return -1; } } // Unified auth query handler int handle_auth_query_unified(cJSON* event, const char* query_type, char* error_message, size_t error_size, struct lws* wsi) { // Suppress unused parameter warning (void)wsi; if (!g_db) { snprintf(error_message, error_size, "database not available"); return -1; } log_info("Processing unified auth query"); printf(" Query type: %s\n", query_type); const char* sql = NULL; int use_pattern_param = 0; char* pattern_value = NULL; // Build appropriate SQL query based on query type if (strcmp(query_type, "all") == 0) { sql = "SELECT rule_type, pattern_type, pattern_value, action FROM auth_rules ORDER BY rule_type, pattern_type"; } else if (strcmp(query_type, "whitelist") == 0) { sql = "SELECT rule_type, pattern_type, pattern_value, action FROM auth_rules WHERE rule_type LIKE '%whitelist%' ORDER BY pattern_type"; } else if (strcmp(query_type, "blacklist") == 0) { sql = "SELECT rule_type, pattern_type, pattern_value, action FROM auth_rules WHERE rule_type LIKE '%blacklist%' ORDER BY pattern_type"; } else if (strcmp(query_type, "pattern") == 0) { // Get pattern value from tags pattern_value = (char*)get_tag_value(event, "auth_query", 2); if (!pattern_value) { snprintf(error_message, error_size, "invalid: pattern query requires pattern value"); return -1; } sql = "SELECT rule_type, pattern_type, pattern_value, action FROM auth_rules WHERE pattern_value = ? ORDER BY rule_type, pattern_type"; use_pattern_param = 1; } else { snprintf(error_message, error_size, "invalid: unknown auth query type '%s'", query_type); return -1; } // Execute query sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { snprintf(error_message, error_size, "failed to prepare auth query"); return -1; } if (use_pattern_param && pattern_value) { sqlite3_bind_text(stmt, 1, pattern_value, -1, SQLITE_STATIC); } // Build results array cJSON* results_array = cJSON_CreateArray(); if (!results_array) { sqlite3_finalize(stmt); snprintf(error_message, error_size, "failed to create results array"); return -1; } int rule_count = 0; printf("=== Auth Query Results (%s) ===\n", query_type); while (sqlite3_step(stmt) == SQLITE_ROW) { const char* rule_type = (const char*)sqlite3_column_text(stmt, 0); const char* pattern_type = (const char*)sqlite3_column_text(stmt, 1); const char* pattern_value_result = (const char*)sqlite3_column_text(stmt, 2); const char* action = (const char*)sqlite3_column_text(stmt, 3); printf(" %s %s:%s -> %s\n", rule_type ? rule_type : "", pattern_type ? pattern_type : "", pattern_value_result ? pattern_value_result : "", action ? action : "allow"); // Add rule to results array cJSON* rule_obj = cJSON_CreateObject(); cJSON_AddStringToObject(rule_obj, "rule_type", rule_type ? rule_type : ""); cJSON_AddStringToObject(rule_obj, "pattern_type", pattern_type ? pattern_type : ""); cJSON_AddStringToObject(rule_obj, "pattern_value", pattern_value_result ? pattern_value_result : ""); cJSON_AddStringToObject(rule_obj, "action", action ? action : "allow"); cJSON_AddItemToArray(results_array, rule_obj); rule_count++; } sqlite3_finalize(stmt); // Build and send response with mapped query type for frontend routing const char* mapped_query_type = map_auth_query_type_to_response(query_type); cJSON* response = build_query_response(mapped_query_type, results_array, rule_count); if (response) { // Get admin pubkey from event for response cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey"); const char* admin_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : NULL; if (!admin_pubkey) { cJSON_Delete(response); cJSON_Delete(results_array); snprintf(error_message, error_size, "missing admin pubkey for response"); return -1; } // Send response as signed kind 23457 event if (send_admin_response_event(response, admin_pubkey, wsi) == 0) { printf("Total results: %d\n", rule_count); log_success("Auth query completed successfully with signed response"); printf(" Response query_type: %s (mapped from %s)\n", mapped_query_type, query_type); cJSON_Delete(response); cJSON_Delete(results_array); return 0; } cJSON_Delete(response); } cJSON_Delete(results_array); snprintf(error_message, error_size, "failed to send auth query response"); return -1; } // Unified config query handler int handle_config_query_unified(cJSON* event, const char* query_type, char* error_message, size_t error_size, struct lws* wsi) { // Suppress unused parameter warning (void)wsi; if (!g_db) { snprintf(error_message, error_size, "database not available"); return -1; } log_info("Processing unified config query"); printf(" Query type: %s\n", query_type); const char* sql = NULL; int use_pattern_param = 0; char* pattern_value = NULL; // Build appropriate SQL query based on query type if (strcmp(query_type, "all") == 0) { sql = "SELECT key, value, data_type, category, description FROM config ORDER BY category, key"; } else if (strcmp(query_type, "category") == 0) { // Get category value from tags pattern_value = (char*)get_tag_value(event, "config_query", 2); if (!pattern_value) { snprintf(error_message, error_size, "invalid: category query requires category value"); return -1; } sql = "SELECT key, value, data_type, category, description FROM config WHERE category = ? ORDER BY key"; use_pattern_param = 1; } else if (strcmp(query_type, "key") == 0) { // Get key value from tags pattern_value = (char*)get_tag_value(event, "config_query", 2); if (!pattern_value) { snprintf(error_message, error_size, "invalid: key query requires key value"); return -1; } sql = "SELECT key, value, data_type, category, description FROM config WHERE key = ? ORDER BY key"; use_pattern_param = 1; } else { snprintf(error_message, error_size, "invalid: unknown config query type '%s'", query_type); return -1; } // Execute query sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { snprintf(error_message, error_size, "failed to prepare config query"); return -1; } if (use_pattern_param && pattern_value) { sqlite3_bind_text(stmt, 1, pattern_value, -1, SQLITE_STATIC); } // Build results array cJSON* results_array = cJSON_CreateArray(); if (!results_array) { sqlite3_finalize(stmt); snprintf(error_message, error_size, "failed to create results array"); return -1; } int config_count = 0; printf("=== Config Query Results (%s) ===\n", query_type); while (sqlite3_step(stmt) == SQLITE_ROW) { const char* key = (const char*)sqlite3_column_text(stmt, 0); const char* value = (const char*)sqlite3_column_text(stmt, 1); const char* data_type = (const char*)sqlite3_column_text(stmt, 2); const char* category = (const char*)sqlite3_column_text(stmt, 3); const char* description = (const char*)sqlite3_column_text(stmt, 4); printf(" %s = %s [%s] (%s)\n", key ? key : "", value ? value : "", data_type ? data_type : "string", category ? category : "general"); // Add config item to results array cJSON* config_obj = cJSON_CreateObject(); cJSON_AddStringToObject(config_obj, "key", key ? key : ""); cJSON_AddStringToObject(config_obj, "value", value ? value : ""); cJSON_AddStringToObject(config_obj, "data_type", data_type ? data_type : "string"); cJSON_AddStringToObject(config_obj, "category", category ? category : "general"); cJSON_AddStringToObject(config_obj, "description", description ? description : ""); cJSON_AddItemToArray(results_array, config_obj); config_count++; } sqlite3_finalize(stmt); // Build and send response with mapped query type for frontend routing const char* mapped_query_type = map_config_query_type_to_response(query_type); cJSON* response = build_query_response(mapped_query_type, results_array, config_count); if (response) { // Get admin pubkey from event for response cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey"); const char* admin_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : NULL; if (!admin_pubkey) { cJSON_Delete(response); cJSON_Delete(results_array); snprintf(error_message, error_size, "missing admin pubkey for response"); return -1; } // Send response as signed kind 23457 event if (send_admin_response_event(response, admin_pubkey, wsi) == 0) { printf("Total results: %d\n", config_count); log_success("Config query completed successfully with signed response"); printf(" Response query_type: %s (mapped from %s)\n", mapped_query_type, query_type); cJSON_Delete(response); cJSON_Delete(results_array); return 0; } cJSON_Delete(response); } cJSON_Delete(results_array); snprintf(error_message, error_size, "failed to send config query response"); return -1; } // Unified config set handler int handle_config_set_unified(cJSON* event, const char* config_key, const char* config_value, char* error_message, size_t error_size, struct lws* wsi) { // Suppress unused parameter warning (void)wsi; if (!g_db) { snprintf(error_message, error_size, "database not available"); return -1; } log_info("Processing unified config set command"); printf(" Key: %s\n", config_key); printf(" Value: %s\n", config_value); // Validate the configuration field before updating char validation_error[512]; if (validate_config_field(config_key, config_value, validation_error, sizeof(validation_error)) != 0) { log_error("Config field validation failed"); printf(" Validation error: %s\n", validation_error); snprintf(error_message, error_size, "validation failed: %s", validation_error); return -1; } // Check if the config key exists in the table const char* check_sql = "SELECT COUNT(*) FROM config WHERE key = ?"; sqlite3_stmt* check_stmt; int check_rc = sqlite3_prepare_v2(g_db, check_sql, -1, &check_stmt, NULL); if (check_rc != SQLITE_OK) { snprintf(error_message, error_size, "failed to prepare config existence check"); return -1; } sqlite3_bind_text(check_stmt, 1, config_key, -1, SQLITE_STATIC); int config_exists = 0; if (sqlite3_step(check_stmt) == SQLITE_ROW) { config_exists = sqlite3_column_int(check_stmt, 0) > 0; } sqlite3_finalize(check_stmt); if (!config_exists) { snprintf(error_message, error_size, "error: configuration key '%s' not found", config_key); return -1; } // Update the configuration value if (update_config_in_table(config_key, config_value) != 0) { snprintf(error_message, error_size, "failed to update configuration in database"); return -1; } // Invalidate cache to ensure fresh reads invalidate_config_cache(); // Build response cJSON* response = cJSON_CreateObject(); cJSON_AddStringToObject(response, "command", "config_set"); cJSON_AddStringToObject(response, "key", config_key); cJSON_AddStringToObject(response, "value", config_value); cJSON_AddStringToObject(response, "status", "success"); cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL)); printf("Updated config: %s = %s\n", config_key, config_value); // Get admin pubkey from event for response cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey"); const char* admin_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : NULL; if (!admin_pubkey) { cJSON_Delete(response); snprintf(error_message, error_size, "missing admin pubkey for response"); return -1; } // Send response as signed kind 23457 event if (send_admin_response_event(response, admin_pubkey, wsi) == 0) { log_success("Config set command completed successfully with signed response"); cJSON_Delete(response); return 0; } cJSON_Delete(response); snprintf(error_message, error_size, "failed to send config set response"); return -1; } // Unified system command handler int handle_system_command_unified(cJSON* event, const char* command, char* error_message, size_t error_size, struct lws* wsi) { // Suppress unused parameter warning (void)wsi; if (!g_db) { snprintf(error_message, error_size, "database not available"); return -1; } log_info("Processing unified system command"); printf(" Command: %s\n", command); if (strcmp(command, "clear_all_auth_rules") == 0) { // Count existing rules first const char* count_sql = "SELECT COUNT(*) FROM auth_rules"; sqlite3_stmt* count_stmt; int rc = sqlite3_prepare_v2(g_db, count_sql, -1, &count_stmt, NULL); if (rc != SQLITE_OK) { snprintf(error_message, error_size, "failed to prepare count query"); return -1; } int rule_count = 0; if (sqlite3_step(count_stmt) == SQLITE_ROW) { rule_count = sqlite3_column_int(count_stmt, 0); } sqlite3_finalize(count_stmt); // Delete all auth rules const char* delete_sql = "DELETE FROM auth_rules"; rc = sqlite3_exec(g_db, delete_sql, NULL, NULL, NULL); if (rc != SQLITE_OK) { snprintf(error_message, error_size, "failed to execute clear auth rules command"); return -1; } // Build response cJSON* response = cJSON_CreateObject(); cJSON_AddStringToObject(response, "command", "clear_all_auth_rules"); cJSON_AddNumberToObject(response, "rules_cleared", rule_count); cJSON_AddStringToObject(response, "status", "success"); cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL)); printf("Cleared %d auth rules from database\n", rule_count); // Get admin pubkey from event for response cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey"); const char* admin_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : NULL; if (!admin_pubkey) { cJSON_Delete(response); snprintf(error_message, error_size, "missing admin pubkey for response"); return -1; } // Send response as signed kind 23457 event if (send_admin_response_event(response, admin_pubkey, wsi) == 0) { log_success("Clear auth rules command completed successfully with signed response"); cJSON_Delete(response); return 0; } cJSON_Delete(response); snprintf(error_message, error_size, "failed to send clear auth rules response"); return -1; } else if (strcmp(command, "delete_auth_rule") == 0) { // Get rule parameters from tags const char* rule_type = get_tag_value(event, "system_command", 2); const char* pattern_type = get_tag_value(event, "system_command", 3); const char* pattern_value = get_tag_value(event, "system_command", 4); if (!rule_type || !pattern_type || !pattern_value) { snprintf(error_message, error_size, "invalid: delete_auth_rule requires rule_type, pattern_type, and pattern_value"); return -1; } log_info("Processing delete auth rule command"); printf(" Rule type: %s\n", rule_type); printf(" Pattern type: %s\n", pattern_type); printf(" Pattern value: %s\n", pattern_value); // Check if rule exists before deletion const char* check_sql = "SELECT COUNT(*) FROM auth_rules WHERE rule_type = ? AND pattern_type = ? AND pattern_value = ?"; sqlite3_stmt* check_stmt; int check_rc = sqlite3_prepare_v2(g_db, check_sql, -1, &check_stmt, NULL); if (check_rc != SQLITE_OK) { snprintf(error_message, error_size, "failed to prepare rule existence check"); return -1; } sqlite3_bind_text(check_stmt, 1, rule_type, -1, SQLITE_STATIC); sqlite3_bind_text(check_stmt, 2, pattern_type, -1, SQLITE_STATIC); sqlite3_bind_text(check_stmt, 3, pattern_value, -1, SQLITE_STATIC); int rule_exists = 0; if (sqlite3_step(check_stmt) == SQLITE_ROW) { rule_exists = sqlite3_column_int(check_stmt, 0) > 0; } sqlite3_finalize(check_stmt); if (!rule_exists) { snprintf(error_message, error_size, "error: auth rule not found"); return -1; } // Delete the specific auth rule if (remove_auth_rule_from_config(rule_type, pattern_type, pattern_value) != 0) { snprintf(error_message, error_size, "failed to delete auth rule from database"); return -1; } // Build response cJSON* response = cJSON_CreateObject(); cJSON_AddStringToObject(response, "command", "delete_auth_rule"); cJSON_AddStringToObject(response, "rule_type", rule_type); cJSON_AddStringToObject(response, "pattern_type", pattern_type); cJSON_AddStringToObject(response, "pattern_value", pattern_value); cJSON_AddStringToObject(response, "status", "success"); cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL)); printf("Deleted auth rule: %s %s:%s\n", rule_type, pattern_type, pattern_value); // Get admin pubkey from event for response cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey"); const char* admin_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : NULL; if (!admin_pubkey) { cJSON_Delete(response); snprintf(error_message, error_size, "missing admin pubkey for response"); return -1; } // Send response as signed kind 23457 event if (send_admin_response_event(response, admin_pubkey, wsi) == 0) { log_success("Delete auth rule command completed successfully with signed response"); cJSON_Delete(response); return 0; } cJSON_Delete(response); snprintf(error_message, error_size, "failed to send delete auth rule response"); return -1; } else if (strcmp(command, "system_status") == 0) { // Build system status response cJSON* response = cJSON_CreateObject(); cJSON_AddStringToObject(response, "command", "system_status"); cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL)); cJSON* status_data = cJSON_CreateObject(); cJSON_AddStringToObject(status_data, "database", g_db ? "connected" : "not_available"); cJSON_AddStringToObject(status_data, "cache_status", g_unified_cache.cache_valid ? "valid" : "invalid"); if (strlen(g_database_path) > 0) { cJSON_AddStringToObject(status_data, "database_path", g_database_path); } // Count configuration items and auth rules if (g_db) { sqlite3_stmt* stmt; // Config count if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM config", -1, &stmt, NULL) == SQLITE_OK) { if (sqlite3_step(stmt) == SQLITE_ROW) { cJSON_AddNumberToObject(status_data, "config_items", sqlite3_column_int(stmt, 0)); } sqlite3_finalize(stmt); } // Auth rules count if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM auth_rules", -1, &stmt, NULL) == SQLITE_OK) { if (sqlite3_step(stmt) == SQLITE_ROW) { cJSON_AddNumberToObject(status_data, "auth_rules", sqlite3_column_int(stmt, 0)); } sqlite3_finalize(stmt); } } cJSON_AddItemToObject(response, "data", status_data); printf("=== System Status ===\n"); printf("Database: %s\n", g_db ? "Connected" : "Not available"); printf("Cache status: %s\n", g_unified_cache.cache_valid ? "Valid" : "Invalid"); // Get admin pubkey from event for response cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey"); const char* admin_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : NULL; if (!admin_pubkey) { cJSON_Delete(response); snprintf(error_message, error_size, "missing admin pubkey for response"); return -1; } // Send response as signed kind 23457 event if (send_admin_response_event(response, admin_pubkey, wsi) == 0) { log_success("System status query completed successfully with signed response"); cJSON_Delete(response); return 0; } cJSON_Delete(response); snprintf(error_message, error_size, "failed to send system status response"); return -1; } else { snprintf(error_message, error_size, "invalid: unknown system command '%s'", command); return -1; } } // Handle auth rule modifications (extracted from process_admin_auth_event) int handle_auth_rule_modification_unified(cJSON* event, char* error_message, size_t error_size, struct lws* wsi) { // Suppress unused parameter warning (void)wsi; cJSON* tags_obj = cJSON_GetObjectItem(event, "tags"); if (!tags_obj || !cJSON_IsArray(tags_obj)) { snprintf(error_message, error_size, "invalid: auth rule event must have tags"); return -1; } // Begin transaction for atomic auth rule updates int rc = sqlite3_exec(g_db, "BEGIN IMMEDIATE TRANSACTION", NULL, NULL, NULL); if (rc != SQLITE_OK) { snprintf(error_message, error_size, "failed to begin auth rule transaction"); return -1; } int rules_processed = 0; cJSON* processed_rules = cJSON_CreateArray(); if (!processed_rules) { sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL); snprintf(error_message, error_size, "failed to create response array"); return -1; } // For Kind 23456 events, only process synthetic tags created from decrypted content // Skip original unencrypted tags (except p tag validation which is done elsewhere) cJSON* auth_tag = NULL; cJSON_ArrayForEach(auth_tag, tags_obj) { if (!cJSON_IsArray(auth_tag) || cJSON_GetArraySize(auth_tag) < 3) { continue; } cJSON* rule_type_obj = cJSON_GetArrayItem(auth_tag, 0); cJSON* pattern_type_obj = cJSON_GetArrayItem(auth_tag, 1); cJSON* pattern_value_obj = cJSON_GetArrayItem(auth_tag, 2); if (!cJSON_IsString(rule_type_obj) || !cJSON_IsString(pattern_type_obj) || !cJSON_IsString(pattern_value_obj)) { continue; } const char* rule_type = cJSON_GetStringValue(rule_type_obj); const char* pattern_type = cJSON_GetStringValue(pattern_type_obj); const char* pattern_value = cJSON_GetStringValue(pattern_value_obj); // Skip p tags - they are for routing, not auth rules if (strcmp(rule_type, "p") == 0) { continue; } // Process auth rule: ["blacklist"|"whitelist", "pubkey"|"hash", "value"] if (strcmp(rule_type, "blacklist") == 0 || strcmp(rule_type, "whitelist") == 0) { if (add_auth_rule_from_config(rule_type, pattern_type, pattern_value, "allow") == 0) { rules_processed++; // Add processed rule to response array cJSON* rule_obj = cJSON_CreateObject(); cJSON_AddStringToObject(rule_obj, "rule_type", rule_type); cJSON_AddStringToObject(rule_obj, "pattern_type", pattern_type); cJSON_AddStringToObject(rule_obj, "pattern_value", pattern_value); cJSON_AddStringToObject(rule_obj, "action", "allow"); cJSON_AddStringToObject(rule_obj, "status", "added"); cJSON_AddItemToArray(processed_rules, rule_obj); } } } if (rules_processed > 0) { sqlite3_exec(g_db, "COMMIT", NULL, NULL, NULL); char success_msg[256]; snprintf(success_msg, sizeof(success_msg), "Processed %d auth rule updates", rules_processed); log_success(success_msg); // Build and send response cJSON* response = cJSON_CreateObject(); cJSON_AddStringToObject(response, "command", "auth_rule_modification"); cJSON_AddNumberToObject(response, "rules_processed", rules_processed); cJSON_AddStringToObject(response, "status", "success"); cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL)); cJSON_AddItemToObject(response, "processed_rules", processed_rules); printf("Processed %d auth rule modifications\n", rules_processed); // Get admin pubkey from event for response cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey"); const char* admin_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : NULL; if (!admin_pubkey) { cJSON_Delete(response); snprintf(error_message, error_size, "missing admin pubkey for response"); return -1; } // Send response as signed kind 23457 event if (send_admin_response_event(response, admin_pubkey, wsi) == 0) { log_success("Auth rule modification completed successfully with signed response"); cJSON_Delete(response); return 0; } cJSON_Delete(response); snprintf(error_message, error_size, "failed to send auth rule modification response"); return -1; } else { sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL); cJSON_Delete(processed_rules); snprintf(error_message, error_size, "no valid auth rules found"); return -1; } } // Unified config update handler - handles multiple config objects in single atomic command int handle_config_update_unified(cJSON* event, char* error_message, size_t error_size, struct lws* wsi) { // Suppress unused parameter warning (void)wsi; if (!g_db) { snprintf(error_message, error_size, "database not available"); return -1; } log_info("Processing unified config update command"); // Extract config objects array from synthetic tags created by NIP-44 decryption // The decryption process creates synthetic tags like: ["config_update", [config_objects]] cJSON* tags_obj = cJSON_GetObjectItem(event, "tags"); if (!tags_obj || !cJSON_IsArray(tags_obj)) { snprintf(error_message, error_size, "invalid: config update event must have tags"); return -1; } // Find the config_update tag with config objects array cJSON* config_objects_array = NULL; cJSON* tag = NULL; cJSON_ArrayForEach(tag, tags_obj) { if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) { continue; } cJSON* tag_name = cJSON_GetArrayItem(tag, 0); if (!tag_name || !cJSON_IsString(tag_name)) { continue; } if (strcmp(cJSON_GetStringValue(tag_name), "config_update") == 0) { // Found config_update tag, get the config objects array cJSON* config_array_item = cJSON_GetArrayItem(tag, 1); if (config_array_item) { // The config objects should be in a JSON string format in the tag if (cJSON_IsString(config_array_item)) { // Parse the JSON string to get the actual array const char* config_json = cJSON_GetStringValue(config_array_item); config_objects_array = cJSON_Parse(config_json); } else if (cJSON_IsArray(config_array_item)) { // Direct array reference config_objects_array = cJSON_Duplicate(config_array_item, 1); } } break; } } if (!config_objects_array || !cJSON_IsArray(config_objects_array)) { snprintf(error_message, error_size, "invalid: config_update command requires config objects array"); return -1; } int config_count = cJSON_GetArraySize(config_objects_array); log_info("Config update command contains config objects"); printf(" Config objects count: %d\n", config_count); if (config_count == 0) { cJSON_Delete(config_objects_array); snprintf(error_message, error_size, "invalid: config_update command requires at least one config object"); return -1; } // Begin transaction for atomic config updates int rc = sqlite3_exec(g_db, "BEGIN IMMEDIATE TRANSACTION", NULL, NULL, NULL); if (rc != SQLITE_OK) { cJSON_Delete(config_objects_array); snprintf(error_message, error_size, "failed to begin config update transaction"); return -1; } int updates_applied = 0; int validation_errors = 0; char first_validation_error[512] = {0}; // Track first specific validation error char first_error_field[128] = {0}; // Track which field failed first cJSON* processed_configs = cJSON_CreateArray(); if (!processed_configs) { sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL); cJSON_Delete(config_objects_array); snprintf(error_message, error_size, "failed to create response array"); return -1; } // Process each config object in the array cJSON* config_obj = NULL; cJSON_ArrayForEach(config_obj, config_objects_array) { if (!cJSON_IsObject(config_obj)) { log_warning("Skipping non-object item in config objects array"); continue; } // Extract required fields from config object cJSON* key_obj = cJSON_GetObjectItem(config_obj, "key"); cJSON* value_obj = cJSON_GetObjectItem(config_obj, "value"); cJSON* data_type_obj = cJSON_GetObjectItem(config_obj, "data_type"); cJSON* category_obj = cJSON_GetObjectItem(config_obj, "category"); if (!key_obj || !cJSON_IsString(key_obj) || !value_obj || !cJSON_IsString(value_obj)) { log_error("Config object missing required key or value fields"); validation_errors++; continue; } const char* key = cJSON_GetStringValue(key_obj); const char* value = cJSON_GetStringValue(value_obj); const char* data_type = data_type_obj && cJSON_IsString(data_type_obj) ? cJSON_GetStringValue(data_type_obj) : "string"; const char* category = category_obj && cJSON_IsString(category_obj) ? cJSON_GetStringValue(category_obj) : "general"; log_info("Processing config object"); printf(" Key: %s\n", key); printf(" Value: %s\n", value); printf(" Data type: %s\n", data_type); printf(" Category: %s\n", category); // Validate the configuration field before updating char validation_error[512]; if (validate_config_field(key, value, validation_error, sizeof(validation_error)) != 0) { log_error("Config field validation failed"); printf(" Validation error: %s\n", validation_error); validation_errors++; // Capture first validation error for enhanced error message if (validation_errors == 1) { strncpy(first_validation_error, validation_error, sizeof(first_validation_error) - 1); first_validation_error[sizeof(first_validation_error) - 1] = '\0'; strncpy(first_error_field, key, sizeof(first_error_field) - 1); first_error_field[sizeof(first_error_field) - 1] = '\0'; } // Add failed config to response array cJSON* failed_config = cJSON_CreateObject(); cJSON_AddStringToObject(failed_config, "key", key); cJSON_AddStringToObject(failed_config, "value", value); cJSON_AddStringToObject(failed_config, "data_type", data_type); cJSON_AddStringToObject(failed_config, "category", category); cJSON_AddStringToObject(failed_config, "status", "validation_failed"); cJSON_AddStringToObject(failed_config, "error", validation_error); cJSON_AddItemToArray(processed_configs, failed_config); continue; } // Check if the config key exists in the table const char* check_sql = "SELECT COUNT(*) FROM config WHERE key = ?"; sqlite3_stmt* check_stmt; int check_rc = sqlite3_prepare_v2(g_db, check_sql, -1, &check_stmt, NULL); if (check_rc != SQLITE_OK) { log_error("Failed to prepare config existence check"); validation_errors++; continue; } sqlite3_bind_text(check_stmt, 1, key, -1, SQLITE_STATIC); int config_exists = 0; if (sqlite3_step(check_stmt) == SQLITE_ROW) { config_exists = sqlite3_column_int(check_stmt, 0) > 0; } sqlite3_finalize(check_stmt); if (!config_exists) { log_error("Configuration key not found"); printf(" Key not found: %s\n", key); validation_errors++; // Add failed config to response array cJSON* failed_config = cJSON_CreateObject(); cJSON_AddStringToObject(failed_config, "key", key); cJSON_AddStringToObject(failed_config, "value", value); cJSON_AddStringToObject(failed_config, "data_type", data_type); cJSON_AddStringToObject(failed_config, "category", category); cJSON_AddStringToObject(failed_config, "status", "key_not_found"); cJSON_AddStringToObject(failed_config, "error", "configuration key not found in database"); cJSON_AddItemToArray(processed_configs, failed_config); continue; } // Check if this config requires restart const char* requires_restart_sql = "SELECT requires_restart FROM config WHERE key = ?"; sqlite3_stmt* restart_stmt; int requires_restart = 0; if (sqlite3_prepare_v2(g_db, requires_restart_sql, -1, &restart_stmt, NULL) == SQLITE_OK) { sqlite3_bind_text(restart_stmt, 1, key, -1, SQLITE_STATIC); if (sqlite3_step(restart_stmt) == SQLITE_ROW) { requires_restart = sqlite3_column_int(restart_stmt, 0); } sqlite3_finalize(restart_stmt); } // Update the configuration value in the table if (update_config_in_table(key, value) == 0) { updates_applied++; // For dynamic configs (requires_restart = 0), refresh cache immediately if (requires_restart == 0) { log_info("Dynamic config updated - refreshing cache"); refresh_unified_cache_from_table(); // Apply selective re-initialization for specific dynamic configs log_info("Applying selective re-initialization for dynamic config changes"); if (strcmp(key, "max_subscriptions_per_client") == 0 || strcmp(key, "max_total_subscriptions") == 0) { log_info("Subscription limits changed - updating subscription manager"); update_subscription_manager_config(); // Also refresh NIP-11 relay info since max_subscriptions_per_client affects limitation field log_info("Subscription limits changed - reinitializing relay info for NIP-11"); init_relay_info(); } else if (strcmp(key, "pow_min_difficulty") == 0 || strcmp(key, "pow_mode") == 0) { log_info("PoW configuration changed - reinitializing PoW system"); init_pow_config(); } else if (strcmp(key, "nip40_expiration_enabled") == 0 || strcmp(key, "nip40_expiration_strict") == 0 || strcmp(key, "nip40_expiration_filter") == 0 || strcmp(key, "nip40_expiration_grace_period") == 0) { log_info("Expiration configuration changed - reinitializing expiration system"); init_expiration_config(); } else if (strcmp(key, "relay_description") == 0 || strcmp(key, "relay_contact") == 0 || strcmp(key, "relay_software") == 0 || strcmp(key, "relay_version") == 0 || strcmp(key, "max_message_length") == 0 || strcmp(key, "max_event_tags") == 0 || strcmp(key, "max_content_length") == 0) { log_info("Relay information changed - reinitializing relay info"); init_relay_info(); } } // Add successful config to response array cJSON* success_config = cJSON_CreateObject(); cJSON_AddStringToObject(success_config, "key", key); cJSON_AddStringToObject(success_config, "value", value); cJSON_AddStringToObject(success_config, "data_type", data_type); cJSON_AddStringToObject(success_config, "category", category); cJSON_AddStringToObject(success_config, "status", "updated"); cJSON_AddBoolToObject(success_config, "requires_restart", requires_restart); cJSON_AddItemToArray(processed_configs, success_config); log_success("Config field updated successfully"); printf(" Updated: %s = %s (restart: %s)\n", key, value, requires_restart ? "yes" : "no"); } else { log_error("Failed to update config field in database"); printf(" Failed to update: %s = %s\n", key, value); validation_errors++; // Add failed config to response array cJSON* failed_config = cJSON_CreateObject(); cJSON_AddStringToObject(failed_config, "key", key); cJSON_AddStringToObject(failed_config, "value", value); cJSON_AddStringToObject(failed_config, "data_type", data_type); cJSON_AddStringToObject(failed_config, "category", category); cJSON_AddStringToObject(failed_config, "status", "database_error"); cJSON_AddStringToObject(failed_config, "error", "failed to update configuration in database"); cJSON_AddItemToArray(processed_configs, failed_config); } } // Clean up config objects array cJSON_Delete(config_objects_array); // Determine transaction outcome if (updates_applied > 0 && validation_errors == 0) { // All updates successful sqlite3_exec(g_db, "COMMIT", NULL, NULL, NULL); invalidate_config_cache(); char success_msg[256]; snprintf(success_msg, sizeof(success_msg), "Applied %d configuration updates successfully", updates_applied); log_success(success_msg); } else if (updates_applied > 0 && validation_errors > 0) { // Partial success - rollback for atomic behavior sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL); char error_msg[256]; snprintf(error_msg, sizeof(error_msg), "Config update failed: %d validation errors (atomic rollback)", validation_errors); log_error(error_msg); // Build error response with validation details cJSON* error_response = cJSON_CreateObject(); cJSON_AddStringToObject(error_response, "query_type", "config_update"); cJSON_AddStringToObject(error_response, "status", "error"); // Create enhanced error message with specific validation details char enhanced_error_message[1024]; if (strlen(first_validation_error) > 0 && strlen(first_error_field) > 0) { snprintf(enhanced_error_message, sizeof(enhanced_error_message), "field validation failed: %s - %s", first_error_field, first_validation_error); } else { snprintf(enhanced_error_message, sizeof(enhanced_error_message), "field validation failed: atomic rollback performed"); } cJSON_AddStringToObject(error_response, "error", enhanced_error_message); cJSON_AddNumberToObject(error_response, "validation_errors", validation_errors); cJSON_AddNumberToObject(error_response, "timestamp", (double)time(NULL)); cJSON_AddItemToObject(error_response, "data", processed_configs); // Get admin pubkey from event for error response cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey"); const char* admin_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : NULL; if (admin_pubkey) { // Send error response as signed kind 23457 event if (send_admin_response_event(error_response, admin_pubkey, wsi) == 0) { log_info("Config update validation error response sent successfully"); cJSON_Delete(error_response); return 0; // Return success after sending error response } } cJSON_Delete(error_response); snprintf(error_message, error_size, "validation failed: %d errors, atomic rollback performed", validation_errors); return -1; } else { // No updates applied sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL); // Build error response for no valid updates cJSON* error_response = cJSON_CreateObject(); cJSON_AddStringToObject(error_response, "query_type", "config_update"); cJSON_AddStringToObject(error_response, "status", "error"); cJSON_AddStringToObject(error_response, "error", "no valid configuration updates found"); cJSON_AddNumberToObject(error_response, "timestamp", (double)time(NULL)); cJSON_AddItemToObject(error_response, "data", processed_configs); // Get admin pubkey from event for error response cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey"); const char* admin_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : NULL; if (admin_pubkey) { // Send error response as signed kind 23457 event if (send_admin_response_event(error_response, admin_pubkey, wsi) == 0) { log_info("Config update 'no valid updates' error response sent successfully"); cJSON_Delete(error_response); return 0; // Return success after sending error response } } cJSON_Delete(error_response); snprintf(error_message, error_size, "no valid configuration updates found"); return -1; } // Build response with query_type for frontend routing cJSON* response = cJSON_CreateObject(); cJSON_AddStringToObject(response, "query_type", "config_update"); cJSON_AddStringToObject(response, "command", "config_update"); cJSON_AddNumberToObject(response, "configs_processed", updates_applied); cJSON_AddNumberToObject(response, "total_configs", config_count); cJSON_AddStringToObject(response, "status", "success"); cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL)); cJSON_AddItemToObject(response, "processed_configs", processed_configs); printf("Config update completed: %d/%d configs updated successfully\n", updates_applied, config_count); // Get admin pubkey from event for response cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey"); const char* admin_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : NULL; if (!admin_pubkey) { cJSON_Delete(response); snprintf(error_message, error_size, "missing admin pubkey for response"); return -1; } // Send response as signed kind 23457 event if (send_admin_response_event(response, admin_pubkey, wsi) == 0) { log_success("Config update command completed successfully with signed response"); printf(" Response query_type: config_update\n"); cJSON_Delete(response); return 0; } cJSON_Delete(response); snprintf(error_message, error_size, "failed to send config update response"); return -1; } // ================================ // CONFIGURATION CACHE MANAGEMENT // ================================ // Invalidate configuration cache void invalidate_config_cache(void) { pthread_mutex_lock(&g_unified_cache.cache_lock); g_unified_cache.cache_valid = 0; g_unified_cache.cache_expires = 0; pthread_mutex_unlock(&g_unified_cache.cache_lock); log_info("Unified configuration cache invalidated"); } // Reload configuration from table int reload_config_from_table(void) { // Trigger a cache refresh by calling the refresh function directly int result = refresh_unified_cache_from_table(); if (result == 0) { log_info("Configuration reloaded from table"); } else { log_error("Failed to reload configuration from table"); } return result; } // ================================ // HYBRID CONFIG ACCESS FUNCTIONS // ================================ // Hybrid config getter (tries table first, falls back to event) const char* get_config_value_hybrid(const char* key) { // Try table-based config first if available if (is_config_table_ready()) { const char* table_value = get_config_value_from_table(key); if (table_value) { return table_value; // Already dynamically allocated } } // Fall back to event-based config, but ensure it's dynamically allocated const char* fallback_value = get_config_value(key); if (fallback_value) { return strdup(fallback_value); // Make a copy since fallback might be static } return NULL; } // Check if config table is ready int is_config_table_ready(void) { if (!g_db) return 0; const char* sql = "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='config'"; sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { return 0; } int table_exists = 0; if (sqlite3_step(stmt) == SQLITE_ROW) { table_exists = sqlite3_column_int(stmt, 0) > 0; } sqlite3_finalize(stmt); if (!table_exists) { return 0; } // Check if table has configuration data const char* count_sql = "SELECT COUNT(*) FROM config"; rc = sqlite3_prepare_v2(g_db, count_sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { return 0; } int config_count = 0; if (sqlite3_step(stmt) == SQLITE_ROW) { config_count = sqlite3_column_int(stmt, 0); } sqlite3_finalize(stmt); return config_count > 0; } // Initialize configuration system with migration support int initialize_config_system_with_migration(void) { log_info("Initializing configuration system with migration support..."); // Initialize unified cache and migration status pthread_mutex_lock(&g_unified_cache.cache_lock); g_unified_cache.cache_valid = 0; g_unified_cache.cache_expires = 0; pthread_mutex_unlock(&g_unified_cache.cache_lock); memset(&g_migration_status, 0, sizeof(g_migration_status)); // For new installations, config table should already exist from embedded schema log_success("Configuration system initialized with table support"); return 0; } // ================================ // RETRY INITIAL CONFIG EVENT STORAGE // ================================ int retry_store_initial_config_event(void) { if (!g_pending_config_event) { // No pending event to store return 0; } log_info("Retrying storage of initial configuration event..."); // Try to process the cached configuration event through admin API if (process_startup_config_event_with_fallback(g_pending_config_event) == 0) { log_success("Initial configuration processed successfully through admin API on retry"); // Clean up the pending event cJSON_Delete(g_pending_config_event); g_pending_config_event = NULL; return 0; } else { log_error("Failed to process initial configuration through admin API on retry"); return -1; } } // ================================ // CONFIG MIGRATION FUNCTIONS // ================================ // Populate config table from a configuration event int populate_config_table_from_event(const cJSON* event) { if (!event || !g_db) { return -1; } log_info("Populating config table from configuration event..."); cJSON* tags = cJSON_GetObjectItem(event, "tags"); if (!tags || !cJSON_IsArray(tags)) { log_error("Configuration event missing tags array"); return -1; } int configs_populated = 0; // Process each tag as a configuration parameter cJSON* tag = NULL; cJSON_ArrayForEach(tag, tags) { if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) { continue; } cJSON* tag_name = cJSON_GetArrayItem(tag, 0); cJSON* tag_value = cJSON_GetArrayItem(tag, 1); if (!cJSON_IsString(tag_name) || !cJSON_IsString(tag_value)) { continue; } const char* key = cJSON_GetStringValue(tag_name); const char* value = cJSON_GetStringValue(tag_value); // Skip relay identifier tag if (strcmp(key, "d") == 0) { continue; } // Determine data type for the config value const char* data_type = "string"; if (strcmp(key, "relay_port") == 0 || strcmp(key, "max_connections") == 0 || strcmp(key, "pow_min_difficulty") == 0 || strcmp(key, "max_subscriptions_per_client") == 0 || strcmp(key, "max_total_subscriptions") == 0 || strcmp(key, "max_filters_per_subscription") == 0 || strcmp(key, "max_event_tags") == 0 || strcmp(key, "max_content_length") == 0 || strcmp(key, "max_message_length") == 0 || strcmp(key, "default_limit") == 0 || strcmp(key, "max_limit") == 0 || strcmp(key, "nip42_challenge_expiration") == 0 || strcmp(key, "nip40_expiration_grace_period") == 0) { data_type = "integer"; } else if (strcmp(key, "auth_enabled") == 0 || strcmp(key, "nip40_expiration_enabled") == 0 || strcmp(key, "nip40_expiration_strict") == 0 || strcmp(key, "nip40_expiration_filter") == 0 || strcmp(key, "nip42_auth_required") == 0) { data_type = "boolean"; } // Set category const char* category = "general"; if (strstr(key, "relay_")) { category = "relay"; } else if (strstr(key, "nip40_")) { category = "expiration"; } else if (strstr(key, "nip42_") || strstr(key, "auth_")) { category = "authentication"; } else if (strstr(key, "pow_")) { category = "proof_of_work"; } else if (strstr(key, "max_")) { category = "limits"; } // Determine if requires restart (0 = dynamic, 1 = restart required) int requires_restart = 0; // Restart required configs if (strcmp(key, "relay_port") == 0 || strcmp(key, "max_connections") == 0 || strcmp(key, "auth_enabled") == 0 || strcmp(key, "nip42_auth_required") == 0 || strcmp(key, "nip42_auth_required_kinds") == 0 || strcmp(key, "nip42_challenge_timeout") == 0 || strcmp(key, "database_path") == 0) { requires_restart = 1; } // Insert into config table if (set_config_value_in_table(key, value, data_type, NULL, category, requires_restart) == 0) { configs_populated++; } else { char error_msg[256]; snprintf(error_msg, sizeof(error_msg), "Failed to populate config: %s = %s", key, value); log_error(error_msg); } } if (configs_populated > 0) { char success_msg[256]; snprintf(success_msg, sizeof(success_msg), "Populated %d configuration values from event", configs_populated); log_success(success_msg); return 0; } else { log_error("No configuration values were populated from event"); return -1; } } // Migrate configuration from existing events to config table int migrate_config_from_events_to_table(void) { if (!g_db) { log_error("Database not available for configuration migration"); return -1; } log_info("Migrating configuration from events to config table..."); // Load the most recent configuration event from database const char* relay_pubkey = get_relay_pubkey_cached(); cJSON* config_event = load_config_event_from_database(relay_pubkey); if (!config_event) { log_info("No existing configuration event found - migration not needed"); return 0; } // Populate config table from the event int result = populate_config_table_from_event(config_event); // Clean up cJSON_Delete(config_event); if (result == 0) { log_success("Configuration migration from events to table completed successfully"); } else { log_error("Configuration migration from events to table failed"); } return result; } // ================================ // STARTUP CONFIGURATION PROCESSING // ================================ // Process startup configuration event - bypasses auth and updates config table int process_startup_config_event(const cJSON* event) { if (!event || !g_db) { log_error("Invalid parameters for startup config processing"); return -1; } log_info("Processing startup configuration event through admin API..."); // Validate event structure first cJSON* kind_obj = cJSON_GetObjectItem(event, "kind"); if (!kind_obj || cJSON_GetNumberValue(kind_obj) != 33334) { log_error("Invalid event kind for startup configuration"); return -1; } cJSON* tags_obj = cJSON_GetObjectItem(event, "tags"); if (!tags_obj || !cJSON_IsArray(tags_obj)) { log_error("Startup configuration event missing tags"); return -1; } // Begin transaction for atomic config updates int rc = sqlite3_exec(g_db, "BEGIN IMMEDIATE TRANSACTION", NULL, NULL, NULL); if (rc != SQLITE_OK) { log_error("Failed to begin startup config transaction"); return -1; } int updates_applied = 0; // Process each tag as a configuration parameter (same logic as process_admin_config_event) cJSON* tag = NULL; cJSON_ArrayForEach(tag, tags_obj) { if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) { continue; } cJSON* tag_name = cJSON_GetArrayItem(tag, 0); cJSON* tag_value = cJSON_GetArrayItem(tag, 1); if (!cJSON_IsString(tag_name) || !cJSON_IsString(tag_value)) { continue; } const char* key = cJSON_GetStringValue(tag_name); const char* value = cJSON_GetStringValue(tag_value); // Skip relay identifier tag and relay_pubkey (already in table) if (strcmp(key, "d") == 0 || strcmp(key, "relay_pubkey") == 0) { continue; } // Update configuration in table if (update_config_in_table(key, value) == 0) { updates_applied++; } } if (updates_applied > 0) { sqlite3_exec(g_db, "COMMIT", NULL, NULL, NULL); invalidate_config_cache(); char success_msg[256]; snprintf(success_msg, sizeof(success_msg), "Processed startup configuration: %d values updated in config table", updates_applied); log_success(success_msg); return 0; } else { sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL); log_error("No valid configuration parameters found in startup event"); return -1; } } // Process startup configuration event with fallback - for retry scenarios int process_startup_config_event_with_fallback(const cJSON* event) { if (!event) { log_error("Invalid event for startup config processing with fallback"); return -1; } // Try to process through admin API first if (process_startup_config_event(event) == 0) { log_success("Startup configuration processed successfully through admin API"); return 0; } // If that fails, populate defaults and try again log_warning("Startup config processing failed - ensuring defaults are populated"); if (populate_default_config_values() != 0) { log_error("Failed to populate default config values"); return -1; } // Retry processing if (process_startup_config_event(event) == 0) { log_success("Startup configuration processed successfully after populating defaults"); return 0; } log_error("Startup configuration processing failed even after populating defaults"); return -1; } // ================================ // DYNAMIC EVENT GENERATION FROM CONFIG TABLE // ================================ // Generate synthetic configuration event from current config table data cJSON* generate_config_event_from_table(void) { if (!g_db) { log_error("Database not available for config event generation"); return NULL; } log_info("Generating synthetic configuration event from config table..."); // Get relay pubkey for event generation const char* relay_pubkey = get_config_value("relay_pubkey"); if (!relay_pubkey || strlen(relay_pubkey) != 64) { // Try to get from unified cache relay_pubkey = get_relay_pubkey_cached(); if (!relay_pubkey || strlen(relay_pubkey) != 64) { log_error("Relay pubkey not available for config event generation"); return NULL; } } // Create the event structure cJSON* event = cJSON_CreateObject(); if (!event) { log_error("Failed to create config event object"); return NULL; } // Set basic event fields - we'll generate a synthetic event cJSON_AddStringToObject(event, "id", "synthetic_config_event_id"); cJSON_AddStringToObject(event, "pubkey", relay_pubkey); // Use relay pubkey as event author cJSON_AddNumberToObject(event, "created_at", (double)time(NULL)); cJSON_AddNumberToObject(event, "kind", 33334); cJSON_AddStringToObject(event, "content", "C Nostr Relay Configuration"); cJSON_AddStringToObject(event, "sig", "synthetic_signature"); // Create tags array from config table cJSON* tags = cJSON_CreateArray(); if (!tags) { log_error("Failed to create tags array for config event"); cJSON_Delete(event); return NULL; } // Add d tag with relay pubkey (addressable event identifier) cJSON* d_tag = cJSON_CreateArray(); cJSON_AddItemToArray(d_tag, cJSON_CreateString("d")); cJSON_AddItemToArray(d_tag, cJSON_CreateString(relay_pubkey)); cJSON_AddItemToArray(tags, d_tag); // Query all configuration values from the config table const char* sql = "SELECT key, value FROM config ORDER BY key"; sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { log_error("Failed to prepare config query for event generation"); cJSON_Delete(tags); cJSON_Delete(event); return NULL; } int config_items_added = 0; // Add each config item as a tag while (sqlite3_step(stmt) == SQLITE_ROW) { const char* key = (const char*)sqlite3_column_text(stmt, 0); const char* value = (const char*)sqlite3_column_text(stmt, 1); if (key && value) { cJSON* config_tag = cJSON_CreateArray(); cJSON_AddItemToArray(config_tag, cJSON_CreateString(key)); cJSON_AddItemToArray(config_tag, cJSON_CreateString(value)); cJSON_AddItemToArray(tags, config_tag); config_items_added++; } } sqlite3_finalize(stmt); if (config_items_added == 0) { log_warning("No configuration items found in config table for event generation"); cJSON_Delete(tags); cJSON_Delete(event); return NULL; } // Add tags to event cJSON_AddItemToObject(event, "tags", tags); char success_msg[256]; snprintf(success_msg, sizeof(success_msg), "Generated synthetic configuration event with %d configuration items", config_items_added); log_success(success_msg); return event; } // Check if a REQ filter requests configuration events int req_filter_requests_config_events(const cJSON* filter) { if (!filter || !cJSON_IsObject(filter)) { return 0; } cJSON* kinds = cJSON_GetObjectItem(filter, "kinds"); if (!kinds || !cJSON_IsArray(kinds)) { return 0; } // Check if kinds array contains configuration event kinds cJSON* kind_item = NULL; cJSON_ArrayForEach(kind_item, kinds) { int kind_val = (int)cJSON_GetNumberValue(kind_item); if (cJSON_IsNumber(kind_item) && kind_val == 33334) { return 1; } } return 0; } // Generate synthetic config event data for subscription (callback approach) cJSON* generate_synthetic_config_event_for_subscription(const char* sub_id, const cJSON* filters) { if (!sub_id || !filters) { return NULL; } // Check if any filter requests configuration events int requests_config = 0; if (cJSON_IsArray(filters)) { cJSON* filter = NULL; cJSON_ArrayForEach(filter, filters) { if (req_filter_requests_config_events(filter)) { requests_config = 1; break; } } } else if (cJSON_IsObject(filters)) { requests_config = req_filter_requests_config_events(filters); } if (!requests_config) { // No config events requested return NULL; } log_info("Generating synthetic configuration event for subscription"); // Generate synthetic config event from table cJSON* config_event = generate_config_event_from_table(); if (!config_event) { log_error("Failed to generate synthetic config event"); return NULL; } // Create EVENT message for the subscription cJSON* event_msg = cJSON_CreateArray(); cJSON_AddItemToArray(event_msg, cJSON_CreateString("EVENT")); cJSON_AddItemToArray(event_msg, cJSON_CreateString(sub_id)); cJSON_AddItemToArray(event_msg, config_event); log_success("Generated synthetic configuration event message"); return event_msg; } /** * Generate a synthetic configuration event from config table data * This allows WebSocket clients to fetch configuration via REQ messages * Returns JSON string that must be freed by caller */ char* generate_config_event_json(void) { // Use the existing cJSON function and convert to string cJSON* event = generate_config_event_from_table(); if (!event) { return NULL; } // Convert to JSON string char* json_string = cJSON_Print(event); cJSON_Delete(event); return json_string; }