2851 lines
98 KiB
C
2851 lines
98 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>
|
|
|
|
// 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);
|
|
int process_admin_auth_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);
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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");
|
|
}
|
|
|
|
// 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 (kind 33334 is replaceable)
|
|
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, "addressable", -1, SQLITE_STATIC); // kind 33334 is addressable
|
|
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;
|
|
}
|
|
|
|
const char* sql;
|
|
sqlite3_stmt* stmt;
|
|
int rc;
|
|
|
|
// Try to get admin pubkey from cache, otherwise find the most recent kind 33334 event
|
|
const char* admin_pubkey = get_admin_pubkey_cached();
|
|
if (admin_pubkey && strlen(admin_pubkey) > 0) {
|
|
sql = "SELECT id, pubkey, created_at, kind, content, sig, tags FROM events WHERE kind = 33334 AND pubkey = ? ORDER BY created_at DESC LIMIT 1";
|
|
rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
|
if (rc != SQLITE_OK) {
|
|
log_error("Failed to prepare configuration event query");
|
|
return NULL;
|
|
}
|
|
sqlite3_bind_text(stmt, 1, admin_pubkey, -1, SQLITE_STATIC);
|
|
} else {
|
|
// During existing relay startup, we don't know the admin pubkey yet
|
|
// Look for any kind 33334 configuration event (should only be one per relay)
|
|
sql = "SELECT id, pubkey, created_at, kind, content, sig, tags FROM events WHERE kind = 33334 ORDER BY created_at DESC LIMIT 1";
|
|
rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
|
if (rc != SQLITE_OK) {
|
|
log_error("Failed to prepare configuration event query");
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
cJSON* event = NULL;
|
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
// Reconstruct the event JSON from database columns
|
|
event = cJSON_CreateObject();
|
|
if (event) {
|
|
const char* event_pubkey = (const char*)sqlite3_column_text(stmt, 1);
|
|
|
|
cJSON_AddStringToObject(event, "id", (const char*)sqlite3_column_text(stmt, 0));
|
|
cJSON_AddStringToObject(event, "pubkey", event_pubkey);
|
|
cJSON_AddNumberToObject(event, "created_at", sqlite3_column_int64(stmt, 2));
|
|
cJSON_AddNumberToObject(event, "kind", sqlite3_column_int(stmt, 3));
|
|
cJSON_AddStringToObject(event, "content", (const char*)sqlite3_column_text(stmt, 4));
|
|
cJSON_AddStringToObject(event, "sig", (const char*)sqlite3_column_text(stmt, 5));
|
|
|
|
// If we didn't have admin pubkey in cache, we should update the cache
|
|
// Note: This will be handled by the cache refresh mechanism automatically
|
|
|
|
// Parse tags JSON
|
|
const char* tags_str = (const char*)sqlite3_column_text(stmt, 6);
|
|
if (tags_str) {
|
|
cJSON* tags = cJSON_Parse(tags_str);
|
|
if (tags) {
|
|
cJSON_AddItemToObject(event, "tags", tags);
|
|
} else {
|
|
cJSON_AddItemToObject(event, "tags", cJSON_CreateArray());
|
|
}
|
|
} else {
|
|
cJSON_AddItemToObject(event, "tags", cJSON_CreateArray());
|
|
}
|
|
}
|
|
}
|
|
|
|
sqlite3_finalize(stmt);
|
|
return event;
|
|
}
|
|
|
|
// ================================
|
|
// 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) {
|
|
return get_admin_pubkey_cached();
|
|
}
|
|
if (strcmp(key, "relay_pubkey") == 0) {
|
|
return get_relay_pubkey_cached();
|
|
}
|
|
|
|
// 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';
|
|
pthread_mutex_unlock(&g_unified_cache.cache_lock);
|
|
return g_unified_cache.temp_buffer;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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') {
|
|
return default_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;
|
|
}
|
|
|
|
if (strcasecmp(str_value, "true") == 0 ||
|
|
strcasecmp(str_value, "yes") == 0 ||
|
|
strcasecmp(str_value, "1") == 0) {
|
|
return 1;
|
|
} else if (strcasecmp(str_value, "false") == 0 ||
|
|
strcasecmp(str_value, "no") == 0 ||
|
|
strcasecmp(str_value, "0") == 0) {
|
|
return 0;
|
|
}
|
|
|
|
return default_value;
|
|
}
|
|
|
|
// ================================
|
|
// 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];
|
|
|
|
if (cli_options && strlen(cli_options->admin_privkey_override) == 64) {
|
|
// Use provided admin private key
|
|
log_info("Using provided admin private key override");
|
|
strncpy(admin_privkey, cli_options->admin_privkey_override, sizeof(admin_privkey) - 1);
|
|
admin_privkey[sizeof(admin_privkey) - 1] = '\0';
|
|
|
|
// Convert hex string to bytes
|
|
if (nostr_hex_to_bytes(admin_privkey, admin_privkey_bytes, 32) != NOSTR_SUCCESS) {
|
|
log_error("Failed to convert admin private key hex to bytes");
|
|
return -1;
|
|
}
|
|
|
|
// Validate the private key
|
|
if (nostr_ec_private_key_verify(admin_privkey_bytes) != NOSTR_SUCCESS) {
|
|
log_error("Provided admin private key is invalid");
|
|
return -1;
|
|
}
|
|
} else {
|
|
// Generate random admin keypair using /dev/urandom + nostr_core_lib
|
|
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);
|
|
}
|
|
|
|
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);
|
|
|
|
// 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. Create initial configuration event using defaults (without private key)
|
|
cJSON* config_event = create_default_config_event(admin_privkey_bytes, relay_privkey, relay_pubkey, cli_options);
|
|
if (!config_event) {
|
|
log_error("Failed to create default configuration event");
|
|
return -1;
|
|
}
|
|
|
|
// 7. Process configuration through admin API instead of storing in events table
|
|
if (process_startup_config_event_with_fallback(config_event) == 0) {
|
|
log_success("Initial configuration processed successfully through admin API");
|
|
} else {
|
|
log_warning("Failed to process initial configuration - will retry after database init");
|
|
// Cache the event for later processing
|
|
if (g_pending_config_event) {
|
|
cJSON_Delete(g_pending_config_event);
|
|
}
|
|
g_pending_config_event = cJSON_Duplicate(config_event, 1);
|
|
}
|
|
|
|
// 8. Cache the current config
|
|
if (g_current_config) {
|
|
cJSON_Delete(g_current_config);
|
|
}
|
|
g_current_config = cJSON_Duplicate(config_event, 1);
|
|
|
|
// 9. Clean up
|
|
cJSON_Delete(config_event);
|
|
|
|
// 10. Print admin private key for user to save
|
|
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");
|
|
|
|
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);
|
|
|
|
// 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 kind 33334 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 kind 33334 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) {
|
|
// Use unified cache buffer with thread safety
|
|
pthread_mutex_lock(&g_unified_cache.cache_lock);
|
|
strncpy(g_unified_cache.temp_buffer, value, sizeof(g_unified_cache.temp_buffer) - 1);
|
|
g_unified_cache.temp_buffer[sizeof(g_unified_cache.temp_buffer) - 1] = '\0';
|
|
result = g_unified_cache.temp_buffer;
|
|
pthread_mutex_unlock(&g_unified_cache.cache_lock);
|
|
}
|
|
}
|
|
|
|
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
|
|
int requires_restart = 0;
|
|
if (strcmp(key, "relay_port") == 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");
|
|
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
|
|
// ================================
|
|
|
|
// Process admin events (moved from main.c)
|
|
int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size) {
|
|
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
|
|
if (!kind_obj || !cJSON_IsNumber(kind_obj)) {
|
|
snprintf(error_message, error_size, "invalid: missing or invalid kind");
|
|
return -1;
|
|
}
|
|
|
|
// Verify admin authorization
|
|
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
|
|
if (!pubkey_obj || !cJSON_IsString(pubkey_obj)) {
|
|
snprintf(error_message, error_size, "invalid: missing pubkey");
|
|
return -1;
|
|
}
|
|
|
|
const char* event_pubkey = cJSON_GetStringValue(pubkey_obj);
|
|
const char* admin_pubkey = get_config_value("admin_pubkey");
|
|
|
|
if (!admin_pubkey || strcmp(event_pubkey, admin_pubkey) != 0) {
|
|
snprintf(error_message, error_size, "auth-required: not authorized admin");
|
|
return -1;
|
|
}
|
|
|
|
int kind = (int)cJSON_GetNumberValue(kind_obj);
|
|
|
|
switch (kind) {
|
|
case 33334:
|
|
return process_admin_config_event(event, error_message, error_size);
|
|
case 33335:
|
|
return process_admin_auth_event(event, error_message, error_size);
|
|
default:
|
|
snprintf(error_message, error_size, "invalid: unsupported admin event kind");
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
// Handle kind 33334 config events
|
|
int process_admin_config_event(cJSON* event, char* error_message, size_t error_size) {
|
|
cJSON* tags_obj = cJSON_GetObjectItem(event, "tags");
|
|
if (!tags_obj || !cJSON_IsArray(tags_obj)) {
|
|
snprintf(error_message, error_size, "invalid: configuration event must have tags");
|
|
return -1;
|
|
}
|
|
|
|
// Config table should already exist from embedded schema
|
|
|
|
// 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* 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
|
|
if (strcmp(key, "d") == 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 33335 auth rule events
|
|
int process_admin_auth_event(cJSON* event, char* error_message, size_t error_size) {
|
|
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;
|
|
}
|
|
|
|
// Extract action from content or tags
|
|
cJSON* content_obj = cJSON_GetObjectItem(event, "content");
|
|
const char* content = content_obj ? cJSON_GetStringValue(content_obj) : "";
|
|
|
|
// Parse the action from content (should be "add" or "remove")
|
|
cJSON* content_json = cJSON_Parse(content);
|
|
const char* action = "add"; // default
|
|
if (content_json) {
|
|
cJSON* action_obj = cJSON_GetObjectItem(content_json, "action");
|
|
if (action_obj && cJSON_IsString(action_obj)) {
|
|
action = cJSON_GetStringValue(action_obj);
|
|
}
|
|
cJSON_Delete(content_json);
|
|
}
|
|
|
|
// 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;
|
|
|
|
// Process each tag as an auth rule specification
|
|
cJSON* tag = NULL;
|
|
cJSON_ArrayForEach(tag, tags_obj) {
|
|
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 3) {
|
|
continue;
|
|
}
|
|
|
|
cJSON* rule_type_obj = cJSON_GetArrayItem(tag, 0);
|
|
cJSON* pattern_type_obj = cJSON_GetArrayItem(tag, 1);
|
|
cJSON* pattern_value_obj = cJSON_GetArrayItem(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);
|
|
|
|
// Process the auth rule based on action
|
|
if (strcmp(action, "add") == 0) {
|
|
if (add_auth_rule_from_config(rule_type, pattern_type, pattern_value, "allow") == 0) {
|
|
rules_processed++;
|
|
}
|
|
} else if (strcmp(action, "remove") == 0) {
|
|
if (remove_auth_rule_from_config(rule_type, pattern_type, pattern_value) == 0) {
|
|
rules_processed++;
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
} else {
|
|
sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL);
|
|
snprintf(error_message, error_size, "no valid auth rules found");
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// ================================
|
|
// 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;
|
|
}
|
|
|
|
// ================================
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
// Fall back to event-based config
|
|
return get_config_value(key);
|
|
}
|
|
|
|
// 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
|
|
int requires_restart = 0;
|
|
if (strcmp(key, "relay_port") == 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 kind 33334 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 kind 33334 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 kind 33334 event with %d configuration items", config_items_added);
|
|
log_success(success_msg);
|
|
|
|
return event;
|
|
}
|
|
|
|
// Check if a REQ filter requests kind 33334 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 33334
|
|
cJSON* kind_item = NULL;
|
|
cJSON_ArrayForEach(kind_item, kinds) {
|
|
if (cJSON_IsNumber(kind_item) && (int)cJSON_GetNumberValue(kind_item) == 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 kind 33334
|
|
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 kind 33334 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 kind 33334 configuration event message");
|
|
return event_msg;
|
|
}
|
|
|
|
/**
|
|
* Generate a synthetic kind 33334 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;
|
|
} |