From 6fd3e531c30a3901b364c757d136fef9f203ccd2 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 27 Sep 2025 15:50:42 -0400 Subject: [PATCH] v0.3.15 - How can administration take so long --- AGENTS.md | 6 +- README.md | 1 + api/index.html | 329 ++++++++++++++++++++++--------------- make_and_restart_relay.sh | 6 +- relay.pid | 2 +- src/config.c | 160 +++++++++++------- src/config.h | 1 + src/default_config_event.h | 3 +- src/main.c | 51 ++++-- src/sql_schema.h | 2 +- 10 files changed, 343 insertions(+), 218 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fcd4dcb..4d82f1b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,7 +27,7 @@ ## Critical Integration Issues ### Event-Based Configuration System -- **No traditional config files** - all configuration stored as kind 33334 Nostr events +- **No traditional config files** - all configuration stored in config table - Admin private key shown **only once** on first startup - Configuration changes require cryptographically signed events - Database path determined by generated relay pubkey @@ -35,7 +35,7 @@ ### First-Time Startup Sequence 1. Relay generates admin keypair and relay keypair 2. Creates database file with relay pubkey as filename -3. Stores default configuration as kind 33334 event +3. Stores default configuration in config table 4. **CRITICAL**: Admin private key displayed once and never stored on disk ### Port Management @@ -51,7 +51,7 @@ ### Configuration Event Structure ```json { - "kind": 33334, + "kind": 23455, "content": "C Nostr Relay Configuration", "tags": [ ["d", ""], diff --git a/README.md b/README.md index 62e46a2..460b0a7 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ All commands are sent as nip44 encrypted content. The following table lists all | **Auth Rules Management** | | `auth_add_blacklist` | `["blacklist", "pubkey", "abc123..."]` | Add pubkey to blacklist | | `auth_add_whitelist` | `["whitelist", "pubkey", "def456..."]` | Add pubkey to whitelist | +| `auth_delete_rule` | `["delete_auth_rule", "blacklist", "pubkey", "abc123..."]` | Delete specific auth rule | | `auth_query_all` | `["auth_query", "all"]` | Query all auth rules | | `auth_query_type` | `["auth_query", "whitelist"]` | Query specific rule type | | `auth_query_pattern` | `["auth_query", "pattern", "abc123..."]` | Query specific pattern | diff --git a/api/index.html b/api/index.html index 71df3d2..274117c 100644 --- a/api/index.html +++ b/api/index.html @@ -33,7 +33,6 @@ h2 { margin: 30px 0 15px 0; font-weight: normal; - border-left: 4px solid black; padding-left: 10px; font-size: 16px; } @@ -438,50 +437,27 @@ - +
- +
-

BLACKLIST PUBKEY

-

Block a specific user from all operations

+

MANAGE PUBKEY ACCESS

+

Add pubkeys to whitelist (allow) or blacklist (deny) access

- - - Enter nsec (will auto-convert) or 64-character hex pubkey -
- -
-
- - -
-

WHITELIST PUBKEY

-

Allow only specific users (converts relay to whitelist-only mode)

-
- - - Enter nsec (will auto-convert) or 64-character hex pubkey + + + Enter nsec (will auto-convert) or 64-character hex pubkey
- -
-
- - -
-

BLACKLIST CONTENT HASH

-

Block specific content by SHA-256 hash

-
- - - Enter 64-character hex SHA-256 hash of content to block +
+ +
- -
+
@@ -1133,12 +1109,25 @@ console.log('Total results:', responseData.total_results); console.log('Data:', responseData.data); + // Update the current auth rules with the response data + if (responseData.data && Array.isArray(responseData.data)) { + currentAuthRules = responseData.data; + displayAuthRules(currentAuthRules); + updateAuthRulesStatus('loaded'); + log(`Loaded ${responseData.total_results} auth rules from relay`, 'INFO'); + } else { + currentAuthRules = []; + displayAuthRules(currentAuthRules); + updateAuthRulesStatus('loaded'); + log('No auth rules found on relay', 'INFO'); + } + if (typeof logTestEvent === 'function') { logTestEvent('RECV', `Auth query response: ${responseData.query_type}, ${responseData.total_results} results`, 'AUTH_QUERY'); if (responseData.data && responseData.data.length > 0) { responseData.data.forEach((rule, index) => { - logTestEvent('RECV', `Rule ${index + 1}: ${rule.rule_type} - ${rule.rule_target}`, 'AUTH_RULE'); + logTestEvent('RECV', `Rule ${index + 1}: ${rule.rule_type} - ${rule.pattern_value || rule.rule_target}`, 'AUTH_RULE'); }); } else { logTestEvent('RECV', 'No auth rules found', 'AUTH_QUERY'); @@ -1152,6 +1141,30 @@ console.log('Command:', responseData.command); console.log('Status:', responseData.status); + // Handle delete auth rule responses + if (responseData.command === 'delete_auth_rule') { + if (responseData.status === 'success') { + log('Auth rule deleted successfully', 'INFO'); + // Refresh the auth rules display + loadAuthRules(); + } else { + log(`Failed to delete auth rule: ${responseData.message || 'Unknown error'}`, 'ERROR'); + } + } + + // Handle clear all auth rules responses + if (responseData.command === 'clear_all_auth_rules') { + if (responseData.status === 'success') { + const rulesCleared = responseData.rules_cleared || 0; + log(`Successfully cleared ${rulesCleared} auth rules`, 'INFO'); + // Clear local auth rules and refresh display + currentAuthRules = []; + displayAuthRules(currentAuthRules); + } else { + log(`Failed to clear auth rules: ${responseData.message || 'Unknown error'}`, 'ERROR'); + } + } + if (typeof logTestEvent === 'function') { logTestEvent('RECV', `System command response: ${responseData.command} - ${responseData.status}`, 'SYSTEM_CMD'); } @@ -1163,12 +1176,25 @@ console.log('Operation:', responseData.operation); console.log('Status:', responseData.status); + // Handle auth rule addition/modification responses + if (responseData.status === 'success') { + const rulesProcessed = responseData.rules_processed || 0; + log(`Successfully processed ${rulesProcessed} auth rule modifications`, 'INFO'); + + // Refresh the auth rules display to show the new rules + if (authRulesTableContainer && authRulesTableContainer.style.display !== 'none') { + loadAuthRules(); + } + } else { + log(`Failed to process auth rule modifications: ${responseData.message || 'Unknown error'}`, 'ERROR'); + } + if (typeof logTestEvent === 'function') { logTestEvent('RECV', `Auth rule response: ${responseData.operation} - ${responseData.status}`, 'AUTH_RULE'); if (responseData.processed_rules) { responseData.processed_rules.forEach((rule, index) => { - logTestEvent('RECV', `Processed rule ${index + 1}: ${rule.rule_type} - ${rule.rule_target}`, 'AUTH_RULE'); + logTestEvent('RECV', `Processed rule ${index + 1}: ${rule.rule_type} - ${rule.pattern_value || rule.rule_target}`, 'AUTH_RULE'); }); } } @@ -1646,23 +1672,61 @@ } } - // Load auth rules from relay (placeholder - will be implemented with WebSocket) + // Load auth rules from relay using admin API async function loadAuthRules() { try { - log('Loading auth rules...', 'INFO'); + log('Loading auth rules via admin API...', 'INFO'); updateAuthRulesStatus('loading'); - // TODO: Implement actual auth rules loading via WebSocket/HTTP - // For now, show empty state - currentAuthRules = []; - displayAuthRules(currentAuthRules); - updateAuthRulesStatus('loaded'); + if (!isLoggedIn || !userPubkey) { + throw new Error('Must be logged in to load auth rules'); + } + + if (!relayPool) { + throw new Error('SimplePool connection not available'); + } + + // Create command array for getting all auth rules + const command_array = '["auth_query", "all"]'; - log('Auth rules loaded (placeholder implementation)', 'INFO'); + // Encrypt the command content using NIP-44 + const encrypted_content = await encryptForRelay(command_array); + if (!encrypted_content) { + throw new Error('Failed to encrypt auth query command'); + } + + // Create kind 23456 admin event + const authEvent = { + kind: 23456, + pubkey: userPubkey, + created_at: Math.floor(Date.now() / 1000), + tags: [ + ["p", getRelayPubkey()] + ], + content: encrypted_content + }; + + // Sign the event + const signedEvent = await window.nostr.signEvent(authEvent); + if (!signedEvent || !signedEvent.sig) { + throw new Error('Event signing failed'); + } + + log('Sending auth rules query to relay...', 'INFO'); + + // Publish via SimplePool + const url = relayUrl.value.trim(); + const publishPromises = relayPool.publish([url], signedEvent); + await Promise.any(publishPromises); + + log('Auth rules query sent successfully - waiting for response...', 'INFO'); + updateAuthRulesStatus('loaded'); } catch (error) { log(`Failed to load auth rules: ${error.message}`, 'ERROR'); updateAuthRulesStatus('error'); + currentAuthRules = []; + displayAuthRules(currentAuthRules); } } @@ -1747,7 +1811,7 @@ } } - // Delete auth rule + // Delete auth rule using admin API async function deleteAuthRule(index) { if (index < 0 || index >= currentAuthRules.length) return; @@ -1759,12 +1823,57 @@ try { log(`Deleting auth rule: ${rule.rule_type} - ${rule.pattern_value || rule.rule_target}`, 'INFO'); - // TODO: Implement actual rule deletion via WebSocket kind 23456 event - // For now, just remove from local array + if (!isLoggedIn || !userPubkey) { + throw new Error('Must be logged in to delete auth rules'); + } + + if (!relayPool) { + throw new Error('SimplePool connection not available'); + } + + // Create command array for deleting auth rule + // Format: ["system_command", "delete_auth_rule", rule_type, pattern_type, pattern_value] + const rule_type = rule.rule_type; + const pattern_type = rule.pattern_type || 'pubkey'; + const pattern_value = rule.pattern_value || rule.rule_target; + + const command_array = `["system_command", "delete_auth_rule", "${rule_type}", "${pattern_type}", "${pattern_value}"]`; + + // Encrypt the command content using NIP-44 + const encrypted_content = await encryptForRelay(command_array); + if (!encrypted_content) { + throw new Error('Failed to encrypt delete auth rule command'); + } + + // Create kind 23456 admin event + const authEvent = { + kind: 23456, + pubkey: userPubkey, + created_at: Math.floor(Date.now() / 1000), + tags: [ + ["p", getRelayPubkey()] + ], + content: encrypted_content + }; + + // Sign the event + const signedEvent = await window.nostr.signEvent(authEvent); + if (!signedEvent || !signedEvent.sig) { + throw new Error('Event signing failed'); + } + + log('Sending delete auth rule command to relay...', 'INFO'); + + // Publish via SimplePool + const url = relayUrl.value.trim(); + const publishPromises = relayPool.publish([url], signedEvent); + await Promise.any(publishPromises); + + log('Delete auth rule command sent successfully - waiting for response...', 'INFO'); + + // Remove from local array immediately for UI responsiveness currentAuthRules.splice(index, 1); displayAuthRules(currentAuthRules); - - log('Auth rule deleted (placeholder implementation)', 'INFO'); } catch (error) { log(`Failed to delete auth rule: ${error.message}`, 'ERROR'); @@ -1953,10 +2062,10 @@ return null; // Invalid format } - // Add blacklist rule + // Add blacklist rule (updated to use combined input) function addBlacklistRule() { - const input = document.getElementById('blacklistPubkey'); - const statusDiv = document.getElementById('blacklistStatus'); + const input = document.getElementById('authRulePubkey'); + const statusDiv = document.getElementById('authRuleStatus'); if (!input || !statusDiv) return; @@ -2011,10 +2120,10 @@ }); } - // Add whitelist rule + // Add whitelist rule (updated to use combined input) function addWhitelistRule() { - const input = document.getElementById('whitelistPubkey'); - const statusDiv = document.getElementById('whitelistStatus'); + const input = document.getElementById('authRulePubkey'); + const statusDiv = document.getElementById('authRuleStatus'); const warningDiv = document.getElementById('whitelistWarning'); if (!input || !statusDiv) return; @@ -2041,6 +2150,11 @@ return; } + // Show whitelist warning + if (warningDiv) { + warningDiv.style.display = 'block'; + } + statusDiv.className = 'rule-status'; statusDiv.textContent = 'Adding to whitelist...'; @@ -2070,57 +2184,7 @@ }); } - // Add hash blacklist rule - function addHashBlacklistRule() { - const input = document.getElementById('hashBlacklist'); - const statusDiv = document.getElementById('hashStatus'); - - if (!input || !statusDiv) return; - - const inputValue = input.value.trim(); - if (!inputValue) { - statusDiv.className = 'rule-status error'; - statusDiv.textContent = 'Please enter a SHA-256 hash'; - return; - } - - // Validate hash format (64-char hex) - if (!/^[0-9a-fA-F]{64}$/.test(inputValue)) { - statusDiv.className = 'rule-status error'; - statusDiv.textContent = 'Invalid hash format. Must be 64-character hex SHA-256 hash'; - return; - } - - statusDiv.className = 'rule-status'; - statusDiv.textContent = 'Adding content hash to blacklist...'; - - // Create auth rule data - const ruleData = { - rule_type: 'hash_blacklist', - pattern_type: 'Global', - pattern_value: inputValue.toLowerCase(), // Normalize to lowercase - action: 'deny' - }; - - // Add to WebSocket queue for processing - addAuthRuleViaWebSocket(ruleData) - .then(() => { - statusDiv.className = 'rule-status success'; - statusDiv.textContent = `Content hash ${inputValue.substring(0, 16)}... added to blacklist`; - input.value = ''; - - // Refresh auth rules display if visible - if (authRulesTableContainer && authRulesTableContainer.style.display !== 'none') { - loadAuthRules(); - } - }) - .catch(error => { - statusDiv.className = 'rule-status error'; - statusDiv.textContent = `Failed to add rule: ${error.message}`; - }); - } - - // Add auth rule via SimplePool (kind 23456 event) + // Add auth rule via SimplePool (kind 23456 event) - FIXED to match working test pattern async function addAuthRuleViaWebSocket(ruleData) { if (!isLoggedIn || !userPubkey) { throw new Error('Must be logged in to add auth rules'); @@ -2133,60 +2197,53 @@ try { log(`Adding auth rule: ${ruleData.rule_type} - ${ruleData.pattern_value.substring(0, 16)}...`, 'INFO'); - // Map client-side rule types to database schema values - let dbRuleType, dbPatternType, dbAction; + // Map client-side rule types to command array format (matching working tests) + let commandRuleType, commandPatternType; switch (ruleData.rule_type) { case 'pubkey_blacklist': - dbRuleType = 'blacklist'; - dbPatternType = 'pubkey'; - dbAction = 'deny'; + commandRuleType = 'blacklist'; + commandPatternType = 'pubkey'; break; case 'pubkey_whitelist': - dbRuleType = 'whitelist'; - dbPatternType = 'pubkey'; - dbAction = 'allow'; + commandRuleType = 'whitelist'; + commandPatternType = 'pubkey'; break; case 'hash_blacklist': - dbRuleType = 'blacklist'; - dbPatternType = 'pubkey'; // Schema supports: pubkey, kind, ip, global - using pubkey for hash for now - dbAction = 'deny'; + commandRuleType = 'blacklist'; + commandPatternType = 'hash'; break; default: throw new Error(`Unknown rule type: ${ruleData.rule_type}`); } - // Map pattern type to database schema values - if (ruleData.pattern_type === 'Global') { - dbPatternType = 'global'; - } else if (ruleData.pattern_type === 'pubkey') { - dbPatternType = 'pubkey'; + // Create command array in the same format as working tests + // Format: ["blacklist", "pubkey", "abc123..."] or ["whitelist", "pubkey", "def456..."] + const command_array = `["${commandRuleType}", "${commandPatternType}", "${ruleData.pattern_value}"]`; + + // Encrypt the command content using NIP-44 (same as working tests) + const encrypted_content = await encryptForRelay(command_array); + if (!encrypted_content) { + throw new Error('Failed to encrypt auth rule command'); } - // Create kind 23456 auth rule event (ephemeral auth management) + // Create kind 23456 admin event with encrypted content (same as working tests) const authEvent = { kind: 23456, pubkey: userPubkey, created_at: Math.floor(Date.now() / 1000), tags: [ - [dbRuleType, dbPatternType, ruleData.pattern_value] + ["p", getRelayPubkey()] ], - content: JSON.stringify({ - action: 'add', - rule_type: dbRuleType, - pattern_type: dbPatternType, - pattern_value: ruleData.pattern_value, - rule_action: dbAction - }) + content: encrypted_content }; // DEBUG: Log the complete event structure being sent - console.log('=== AUTH RULE EVENT DEBUG ==='); + console.log('=== AUTH RULE EVENT DEBUG (FIXED FORMAT) ==='); console.log('Original Rule Data:', ruleData); - console.log('Mapped DB Values:', { dbRuleType, dbPatternType, dbAction }); + console.log('Command Array:', command_array); + console.log('Encrypted Content:', encrypted_content.substring(0, 50) + '...'); console.log('Auth Event (before signing):', JSON.stringify(authEvent, null, 2)); - console.log('Auth Event Tags:', authEvent.tags); - console.log('Auth Event Content:', authEvent.content); console.log('=== END AUTH RULE EVENT DEBUG ==='); // Sign the event using the standard NIP-07 interface diff --git a/make_and_restart_relay.sh b/make_and_restart_relay.sh index 9763fb3..a9ff6d0 100755 --- a/make_and_restart_relay.sh +++ b/make_and_restart_relay.sh @@ -282,14 +282,14 @@ cd build # Start relay in background and capture its PID if [ "$USE_TEST_KEYS" = true ]; then echo "Using deterministic test keys for development..." - ./$(basename $BINARY_PATH) -a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -r 1111111111111111111111111111111111111111111111111111111111111111 > ../relay.log 2>&1 & + ./$(basename $BINARY_PATH) -a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -r 1111111111111111111111111111111111111111111111111111111111111111 --strict-port > ../relay.log 2>&1 & elif [ -n "$RELAY_ARGS" ]; then echo "Starting relay with custom configuration..." - ./$(basename $BINARY_PATH) $RELAY_ARGS > ../relay.log 2>&1 & + ./$(basename $BINARY_PATH) $RELAY_ARGS --strict-port > ../relay.log 2>&1 & else # No command line arguments needed for random key generation echo "Starting relay with random key generation..." - ./$(basename $BINARY_PATH) > ../relay.log 2>&1 & + ./$(basename $BINARY_PATH) --strict-port > ../relay.log 2>&1 & fi RELAY_PID=$! # Change back to original directory diff --git a/relay.pid b/relay.pid index e2fe4b0..4964777 100644 --- a/relay.pid +++ b/relay.pid @@ -1 +1 @@ -652192 +659207 diff --git a/src/config.c b/src/config.c index 2f3898c..a68d6a6 100644 --- a/src/config.c +++ b/src/config.c @@ -342,7 +342,7 @@ int store_config_event_in_database(const cJSON* event) { return -1; } - // Insert or replace the configuration event (kind 33334 is replaceable) + // 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; @@ -357,7 +357,7 @@ int store_config_event_in_database(const cJSON* event) { sqlite3_bind_text(stmt, 2, cJSON_GetStringValue(pubkey_obj), -1, SQLITE_STATIC); sqlite3_bind_int64(stmt, 3, (sqlite3_int64)cJSON_GetNumberValue(created_at_obj)); sqlite3_bind_int(stmt, 4, (int)cJSON_GetNumberValue(kind_obj)); - sqlite3_bind_text(stmt, 5, "addressable", -1, SQLITE_STATIC); // kind 33334 is addressable + sqlite3_bind_text(stmt, 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); @@ -384,26 +384,9 @@ cJSON* load_config_event_from_database(const char* relay_pubkey) { sqlite3_stmt* stmt; int rc; - // Try to get admin pubkey from cache, otherwise find the most recent kind 33334 event - const char* admin_pubkey = get_admin_pubkey_cached(); - if (admin_pubkey && strlen(admin_pubkey) > 0) { - sql = "SELECT id, pubkey, created_at, kind, content, sig, tags FROM events WHERE kind = 33334 AND pubkey = ? ORDER BY created_at DESC LIMIT 1"; - rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); - if (rc != SQLITE_OK) { - log_error("Failed to prepare configuration event query"); - return NULL; - } - sqlite3_bind_text(stmt, 1, admin_pubkey, -1, SQLITE_STATIC); - } else { - // During existing relay startup, we don't know the admin pubkey yet - // Look for any kind 33334 configuration event (should only be one per relay) - sql = "SELECT id, pubkey, created_at, kind, content, sig, tags FROM events WHERE kind = 33334 ORDER BY created_at DESC LIMIT 1"; - rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); - if (rc != SQLITE_OK) { - log_error("Failed to prepare configuration event query"); - return NULL; - } - } + // Configuration is now managed through config table, not events + log_info("Configuration events are no longer stored in events table"); + return NULL; cJSON* event = NULL; if (sqlite3_step(stmt) == SQLITE_ROW) { @@ -937,7 +920,7 @@ cJSON* create_default_config_event(const unsigned char* admin_privkey_bytes, // Create and sign event using nostr_core_lib cJSON* event = nostr_create_and_sign_event( - 33334, // kind + 23455, // kind "C Nostr Relay Configuration", // content tags, // tags admin_privkey_bytes, // private key bytes for signing @@ -1614,7 +1597,7 @@ int process_configuration_event(const cJSON* event) { cJSON* kind_obj = cJSON_GetObjectItem(event, "kind"); cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey"); - if (!kind_obj || cJSON_GetNumberValue(kind_obj) != 33334) { + if (!kind_obj || (cJSON_GetNumberValue(kind_obj) != 23455 && cJSON_GetNumberValue(kind_obj) != 23456)) { log_error("Invalid event kind for configuration"); return -1; } @@ -1775,7 +1758,7 @@ int apply_runtime_config_handlers(const cJSON* old_config, const cJSON* new_conf if (handlers_applied > 0) { char audit_msg[512]; snprintf(audit_msg, sizeof(audit_msg), - "Configuration updated via kind 33334 event - %d system components reinitialized", + "Configuration updated via admin event - %d system components reinitialized", handlers_applied); log_success(audit_msg); } else { @@ -1832,7 +1815,7 @@ int apply_configuration_from_event(const cJSON* event) { // REAL-TIME EVENT HANDLER (called from main.c) // ================================ -// Handle kind 33334 configuration events received via WebSocket +// 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"); @@ -2121,12 +2104,6 @@ int process_admin_event_in_config(cJSON* event, char* error_message, size_t erro 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); - case 33334: // Legacy addressable config events (backward compatibility) - log_info("DEBUG: Routing to process_admin_config_event (legacy kind 33334)"); - return process_admin_config_event(event, error_message, error_size); - case 33335: // Legacy addressable auth events (backward compatibility) - log_info("DEBUG: Routing to process_admin_auth_event (legacy kind 33335)"); - 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); @@ -2135,7 +2112,7 @@ int process_admin_event_in_config(cJSON* event, char* error_message, size_t erro } } -// Handle Kind 23455 configuration management events and legacy Kind 33334 +// Handle Kind 23455 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; @@ -2211,10 +2188,6 @@ int process_admin_config_event(cJSON* event, char* error_message, size_t error_s continue; } - // Skip relay identifier tag (only for legacy addressable events) - if (kind == 33334 && strcmp(key, "d") == 0) { - continue; - } // Update configuration in table if (update_config_in_table(key, value) == 0) { @@ -2238,7 +2211,7 @@ int process_admin_config_event(cJSON* event, char* error_message, size_t error_s return 0; } -// Handle Kind 23456 auth rules management and legacy Kind 33335 +// 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()"); @@ -2267,13 +2240,6 @@ int process_admin_auth_event(cJSON* event, char* error_message, size_t error_siz return handle_kind_23456_unified(event, error_message, error_size, wsi); } - // Legacy Kind 33335 events use the unified handler as well - if (kind == 33335) { - log_info("DEBUG: Routing legacy Kind 33335 to unified handler"); - // For legacy events, we still use the unified handler but may need special processing - // The unified handler already supports all the functionality - 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); @@ -3072,6 +3038,85 @@ int handle_system_command_unified(cJSON* event, const char* command, char* error 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(); @@ -3535,7 +3580,7 @@ int process_startup_config_event(const cJSON* event) { // Validate event structure first cJSON* kind_obj = cJSON_GetObjectItem(event, "kind"); - if (!kind_obj || cJSON_GetNumberValue(kind_obj) != 33334) { + if (!kind_obj || cJSON_GetNumberValue(kind_obj) != 23455) { log_error("Invalid event kind for startup configuration"); return -1; } @@ -3633,14 +3678,14 @@ int process_startup_config_event_with_fallback(const cJSON* event) { // DYNAMIC EVENT GENERATION FROM CONFIG TABLE // ================================ -// Generate synthetic kind 33334 configuration event from current config table data +// 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 kind 33334 event from config table..."); + log_info("Generating synthetic configuration event from config table..."); // Get relay pubkey for event generation const char* relay_pubkey = get_config_value("relay_pubkey"); @@ -3664,7 +3709,7 @@ cJSON* generate_config_event_from_table(void) { 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_AddNumberToObject(event, "kind", 23455); cJSON_AddStringToObject(event, "content", "C Nostr Relay Configuration"); cJSON_AddStringToObject(event, "sig", "synthetic_signature"); @@ -3724,13 +3769,13 @@ cJSON* generate_config_event_from_table(void) { char success_msg[256]; snprintf(success_msg, sizeof(success_msg), - "Generated synthetic kind 33334 event with %d configuration items", config_items_added); + "Generated synthetic configuration event with %d configuration items", config_items_added); log_success(success_msg); return event; } -// Check if a REQ filter requests kind 33334 events +// Check if a REQ filter requests configuration events int req_filter_requests_config_events(const cJSON* filter) { if (!filter || !cJSON_IsObject(filter)) { return 0; @@ -3741,10 +3786,11 @@ int req_filter_requests_config_events(const cJSON* filter) { return 0; } - // Check if kinds array contains 33334 + // Check if kinds array contains configuration event kinds cJSON* kind_item = NULL; cJSON_ArrayForEach(kind_item, kinds) { - if (cJSON_IsNumber(kind_item) && (int)cJSON_GetNumberValue(kind_item) == 33334) { + int kind_val = (int)cJSON_GetNumberValue(kind_item); + if (cJSON_IsNumber(kind_item) && (kind_val == 23455 || kind_val == 23456)) { return 1; } } @@ -3758,7 +3804,7 @@ cJSON* generate_synthetic_config_event_for_subscription(const char* sub_id, cons return NULL; } - // Check if any filter requests kind 33334 + // Check if any filter requests configuration events int requests_config = 0; if (cJSON_IsArray(filters)) { @@ -3778,7 +3824,7 @@ cJSON* generate_synthetic_config_event_for_subscription(const char* sub_id, cons return NULL; } - log_info("Generating synthetic kind 33334 event for subscription"); + log_info("Generating synthetic configuration event for subscription"); // Generate synthetic config event from table cJSON* config_event = generate_config_event_from_table(); @@ -3793,12 +3839,12 @@ cJSON* generate_synthetic_config_event_for_subscription(const char* sub_id, cons cJSON_AddItemToArray(event_msg, cJSON_CreateString(sub_id)); cJSON_AddItemToArray(event_msg, config_event); - log_success("Generated synthetic kind 33334 configuration event message"); + log_success("Generated synthetic configuration event message"); return event_msg; } /** - * Generate a synthetic kind 33334 configuration event from config table data + * 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 */ diff --git a/src/config.h b/src/config.h index a18d563..39af68a 100644 --- a/src/config.h +++ b/src/config.h @@ -98,6 +98,7 @@ typedef struct { int port_override; // -1 = not set, >0 = port value char admin_privkey_override[65]; // Empty string = not set, 64-char hex = override char relay_privkey_override[65]; // Empty string = not set, 64-char hex = override + int strict_port; // 0 = allow port increment, 1 = fail if exact port unavailable } cli_options_t; // Global unified configuration cache diff --git a/src/default_config_event.h b/src/default_config_event.h index 640e981..f857548 100644 --- a/src/default_config_event.h +++ b/src/default_config_event.h @@ -8,8 +8,7 @@ * Default Configuration Event Template * * This header contains the default configuration values for the C Nostr Relay. - * These values are used to create the initial kind 33334 configuration event - * during first-time startup. + * These values are used to populate the config table during first-time startup. * * IMPORTANT: These values should never be accessed directly by other parts * of the program. They are only used during initial configuration event creation. diff --git a/src/main.c b/src/main.c index 3b2f90e..9006889 100644 --- a/src/main.c +++ b/src/main.c @@ -224,10 +224,7 @@ int handle_event_message(cJSON* event, char* error_message, size_t error_size); // Forward declaration for unified validation int nostr_validate_unified_request(const char* json_string, size_t json_length); -// Forward declaration for configuration event handling (kind 33334) -int handle_configuration_event(cJSON* event, char* error_message, size_t error_size); - -// Forward declaration for admin event processing (kinds 33334 and 33335) +// Forward declaration for admin event processing (kinds 23455 and 23456) int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size, struct lws* wsi); // Forward declaration for enhanced admin event authorization @@ -3035,7 +3032,7 @@ int is_authorized_admin_event(cJSON* event, char* error_buffer, size_t error_buf } int event_kind = kind_json->valueint; - if (event_kind != 33334 && event_kind != 33335 && event_kind != 23455 && event_kind != 23456) { + if (event_kind != 23455 && event_kind != 23456) { snprintf(error_buffer, error_buffer_size, "Event kind %d is not an admin event type", event_kind); return -1; } @@ -3356,7 +3353,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso // Cleanup event JSON string free(event_json_str); - // Check for admin events (kinds 33334, 33335, 23455, and 23456) and intercept them + // Check for admin events (kinds 23455 and 23456) and intercept them if (result == 0) { cJSON* kind_obj = cJSON_GetObjectItem(event, "kind"); if (kind_obj && cJSON_IsNumber(kind_obj)) { @@ -3378,7 +3375,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso } } - if (event_kind == 33334 || event_kind == 33335 || event_kind == 23455 || event_kind == 23456) { + if (event_kind == 23455 || event_kind == 23456) { // Enhanced admin event security - check authorization first log_info("DEBUG ADMIN: Admin event detected, checking authorization"); @@ -3697,7 +3694,7 @@ int check_port_available(int port) { } // Start libwebsockets-based WebSocket Nostr relay server -int start_websocket_relay(int port_override) { +int start_websocket_relay(int port_override, int strict_port) { struct lws_context_creation_info info; log_info("Starting libwebsockets-based Nostr relay server..."); @@ -3707,7 +3704,7 @@ int start_websocket_relay(int port_override) { int configured_port = (port_override > 0) ? port_override : get_config_int("relay_port", DEFAULT_PORT); int actual_port = configured_port; int port_attempts = 0; - const int max_port_attempts = 5; + const int max_port_attempts = 10; // Increased from 5 to 10 // Minimal libwebsockets configuration info.protocols = protocols; @@ -3726,8 +3723,8 @@ int start_websocket_relay(int port_override) { // Max payload size for Nostr events info.max_http_header_data = 4096; - // Find an available port with pre-checking - while (port_attempts < max_port_attempts) { + // Find an available port with pre-checking (or fail immediately in strict mode) + while (port_attempts < (strict_port ? 1 : max_port_attempts)) { char attempt_msg[256]; snprintf(attempt_msg, sizeof(attempt_msg), "Checking port availability: %d", actual_port); log_info(attempt_msg); @@ -3735,7 +3732,13 @@ int start_websocket_relay(int port_override) { // Pre-check if port is available if (!check_port_available(actual_port)) { port_attempts++; - if (port_attempts < max_port_attempts) { + if (strict_port) { + char error_msg[256]; + snprintf(error_msg, sizeof(error_msg), + "Strict port mode: port %d is not available", actual_port); + log_error(error_msg); + return -1; + } else if (port_attempts < max_port_attempts) { char retry_msg[256]; snprintf(retry_msg, sizeof(retry_msg), "Port %d is in use, trying port %d (attempt %d/%d)", actual_port, actual_port + 1, port_attempts + 1, max_port_attempts); @@ -3774,7 +3777,13 @@ int start_websocket_relay(int port_override) { log_warning(lws_error_msg); port_attempts++; - if (port_attempts < max_port_attempts) { + if (strict_port) { + char error_msg[256]; + snprintf(error_msg, sizeof(error_msg), + "Strict port mode: failed to bind to port %d", actual_port); + log_error(error_msg); + break; + } else if (port_attempts < max_port_attempts) { actual_port++; continue; } @@ -3840,6 +3849,7 @@ void print_usage(const char* program_name) { printf(" -p, --port PORT Override relay port (first-time startup only)\n"); printf(" -a, --admin-privkey HEX Override admin private key (64-char hex)\n"); printf(" -r, --relay-privkey HEX Override relay private key (64-char hex)\n"); + printf(" --strict-port Fail if exact port is unavailable (no port increment)\n"); printf("\n"); printf("Configuration:\n"); printf(" This relay uses event-based configuration stored in the database.\n"); @@ -3848,10 +3858,16 @@ void print_usage(const char* program_name) { printf(" After initial setup, all configuration is managed via database events.\n"); printf(" Database file: .db (created automatically)\n"); printf("\n"); + printf("Port Binding:\n"); + printf(" Default: Try up to 10 consecutive ports if requested port is busy\n"); + printf(" --strict-port: Fail immediately if exact requested port is unavailable\n"); + printf("\n"); printf("Examples:\n"); printf(" %s # Start relay (auto-configure on first run)\n", program_name); printf(" %s -p 8080 # First-time setup with port 8080\n", program_name); printf(" %s --port 9000 # First-time setup with port 9000\n", program_name); + printf(" %s --strict-port # Fail if default port 8888 is unavailable\n", program_name); + printf(" %s -p 8080 --strict-port # Fail if port 8080 is unavailable\n", program_name); printf(" %s --help # Show this help\n", program_name); printf(" %s --version # Show version info\n", program_name); printf("\n"); @@ -3870,7 +3886,8 @@ int main(int argc, char* argv[]) { cli_options_t cli_options = { .port_override = -1, // -1 = not set .admin_privkey_override = {0}, // Empty string = not set - .relay_privkey_override = {0} // Empty string = not set + .relay_privkey_override = {0}, // Empty string = not set + .strict_port = 0 // 0 = allow port increment (default) }; // Parse command line arguments @@ -3965,6 +3982,10 @@ int main(int argc, char* argv[]) { i++; // Skip the key argument log_info("Relay private key override specified"); + } else if (strcmp(argv[i], "--strict-port") == 0) { + // Strict port mode option + cli_options.strict_port = 1; + log_info("Strict port mode enabled - will fail if exact port is unavailable"); } else { log_error("Unknown argument. Use --help for usage information."); print_usage(argv[0]); @@ -4186,7 +4207,7 @@ int main(int argc, char* argv[]) { log_info("Starting relay server..."); // Start WebSocket Nostr relay server (port from configuration) - int result = start_websocket_relay(-1); // Let config system determine port + int result = start_websocket_relay(-1, cli_options.strict_port); // Let config system determine port, pass strict_port flag // Cleanup cleanup_relay_info(); diff --git a/src/sql_schema.h b/src/sql_schema.h index 19acdb8..0c9801e 100644 --- a/src/sql_schema.h +++ b/src/sql_schema.h @@ -12,7 +12,7 @@ static const char* const EMBEDDED_SCHEMA_SQL = "-- C Nostr Relay Database Schema\n\ -- SQLite schema for storing Nostr events with JSON tags support\n\ --- Event-based configuration system using kind 33334 Nostr events\n\ +-- Configuration system using config table\n\ \n\ -- Schema version tracking\n\ PRAGMA user_version = 7;\n\