Files
c-relay/src/config.c
2025-10-01 17:23:50 -04:00

4432 lines
167 KiB
C

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