v0.3.15 - How can administration take so long

This commit is contained in:
Your Name
2025-09-27 15:50:42 -04:00
parent c1c05991cf
commit 6fd3e531c3
10 changed files with 343 additions and 218 deletions

View File

@@ -27,7 +27,7 @@
## Critical Integration Issues ## Critical Integration Issues
### Event-Based Configuration System ### 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 - Admin private key shown **only once** on first startup
- Configuration changes require cryptographically signed events - Configuration changes require cryptographically signed events
- Database path determined by generated relay pubkey - Database path determined by generated relay pubkey
@@ -35,7 +35,7 @@
### First-Time Startup Sequence ### First-Time Startup Sequence
1. Relay generates admin keypair and relay keypair 1. Relay generates admin keypair and relay keypair
2. Creates database file with relay pubkey as filename 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 4. **CRITICAL**: Admin private key displayed once and never stored on disk
### Port Management ### Port Management
@@ -51,7 +51,7 @@
### Configuration Event Structure ### Configuration Event Structure
```json ```json
{ {
"kind": 33334, "kind": 23455,
"content": "C Nostr Relay Configuration", "content": "C Nostr Relay Configuration",
"tags": [ "tags": [
["d", "<relay_pubkey>"], ["d", "<relay_pubkey>"],

View File

@@ -76,6 +76,7 @@ All commands are sent as nip44 encrypted content. The following table lists all
| **Auth Rules Management** | | **Auth Rules Management** |
| `auth_add_blacklist` | `["blacklist", "pubkey", "abc123..."]` | Add pubkey to blacklist | | `auth_add_blacklist` | `["blacklist", "pubkey", "abc123..."]` | Add pubkey to blacklist |
| `auth_add_whitelist` | `["whitelist", "pubkey", "def456..."]` | Add pubkey to whitelist | | `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_all` | `["auth_query", "all"]` | Query all auth rules |
| `auth_query_type` | `["auth_query", "whitelist"]` | Query specific rule type | | `auth_query_type` | `["auth_query", "whitelist"]` | Query specific rule type |
| `auth_query_pattern` | `["auth_query", "pattern", "abc123..."]` | Query specific pattern | | `auth_query_pattern` | `["auth_query", "pattern", "abc123..."]` | Query specific pattern |

View File

@@ -33,7 +33,6 @@
h2 { h2 {
margin: 30px 0 15px 0; margin: 30px 0 15px 0;
font-weight: normal; font-weight: normal;
border-left: 4px solid black;
padding-left: 10px; padding-left: 10px;
font-size: 16px; font-size: 16px;
} }
@@ -438,50 +437,27 @@
</table> </table>
</div> </div>
<!-- Streamlined Auth Rule Input Sections --> <!-- Simplified Auth Rule Input Section -->
<div id="authRuleInputSections" style="display: block;"> <div id="authRuleInputSections" style="display: block;">
<!-- Blacklist Section --> <!-- Combined Pubkey Auth Rule Section -->
<div class="auth-rule-section"> <div class="auth-rule-section">
<h3>BLACKLIST PUBKEY</h3> <h3>MANAGE PUBKEY ACCESS</h3>
<p>Block a specific user from all operations</p> <p>Add pubkeys to whitelist (allow) or blacklist (deny) access</p>
<div class="input-group"> <div class="input-group">
<label for="blacklistPubkey">Pubkey (nsec or hex):</label> <label for="authRulePubkey">Pubkey (nsec or hex):</label>
<input type="text" id="blacklistPubkey" placeholder="nsec1... or 64-character hex pubkey"> <input type="text" id="authRulePubkey" placeholder="nsec1... or 64-character hex pubkey">
<small id="blacklistHelp">Enter nsec (will auto-convert) or 64-character hex pubkey</small> <small id="authRuleHelp">Enter nsec (will auto-convert) or 64-character hex pubkey</small>
</div>
<button type="button" id="addBlacklistBtn" onclick="addBlacklistRule()">ADD TO BLACKLIST</button>
<div id="blacklistStatus" class="rule-status"></div>
</div>
<!-- Whitelist Section -->
<div class="auth-rule-section">
<h3>WHITELIST PUBKEY</h3>
<p>Allow only specific users (converts relay to whitelist-only mode)</p>
<div class="input-group">
<label for="whitelistPubkey">Pubkey (nsec or hex):</label>
<input type="text" id="whitelistPubkey" placeholder="nsec1... or 64-character hex pubkey">
<small id="whitelistHelp">Enter nsec (will auto-convert) or 64-character hex pubkey</small>
</div> </div>
<div id="whitelistWarning" class="warning-box" style="display: none;"> <div id="whitelistWarning" class="warning-box" style="display: none;">
<strong>⚠️ WARNING:</strong> Adding whitelist rules changes relay behavior to whitelist-only mode. <strong>⚠️ WARNING:</strong> Adding whitelist rules changes relay behavior to whitelist-only mode.
Only whitelisted users will be able to interact with the relay. Only whitelisted users will be able to interact with the relay.
</div> </div>
<button type="button" id="addWhitelistBtn" onclick="addWhitelistRule()">ADD TO WHITELIST</button> <div class="inline-buttons">
<div id="whitelistStatus" class="rule-status"></div> <button type="button" id="addWhitelistBtn" onclick="addWhitelistRule()">ADD TO WHITELIST</button>
</div> <button type="button" id="addBlacklistBtn" onclick="addBlacklistRule()">ADD TO BLACKLIST</button>
<!-- Hash Blacklist Section -->
<div class="auth-rule-section">
<h3>BLACKLIST CONTENT HASH</h3>
<p>Block specific content by SHA-256 hash</p>
<div class="input-group">
<label for="hashBlacklist">Content Hash (SHA-256):</label>
<input type="text" id="hashBlacklist" placeholder="64-character hex SHA-256 hash">
<small id="hashHelp">Enter 64-character hex SHA-256 hash of content to block</small>
</div> </div>
<button type="button" id="addHashBlacklistBtn" onclick="addHashBlacklistRule()">BLOCK CONTENT HASH</button> <div id="authRuleStatus" class="rule-status"></div>
<div id="hashStatus" class="rule-status"></div>
</div> </div>
</div> </div>
@@ -1133,12 +1109,25 @@
console.log('Total results:', responseData.total_results); console.log('Total results:', responseData.total_results);
console.log('Data:', responseData.data); 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') { if (typeof logTestEvent === 'function') {
logTestEvent('RECV', `Auth query response: ${responseData.query_type}, ${responseData.total_results} results`, 'AUTH_QUERY'); logTestEvent('RECV', `Auth query response: ${responseData.query_type}, ${responseData.total_results} results`, 'AUTH_QUERY');
if (responseData.data && responseData.data.length > 0) { if (responseData.data && responseData.data.length > 0) {
responseData.data.forEach((rule, index) => { 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 { } else {
logTestEvent('RECV', 'No auth rules found', 'AUTH_QUERY'); logTestEvent('RECV', 'No auth rules found', 'AUTH_QUERY');
@@ -1152,6 +1141,30 @@
console.log('Command:', responseData.command); console.log('Command:', responseData.command);
console.log('Status:', responseData.status); 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') { if (typeof logTestEvent === 'function') {
logTestEvent('RECV', `System command response: ${responseData.command} - ${responseData.status}`, 'SYSTEM_CMD'); logTestEvent('RECV', `System command response: ${responseData.command} - ${responseData.status}`, 'SYSTEM_CMD');
} }
@@ -1163,12 +1176,25 @@
console.log('Operation:', responseData.operation); console.log('Operation:', responseData.operation);
console.log('Status:', responseData.status); 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') { if (typeof logTestEvent === 'function') {
logTestEvent('RECV', `Auth rule response: ${responseData.operation} - ${responseData.status}`, 'AUTH_RULE'); logTestEvent('RECV', `Auth rule response: ${responseData.operation} - ${responseData.status}`, 'AUTH_RULE');
if (responseData.processed_rules) { if (responseData.processed_rules) {
responseData.processed_rules.forEach((rule, index) => { 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() { async function loadAuthRules() {
try { try {
log('Loading auth rules...', 'INFO'); log('Loading auth rules via admin API...', 'INFO');
updateAuthRulesStatus('loading'); updateAuthRulesStatus('loading');
// TODO: Implement actual auth rules loading via WebSocket/HTTP if (!isLoggedIn || !userPubkey) {
// For now, show empty state throw new Error('Must be logged in to load auth rules');
currentAuthRules = []; }
displayAuthRules(currentAuthRules);
updateAuthRulesStatus('loaded'); 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) { } catch (error) {
log(`Failed to load auth rules: ${error.message}`, 'ERROR'); log(`Failed to load auth rules: ${error.message}`, 'ERROR');
updateAuthRulesStatus('error'); updateAuthRulesStatus('error');
currentAuthRules = [];
displayAuthRules(currentAuthRules);
} }
} }
@@ -1747,7 +1811,7 @@
} }
} }
// Delete auth rule // Delete auth rule using admin API
async function deleteAuthRule(index) { async function deleteAuthRule(index) {
if (index < 0 || index >= currentAuthRules.length) return; if (index < 0 || index >= currentAuthRules.length) return;
@@ -1759,12 +1823,57 @@
try { try {
log(`Deleting auth rule: ${rule.rule_type} - ${rule.pattern_value || rule.rule_target}`, 'INFO'); log(`Deleting auth rule: ${rule.rule_type} - ${rule.pattern_value || rule.rule_target}`, 'INFO');
// TODO: Implement actual rule deletion via WebSocket kind 23456 event if (!isLoggedIn || !userPubkey) {
// For now, just remove from local array 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); currentAuthRules.splice(index, 1);
displayAuthRules(currentAuthRules); displayAuthRules(currentAuthRules);
log('Auth rule deleted (placeholder implementation)', 'INFO');
} catch (error) { } catch (error) {
log(`Failed to delete auth rule: ${error.message}`, 'ERROR'); log(`Failed to delete auth rule: ${error.message}`, 'ERROR');
@@ -1953,10 +2062,10 @@
return null; // Invalid format return null; // Invalid format
} }
// Add blacklist rule // Add blacklist rule (updated to use combined input)
function addBlacklistRule() { function addBlacklistRule() {
const input = document.getElementById('blacklistPubkey'); const input = document.getElementById('authRulePubkey');
const statusDiv = document.getElementById('blacklistStatus'); const statusDiv = document.getElementById('authRuleStatus');
if (!input || !statusDiv) return; if (!input || !statusDiv) return;
@@ -2011,10 +2120,10 @@
}); });
} }
// Add whitelist rule // Add whitelist rule (updated to use combined input)
function addWhitelistRule() { function addWhitelistRule() {
const input = document.getElementById('whitelistPubkey'); const input = document.getElementById('authRulePubkey');
const statusDiv = document.getElementById('whitelistStatus'); const statusDiv = document.getElementById('authRuleStatus');
const warningDiv = document.getElementById('whitelistWarning'); const warningDiv = document.getElementById('whitelistWarning');
if (!input || !statusDiv) return; if (!input || !statusDiv) return;
@@ -2041,6 +2150,11 @@
return; return;
} }
// Show whitelist warning
if (warningDiv) {
warningDiv.style.display = 'block';
}
statusDiv.className = 'rule-status'; statusDiv.className = 'rule-status';
statusDiv.textContent = 'Adding to whitelist...'; statusDiv.textContent = 'Adding to whitelist...';
@@ -2070,57 +2184,7 @@
}); });
} }
// Add hash blacklist rule // Add auth rule via SimplePool (kind 23456 event) - FIXED to match working test pattern
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)
async function addAuthRuleViaWebSocket(ruleData) { async function addAuthRuleViaWebSocket(ruleData) {
if (!isLoggedIn || !userPubkey) { if (!isLoggedIn || !userPubkey) {
throw new Error('Must be logged in to add auth rules'); throw new Error('Must be logged in to add auth rules');
@@ -2133,60 +2197,53 @@
try { try {
log(`Adding auth rule: ${ruleData.rule_type} - ${ruleData.pattern_value.substring(0, 16)}...`, 'INFO'); log(`Adding auth rule: ${ruleData.rule_type} - ${ruleData.pattern_value.substring(0, 16)}...`, 'INFO');
// Map client-side rule types to database schema values // Map client-side rule types to command array format (matching working tests)
let dbRuleType, dbPatternType, dbAction; let commandRuleType, commandPatternType;
switch (ruleData.rule_type) { switch (ruleData.rule_type) {
case 'pubkey_blacklist': case 'pubkey_blacklist':
dbRuleType = 'blacklist'; commandRuleType = 'blacklist';
dbPatternType = 'pubkey'; commandPatternType = 'pubkey';
dbAction = 'deny';
break; break;
case 'pubkey_whitelist': case 'pubkey_whitelist':
dbRuleType = 'whitelist'; commandRuleType = 'whitelist';
dbPatternType = 'pubkey'; commandPatternType = 'pubkey';
dbAction = 'allow';
break; break;
case 'hash_blacklist': case 'hash_blacklist':
dbRuleType = 'blacklist'; commandRuleType = 'blacklist';
dbPatternType = 'pubkey'; // Schema supports: pubkey, kind, ip, global - using pubkey for hash for now commandPatternType = 'hash';
dbAction = 'deny';
break; break;
default: default:
throw new Error(`Unknown rule type: ${ruleData.rule_type}`); throw new Error(`Unknown rule type: ${ruleData.rule_type}`);
} }
// Map pattern type to database schema values // Create command array in the same format as working tests
if (ruleData.pattern_type === 'Global') { // Format: ["blacklist", "pubkey", "abc123..."] or ["whitelist", "pubkey", "def456..."]
dbPatternType = 'global'; const command_array = `["${commandRuleType}", "${commandPatternType}", "${ruleData.pattern_value}"]`;
} else if (ruleData.pattern_type === 'pubkey') {
dbPatternType = 'pubkey'; // 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 = { const authEvent = {
kind: 23456, kind: 23456,
pubkey: userPubkey, pubkey: userPubkey,
created_at: Math.floor(Date.now() / 1000), created_at: Math.floor(Date.now() / 1000),
tags: [ tags: [
[dbRuleType, dbPatternType, ruleData.pattern_value] ["p", getRelayPubkey()]
], ],
content: JSON.stringify({ content: encrypted_content
action: 'add',
rule_type: dbRuleType,
pattern_type: dbPatternType,
pattern_value: ruleData.pattern_value,
rule_action: dbAction
})
}; };
// DEBUG: Log the complete event structure being sent // 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('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 (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 ==='); console.log('=== END AUTH RULE EVENT DEBUG ===');
// Sign the event using the standard NIP-07 interface // Sign the event using the standard NIP-07 interface

View File

@@ -282,14 +282,14 @@ cd build
# Start relay in background and capture its PID # Start relay in background and capture its PID
if [ "$USE_TEST_KEYS" = true ]; then if [ "$USE_TEST_KEYS" = true ]; then
echo "Using deterministic test keys for development..." 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 elif [ -n "$RELAY_ARGS" ]; then
echo "Starting relay with custom configuration..." 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 else
# No command line arguments needed for random key generation # No command line arguments needed for random key generation
echo "Starting relay with 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 fi
RELAY_PID=$! RELAY_PID=$!
# Change back to original directory # Change back to original directory

View File

@@ -1 +1 @@
652192 659207

View File

@@ -342,7 +342,7 @@ int store_config_event_in_database(const cJSON* event) {
return -1; 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 (?, ?, ?, ?, ?, ?, ?, ?)"; const char* sql = "INSERT OR REPLACE INTO events (id, pubkey, created_at, kind, event_type, content, sig, tags) VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
sqlite3_stmt* stmt; 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_text(stmt, 2, cJSON_GetStringValue(pubkey_obj), -1, SQLITE_STATIC);
sqlite3_bind_int64(stmt, 3, (sqlite3_int64)cJSON_GetNumberValue(created_at_obj)); sqlite3_bind_int64(stmt, 3, (sqlite3_int64)cJSON_GetNumberValue(created_at_obj));
sqlite3_bind_int(stmt, 4, (int)cJSON_GetNumberValue(kind_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, 6, cJSON_GetStringValue(content_obj), -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 7, cJSON_GetStringValue(sig_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); 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; sqlite3_stmt* stmt;
int rc; int rc;
// Try to get admin pubkey from cache, otherwise find the most recent kind 33334 event // Configuration is now managed through config table, not events
const char* admin_pubkey = get_admin_pubkey_cached(); log_info("Configuration events are no longer stored in events table");
if (admin_pubkey && strlen(admin_pubkey) > 0) { return NULL;
sql = "SELECT id, pubkey, created_at, kind, content, sig, tags FROM events WHERE kind = 33334 AND pubkey = ? ORDER BY created_at DESC LIMIT 1";
rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
log_error("Failed to prepare configuration event query");
return NULL;
}
sqlite3_bind_text(stmt, 1, admin_pubkey, -1, SQLITE_STATIC);
} else {
// During existing relay startup, we don't know the admin pubkey yet
// Look for any kind 33334 configuration event (should only be one per relay)
sql = "SELECT id, pubkey, created_at, kind, content, sig, tags FROM events WHERE kind = 33334 ORDER BY created_at DESC LIMIT 1";
rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
log_error("Failed to prepare configuration event query");
return NULL;
}
}
cJSON* event = NULL; cJSON* event = NULL;
if (sqlite3_step(stmt) == SQLITE_ROW) { 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 // Create and sign event using nostr_core_lib
cJSON* event = nostr_create_and_sign_event( cJSON* event = nostr_create_and_sign_event(
33334, // kind 23455, // kind
"C Nostr Relay Configuration", // content "C Nostr Relay Configuration", // content
tags, // tags tags, // tags
admin_privkey_bytes, // private key bytes for signing 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* kind_obj = cJSON_GetObjectItem(event, "kind");
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey"); 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"); log_error("Invalid event kind for configuration");
return -1; return -1;
} }
@@ -1775,7 +1758,7 @@ int apply_runtime_config_handlers(const cJSON* old_config, const cJSON* new_conf
if (handlers_applied > 0) { if (handlers_applied > 0) {
char audit_msg[512]; char audit_msg[512];
snprintf(audit_msg, sizeof(audit_msg), 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); handlers_applied);
log_success(audit_msg); log_success(audit_msg);
} else { } else {
@@ -1832,7 +1815,7 @@ int apply_configuration_from_event(const cJSON* event) {
// REAL-TIME EVENT HANDLER (called from main.c) // 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) { int handle_configuration_event(cJSON* event, char* error_message, size_t error_size) {
if (!event) { if (!event) {
snprintf(error_message, error_size, "invalid: null configuration 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 case 23456: // New ephemeral auth rules management
log_info("DEBUG: Routing to process_admin_auth_event (kind 23456)"); log_info("DEBUG: Routing to process_admin_auth_event (kind 23456)");
return process_admin_auth_event(event, error_message, error_size, wsi); 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: default:
log_error("DEBUG: Unsupported admin event kind"); log_error("DEBUG: Unsupported admin event kind");
printf(" Unsupported kind: %d\n", 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) { int process_admin_config_event(cJSON* event, char* error_message, size_t error_size) {
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind"); cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
int kind = kind_obj ? (int)cJSON_GetNumberValue(kind_obj) : 0; 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; continue;
} }
// Skip relay identifier tag (only for legacy addressable events)
if (kind == 33334 && strcmp(key, "d") == 0) {
continue;
}
// Update configuration in table // Update configuration in table
if (update_config_in_table(key, value) == 0) { 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; 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) { 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()"); 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); 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"); log_error("DEBUG: Unsupported auth event kind in process_admin_auth_event");
printf(" Unsupported kind: %d\n", kind); 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"); snprintf(error_message, error_size, "failed to send clear auth rules response");
return -1; 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) { else if (strcmp(command, "system_status") == 0) {
// Build system status response // Build system status response
cJSON* response = cJSON_CreateObject(); cJSON* response = cJSON_CreateObject();
@@ -3535,7 +3580,7 @@ int process_startup_config_event(const cJSON* event) {
// Validate event structure first // Validate event structure first
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind"); 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"); log_error("Invalid event kind for startup configuration");
return -1; return -1;
} }
@@ -3633,14 +3678,14 @@ int process_startup_config_event_with_fallback(const cJSON* event) {
// DYNAMIC EVENT GENERATION FROM CONFIG TABLE // 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) { cJSON* generate_config_event_from_table(void) {
if (!g_db) { if (!g_db) {
log_error("Database not available for config event generation"); log_error("Database not available for config event generation");
return NULL; 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 // Get relay pubkey for event generation
const char* relay_pubkey = get_config_value("relay_pubkey"); 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, "id", "synthetic_config_event_id");
cJSON_AddStringToObject(event, "pubkey", relay_pubkey); // Use relay pubkey as event author cJSON_AddStringToObject(event, "pubkey", relay_pubkey); // Use relay pubkey as event author
cJSON_AddNumberToObject(event, "created_at", (double)time(NULL)); 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, "content", "C Nostr Relay Configuration");
cJSON_AddStringToObject(event, "sig", "synthetic_signature"); cJSON_AddStringToObject(event, "sig", "synthetic_signature");
@@ -3724,13 +3769,13 @@ cJSON* generate_config_event_from_table(void) {
char success_msg[256]; char success_msg[256];
snprintf(success_msg, sizeof(success_msg), 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); log_success(success_msg);
return event; 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) { int req_filter_requests_config_events(const cJSON* filter) {
if (!filter || !cJSON_IsObject(filter)) { if (!filter || !cJSON_IsObject(filter)) {
return 0; return 0;
@@ -3741,10 +3786,11 @@ int req_filter_requests_config_events(const cJSON* filter) {
return 0; return 0;
} }
// Check if kinds array contains 33334 // Check if kinds array contains configuration event kinds
cJSON* kind_item = NULL; cJSON* kind_item = NULL;
cJSON_ArrayForEach(kind_item, kinds) { 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; return 1;
} }
} }
@@ -3758,7 +3804,7 @@ cJSON* generate_synthetic_config_event_for_subscription(const char* sub_id, cons
return NULL; return NULL;
} }
// Check if any filter requests kind 33334 // Check if any filter requests configuration events
int requests_config = 0; int requests_config = 0;
if (cJSON_IsArray(filters)) { if (cJSON_IsArray(filters)) {
@@ -3778,7 +3824,7 @@ cJSON* generate_synthetic_config_event_for_subscription(const char* sub_id, cons
return NULL; 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 // Generate synthetic config event from table
cJSON* config_event = generate_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, cJSON_CreateString(sub_id));
cJSON_AddItemToArray(event_msg, config_event); 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; 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 * This allows WebSocket clients to fetch configuration via REQ messages
* Returns JSON string that must be freed by caller * Returns JSON string that must be freed by caller
*/ */

View File

@@ -98,6 +98,7 @@ typedef struct {
int port_override; // -1 = not set, >0 = port value int port_override; // -1 = not set, >0 = port value
char admin_privkey_override[65]; // Empty string = not set, 64-char hex = override 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 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; } cli_options_t;
// Global unified configuration cache // Global unified configuration cache

View File

@@ -8,8 +8,7 @@
* Default Configuration Event Template * Default Configuration Event Template
* *
* This header contains the default configuration values for the C Nostr Relay. * This header contains the default configuration values for the C Nostr Relay.
* These values are used to create the initial kind 33334 configuration event * These values are used to populate the config table during first-time startup.
* during first-time startup.
* *
* IMPORTANT: These values should never be accessed directly by other parts * IMPORTANT: These values should never be accessed directly by other parts
* of the program. They are only used during initial configuration event creation. * of the program. They are only used during initial configuration event creation.

View File

@@ -224,10 +224,7 @@ int handle_event_message(cJSON* event, char* error_message, size_t error_size);
// Forward declaration for unified validation // Forward declaration for unified validation
int nostr_validate_unified_request(const char* json_string, size_t json_length); int nostr_validate_unified_request(const char* json_string, size_t json_length);
// Forward declaration for configuration event handling (kind 33334) // Forward declaration for admin event processing (kinds 23455 and 23456)
int handle_configuration_event(cJSON* event, char* error_message, size_t error_size);
// Forward declaration for admin event processing (kinds 33334 and 33335)
int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size, struct lws* wsi); 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 // 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; 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); snprintf(error_buffer, error_buffer_size, "Event kind %d is not an admin event type", event_kind);
return -1; return -1;
} }
@@ -3356,7 +3353,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
// Cleanup event JSON string // Cleanup event JSON string
free(event_json_str); 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) { if (result == 0) {
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind"); cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
if (kind_obj && cJSON_IsNumber(kind_obj)) { 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 // Enhanced admin event security - check authorization first
log_info("DEBUG ADMIN: Admin event detected, checking authorization"); 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 // 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; struct lws_context_creation_info info;
log_info("Starting libwebsockets-based Nostr relay server..."); 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 configured_port = (port_override > 0) ? port_override : get_config_int("relay_port", DEFAULT_PORT);
int actual_port = configured_port; int actual_port = configured_port;
int port_attempts = 0; int port_attempts = 0;
const int max_port_attempts = 5; const int max_port_attempts = 10; // Increased from 5 to 10
// Minimal libwebsockets configuration // Minimal libwebsockets configuration
info.protocols = protocols; info.protocols = protocols;
@@ -3726,8 +3723,8 @@ int start_websocket_relay(int port_override) {
// Max payload size for Nostr events // Max payload size for Nostr events
info.max_http_header_data = 4096; info.max_http_header_data = 4096;
// Find an available port with pre-checking // Find an available port with pre-checking (or fail immediately in strict mode)
while (port_attempts < max_port_attempts) { while (port_attempts < (strict_port ? 1 : max_port_attempts)) {
char attempt_msg[256]; char attempt_msg[256];
snprintf(attempt_msg, sizeof(attempt_msg), "Checking port availability: %d", actual_port); snprintf(attempt_msg, sizeof(attempt_msg), "Checking port availability: %d", actual_port);
log_info(attempt_msg); log_info(attempt_msg);
@@ -3735,7 +3732,13 @@ int start_websocket_relay(int port_override) {
// Pre-check if port is available // Pre-check if port is available
if (!check_port_available(actual_port)) { if (!check_port_available(actual_port)) {
port_attempts++; 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]; char retry_msg[256];
snprintf(retry_msg, sizeof(retry_msg), "Port %d is in use, trying port %d (attempt %d/%d)", 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); 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); log_warning(lws_error_msg);
port_attempts++; 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++; actual_port++;
continue; 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(" -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(" -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(" -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("\n");
printf("Configuration:\n"); printf("Configuration:\n");
printf(" This relay uses event-based configuration stored in the database.\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(" After initial setup, all configuration is managed via database events.\n");
printf(" Database file: <relay_pubkey>.db (created automatically)\n"); printf(" Database file: <relay_pubkey>.db (created automatically)\n");
printf("\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("Examples:\n");
printf(" %s # Start relay (auto-configure on first run)\n", program_name); 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 -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 --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 --help # Show this help\n", program_name);
printf(" %s --version # Show version info\n", program_name); printf(" %s --version # Show version info\n", program_name);
printf("\n"); printf("\n");
@@ -3870,7 +3886,8 @@ int main(int argc, char* argv[]) {
cli_options_t cli_options = { cli_options_t cli_options = {
.port_override = -1, // -1 = not set .port_override = -1, // -1 = not set
.admin_privkey_override = {0}, // Empty string = 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 // Parse command line arguments
@@ -3965,6 +3982,10 @@ int main(int argc, char* argv[]) {
i++; // Skip the key argument i++; // Skip the key argument
log_info("Relay private key override specified"); 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 { } else {
log_error("Unknown argument. Use --help for usage information."); log_error("Unknown argument. Use --help for usage information.");
print_usage(argv[0]); print_usage(argv[0]);
@@ -4186,7 +4207,7 @@ int main(int argc, char* argv[]) {
log_info("Starting relay server..."); log_info("Starting relay server...");
// Start WebSocket Nostr relay server (port from configuration) // 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
cleanup_relay_info(); cleanup_relay_info();

View File

@@ -12,7 +12,7 @@
static const char* const EMBEDDED_SCHEMA_SQL = static const char* const EMBEDDED_SCHEMA_SQL =
"-- C Nostr Relay Database Schema\n\ "-- C Nostr Relay Database Schema\n\
-- SQLite schema for storing Nostr events with JSON tags support\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\ \n\
-- Schema version tracking\n\ -- Schema version tracking\n\
PRAGMA user_version = 7;\n\ PRAGMA user_version = 7;\n\