From c3bab033ed14fd2643575a11c8593cb63a84eabe Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 1 Oct 2025 17:23:50 -0400 Subject: [PATCH] v0.4.1 - Fixed startup bug --- api/index.html | 63 +-- ...03a9d11057e9afabf61a89cd5a31375c96eacde.db | Bin 0 -> 155648 bytes nip11_relay_connection_implementation_plan.md | 455 ------------------ relay.pid | 2 +- src/config.c | 84 ++-- src/main.c | 113 +++-- tests/event_config_tests.sh | 51 +- 7 files changed, 174 insertions(+), 594 deletions(-) create mode 100644 e1d3c52a4a330293e4de1327403a9d11057e9afabf61a89cd5a31375c96eacde.db delete mode 100644 nip11_relay_connection_implementation_plan.md diff --git a/api/index.html b/api/index.html index d474cec..e37e414 100644 --- a/api/index.html +++ b/api/index.html @@ -932,7 +932,7 @@ description: 'C-Relay instance - pubkey provided manually', pubkey: manualPubkey, contact: 'admin@manual.config.relay', - supported_nips: [1, 2, 4, 9, 11, 12, 15, 16, 20, 22], + supported_nips: [1, 9, 11, 13, 15, 20, 33, 40, 42], software: 'https://github.com/0xtrr/c-relay', version: '1.0.0' }; @@ -958,7 +958,7 @@ description: 'C-Relay instance - pubkey provided manually', pubkey: manualPubkey, contact: 'admin@manual.config.relay', - supported_nips: [1, 2, 4, 9, 11, 12, 15, 16, 20, 22], + supported_nips: [1, 9, 11, 13, 15, 20, 33, 40, 42], software: 'https://github.com/0xtrr/c-relay', version: '1.0.0' }; @@ -1286,18 +1286,6 @@ console.log('Logout event handled successfully'); } - // Disconnect from relay and clean up connections - function disconnectFromRelay() { - if (relayPool) { - console.log('Cleaning up relay pool connection...'); - const url = relayConnectionUrl.value.trim(); - if (url) { - relayPool.close([url]); - } - relayPool = null; - subscriptionId = null; - } - } // Update visibility of admin sections based on login and relay connection status function updateAdminSectionsVisibility() { @@ -2030,56 +2018,33 @@ configForm.innerHTML = ''; - // Define field types and validation for different config parameters + // Define field types and validation for different config parameters (aligned with README.md) const fieldTypes = { 'auth_enabled': 'boolean', - 'nip42_auth_required_events': 'boolean', - 'nip42_auth_required_subscriptions': 'boolean', + 'nip42_auth_required': 'boolean', 'nip40_expiration_enabled': 'boolean', - 'nip40_expiration_strict': 'boolean', - 'nip40_expiration_filter': 'boolean', - 'relay_port': 'number', 'max_connections': 'number', 'pow_min_difficulty': 'number', - 'nip42_challenge_expiration': 'number', - 'nip40_expiration_grace_period': 'number', + 'nip42_challenge_timeout': 'number', 'max_subscriptions_per_client': 'number', - 'max_total_subscriptions': 'number', - 'max_filters_per_subscription': 'number', 'max_event_tags': 'number', - 'max_content_length': 'number', - 'max_message_length': 'number', - 'default_limit': 'number', - 'max_limit': 'number' + 'max_content_length': 'number' }; const descriptions = { 'relay_pubkey': 'Relay Public Key (Read-only)', 'auth_enabled': 'Enable Authentication', - 'nip42_auth_required_events': 'Require Auth for Events', - 'nip42_auth_required_subscriptions': 'Require Auth for Subscriptions', - 'nip42_auth_required_kinds': 'Auth Required Event Kinds', - 'nip42_challenge_expiration': 'Auth Challenge Expiration (seconds)', - 'relay_port': 'Relay Port', + 'nip42_auth_required': 'Enable NIP-42 Cryptographic Authentication', + 'nip42_auth_required_kinds': 'Event Kinds Requiring NIP-42 Auth', + 'nip42_challenge_timeout': 'NIP-42 Challenge Expiration Seconds', 'max_connections': 'Maximum Connections', 'relay_description': 'Relay Description', 'relay_contact': 'Relay Contact', - 'relay_software': 'Relay Software URL', - 'relay_version': 'Relay Version', - 'pow_min_difficulty': 'Minimum PoW Difficulty', - 'pow_mode': 'PoW Mode', + 'pow_min_difficulty': 'Minimum Proof-of-Work Difficulty', 'nip40_expiration_enabled': 'Enable Event Expiration', - 'nip40_expiration_strict': 'Strict Expiration Mode', - 'nip40_expiration_filter': 'Filter Expired Events', - 'nip40_expiration_grace_period': 'Expiration Grace Period (seconds)', 'max_subscriptions_per_client': 'Max Subscriptions per Client', - 'max_total_subscriptions': 'Max Total Subscriptions', - 'max_filters_per_subscription': 'Max Filters per Subscription', - 'max_event_tags': 'Max Event Tags', - 'max_content_length': 'Max Content Length', - 'max_message_length': 'Max Message Length', - 'default_limit': 'Default Query Limit', - 'max_limit': 'Maximum Query Limit' + 'max_event_tags': 'Maximum Tags per Event', + 'max_content_length': 'Maximum Event Content Length' }; // Process configuration tags (no d tag filtering for ephemeral events) @@ -3452,7 +3417,7 @@ logTestEvent('SENT', `Add Whitelist event: ${JSON.stringify(signedEvent)}`, 'EVENT'); // Publish via SimplePool - const url = relayUrl.value.trim(); + const url = relayConnectionUrl.value.trim(); const publishPromises = relayPool.publish([url], signedEvent); // Use Promise.allSettled to capture per-relay outcomes instead of Promise.any @@ -3594,7 +3559,7 @@ logTestEvent('SENT', `Signed test event: ${JSON.stringify(signedEvent)}`, 'EVENT'); // Publish via SimplePool to the same relay with detailed error diagnostics - const url = relayUrl.value.trim(); + const url = relayConnectionUrl.value.trim(); logTestEvent('INFO', `Publishing to relay: ${url}`, 'INFO'); const publishPromises = relayPool.publish([url], signedEvent); diff --git a/e1d3c52a4a330293e4de1327403a9d11057e9afabf61a89cd5a31375c96eacde.db b/e1d3c52a4a330293e4de1327403a9d11057e9afabf61a89cd5a31375c96eacde.db new file mode 100644 index 0000000000000000000000000000000000000000..d1c5ad40bfe5534e47043992701374b60faf4200 GIT binary patch literal 155648 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 -
-

RELAY CONNECTION

-
- - -
-
- - -
-
NOT CONNECTED
- - - -
-``` - -### 2. JavaScript Implementation - -#### Global State Variables -Add to global state section (around line 535): - -```javascript -// Relay connection state -let relayInfo = null; -let isRelayConnected = false; -let relayWebSocket = null; -``` - -#### NIP-11 Fetching Function -Add new function: - -```javascript -// Fetch relay information using NIP-11 -async function fetchRelayInfo(relayUrl) { - try { - console.log('=== FETCHING RELAY INFO VIA NIP-11 ==='); - console.log('Relay URL:', relayUrl); - - // Convert WebSocket URL to HTTP URL for NIP-11 - let httpUrl = relayUrl; - if (relayUrl.startsWith('ws://')) { - httpUrl = relayUrl.replace('ws://', 'http://'); - } else if (relayUrl.startsWith('wss://')) { - httpUrl = relayUrl.replace('wss://', 'https://'); - } - - console.log('HTTP URL for NIP-11:', httpUrl); - - // Fetch relay information document - const response = await fetch(httpUrl, { - method: 'GET', - headers: { - 'Accept': 'application/nostr+json' - }, - // Add timeout - signal: AbortSignal.timeout(10000) // 10 second timeout - }); - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - - const contentType = response.headers.get('content-type'); - if (!contentType || !contentType.includes('application/json')) { - throw new Error(`Invalid content type: ${contentType}. Expected application/json or application/nostr+json`); - } - - const relayInfoData = await response.json(); - console.log('Fetched relay info:', relayInfoData); - - // Validate required fields - if (!relayInfoData.pubkey) { - throw new Error('Relay information missing required pubkey field'); - } - - // Validate pubkey format (64 hex characters) - if (!/^[0-9a-fA-F]{64}$/.test(relayInfoData.pubkey)) { - throw new Error(`Invalid relay pubkey format: ${relayInfoData.pubkey}`); - } - - return relayInfoData; - - } catch (error) { - console.error('Failed to fetch relay info:', error); - throw error; - } -} -``` - -#### Relay Connection Function -Add new function: - -```javascript -// Connect to relay and fetch information -async function connectToRelay() { - try { - const relayUrlInput = document.getElementById('relay-url-input'); - const connectBtn = document.getElementById('connect-relay-btn'); - const disconnectBtn = document.getElementById('disconnect-relay-btn'); - const statusDiv = document.getElementById('relay-connection-status'); - const infoDisplay = document.getElementById('relay-info-display'); - - const url = relayUrlInput.value.trim(); - if (!url) { - throw new Error('Please enter a relay URL'); - } - - // Update UI to show connecting state - connectBtn.disabled = true; - statusDiv.textContent = 'CONNECTING...'; - statusDiv.className = 'status connected'; - - console.log('Connecting to relay:', url); - - // Fetch relay information via NIP-11 - console.log('Fetching relay information...'); - const fetchedRelayInfo = await fetchRelayInfo(url); - - // Test WebSocket connection - console.log('Testing WebSocket connection...'); - await testWebSocketConnection(url); - - // Store relay information - relayInfo = fetchedRelayInfo; - isRelayConnected = true; - - // Update UI with relay information - displayRelayInfo(relayInfo); - - // Update connection status - statusDiv.textContent = 'CONNECTED'; - statusDiv.className = 'status connected'; - - // Update button states - connectBtn.style.display = 'none'; - disconnectBtn.style.display = 'inline-block'; - relayUrlInput.disabled = true; - - // Show relay info - infoDisplay.classList.remove('hidden'); - - console.log('Successfully connected to relay:', relayInfo.name || url); - log(`Connected to relay: ${relayInfo.name || url}`, 'INFO'); - - } catch (error) { - console.error('Failed to connect to relay:', error); - - // Reset UI state - const connectBtn = document.getElementById('connect-relay-btn'); - const statusDiv = document.getElementById('relay-connection-status'); - - connectBtn.disabled = false; - statusDiv.textContent = `CONNECTION FAILED: ${error.message}`; - statusDiv.className = 'status error'; - - // Clear any partial state - relayInfo = null; - isRelayConnected = false; - - log(`Failed to connect to relay: ${error.message}`, 'ERROR'); - } -} -``` - -#### WebSocket Connection Test -Add new function: - -```javascript -// Test WebSocket connection to relay -async function testWebSocketConnection(url) { - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - ws.close(); - reject(new Error('WebSocket connection timeout')); - }, 5000); - - const ws = new WebSocket(url); - - ws.onopen = () => { - clearTimeout(timeout); - console.log('WebSocket connection successful'); - ws.close(); - resolve(); - }; - - ws.onerror = (error) => { - clearTimeout(timeout); - console.error('WebSocket connection failed:', error); - reject(new Error('WebSocket connection failed')); - }; - - ws.onclose = (event) => { - if (event.code !== 1000) { - clearTimeout(timeout); - reject(new Error(`WebSocket closed with code ${event.code}: ${event.reason}`)); - } - }; - }); -} -``` - -#### Display Relay Information -Add new function: - -```javascript -// Display relay information in the UI -function displayRelayInfo(info) { - document.getElementById('relay-name').textContent = info.name || 'Unknown'; - document.getElementById('relay-description').textContent = info.description || 'No description'; - document.getElementById('relay-pubkey-display').textContent = info.pubkey || 'Unknown'; - document.getElementById('relay-software').textContent = info.software || 'Unknown'; - document.getElementById('relay-version').textContent = info.version || 'Unknown'; - document.getElementById('relay-contact').textContent = info.contact || 'No contact info'; - - // Format supported NIPs - let nipsText = 'None specified'; - if (info.supported_nips && Array.isArray(info.supported_nips) && info.supported_nips.length > 0) { - nipsText = info.supported_nips.map(nip => `NIP-${nip.toString().padStart(2, '0')}`).join(', '); - } - document.getElementById('relay-nips').textContent = nipsText; -} -``` - -#### Disconnect Function -Add new function: - -```javascript -// Disconnect from relay -function disconnectFromRelay() { - console.log('Disconnecting from relay...'); - - // Clear relay state - relayInfo = null; - isRelayConnected = false; - - // Close any existing connections - if (relayPool) { - const url = document.getElementById('relay-url-input').value.trim(); - if (url) { - relayPool.close([url]); - } - relayPool = null; - subscriptionId = null; - } - - // Reset UI - const connectBtn = document.getElementById('connect-relay-btn'); - const disconnectBtn = document.getElementById('disconnect-relay-btn'); - const statusDiv = document.getElementById('relay-connection-status'); - const infoDisplay = document.getElementById('relay-info-display'); - const relayUrlInput = document.getElementById('relay-url-input'); - - connectBtn.style.display = 'inline-block'; - disconnectBtn.style.display = 'none'; - connectBtn.disabled = false; - relayUrlInput.disabled = false; - - statusDiv.textContent = 'NOT CONNECTED'; - statusDiv.className = 'status disconnected'; - - infoDisplay.classList.add('hidden'); - - // Reset configuration status - updateConfigStatus(false); - - log('Disconnected from relay', 'INFO'); -} -``` - -#### Update getRelayPubkey Function -Replace existing function (around line 3142): - -```javascript -// Helper function to get relay pubkey from connected relay info -function getRelayPubkey() { - if (relayInfo && relayInfo.pubkey) { - return relayInfo.pubkey; - } - - // Fallback to hardcoded value if no relay connected (for testing) - console.warn('No relay connected, using fallback pubkey'); - return '4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa'; -} -``` - -### 3. Event Handlers - -Add event handlers in the DOMContentLoaded section: - -```javascript -// Relay connection event handlers -const connectRelayBtn = document.getElementById('connect-relay-btn'); -const disconnectRelayBtn = document.getElementById('disconnect-relay-btn'); - -if (connectRelayBtn) { - connectRelayBtn.addEventListener('click', function(e) { - e.preventDefault(); - connectToRelay().catch(error => { - console.error('Connect to relay failed:', error); - }); - }); -} - -if (disconnectRelayBtn) { - disconnectRelayBtn.addEventListener('click', function(e) { - e.preventDefault(); - disconnectFromRelay(); - }); -} -``` - -### 4. Update Existing Functions - -#### Update fetchConfiguration Function -Add relay connection check at the beginning: - -```javascript -async function fetchConfiguration() { - try { - console.log('=== FETCHING CONFIGURATION VIA ADMIN API ==='); - - // Check if relay is connected - if (!isRelayConnected || !relayInfo) { - throw new Error('Must be connected to relay first. Please connect to relay in the Relay Connection section.'); - } - - // ... rest of existing function - } catch (error) { - // ... existing error handling - } -} -``` - -#### Update subscribeToConfiguration Function -Add relay connection check: - -```javascript -async function subscribeToConfiguration() { - try { - console.log('=== STARTING SIMPLEPOOL CONFIGURATION SUBSCRIPTION ==='); - - if (!isRelayConnected || !relayInfo) { - console.error('Must be connected to relay first'); - return false; - } - - // Use the relay URL from the connection section instead of the debug section - const url = document.getElementById('relay-url-input').value.trim(); - - // ... rest of existing function - } catch (error) { - // ... existing error handling - } -} -``` - -### 5. Update UI Flow - -#### Modify showMainInterface Function -Update to show relay connection requirement: - -```javascript -function showMainInterface() { - loginSection.classList.add('hidden'); - mainInterface.classList.remove('hidden'); - userPubkeyDisplay.textContent = userPubkey; - - // Show message about relay connection requirement - if (!isRelayConnected) { - log('Please connect to a relay to access admin functions', 'INFO'); - } -} -``` - -### 6. Remove/Update Debug Section - -#### Option 1: Remove Debug Section Entirely -Remove the "DEBUG - TEST FETCH WITHOUT LOGIN" section (lines 335-385) since relay URL is now in the proper connection section. - -#### Option 2: Keep Debug Section for Testing -Update the debug section to use the connected relay URL and add a note that it's for testing purposes. - -### 7. Error Handling - -Add comprehensive error handling for: -- Network timeouts -- Invalid relay URLs -- Missing NIP-11 support -- Invalid relay pubkey format -- WebSocket connection failures -- CORS issues - -### 8. Security Considerations - -- Validate relay pubkey format (64 hex characters) -- Verify relay identity before admin operations -- Handle CORS properly for NIP-11 requests -- Sanitize relay information display -- Warn users about connecting to untrusted relays - -## Testing Plan - -1. **NIP-11 Fetching**: Test with various relay URLs (localhost, remote relays) -2. **Error Handling**: Test with invalid URLs, non-Nostr servers, network failures -3. **WebSocket Connection**: Verify WebSocket connectivity after NIP-11 fetch -4. **Admin API Integration**: Ensure admin commands use correct relay pubkey -5. **UI Flow**: Test complete user journey from login → relay connection → admin operations - -## Benefits - -1. **Proper Relay Identification**: Uses actual relay pubkey instead of hardcoded value -2. **Better UX**: Clear connection flow and relay information display -3. **Protocol Compliance**: Implements NIP-11 standard for relay discovery -4. **Security**: Verifies relay identity before admin operations -5. **Flexibility**: Works with any NIP-11 compliant relay - -## Migration Notes - -- Existing users will need to connect to relay after this update -- Debug section can be kept for development/testing purposes -- All admin functions will require relay connection -- Relay pubkey will be dynamically fetched instead of hardcoded \ No newline at end of file diff --git a/relay.pid b/relay.pid index 7ccf87c..4a842f6 100644 --- a/relay.pid +++ b/relay.pid @@ -1 +1 @@ -1796483 +1878384 diff --git a/src/config.c b/src/config.c index f7c69be..1706337 100644 --- a/src/config.c +++ b/src/config.c @@ -917,10 +917,11 @@ cJSON* create_default_config_event(const unsigned char* admin_privkey_bytes, 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 @@ -943,6 +944,7 @@ int first_time_startup_sequence(const cli_options_t* cli_options) { // 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"); @@ -959,6 +961,7 @@ int first_time_startup_sequence(const cli_options_t* cli_options) { 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 @@ -1017,57 +1020,40 @@ int first_time_startup_sequence(const cli_options_t* cli_options) { 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 based on admin key availability - if (cli_options && strlen(cli_options->admin_pubkey_override) == 64) { - // Admin pubkey provided - will populate config table after database initialization - log_info("Admin pubkey provided - config table will be populated 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 { - // Admin private key available - create signed configuration event - log_info("Admin private key available - creating signed configuration event"); - - // Create initial configuration event using defaults - cJSON* config_event = create_default_config_event(admin_privkey_bytes, relay_privkey, relay_pubkey, cli_options); - if (!config_event) { - log_error("Failed to create default configuration event"); - return -1; - } - - // Process configuration through admin API instead of storing in events table - if (process_startup_config_event_with_fallback(config_event) == 0) { - log_success("Initial configuration processed successfully through admin API"); - } else { - log_warning("Failed to process initial configuration - will retry after database init"); - // Cache the event for later processing - if (g_pending_config_event) { - cJSON_Delete(g_pending_config_event); - } - g_pending_config_event = cJSON_Duplicate(config_event, 1); - } - - // Cache the current config - if (g_current_config) { - cJSON_Delete(g_current_config); - } - g_current_config = cJSON_Duplicate(config_event, 1); - - // Clean up - cJSON_Delete(config_event); + 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"); } - // 10. Print admin private key for user to save - printf("\n"); - printf("=================================================================\n"); - printf("IMPORTANT: SAVE THIS ADMIN PRIVATE KEY SECURELY!\n"); - printf("=================================================================\n"); - printf("Admin Private Key: %s\n", admin_privkey); - printf("Admin Public Key: %s\n", admin_pubkey); - printf("Relay Public Key: %s\n", relay_pubkey); - printf("\nDatabase: %s\n", g_database_path); - printf("\nThis admin private key is needed to update configuration!\n"); - printf("Store it safely - it will not be displayed again.\n"); - printf("=================================================================\n"); - printf("\n"); - log_success("First-time startup sequence completed"); return 0; } diff --git a/src/main.c b/src/main.c index eb57682..669dc55 100644 --- a/src/main.c +++ b/src/main.c @@ -348,18 +348,18 @@ int init_database(const char* database_path_override) { } if (!has_auth_rules) { - // Add auth_rules table + // Add auth_rules table matching sql_schema.h const char* create_auth_rules_sql = "CREATE TABLE IF NOT EXISTS auth_rules (" " id INTEGER PRIMARY KEY AUTOINCREMENT," - " rule_type TEXT NOT NULL," // 'pubkey_whitelist', 'pubkey_blacklist', 'hash_blacklist' - " operation TEXT NOT NULL," // 'event', 'event_kind_1', etc. - " rule_target TEXT NOT NULL," // pubkey, hash, or other identifier - " enabled INTEGER DEFAULT 1," // 0 = disabled, 1 = enabled - " priority INTEGER DEFAULT 1000," // Lower numbers = higher priority - " description TEXT," // Optional description - " created_at INTEGER DEFAULT (strftime('%s', 'now'))," - " UNIQUE(rule_type, operation, rule_target)" + " rule_type TEXT NOT NULL CHECK (rule_type IN ('whitelist', 'blacklist', 'rate_limit', 'auth_required'))," + " pattern_type TEXT NOT NULL CHECK (pattern_type IN ('pubkey', 'kind', 'ip', 'global'))," + " pattern_value TEXT," + " action TEXT NOT NULL CHECK (action IN ('allow', 'deny', 'require_auth', 'rate_limit'))," + " parameters TEXT," + " active INTEGER NOT NULL DEFAULT 1," + " created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))," + " updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))" ");"; char* error_msg = NULL; @@ -373,6 +373,24 @@ int init_database(const char* database_path_override) { return -1; } log_success("Created auth_rules table"); + + // Add indexes for auth_rules table + const char* create_auth_rules_indexes_sql = + "CREATE INDEX IF NOT EXISTS idx_auth_rules_pattern ON auth_rules(pattern_type, pattern_value);" + "CREATE INDEX IF NOT EXISTS idx_auth_rules_type ON auth_rules(rule_type);" + "CREATE INDEX IF NOT EXISTS idx_auth_rules_active ON auth_rules(active);"; + + char* index_error_msg = NULL; + int index_rc = sqlite3_exec(g_db, create_auth_rules_indexes_sql, NULL, NULL, &index_error_msg); + if (index_rc != SQLITE_OK) { + char index_error_log[512]; + snprintf(index_error_log, sizeof(index_error_log), "Failed to create auth_rules indexes: %s", + index_error_msg ? index_error_msg : "unknown error"); + log_error(index_error_log); + if (index_error_msg) sqlite3_free(index_error_msg); + return -1; + } + log_success("Created auth_rules indexes"); } else { log_info("auth_rules table already exists, skipping creation"); } @@ -1408,35 +1426,43 @@ int main(int argc, char* argv[]) { } // Handle configuration setup after database is initialized - if (cli_options.admin_pubkey_override && strlen(cli_options.admin_pubkey_override) == 64) { - // Admin pubkey provided - populate config table directly - log_info("Populating config table for admin pubkey override after database initialization"); + // Always populate defaults directly in config table (abandoning legacy event signing) + log_info("Populating config table with defaults after database initialization"); - // Populate default config values in table - if (populate_default_config_values() != 0) { - log_error("Failed to populate default config values"); - cleanup_configuration_system(); - nostr_cleanup(); - close_database(); - return 1; - } - - // Add pubkeys to config table - if (add_pubkeys_to_config_table() != 0) { - log_error("Failed to add pubkeys to config table"); - cleanup_configuration_system(); - nostr_cleanup(); - close_database(); - return 1; - } - - log_success("Configuration populated directly in config table after database initialization"); - } else { - // Admin private key available - retry storing initial config event - if (retry_store_initial_config_event() != 0) { - log_warning("Failed to store initial config event - will retry later"); - } + // Populate default config values in table + if (populate_default_config_values() != 0) { + log_error("Failed to populate default config values"); + cleanup_configuration_system(); + nostr_cleanup(); + close_database(); + return 1; } + + // Apply CLI overrides now that database is available + if (cli_options.port_override > 0) { + char port_str[16]; + snprintf(port_str, sizeof(port_str), "%d", cli_options.port_override); + if (update_config_in_table("relay_port", port_str) != 0) { + log_error("Failed to update relay port override in config table"); + cleanup_configuration_system(); + nostr_cleanup(); + close_database(); + return 1; + } + log_info("Applied port override from command line"); + printf(" Port: %d (overriding default)\n", cli_options.port_override); + } + + // Add pubkeys to config table + if (add_pubkeys_to_config_table() != 0) { + log_error("Failed to add pubkeys to config table"); + cleanup_configuration_system(); + nostr_cleanup(); + close_database(); + return 1; + } + + log_success("Configuration populated directly in config table after database initialization"); // Now store the pubkeys in config table since database is available const char* admin_pubkey = get_admin_pubkey_cached(); @@ -1539,6 +1565,21 @@ int main(int argc, char* argv[]) { log_warning("No configuration event found in existing database"); } + // Apply CLI overrides for existing relay (port override should work even for existing relays) + if (cli_options.port_override > 0) { + char port_str[16]; + snprintf(port_str, sizeof(port_str), "%d", cli_options.port_override); + if (update_config_in_table("relay_port", port_str) != 0) { + log_error("Failed to update relay port override in config table for existing relay"); + cleanup_configuration_system(); + nostr_cleanup(); + close_database(); + return 1; + } + log_info("Applied port override from command line for existing relay"); + printf(" Port: %d (overriding configured port)\n", cli_options.port_override); + } + // Free memory free(relay_pubkey); for (int i = 0; existing_files[i]; i++) { diff --git a/tests/event_config_tests.sh b/tests/event_config_tests.sh index 7f64c91..11e4bdc 100755 --- a/tests/event_config_tests.sh +++ b/tests/event_config_tests.sh @@ -310,8 +310,51 @@ else print_failure "Relay failed to start for network test" fi -# TEST 10: Multiple Startup Attempts (Port Conflict) -print_test_header "Test 10: Port Conflict Handling" +# TEST 10: Port Override with Admin/Relay Key Overrides +print_test_header "Test 10: Port Override with -a/-r Flags" + +cleanup_test_files + +# Generate test keys (64 hex chars each) +TEST_ADMIN_PUBKEY="1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" +TEST_RELAY_PRIVKEY="abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" + +print_info "Testing port override with -p 9999 -a $TEST_ADMIN_PUBKEY -r $TEST_RELAY_PRIVKEY" + +# Start relay with port override and key overrides +timeout 15 $RELAY_BINARY -p 9999 -a $TEST_ADMIN_PUBKEY -r $TEST_RELAY_PRIVKEY > "test_port_override.log" 2>&1 & +relay_pid=$! +sleep 5 + +if kill -0 $relay_pid 2>/dev/null; then + # Check if relay bound to port 9999 (not default 8888) + if netstat -tln 2>/dev/null | grep -q ":9999"; then + print_success "Relay successfully bound to overridden port 9999" + else + print_failure "Relay not bound to overridden port 9999" + fi + + # Check that relay started successfully + if check_relay_startup "test_port_override.log"; then + print_success "Relay startup completed with overrides" + else + print_failure "Relay failed to complete startup with overrides" + fi + + # Check that admin keys were NOT generated (since -a was provided) + if ! check_admin_keys "test_port_override.log"; then + print_success "Admin keys not generated (correctly using provided -a key)" + else + print_failure "Admin keys generated despite -a override" + fi + + stop_relay_test $relay_pid +else + print_failure "Relay failed to start with port/key overrides" +fi + +# TEST 11: Multiple Startup Attempts (Port Conflict) +print_test_header "Test 11: Port Conflict Handling" relay_pid1=$(start_relay_test "port_conflict_1" 10) sleep 2 @@ -320,14 +363,14 @@ if kill -0 $relay_pid1 2>/dev/null; then # Try to start a second relay (should fail due to port conflict) relay_pid2=$(start_relay_test "port_conflict_2" 5) sleep 1 - + if [ "$relay_pid2" = "0" ] || ! kill -0 $relay_pid2 2>/dev/null; then print_success "Port conflict properly handled (second instance failed to start)" else print_failure "Multiple relay instances started (port conflict not handled)" stop_relay_test $relay_pid2 fi - + stop_relay_test $relay_pid1 else print_failure "First relay instance failed to start"