From 80b15e16e2d0793ee72ee92336bca874ca2e43b7 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 2 Oct 2025 15:53:26 -0400 Subject: [PATCH] v0.4.3 - feat: Implement dynamic configuration updates without restart - Add cache refresh mechanism for config updates - Implement selective re-initialization for NIP-11 relay info changes - Categorize configs as dynamic vs restart-required using requires_restart field - Enhance admin API responses with restart requirement information - Add comprehensive test for dynamic config updates - Update documentation for dynamic configuration capabilities Most relay settings can now be updated via admin API without requiring restart, improving operational flexibility while maintaining stability for critical changes. --- README.md | 18 ++++ relay.pid | 2 +- src/config.c | 197 +++++++++++++++++++++++++++++++++++------ src/main.h | 4 +- test_dynamic_config.sh | 133 ++++++++++++++++++++++++++++ 5 files changed, 325 insertions(+), 29 deletions(-) create mode 100755 test_dynamic_config.sh diff --git a/README.md b/README.md index f2b04ab..cd7e794 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,24 @@ All commands are sent as NIP-44 encrypted JSON arrays in the event content. The - `pow_min_difficulty`: Minimum proof-of-work difficulty - `nip40_expiration_enabled`: Enable event expiration (`true`/`false`) +### Dynamic Configuration Updates + +C-Relay supports **dynamic configuration updates** without requiring a restart for most settings. Configuration parameters are categorized as either **dynamic** (can be updated immediately) or **restart-required** (require relay restart to take effect). + +**Dynamic Configuration Parameters (No Restart Required):** +- All relay information (NIP-11) settings: `relay_name`, `relay_description`, `relay_contact`, `relay_software`, `relay_version`, `supported_nips`, `language_tags`, `relay_countries`, `posting_policy`, `payments_url` +- Authentication settings: `auth_enabled`, `nip42_auth_required`, `nip42_auth_required_kinds`, `nip42_challenge_timeout` +- Subscription limits: `max_subscriptions_per_client`, `max_total_subscriptions` +- Event validation limits: `max_event_tags`, `max_content_length`, `max_message_length` +- Proof of Work settings: `pow_min_difficulty`, `pow_mode` +- Event expiration settings: `nip40_expiration_enabled`, `nip40_expiration_strict`, `nip40_expiration_filter`, `nip40_expiration_grace_period` + +**Restart-Required Configuration Parameters:** +- Connection settings: `max_connections`, `relay_port` +- Database and core system settings + +When updating configuration, the admin API response will indicate whether a restart is required for each parameter. Dynamic updates take effect immediately and are reflected in NIP-11 relay information documents without restart. + ### Response Format All admin commands return **signed EVENT responses** via WebSocket following standard Nostr protocol. Responses use JSON content with structured data. diff --git a/relay.pid b/relay.pid index 599b812..d38a191 100644 --- a/relay.pid +++ b/relay.pid @@ -1 +1 @@ -2263673 +2356846 diff --git a/src/config.c b/src/config.c index 55a09c9..6cb13dd 100644 --- a/src/config.c +++ b/src/config.c @@ -127,40 +127,119 @@ void force_config_cache_refresh(void) { log_info("Configuration cache forcibly invalidated"); } +// Update specific cache value without full refresh +int update_cache_value(const char* key, const char* value) { + if (!key || !value) { + return -1; + } + + pthread_mutex_lock(&g_unified_cache.cache_lock); + + // Update specific cache fields + if (strcmp(key, "admin_pubkey") == 0) { + strncpy(g_unified_cache.admin_pubkey, value, sizeof(g_unified_cache.admin_pubkey) - 1); + g_unified_cache.admin_pubkey[sizeof(g_unified_cache.admin_pubkey) - 1] = '\0'; + } else if (strcmp(key, "relay_pubkey") == 0) { + strncpy(g_unified_cache.relay_pubkey, value, sizeof(g_unified_cache.relay_pubkey) - 1); + g_unified_cache.relay_pubkey[sizeof(g_unified_cache.relay_pubkey) - 1] = '\0'; + } else if (strcmp(key, "auth_required") == 0) { + g_unified_cache.auth_required = (strcmp(value, "true") == 0) ? 1 : 0; + } else if (strcmp(key, "admin_enabled") == 0) { + g_unified_cache.admin_enabled = (strcmp(value, "true") == 0) ? 1 : 0; + } else if (strcmp(key, "max_file_size") == 0) { + g_unified_cache.max_file_size = atol(value); + } else if (strcmp(key, "nip42_mode") == 0) { + if (strcmp(value, "disabled") == 0) { + g_unified_cache.nip42_mode = 0; + } else if (strcmp(value, "required") == 0) { + g_unified_cache.nip42_mode = 2; + } else { + g_unified_cache.nip42_mode = 1; // Optional/enabled + } + } else if (strcmp(key, "nip42_challenge_timeout") == 0) { + g_unified_cache.nip42_challenge_timeout = atoi(value); + } else if (strcmp(key, "nip42_time_tolerance") == 0) { + g_unified_cache.nip42_time_tolerance = atoi(value); + } else { + // For NIP-11 relay info fields, update the cache buffers + if (strcmp(key, "relay_name") == 0) { + strncpy(g_unified_cache.relay_info.name, value, sizeof(g_unified_cache.relay_info.name) - 1); + g_unified_cache.relay_info.name[sizeof(g_unified_cache.relay_info.name) - 1] = '\0'; + } else if (strcmp(key, "relay_description") == 0) { + strncpy(g_unified_cache.relay_info.description, value, sizeof(g_unified_cache.relay_info.description) - 1); + g_unified_cache.relay_info.description[sizeof(g_unified_cache.relay_info.description) - 1] = '\0'; + } else if (strcmp(key, "relay_contact") == 0) { + strncpy(g_unified_cache.relay_info.contact, value, sizeof(g_unified_cache.relay_info.contact) - 1); + g_unified_cache.relay_info.contact[sizeof(g_unified_cache.relay_info.contact) - 1] = '\0'; + } else if (strcmp(key, "relay_software") == 0) { + strncpy(g_unified_cache.relay_info.software, value, sizeof(g_unified_cache.relay_info.software) - 1); + g_unified_cache.relay_info.software[sizeof(g_unified_cache.relay_info.software) - 1] = '\0'; + } else if (strcmp(key, "relay_version") == 0) { + strncpy(g_unified_cache.relay_info.version, value, sizeof(g_unified_cache.relay_info.version) - 1); + g_unified_cache.relay_info.version[sizeof(g_unified_cache.relay_info.version) - 1] = '\0'; + } else if (strcmp(key, "supported_nips") == 0) { + strncpy(g_unified_cache.relay_info.supported_nips_str, value, sizeof(g_unified_cache.relay_info.supported_nips_str) - 1); + g_unified_cache.relay_info.supported_nips_str[sizeof(g_unified_cache.relay_info.supported_nips_str) - 1] = '\0'; + } else if (strcmp(key, "language_tags") == 0) { + strncpy(g_unified_cache.relay_info.language_tags_str, value, sizeof(g_unified_cache.relay_info.language_tags_str) - 1); + g_unified_cache.relay_info.language_tags_str[sizeof(g_unified_cache.relay_info.language_tags_str) - 1] = '\0'; + } else if (strcmp(key, "relay_countries") == 0) { + strncpy(g_unified_cache.relay_info.relay_countries_str, value, sizeof(g_unified_cache.relay_info.relay_countries_str) - 1); + g_unified_cache.relay_info.relay_countries_str[sizeof(g_unified_cache.relay_info.relay_countries_str) - 1] = '\0'; + } else if (strcmp(key, "posting_policy") == 0) { + strncpy(g_unified_cache.relay_info.posting_policy, value, sizeof(g_unified_cache.relay_info.posting_policy) - 1); + g_unified_cache.relay_info.posting_policy[sizeof(g_unified_cache.relay_info.posting_policy) - 1] = '\0'; + } else if (strcmp(key, "payments_url") == 0) { + strncpy(g_unified_cache.relay_info.payments_url, value, sizeof(g_unified_cache.relay_info.payments_url) - 1); + g_unified_cache.relay_info.payments_url[sizeof(g_unified_cache.relay_info.payments_url) - 1] = '\0'; + } + } + + // Reset cache expiration to extend validity + int cache_timeout = get_cache_timeout(); + g_unified_cache.cache_expires = time(NULL) + cache_timeout; + + pthread_mutex_unlock(&g_unified_cache.cache_lock); + + log_info("Updated specific cache value"); + printf(" Key: %s\n", key); + return 0; +} + // Refresh unified cache from database static int refresh_unified_cache_from_table(void) { if (!g_db) { log_error("Database not available for cache refresh"); return -1; } - + // Clear cache memset(&g_unified_cache, 0, sizeof(g_unified_cache)); g_unified_cache.cache_lock = (pthread_mutex_t)PTHREAD_MUTEX_INITIALIZER; - + // Load critical config values from table const char* admin_pubkey = get_config_value_from_table("admin_pubkey"); if (admin_pubkey) { strncpy(g_unified_cache.admin_pubkey, admin_pubkey, sizeof(g_unified_cache.admin_pubkey) - 1); g_unified_cache.admin_pubkey[sizeof(g_unified_cache.admin_pubkey) - 1] = '\0'; } - + const char* relay_pubkey = get_config_value_from_table("relay_pubkey"); if (relay_pubkey) { strncpy(g_unified_cache.relay_pubkey, relay_pubkey, sizeof(g_unified_cache.relay_pubkey) - 1); g_unified_cache.relay_pubkey[sizeof(g_unified_cache.relay_pubkey) - 1] = '\0'; } - + // Load auth-related config const char* auth_required = get_config_value_from_table("auth_required"); g_unified_cache.auth_required = (auth_required && strcmp(auth_required, "true") == 0) ? 1 : 0; - + const char* admin_enabled = get_config_value_from_table("admin_enabled"); g_unified_cache.admin_enabled = (admin_enabled && strcmp(admin_enabled, "true") == 0) ? 1 : 0; - + const char* max_file_size = get_config_value_from_table("max_file_size"); g_unified_cache.max_file_size = max_file_size ? atol(max_file_size) : 104857600; // 100MB default - + const char* nip42_mode = get_config_value_from_table("nip42_mode"); if (nip42_mode) { if (strcmp(nip42_mode, "disabled") == 0) { @@ -173,18 +252,18 @@ static int refresh_unified_cache_from_table(void) { } 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; } @@ -1980,12 +2059,12 @@ int update_config_in_table(const char* key, const char* value) { // 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 || @@ -2009,7 +2088,7 @@ int populate_default_config_values(void) { strcmp(key, "nip42_auth_required") == 0) { data_type = "boolean"; } - + // Set category const char* category = "general"; if (strstr(key, "relay_")) { @@ -2023,21 +2102,29 @@ int populate_default_config_values(void) { } else if (strstr(key, "max_")) { category = "limits"; } - - // Determine if requires restart + + // Determine if requires restart (0 = dynamic, 1 = restart required) int requires_restart = 0; - if (strcmp(key, "relay_port") == 0) { + + // Restart required configs + if (strcmp(key, "relay_port") == 0 || + strcmp(key, "max_connections") == 0 || + strcmp(key, "auth_enabled") == 0 || + strcmp(key, "nip42_auth_required") == 0 || + strcmp(key, "nip42_auth_required_kinds") == 0 || + strcmp(key, "nip42_challenge_timeout") == 0 || + strcmp(key, "database_path") == 0) { requires_restart = 1; } - + if (set_config_value_in_table(key, value, data_type, NULL, category, requires_restart) != 0) { char error_msg[256]; snprintf(error_msg, sizeof(error_msg), "Failed to set default config: %s = %s", key, value); log_error(error_msg); } } - - log_success("Default configuration values populated"); + + log_success("Default configuration values populated with restart requirements"); return 0; } @@ -3797,10 +3884,59 @@ int handle_config_update_unified(cJSON* event, char* error_message, size_t error continue; } + // Check if this config requires restart + const char* requires_restart_sql = "SELECT requires_restart FROM config WHERE key = ?"; + sqlite3_stmt* restart_stmt; + int requires_restart = 0; + + if (sqlite3_prepare_v2(g_db, requires_restart_sql, -1, &restart_stmt, NULL) == SQLITE_OK) { + sqlite3_bind_text(restart_stmt, 1, key, -1, SQLITE_STATIC); + if (sqlite3_step(restart_stmt) == SQLITE_ROW) { + requires_restart = sqlite3_column_int(restart_stmt, 0); + } + sqlite3_finalize(restart_stmt); + } + // Update the configuration value in the table if (update_config_in_table(key, value) == 0) { updates_applied++; - + + // For dynamic configs (requires_restart = 0), refresh cache immediately + if (requires_restart == 0) { + log_info("Dynamic config updated - refreshing cache"); + refresh_unified_cache_from_table(); + + // Apply selective re-initialization for specific dynamic configs + log_info("Applying selective re-initialization for dynamic config changes"); + if (strcmp(key, "max_subscriptions_per_client") == 0 || + strcmp(key, "max_total_subscriptions") == 0) { + log_info("Subscription limits changed - updating subscription manager"); + update_subscription_manager_config(); + // Also refresh NIP-11 relay info since max_subscriptions_per_client affects limitation field + log_info("Subscription limits changed - reinitializing relay info for NIP-11"); + init_relay_info(); + } else if (strcmp(key, "pow_min_difficulty") == 0 || + strcmp(key, "pow_mode") == 0) { + log_info("PoW configuration changed - reinitializing PoW system"); + init_pow_config(); + } else if (strcmp(key, "nip40_expiration_enabled") == 0 || + strcmp(key, "nip40_expiration_strict") == 0 || + strcmp(key, "nip40_expiration_filter") == 0 || + strcmp(key, "nip40_expiration_grace_period") == 0) { + log_info("Expiration configuration changed - reinitializing expiration system"); + init_expiration_config(); + } else if (strcmp(key, "relay_description") == 0 || + strcmp(key, "relay_contact") == 0 || + strcmp(key, "relay_software") == 0 || + strcmp(key, "relay_version") == 0 || + strcmp(key, "max_message_length") == 0 || + strcmp(key, "max_event_tags") == 0 || + strcmp(key, "max_content_length") == 0) { + log_info("Relay information changed - reinitializing relay info"); + init_relay_info(); + } + } + // Add successful config to response array cJSON* success_config = cJSON_CreateObject(); cJSON_AddStringToObject(success_config, "key", key); @@ -3808,15 +3944,16 @@ int handle_config_update_unified(cJSON* event, char* error_message, size_t error cJSON_AddStringToObject(success_config, "data_type", data_type); cJSON_AddStringToObject(success_config, "category", category); cJSON_AddStringToObject(success_config, "status", "updated"); + cJSON_AddBoolToObject(success_config, "requires_restart", requires_restart); cJSON_AddItemToArray(processed_configs, success_config); - + log_success("Config field updated successfully"); - printf(" Updated: %s = %s\n", key, value); + printf(" Updated: %s = %s (restart: %s)\n", key, value, requires_restart ? "yes" : "no"); } else { log_error("Failed to update config field in database"); printf(" Failed to update: %s = %s\n", key, value); validation_errors++; - + // Add failed config to response array cJSON* failed_config = cJSON_CreateObject(); cJSON_AddStringToObject(failed_config, "key", key); @@ -4162,9 +4299,17 @@ int populate_config_table_from_event(const cJSON* event) { category = "limits"; } - // Determine if requires restart + // Determine if requires restart (0 = dynamic, 1 = restart required) int requires_restart = 0; - if (strcmp(key, "relay_port") == 0) { + + // Restart required configs + if (strcmp(key, "relay_port") == 0 || + strcmp(key, "max_connections") == 0 || + strcmp(key, "auth_enabled") == 0 || + strcmp(key, "nip42_auth_required") == 0 || + strcmp(key, "nip42_auth_required_kinds") == 0 || + strcmp(key, "nip42_challenge_timeout") == 0 || + strcmp(key, "database_path") == 0) { requires_restart = 1; } diff --git a/src/main.h b/src/main.h index 5020369..5c5d215 100644 --- a/src/main.h +++ b/src/main.h @@ -12,10 +12,10 @@ #define MAIN_H // Version information (auto-updated by build_and_push.sh) -#define VERSION "v0.4.2" +#define VERSION "v0.4.3" #define VERSION_MAJOR 0 #define VERSION_MINOR 4 -#define VERSION_PATCH 2 +#define VERSION_PATCH 3 // Relay metadata (authoritative source for NIP-11 information) #define RELAY_NAME "C-Relay" diff --git a/test_dynamic_config.sh b/test_dynamic_config.sh new file mode 100755 index 0000000..0ab2d43 --- /dev/null +++ b/test_dynamic_config.sh @@ -0,0 +1,133 @@ +#!/bin/bash + +# Test dynamic config updates without restart + +set -e + +# Configuration from relay startup +ADMIN_PRIVKEY="ddea442930976541e199a05248eb6cd92f2a65ba366a883a8f6880add9bdc9c9" +RELAY_PUBKEY="1bd4a5e2e32401737f8c16cc0dfa89b93f25f395770a2896fe78c9fb61582dfc" +RELAY_URL="ws://localhost:8888" + +# Colors +GREEN='\033[0;32m' +RED='\033[0;31m' +BLUE='\033[0;34m' +NC='\033[0m' + +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Check if nak is available +if ! command -v nak &> /dev/null; then + log_error "nak command not found. Please install nak first." + exit 1 +fi + +log_info "Testing dynamic config updates without restart..." + +# Test 1: Check current NIP-11 info +log_info "Checking current NIP-11 relay info..." +CURRENT_DESC=$(curl -s -H "Accept: application/nostr+json" http://localhost:8888 | jq -r '.description') +log_info "Current description: $CURRENT_DESC" + +# Test 2: Update relay description dynamically +NEW_DESC="Dynamic Config Test - Updated at $(date)" +log_info "Updating relay description to: $NEW_DESC" + +COMMAND="[\"config_update\", [{\"key\": \"relay_description\", \"value\": \"$NEW_DESC\", \"data_type\": \"string\", \"category\": \"relay\"}]]" + +# Encrypt the command +ENCRYPTED_COMMAND=$(nak encrypt "$COMMAND" --sec "$ADMIN_PRIVKEY" --recipient-pubkey "$RELAY_PUBKEY") + +if [ -z "$ENCRYPTED_COMMAND" ]; then + log_error "Failed to encrypt config update command" + exit 1 +fi + +# Create admin event +ADMIN_EVENT=$(nak event \ + --kind 23456 \ + --content "$ENCRYPTED_COMMAND" \ + --sec "$ADMIN_PRIVKEY" \ + --tag "p=$RELAY_PUBKEY") + +# Send the admin command +log_info "Sending config update command..." +ADMIN_RESULT=$(echo "$ADMIN_EVENT" | nak event "$RELAY_URL") + +if echo "$ADMIN_RESULT" | grep -q "error\|failed\|denied"; then + log_error "Failed to send config update: $ADMIN_RESULT" + exit 1 +fi + +log_success "Config update command sent successfully" + +# Wait for processing +sleep 3 + +# Test 3: Check if NIP-11 info updated without restart +log_info "Checking if NIP-11 info was updated without restart..." +UPDATED_DESC=$(curl -s -H "Accept: application/nostr+json" http://localhost:8888 | jq -r '.description') + +if [ "$UPDATED_DESC" = "$NEW_DESC" ]; then + log_success "SUCCESS: Relay description updated dynamically without restart!" + log_success "Old: $CURRENT_DESC" + log_success "New: $UPDATED_DESC" +else + log_error "FAILED: Relay description was not updated" + log_error "Expected: $NEW_DESC" + log_error "Got: $UPDATED_DESC" + exit 1 +fi + +# Test 4: Test another dynamic config - max_subscriptions_per_client +log_info "Testing another dynamic config: max_subscriptions_per_client" + +# Get current value from database +OLD_LIMIT=$(sqlite3 build/*.db "SELECT value FROM config WHERE key = 'max_subscriptions_per_client';" 2>/dev/null || echo "25") +log_info "Current max_subscriptions_per_client: $OLD_LIMIT" + +NEW_LIMIT=50 + +COMMAND2="[\"config_update\", [{\"key\": \"max_subscriptions_per_client\", \"value\": \"$NEW_LIMIT\", \"data_type\": \"integer\", \"category\": \"limits\"}]]" + +ENCRYPTED_COMMAND2=$(nak encrypt "$COMMAND2" --sec "$ADMIN_PRIVKEY" --recipient-pubkey "$RELAY_PUBKEY") + +ADMIN_EVENT2=$(nak event \ + --kind 23456 \ + --content "$ENCRYPTED_COMMAND2" \ + --sec "$ADMIN_PRIVKEY" \ + --tag "p=$RELAY_PUBKEY") + +log_info "Updating max_subscriptions_per_client to $NEW_LIMIT..." +ADMIN_RESULT2=$(echo "$ADMIN_EVENT2" | nak event "$RELAY_URL") + +if echo "$ADMIN_RESULT2" | grep -q "error\|failed\|denied"; then + log_error "Failed to send second config update: $ADMIN_RESULT2" + exit 1 +fi + +sleep 3 + +# Check updated value from database +UPDATED_LIMIT=$(sqlite3 build/*.db "SELECT value FROM config WHERE key = 'max_subscriptions_per_client';" 2>/dev/null || echo "25") + +if [ "$UPDATED_LIMIT" = "$NEW_LIMIT" ]; then + log_success "SUCCESS: max_subscriptions_per_client updated dynamically!" + log_success "Old: $OLD_LIMIT, New: $UPDATED_LIMIT" +else + log_error "FAILED: max_subscriptions_per_client was not updated" + log_error "Expected: $NEW_LIMIT, Got: $UPDATED_LIMIT" +fi + +log_success "Dynamic config update testing completed successfully!" \ No newline at end of file