- Add cache refresh mechanism for config updates - Implement selective re-initialization for NIP-11 relay info changes - Categorize configs as dynamic vs restart-required using requires_restart field - Enhance admin API responses with restart requirement information - Add comprehensive test for dynamic config updates - Update documentation for dynamic configuration capabilities Most relay settings can now be updated via admin API without requiring restart, improving operational flexibility while maintaining stability for critical changes.
4664 lines
181 KiB
C
4664 lines
181 KiB
C
#define _GNU_SOURCE
|
|
#include "config.h"
|
|
#include "default_config_event.h"
|
|
#include "../nostr_core_lib/nostr_core/nostr_core.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <strings.h>
|
|
#include <unistd.h>
|
|
#include <dirent.h>
|
|
#include <sys/stat.h>
|
|
#include <errno.h>
|
|
#include <libwebsockets.h>
|
|
|
|
// 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;
|
|
} |