Compare commits

...

2 Commits

16 changed files with 670 additions and 823 deletions

View File

@@ -1,94 +0,0 @@
Inconsistency audit with exact fixes (treating README.md as authoritative)
Backend auth_rules schema mismatch
Evidence:
Migration (creates mismatched columns):
See "CREATE TABLE IF NOT EXISTS auth_rules ... UNIQUE(rule_type, operation, rule_target)".
Active code uses rule_type, pattern_type, pattern_value, action:
Insert: "INSERT INTO auth_rules (rule_type, pattern_type, pattern_value, action)"
Delete: "DELETE FROM auth_rules WHERE rule_type = ? AND pattern_type = ? AND pattern_value = ?"
Query mapping: map_auth_query_type_to_response()
Queries: "... WHERE rule_type LIKE '%blacklist%'"
Validator checks:
"... WHERE rule_type = 'blacklist' AND pattern_type = 'pubkey' AND pattern_value = ?"
"... WHERE rule_type = 'blacklist' AND pattern_type = 'hash' AND pattern_value = ?"
"... WHERE rule_type = 'whitelist' AND pattern_type = 'pubkey' AND pattern_value = ?"
Embedded schema expects pattern columns and active/indexes:
"CREATE TABLE auth_rules ( ... )"
"CREATE INDEX idx_auth_rules_pattern ON auth_rules(pattern_type, pattern_value)"
"CREATE INDEX idx_auth_rules_active ON auth_rules(active)"
Fix (update migration to align with sql_schema.h/config.c):
Replace the DDL at "create_auth_rules_sql" with: CREATE TABLE IF NOT EXISTS auth_rules ( id INTEGER PRIMARY KEY AUTOINCREMENT, rule_type TEXT NOT NULL, -- 'whitelist' | 'blacklist' pattern_type TEXT NOT NULL, -- 'pubkey' | 'hash' | future pattern_value TEXT NOT NULL, -- hex pubkey/hash action TEXT NOT NULL, -- 'allow' | 'deny' active INTEGER DEFAULT 1, created_at INTEGER DEFAULT (strftime('%s','now')), UNIQUE(rule_type, pattern_type, pattern_value) );
After creation, also create indexes as in "sql_schema.h":
CREATE INDEX idx_auth_rules_pattern ON auth_rules(pattern_type, pattern_value);
CREATE INDEX idx_auth_rules_type ON auth_rules(rule_type);
CREATE INDEX idx_auth_rules_active ON auth_rules(active);
Duplicate UI function + stale DOM id usage
Evidence:
Duplicate definition of disconnectFromRelay() and disconnectFromRelay(); the second overwrites the first and uses legacy element access paths.
Stale variable: "const relayUrl = document.getElementById('relay-url');" — no element with id="relay-url" exists; the real input is "relay-connection-url" and is referenced as "relayConnectionUrl".
Calls using relayUrl.value.trim() (must use relayConnectionUrl):
"sendConfigUpdateCommand() publish URL"
"loadAuthRules() publish URL"
"deleteAuthRule() publish URL"
Tests:
"testGetAuthRules()"
"testClearAuthRules()"
"testAddBlacklist()"
"testAddWhitelist()"
"testConfigQuery()"
"testPostEvent()"
Fix:
Remove the duplicate legacy function entirely: delete the second disconnectFromRelay().
Remove stale variable: delete "const relayUrl = document.getElementById('relay-url');".
Replace every relayUrl.value.trim() occurrence with relayConnectionUrl.value.trim() at the lines listed above.
Supported NIPs inconsistency (README vs UI fallback)
Evidence:
README implemented NIPs checklist (authoritative): "NIPs list" shows: 1, 9, 11, 13, 15, 20, 33, 40, 42 implemented.
UI fallback for manual relay info includes unsupported/undocumented NIPs and misses implemented ones:
"supported_nips: [1, 2, 4, 9, 11, 12, 15, 16, 20, 22]"
"supported_nips: [1, 2, 4, 9, 11, 12, 15, 16, 20, 22]"
Fix:
Replace both arrays with: [1, 9, 11, 13, 15, 20, 33, 40, 42]
Config key mismatches (README vs UI edit form)
Evidence:
README keys (authoritative): "Available Configuration Keys"(README.md:110)
relay_description, relay_contact, max_connections, max_subscriptions_per_client, max_event_tags, max_content_length, auth_enabled, nip42_auth_required, nip42_auth_required_kinds, nip42_challenge_timeout, pow_min_difficulty, nip40_expiration_enabled
UI currently declares/uses many non-README keys:
Field types: "fieldTypes" include nip42_auth_required_events, nip42_auth_required_subscriptions, relay_port, pow_mode, nip40_expiration_strict, nip40_expiration_filter, nip40_expiration_grace_period, max_total_subscriptions, max_filters_per_subscription, max_message_length, default_limit, max_limit.
Descriptions: "descriptions" reflect the same non-README keys.
Fix:
Restrict UI form generation to README keys and rename mismatches:
Combine nip42_auth_required_events/subscriptions into READMEs "nip42_auth_required" (boolean).
Rename nip42_challenge_expiration to "nip42_challenge_timeout".
Remove or hide (advanced section) non-README keys: relay_port, pow_mode, nip40_expiration_strict, nip40_expiration_filter, nip40_expiration_grace_period, max_total_subscriptions, max_filters_per_subscription, max_message_length, default_limit, max_limit.
Update both "fieldTypes" and "descriptions" to reflect only README keys (data types and labels consistent).
First-time startup port override (-p) ignored when -a and -r are also provided
Observation:
You confirmed: first run with -p 7777 works, but with -p plus -a and -r the override isnt honored.
Likely cause:
The code path that handles admin/relay key overrides on first-time setup bypasses persisting the CLI port override to config/unified cache before server start, so "start_websocket_relay(-1, ...)" falls back to default.
Fix:
Ensure first_time_startup_sequence applies cli_options.port_override to persistent config and cache BEFORE default config insertion and before starting the server. Specifically:
In the first-time path (main):
After "first_time_startup_sequence(&cli_options)" and before creating defaults on the -a/-r path at "populate_default_config_values()", write the port override:
set_config_value_in_table("relay_port", "<port>", "integer", "WebSocket port", "relay", 0);
and update unified cache if required by the port resolution code.
Verify the code path where -a/-r trigger direct table population also applies/overwrites the port with the CLI-provided value.
Add a regression test to assert that -p is honored with and without -a/-r on first run.
Minor consistency recommendations
UI NIP-11 fallback version string:
Consider aligning with backend version source (e.g., src/version.h). The UI currently hardcodes "1.0.0" at "version: '1.0.0'".
UI hardcoded relay pubkey fallback:
"getRelayPubkey()" returns a constant when not connected. Safe for dev, but should not leak into production paths.
Added TODO items (as requested)
The following todos were added/organized:
Remove duplicate disconnectFromRelay() and standardize to relay-connection-url
Replace all relayUrl.value references with relayConnectionUrl.value in api/index.html
Align Supported NIPs fallback arrays in api/index.html with README (1,9,11,13,15,20,33,40,42)
Update config form keys/descriptions in api/index.html to match README keys
Fix backend auth_rules migration in src/main.c to match src/sql_schema.h/src/config.c
Investigate and fix first-time startup port override ignored when -a and -r are provided
Add tests for port override and auth_rules flows
Rebuild via ./make_and_restart_relay.sh and validate against README

118
Makefile
View File

@@ -36,10 +36,10 @@ $(NOSTR_CORE_LIB):
@echo "Building nostr_core_lib..."
cd nostr_core_lib && ./build.sh
# Generate version.h from git tags
src/version.h:
# Generate main.h from git tags
src/main.h:
@if [ -d .git ]; then \
echo "Generating version.h from git tags..."; \
echo "Generating main.h from git tags..."; \
RAW_VERSION=$$(git describe --tags --always 2>/dev/null || echo "unknown"); \
if echo "$$RAW_VERSION" | grep -q "^v[0-9]"; then \
CLEAN_VERSION=$$(echo "$$RAW_VERSION" | sed 's/^v//' | cut -d- -f1); \
@@ -51,54 +51,98 @@ src/version.h:
VERSION="v0.0.0"; \
MAJOR=0; MINOR=0; PATCH=0; \
fi; \
echo "/* Auto-generated version information */" > src/version.h; \
echo "#ifndef VERSION_H" >> src/version.h; \
echo "#define VERSION_H" >> src/version.h; \
echo "" >> src/version.h; \
echo "#define VERSION \"$$VERSION\"" >> src/version.h; \
echo "#define VERSION_MAJOR $$MAJOR" >> src/version.h; \
echo "#define VERSION_MINOR $$MINOR" >> src/version.h; \
echo "#define VERSION_PATCH $$PATCH" >> src/version.h; \
echo "" >> src/version.h; \
echo "#endif /* VERSION_H */" >> src/version.h; \
echo "Generated version.h with clean version: $$VERSION"; \
elif [ ! -f src/version.h ]; then \
echo "Git not available and version.h missing, creating fallback version.h..."; \
echo "/*" > src/main.h; \
echo " * C-Relay Main Header - Version and Metadata Information" >> src/main.h; \
echo " *" >> src/main.h; \
echo " * This header contains version information and relay metadata that is" >> src/main.h; \
echo " * automatically updated by the build system (build_and_push.sh)." >> src/main.h; \
echo " *" >> src/main.h; \
echo " * The build_and_push.sh script updates VERSION and related macros when" >> src/main.h; \
echo " * creating new releases." >> src/main.h; \
echo " */" >> src/main.h; \
echo "" >> src/main.h; \
echo "#ifndef MAIN_H" >> src/main.h; \
echo "#define MAIN_H" >> src/main.h; \
echo "" >> src/main.h; \
echo "// Version information (auto-updated by build_and_push.sh)" >> src/main.h; \
echo "#define VERSION \"$$VERSION\"" >> src/main.h; \
echo "#define VERSION_MAJOR $$MAJOR" >> src/main.h; \
echo "#define VERSION_MINOR $$MINOR" >> src/main.h; \
echo "#define VERSION_PATCH $$PATCH" >> src/main.h; \
echo "" >> src/main.h; \
echo "// Relay metadata (authoritative source for NIP-11 information)" >> src/main.h; \
echo "#define RELAY_NAME \"C-Relay\"" >> src/main.h; \
echo "#define RELAY_DESCRIPTION \"High-performance C Nostr relay with SQLite storage\"" >> src/main.h; \
echo "#define RELAY_CONTACT \"\"" >> src/main.h; \
echo "#define RELAY_SOFTWARE \"https://git.laantungir.net/laantungir/c-relay.git\"" >> src/main.h; \
echo "#define RELAY_VERSION VERSION // Use the same version as the build" >> src/main.h; \
echo "#define SUPPORTED_NIPS \"1,2,4,9,11,12,13,15,16,20,22,33,40,42\"" >> src/main.h; \
echo "#define LANGUAGE_TAGS \"\"" >> src/main.h; \
echo "#define RELAY_COUNTRIES \"\"" >> src/main.h; \
echo "#define POSTING_POLICY \"\"" >> src/main.h; \
echo "#define PAYMENTS_URL \"\"" >> src/main.h; \
echo "" >> src/main.h; \
echo "#endif /* MAIN_H */" >> src/main.h; \
echo "Generated main.h with clean version: $$VERSION"; \
elif [ ! -f src/main.h ]; then \
echo "Git not available and main.h missing, creating fallback main.h..."; \
VERSION="v0.0.0"; \
echo "/* Auto-generated version information */" > src/version.h; \
echo "#ifndef VERSION_H" >> src/version.h; \
echo "#define VERSION_H" >> src/version.h; \
echo "" >> src/version.h; \
echo "#define VERSION \"$$VERSION\"" >> src/version.h; \
echo "#define VERSION_MAJOR 0" >> src/version.h; \
echo "#define VERSION_MINOR 0" >> src/version.h; \
echo "#define VERSION_PATCH 0" >> src/version.h; \
echo "" >> src/version.h; \
echo "#endif /* VERSION_H */" >> src/version.h; \
echo "Created fallback version.h with version: $$VERSION"; \
echo "/*" > src/main.h; \
echo " * C-Relay Main Header - Version and Metadata Information" >> src/main.h; \
echo " *" >> src/main.h; \
echo " * This header contains version information and relay metadata that is" >> src/main.h; \
echo " * automatically updated by the build system (build_and_push.sh)." >> src/main.h; \
echo " *" >> src/main.h; \
echo " * The build_and_push.sh script updates VERSION and related macros when" >> src/main.h; \
echo " * creating new releases." >> src/main.h; \
echo " */" >> src/main.h; \
echo "" >> src/main.h; \
echo "#ifndef MAIN_H" >> src/main.h; \
echo "#define MAIN_H" >> src/main.h; \
echo "" >> src/main.h; \
echo "// Version information (auto-updated by build_and_push.sh)" >> src/main.h; \
echo "#define VERSION \"$$VERSION\"" >> src/main.h; \
echo "#define VERSION_MAJOR 0" >> src/main.h; \
echo "#define VERSION_MINOR 0" >> src/main.h; \
echo "#define VERSION_PATCH 0" >> src/main.h; \
echo "" >> src/main.h; \
echo "// Relay metadata (authoritative source for NIP-11 information)" >> src/main.h; \
echo "#define RELAY_NAME \"C-Relay\"" >> src/main.h; \
echo "#define RELAY_DESCRIPTION \"High-performance C Nostr relay with SQLite storage\"" >> src/main.h; \
echo "#define RELAY_CONTACT \"\"" >> src/main.h; \
echo "#define RELAY_SOFTWARE \"https://git.laantungir.net/laantungir/c-relay.git\"" >> src/main.h; \
echo "#define RELAY_VERSION VERSION // Use the same version as the build" >> src/main.h; \
echo "#define SUPPORTED_NIPS \"1,2,4,9,11,12,13,15,16,20,22,33,40,42\"" >> src/main.h; \
echo "#define LANGUAGE_TAGS \"\"" >> src/main.h; \
echo "#define RELAY_COUNTRIES \"\"" >> src/main.h; \
echo "#define POSTING_POLICY \"\"" >> src/main.h; \
echo "#define PAYMENTS_URL \"\"" >> src/main.h; \
echo "" >> src/main.h; \
echo "#endif /* MAIN_H */" >> src/main.h; \
echo "Created fallback main.h with version: $$VERSION"; \
else \
echo "Git not available, preserving existing version.h"; \
echo "Git not available, preserving existing main.h"; \
fi
# Force version.h regeneration (useful for development)
# Force main.h regeneration (useful for development)
force-version:
@echo "Force regenerating version.h..."
@rm -f src/version.h
@$(MAKE) src/version.h
@echo "Force regenerating main.h..."
@rm -f src/main.h
@$(MAKE) src/main.h
# Build the relay
$(TARGET): $(BUILD_DIR) src/version.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB)
$(TARGET): $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB)
@echo "Compiling C-Relay for architecture: $(ARCH)"
$(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(TARGET) $(NOSTR_CORE_LIB) $(LIBS)
@echo "Build complete: $(TARGET)"
# Build for specific architectures
x86: $(BUILD_DIR) src/version.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB)
x86: $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB)
@echo "Building C-Relay for x86_64..."
$(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(BUILD_DIR)/c_relay_x86 $(NOSTR_CORE_LIB) $(LIBS)
@echo "Build complete: $(BUILD_DIR)/c_relay_x86"
arm64: $(BUILD_DIR) src/version.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB)
arm64: $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB)
@echo "Cross-compiling C-Relay for ARM64..."
@if ! command -v aarch64-linux-gnu-gcc >/dev/null 2>&1; then \
echo "ERROR: ARM64 cross-compiler not found."; \
@@ -171,7 +215,7 @@ init-db:
# Clean build artifacts
clean:
rm -rf $(BUILD_DIR)
rm -f src/version.h
rm -f src/main.h
@echo "Clean complete"
# Clean everything including nostr_core_lib
@@ -210,6 +254,6 @@ help:
@echo " make check-toolchain # Check what compilers are available"
@echo " make test # Run tests"
@echo " make init-db # Set up database"
@echo " make force-version # Force regenerate version.h from git"
@echo " make force-version # Force regenerate main.h from git"
.PHONY: all x86 arm64 test init-db clean clean-all install-deps install-cross-tools install-arm64-deps check-toolchain help force-version

View File

@@ -91,8 +91,16 @@ All commands are sent as NIP-44 encrypted JSON arrays in the event content. The
### Available Configuration Keys
**Basic Relay Settings:**
- `relay_name`: Relay name (displayed in NIP-11)
- `relay_description`: Relay description text
- `relay_contact`: Contact information
- `relay_software`: Software URL
- `relay_version`: Software version
- `supported_nips`: Comma-separated list of supported NIP numbers (e.g., "1,2,4,9,11,12,13,15,16,20,22,33,40,42")
- `language_tags`: Comma-separated list of supported language tags (e.g., "en,es,fr" or "*" for all)
- `relay_countries`: Comma-separated list of supported country codes (e.g., "US,CA,MX" or "*" for all)
- `posting_policy`: Posting policy URL or text
- `payments_url`: Payment URL for premium features
- `max_connections`: Maximum concurrent connections
- `max_subscriptions_per_client`: Max subscriptions per client
- `max_event_tags`: Maximum tags per event

View File

@@ -932,7 +932,7 @@
description: 'C-Relay instance - pubkey provided manually',
pubkey: manualPubkey,
contact: 'admin@manual.config.relay',
supported_nips: [1, 2, 4, 9, 11, 12, 15, 16, 20, 22],
supported_nips: [1, 9, 11, 13, 15, 20, 33, 40, 42],
software: 'https://github.com/0xtrr/c-relay',
version: '1.0.0'
};
@@ -958,7 +958,7 @@
description: 'C-Relay instance - pubkey provided manually',
pubkey: manualPubkey,
contact: 'admin@manual.config.relay',
supported_nips: [1, 2, 4, 9, 11, 12, 15, 16, 20, 22],
supported_nips: [1, 9, 11, 13, 15, 20, 33, 40, 42],
software: 'https://github.com/0xtrr/c-relay',
version: '1.0.0'
};
@@ -1286,18 +1286,6 @@
console.log('Logout event handled successfully');
}
// Disconnect from relay and clean up connections
function disconnectFromRelay() {
if (relayPool) {
console.log('Cleaning up relay pool connection...');
const url = relayConnectionUrl.value.trim();
if (url) {
relayPool.close([url]);
}
relayPool = null;
subscriptionId = null;
}
}
// Update visibility of admin sections based on login and relay connection status
function updateAdminSectionsVisibility() {
@@ -2030,56 +2018,33 @@
configForm.innerHTML = '';
// Define field types and validation for different config parameters
// Define field types and validation for different config parameters (aligned with README.md)
const fieldTypes = {
'auth_enabled': 'boolean',
'nip42_auth_required_events': 'boolean',
'nip42_auth_required_subscriptions': 'boolean',
'nip42_auth_required': 'boolean',
'nip40_expiration_enabled': 'boolean',
'nip40_expiration_strict': 'boolean',
'nip40_expiration_filter': 'boolean',
'relay_port': 'number',
'max_connections': 'number',
'pow_min_difficulty': 'number',
'nip42_challenge_expiration': 'number',
'nip40_expiration_grace_period': 'number',
'nip42_challenge_timeout': 'number',
'max_subscriptions_per_client': 'number',
'max_total_subscriptions': 'number',
'max_filters_per_subscription': 'number',
'max_event_tags': 'number',
'max_content_length': 'number',
'max_message_length': 'number',
'default_limit': 'number',
'max_limit': 'number'
'max_content_length': 'number'
};
const descriptions = {
'relay_pubkey': 'Relay Public Key (Read-only)',
'auth_enabled': 'Enable Authentication',
'nip42_auth_required_events': 'Require Auth for Events',
'nip42_auth_required_subscriptions': 'Require Auth for Subscriptions',
'nip42_auth_required_kinds': 'Auth Required Event Kinds',
'nip42_challenge_expiration': 'Auth Challenge Expiration (seconds)',
'relay_port': 'Relay Port',
'nip42_auth_required': 'Enable NIP-42 Cryptographic Authentication',
'nip42_auth_required_kinds': 'Event Kinds Requiring NIP-42 Auth',
'nip42_challenge_timeout': 'NIP-42 Challenge Expiration Seconds',
'max_connections': 'Maximum Connections',
'relay_description': 'Relay Description',
'relay_contact': 'Relay Contact',
'relay_software': 'Relay Software URL',
'relay_version': 'Relay Version',
'pow_min_difficulty': 'Minimum PoW Difficulty',
'pow_mode': 'PoW Mode',
'pow_min_difficulty': 'Minimum Proof-of-Work Difficulty',
'nip40_expiration_enabled': 'Enable Event Expiration',
'nip40_expiration_strict': 'Strict Expiration Mode',
'nip40_expiration_filter': 'Filter Expired Events',
'nip40_expiration_grace_period': 'Expiration Grace Period (seconds)',
'max_subscriptions_per_client': 'Max Subscriptions per Client',
'max_total_subscriptions': 'Max Total Subscriptions',
'max_filters_per_subscription': 'Max Filters per Subscription',
'max_event_tags': 'Max Event Tags',
'max_content_length': 'Max Content Length',
'max_message_length': 'Max Message Length',
'default_limit': 'Default Query Limit',
'max_limit': 'Maximum Query Limit'
'max_event_tags': 'Maximum Tags per Event',
'max_content_length': 'Maximum Event Content Length'
};
// Process configuration tags (no d tag filtering for ephemeral events)
@@ -3452,7 +3417,7 @@
logTestEvent('SENT', `Add Whitelist event: ${JSON.stringify(signedEvent)}`, 'EVENT');
// Publish via SimplePool
const url = relayUrl.value.trim();
const url = relayConnectionUrl.value.trim();
const publishPromises = relayPool.publish([url], signedEvent);
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
@@ -3594,7 +3559,7 @@
logTestEvent('SENT', `Signed test event: ${JSON.stringify(signedEvent)}`, 'EVENT');
// Publish via SimplePool to the same relay with detailed error diagnostics
const url = relayUrl.value.trim();
const url = relayConnectionUrl.value.trim();
logTestEvent('INFO', `Publishing to relay: ${url}`, 'INFO');
const publishPromises = relayPool.publish([url], signedEvent);

View File

@@ -139,11 +139,11 @@ compile_project() {
print_warning "Clean failed or no Makefile found"
fi
# Force regenerate version.h to pick up new tags
# Force regenerate main.h to pick up new tags
if make force-version > /dev/null 2>&1; then
print_success "Regenerated version.h"
print_success "Regenerated main.h"
else
print_warning "Failed to regenerate version.h"
print_warning "Failed to regenerate main.h"
fi
# Compile the project

View File

@@ -1,455 +0,0 @@
# NIP-11 Relay Connection Implementation Plan
## Overview
Implement NIP-11 relay information fetching in the web admin interface to replace hardcoded relay pubkey and provide proper relay connection flow.
## Current Issues
1. **Hardcoded Relay Pubkey**: `getRelayPubkey()` returns hardcoded value `'4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa'`
2. **Relay URL in Debug Section**: Currently in "DEBUG - TEST FETCH WITHOUT LOGIN" section (lines 336-385)
3. **No Relay Verification**: Users can attempt admin operations without verifying relay identity
4. **Missing NIP-11 Support**: No fetching of relay information document
## Implementation Plan
### 1. New Relay Connection Section (HTML Structure)
Add after User Info section (around line 332):
```html
<!-- Relay Connection Section -->
<div class="section">
<h2>RELAY CONNECTION</h2>
<div class="input-group">
<label for="relay-url-input">Relay URL:</label>
<input type="text" id="relay-url-input" value="ws://localhost:8888" placeholder="ws://localhost:8888 or wss://relay.example.com">
</div>
<div class="inline-buttons">
<button type="button" id="connect-relay-btn">CONNECT TO RELAY</button>
<button type="button" id="disconnect-relay-btn" style="display: none;">DISCONNECT</button>
</div>
<div class="status disconnected" id="relay-connection-status">NOT CONNECTED</div>
<!-- Relay Information Display -->
<div id="relay-info-display" class="hidden">
<h3>Relay Information</h3>
<div class="user-info">
<div><strong>Name:</strong> <span id="relay-name">-</span></div>
<div><strong>Description:</strong> <span id="relay-description">-</span></div>
<div><strong>Public Key:</strong>
<div class="user-pubkey" id="relay-pubkey-display">-</div>
</div>
<div><strong>Software:</strong> <span id="relay-software">-</span></div>
<div><strong>Version:</strong> <span id="relay-version">-</span></div>
<div><strong>Contact:</strong> <span id="relay-contact">-</span></div>
<div><strong>Supported NIPs:</strong> <span id="relay-nips">-</span></div>
</div>
</div>
</div>
```
### 2. JavaScript Implementation
#### Global State Variables
Add to global state section (around line 535):
```javascript
// Relay connection state
let relayInfo = null;
let isRelayConnected = false;
let relayWebSocket = null;
```
#### NIP-11 Fetching Function
Add new function:
```javascript
// Fetch relay information using NIP-11
async function fetchRelayInfo(relayUrl) {
try {
console.log('=== FETCHING RELAY INFO VIA NIP-11 ===');
console.log('Relay URL:', relayUrl);
// Convert WebSocket URL to HTTP URL for NIP-11
let httpUrl = relayUrl;
if (relayUrl.startsWith('ws://')) {
httpUrl = relayUrl.replace('ws://', 'http://');
} else if (relayUrl.startsWith('wss://')) {
httpUrl = relayUrl.replace('wss://', 'https://');
}
console.log('HTTP URL for NIP-11:', httpUrl);
// Fetch relay information document
const response = await fetch(httpUrl, {
method: 'GET',
headers: {
'Accept': 'application/nostr+json'
},
// Add timeout
signal: AbortSignal.timeout(10000) // 10 second timeout
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
throw new Error(`Invalid content type: ${contentType}. Expected application/json or application/nostr+json`);
}
const relayInfoData = await response.json();
console.log('Fetched relay info:', relayInfoData);
// Validate required fields
if (!relayInfoData.pubkey) {
throw new Error('Relay information missing required pubkey field');
}
// Validate pubkey format (64 hex characters)
if (!/^[0-9a-fA-F]{64}$/.test(relayInfoData.pubkey)) {
throw new Error(`Invalid relay pubkey format: ${relayInfoData.pubkey}`);
}
return relayInfoData;
} catch (error) {
console.error('Failed to fetch relay info:', error);
throw error;
}
}
```
#### Relay Connection Function
Add new function:
```javascript
// Connect to relay and fetch information
async function connectToRelay() {
try {
const relayUrlInput = document.getElementById('relay-url-input');
const connectBtn = document.getElementById('connect-relay-btn');
const disconnectBtn = document.getElementById('disconnect-relay-btn');
const statusDiv = document.getElementById('relay-connection-status');
const infoDisplay = document.getElementById('relay-info-display');
const url = relayUrlInput.value.trim();
if (!url) {
throw new Error('Please enter a relay URL');
}
// Update UI to show connecting state
connectBtn.disabled = true;
statusDiv.textContent = 'CONNECTING...';
statusDiv.className = 'status connected';
console.log('Connecting to relay:', url);
// Fetch relay information via NIP-11
console.log('Fetching relay information...');
const fetchedRelayInfo = await fetchRelayInfo(url);
// Test WebSocket connection
console.log('Testing WebSocket connection...');
await testWebSocketConnection(url);
// Store relay information
relayInfo = fetchedRelayInfo;
isRelayConnected = true;
// Update UI with relay information
displayRelayInfo(relayInfo);
// Update connection status
statusDiv.textContent = 'CONNECTED';
statusDiv.className = 'status connected';
// Update button states
connectBtn.style.display = 'none';
disconnectBtn.style.display = 'inline-block';
relayUrlInput.disabled = true;
// Show relay info
infoDisplay.classList.remove('hidden');
console.log('Successfully connected to relay:', relayInfo.name || url);
log(`Connected to relay: ${relayInfo.name || url}`, 'INFO');
} catch (error) {
console.error('Failed to connect to relay:', error);
// Reset UI state
const connectBtn = document.getElementById('connect-relay-btn');
const statusDiv = document.getElementById('relay-connection-status');
connectBtn.disabled = false;
statusDiv.textContent = `CONNECTION FAILED: ${error.message}`;
statusDiv.className = 'status error';
// Clear any partial state
relayInfo = null;
isRelayConnected = false;
log(`Failed to connect to relay: ${error.message}`, 'ERROR');
}
}
```
#### WebSocket Connection Test
Add new function:
```javascript
// Test WebSocket connection to relay
async function testWebSocketConnection(url) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
ws.close();
reject(new Error('WebSocket connection timeout'));
}, 5000);
const ws = new WebSocket(url);
ws.onopen = () => {
clearTimeout(timeout);
console.log('WebSocket connection successful');
ws.close();
resolve();
};
ws.onerror = (error) => {
clearTimeout(timeout);
console.error('WebSocket connection failed:', error);
reject(new Error('WebSocket connection failed'));
};
ws.onclose = (event) => {
if (event.code !== 1000) {
clearTimeout(timeout);
reject(new Error(`WebSocket closed with code ${event.code}: ${event.reason}`));
}
};
});
}
```
#### Display Relay Information
Add new function:
```javascript
// Display relay information in the UI
function displayRelayInfo(info) {
document.getElementById('relay-name').textContent = info.name || 'Unknown';
document.getElementById('relay-description').textContent = info.description || 'No description';
document.getElementById('relay-pubkey-display').textContent = info.pubkey || 'Unknown';
document.getElementById('relay-software').textContent = info.software || 'Unknown';
document.getElementById('relay-version').textContent = info.version || 'Unknown';
document.getElementById('relay-contact').textContent = info.contact || 'No contact info';
// Format supported NIPs
let nipsText = 'None specified';
if (info.supported_nips && Array.isArray(info.supported_nips) && info.supported_nips.length > 0) {
nipsText = info.supported_nips.map(nip => `NIP-${nip.toString().padStart(2, '0')}`).join(', ');
}
document.getElementById('relay-nips').textContent = nipsText;
}
```
#### Disconnect Function
Add new function:
```javascript
// Disconnect from relay
function disconnectFromRelay() {
console.log('Disconnecting from relay...');
// Clear relay state
relayInfo = null;
isRelayConnected = false;
// Close any existing connections
if (relayPool) {
const url = document.getElementById('relay-url-input').value.trim();
if (url) {
relayPool.close([url]);
}
relayPool = null;
subscriptionId = null;
}
// Reset UI
const connectBtn = document.getElementById('connect-relay-btn');
const disconnectBtn = document.getElementById('disconnect-relay-btn');
const statusDiv = document.getElementById('relay-connection-status');
const infoDisplay = document.getElementById('relay-info-display');
const relayUrlInput = document.getElementById('relay-url-input');
connectBtn.style.display = 'inline-block';
disconnectBtn.style.display = 'none';
connectBtn.disabled = false;
relayUrlInput.disabled = false;
statusDiv.textContent = 'NOT CONNECTED';
statusDiv.className = 'status disconnected';
infoDisplay.classList.add('hidden');
// Reset configuration status
updateConfigStatus(false);
log('Disconnected from relay', 'INFO');
}
```
#### Update getRelayPubkey Function
Replace existing function (around line 3142):
```javascript
// Helper function to get relay pubkey from connected relay info
function getRelayPubkey() {
if (relayInfo && relayInfo.pubkey) {
return relayInfo.pubkey;
}
// Fallback to hardcoded value if no relay connected (for testing)
console.warn('No relay connected, using fallback pubkey');
return '4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa';
}
```
### 3. Event Handlers
Add event handlers in the DOMContentLoaded section:
```javascript
// Relay connection event handlers
const connectRelayBtn = document.getElementById('connect-relay-btn');
const disconnectRelayBtn = document.getElementById('disconnect-relay-btn');
if (connectRelayBtn) {
connectRelayBtn.addEventListener('click', function(e) {
e.preventDefault();
connectToRelay().catch(error => {
console.error('Connect to relay failed:', error);
});
});
}
if (disconnectRelayBtn) {
disconnectRelayBtn.addEventListener('click', function(e) {
e.preventDefault();
disconnectFromRelay();
});
}
```
### 4. Update Existing Functions
#### Update fetchConfiguration Function
Add relay connection check at the beginning:
```javascript
async function fetchConfiguration() {
try {
console.log('=== FETCHING CONFIGURATION VIA ADMIN API ===');
// Check if relay is connected
if (!isRelayConnected || !relayInfo) {
throw new Error('Must be connected to relay first. Please connect to relay in the Relay Connection section.');
}
// ... rest of existing function
} catch (error) {
// ... existing error handling
}
}
```
#### Update subscribeToConfiguration Function
Add relay connection check:
```javascript
async function subscribeToConfiguration() {
try {
console.log('=== STARTING SIMPLEPOOL CONFIGURATION SUBSCRIPTION ===');
if (!isRelayConnected || !relayInfo) {
console.error('Must be connected to relay first');
return false;
}
// Use the relay URL from the connection section instead of the debug section
const url = document.getElementById('relay-url-input').value.trim();
// ... rest of existing function
} catch (error) {
// ... existing error handling
}
}
```
### 5. Update UI Flow
#### Modify showMainInterface Function
Update to show relay connection requirement:
```javascript
function showMainInterface() {
loginSection.classList.add('hidden');
mainInterface.classList.remove('hidden');
userPubkeyDisplay.textContent = userPubkey;
// Show message about relay connection requirement
if (!isRelayConnected) {
log('Please connect to a relay to access admin functions', 'INFO');
}
}
```
### 6. Remove/Update Debug Section
#### Option 1: Remove Debug Section Entirely
Remove the "DEBUG - TEST FETCH WITHOUT LOGIN" section (lines 335-385) since relay URL is now in the proper connection section.
#### Option 2: Keep Debug Section for Testing
Update the debug section to use the connected relay URL and add a note that it's for testing purposes.
### 7. Error Handling
Add comprehensive error handling for:
- Network timeouts
- Invalid relay URLs
- Missing NIP-11 support
- Invalid relay pubkey format
- WebSocket connection failures
- CORS issues
### 8. Security Considerations
- Validate relay pubkey format (64 hex characters)
- Verify relay identity before admin operations
- Handle CORS properly for NIP-11 requests
- Sanitize relay information display
- Warn users about connecting to untrusted relays
## Testing Plan
1. **NIP-11 Fetching**: Test with various relay URLs (localhost, remote relays)
2. **Error Handling**: Test with invalid URLs, non-Nostr servers, network failures
3. **WebSocket Connection**: Verify WebSocket connectivity after NIP-11 fetch
4. **Admin API Integration**: Ensure admin commands use correct relay pubkey
5. **UI Flow**: Test complete user journey from login → relay connection → admin operations
## Benefits
1. **Proper Relay Identification**: Uses actual relay pubkey instead of hardcoded value
2. **Better UX**: Clear connection flow and relay information display
3. **Protocol Compliance**: Implements NIP-11 standard for relay discovery
4. **Security**: Verifies relay identity before admin operations
5. **Flexibility**: Works with any NIP-11 compliant relay
## Migration Notes
- Existing users will need to connect to relay after this update
- Debug section can be kept for development/testing purposes
- All admin functions will require relay connection
- Relay pubkey will be dynamically fetched instead of hardcoded

View File

@@ -1 +1 @@
1796483
2263673

View File

@@ -99,19 +99,25 @@ static char g_temp_relay_privkey[65] = {0};
static int get_cache_timeout(void) {
char *no_cache = getenv("GINX_NO_CACHE");
char *cache_timeout = getenv("GINX_CACHE_TIMEOUT");
if (no_cache && strcmp(no_cache, "1") == 0) {
return 0; // No caching
}
if (cache_timeout) {
int timeout = atoi(cache_timeout);
return (timeout >= 0) ? timeout : 300; // Use provided value or default
}
return 300; // Default 5 minutes
}
// Helper function to safely return dynamically allocated string from static buffer
static char* safe_strdup_from_static(const char* static_str) {
if (!static_str) return NULL;
return strdup(static_str);
}
// Force cache refresh - invalidates current cache
void force_config_cache_refresh(void) {
pthread_mutex_lock(&g_unified_cache.cache_lock);
@@ -396,58 +402,61 @@ const char* get_config_value(const char* key) {
if (!key) {
return NULL;
}
// Special fast path for frequently accessed keys via unified cache
if (strcmp(key, "admin_pubkey") == 0) {
return get_admin_pubkey_cached();
const char* cached_value = get_admin_pubkey_cached();
return safe_strdup_from_static(cached_value);
}
if (strcmp(key, "relay_pubkey") == 0) {
return get_relay_pubkey_cached();
const char* cached_value = get_relay_pubkey_cached();
return safe_strdup_from_static(cached_value);
}
// For other keys, try config table first
const char* table_value = get_config_value_from_table(key);
if (table_value) {
return table_value;
}
// Fallback to legacy event-based config for backward compatibility
// Use unified cache buffer instead of static buffer
if (!g_current_config) {
return NULL;
}
// Look for key in current configuration tags
cJSON* tags = cJSON_GetObjectItem(g_current_config, "tags");
if (!tags || !cJSON_IsArray(tags)) {
return NULL;
}
pthread_mutex_lock(&g_unified_cache.cache_lock);
cJSON* tag = NULL;
cJSON_ArrayForEach(tag, tags) {
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
cJSON* tag_key = cJSON_GetArrayItem(tag, 0);
cJSON* tag_value = cJSON_GetArrayItem(tag, 1);
if (tag_key && tag_value &&
cJSON_IsString(tag_key) && cJSON_IsString(tag_value)) {
if (strcmp(cJSON_GetStringValue(tag_key), key) == 0) {
strncpy(g_unified_cache.temp_buffer, cJSON_GetStringValue(tag_value),
sizeof(g_unified_cache.temp_buffer) - 1);
g_unified_cache.temp_buffer[sizeof(g_unified_cache.temp_buffer) - 1] = '\0';
const char* result = safe_strdup_from_static(g_unified_cache.temp_buffer);
pthread_mutex_unlock(&g_unified_cache.cache_lock);
return g_unified_cache.temp_buffer;
return result;
}
}
}
}
pthread_mutex_unlock(&g_unified_cache.cache_lock);
return NULL;
}
@@ -456,14 +465,18 @@ int get_config_int(const char* key, int default_value) {
if (!str_value) {
return default_value;
}
char* endptr;
long val = strtol(str_value, &endptr, 10);
if (endptr == str_value || *endptr != '\0') {
// Free the dynamically allocated string
free((char*)str_value);
return default_value;
}
// Free the dynamically allocated string
free((char*)str_value);
return (int)val;
}
@@ -472,18 +485,23 @@ int get_config_bool(const char* key, int default_value) {
if (!str_value) {
return default_value;
}
if (strcasecmp(str_value, "true") == 0 ||
strcasecmp(str_value, "yes") == 0 ||
int result;
if (strcasecmp(str_value, "true") == 0 ||
strcasecmp(str_value, "yes") == 0 ||
strcasecmp(str_value, "1") == 0) {
return 1;
} else if (strcasecmp(str_value, "false") == 0 ||
strcasecmp(str_value, "no") == 0 ||
result = 1;
} else if (strcasecmp(str_value, "false") == 0 ||
strcasecmp(str_value, "no") == 0 ||
strcasecmp(str_value, "0") == 0) {
return 0;
result = 0;
} else {
result = default_value;
}
return default_value;
// Free the dynamically allocated string
free((char*)str_value);
return result;
}
// ================================
@@ -917,10 +935,11 @@ cJSON* create_default_config_event(const unsigned char* admin_privkey_bytes,
int first_time_startup_sequence(const cli_options_t* cli_options) {
log_info("Starting first-time startup sequence...");
// 1. Generate or use provided admin keypair
unsigned char admin_privkey_bytes[32];
char admin_privkey[65], admin_pubkey[65];
int generated_admin_key = 0; // Track if we generated a new admin key
if (cli_options && strlen(cli_options->admin_pubkey_override) == 64) {
// Use provided admin public key directly - skip private key generation entirely
@@ -943,6 +962,7 @@ int first_time_startup_sequence(const cli_options_t* cli_options) {
// Set a dummy private key that will never be used (not displayed or stored)
memset(admin_privkey_bytes, 0, 32); // Zero out for security
memset(admin_privkey, 0, sizeof(admin_privkey)); // Zero out the hex string
generated_admin_key = 0; // Did not generate a new key
} else {
// Generate random admin keypair using /dev/urandom + nostr_core_lib
log_info("Generating random admin keypair");
@@ -959,6 +979,7 @@ int first_time_startup_sequence(const cli_options_t* cli_options) {
return -1;
}
nostr_bytes_to_hex(admin_pubkey_bytes, 32, admin_pubkey);
generated_admin_key = 1; // Generated a new key
}
// 2. Generate or use provided relay keypair
@@ -1017,57 +1038,40 @@ int first_time_startup_sequence(const cli_options_t* cli_options) {
g_temp_relay_privkey[sizeof(g_temp_relay_privkey) - 1] = '\0';
log_info("Relay private key cached for secure storage after database initialization");
// 6. Handle configuration setup based on admin key availability
if (cli_options && strlen(cli_options->admin_pubkey_override) == 64) {
// Admin pubkey provided - will populate config table after database initialization
log_info("Admin pubkey provided - config table will be populated after database initialization");
// 6. Handle configuration setup - defaults will be populated after database initialization
log_info("Configuration setup prepared - defaults will be populated after database initialization");
// CLI overrides will be applied after database initialization in main.c
// This prevents "g_db is NULL" errors during first-time startup
// 10. Print admin private key for user to save (only if we generated a new key)
if (generated_admin_key) {
printf("\n");
printf("=================================================================\n");
printf("IMPORTANT: SAVE THIS ADMIN PRIVATE KEY SECURELY!\n");
printf("=================================================================\n");
printf("Admin Private Key: %s\n", admin_privkey);
printf("Admin Public Key: %s\n", admin_pubkey);
printf("Relay Public Key: %s\n", relay_pubkey);
printf("\nDatabase: %s\n", g_database_path);
printf("\nThis admin private key is needed to update configuration!\n");
printf("Store it safely - it will not be displayed again.\n");
printf("=================================================================\n");
printf("\n");
} else {
// Admin private key available - create signed configuration event
log_info("Admin private key available - creating signed configuration event");
// Create initial configuration event using defaults
cJSON* config_event = create_default_config_event(admin_privkey_bytes, relay_privkey, relay_pubkey, cli_options);
if (!config_event) {
log_error("Failed to create default configuration event");
return -1;
}
// Process configuration through admin API instead of storing in events table
if (process_startup_config_event_with_fallback(config_event) == 0) {
log_success("Initial configuration processed successfully through admin API");
} else {
log_warning("Failed to process initial configuration - will retry after database init");
// Cache the event for later processing
if (g_pending_config_event) {
cJSON_Delete(g_pending_config_event);
}
g_pending_config_event = cJSON_Duplicate(config_event, 1);
}
// Cache the current config
if (g_current_config) {
cJSON_Delete(g_current_config);
}
g_current_config = cJSON_Duplicate(config_event, 1);
// Clean up
cJSON_Delete(config_event);
printf("\n");
printf("=================================================================\n");
printf("RELAY STARTUP COMPLETE\n");
printf("=================================================================\n");
printf("Using provided admin public key for authentication\n");
printf("Admin Public Key: %s\n", admin_pubkey);
printf("Relay Public Key: %s\n", relay_pubkey);
printf("\nDatabase: %s\n", g_database_path);
printf("=================================================================\n");
printf("\n");
}
// 10. Print admin private key for user to save
printf("\n");
printf("=================================================================\n");
printf("IMPORTANT: SAVE THIS ADMIN PRIVATE KEY SECURELY!\n");
printf("=================================================================\n");
printf("Admin Private Key: %s\n", admin_privkey);
printf("Admin Public Key: %s\n", admin_pubkey);
printf("Relay Public Key: %s\n", relay_pubkey);
printf("\nDatabase: %s\n", g_database_path);
printf("\nThis admin private key is needed to update configuration!\n");
printf("Store it safely - it will not be displayed again.\n");
printf("=================================================================\n");
printf("\n");
log_success("First-time startup sequence completed");
return 0;
}
@@ -1077,33 +1081,38 @@ int startup_existing_relay(const char* relay_pubkey) {
log_error("Invalid relay pubkey for existing relay startup");
return -1;
}
log_info("Starting existing relay...");
printf(" Relay pubkey: %s\n", relay_pubkey);
// Store relay pubkey in unified cache
pthread_mutex_lock(&g_unified_cache.cache_lock);
strncpy(g_unified_cache.relay_pubkey, relay_pubkey, sizeof(g_unified_cache.relay_pubkey) - 1);
g_unified_cache.relay_pubkey[sizeof(g_unified_cache.relay_pubkey) - 1] = '\0';
pthread_mutex_unlock(&g_unified_cache.cache_lock);
// Set database path
char* db_name = get_database_name_from_relay_pubkey(relay_pubkey);
if (!db_name) {
log_error("Failed to generate database name");
return -1;
}
strncpy(g_database_path, db_name, sizeof(g_database_path) - 1);
g_database_path[sizeof(g_database_path) - 1] = '\0';
free(db_name);
// Ensure default configuration values are populated (for any missing keys)
if (populate_default_config_values() != 0) {
log_warning("Failed to populate default config values for existing relay - continuing");
}
// Configuration will be migrated from events to table after database initialization
log_info("Configuration migration will be performed after database is available");
// Load configuration event from database (after database is initialized)
// This will be done in apply_configuration_from_database()
log_success("Existing relay startup prepared");
return 0;
}
@@ -1828,31 +1837,90 @@ int handle_configuration_event(cJSON* event, char* error_message, size_t error_s
// Get value from config table
const char* get_config_value_from_table(const char* key) {
if (!g_db || !key) return NULL;
const char* sql = "SELECT value FROM config WHERE key = ?";
sqlite3_stmt* stmt;
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
return NULL;
}
sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC);
const char* result = NULL;
if (sqlite3_step(stmt) == SQLITE_ROW) {
const char* value = (char*)sqlite3_column_text(stmt, 0);
if (value) {
// Use unified cache buffer with thread safety
pthread_mutex_lock(&g_unified_cache.cache_lock);
strncpy(g_unified_cache.temp_buffer, value, sizeof(g_unified_cache.temp_buffer) - 1);
g_unified_cache.temp_buffer[sizeof(g_unified_cache.temp_buffer) - 1] = '\0';
result = g_unified_cache.temp_buffer;
pthread_mutex_unlock(&g_unified_cache.cache_lock);
// For NIP-11 fields, store in cache buffers but return dynamically allocated strings for consistency
if (strcmp(key, "relay_name") == 0) {
pthread_mutex_lock(&g_unified_cache.cache_lock);
strncpy(g_unified_cache.relay_info.name, value, sizeof(g_unified_cache.relay_info.name) - 1);
g_unified_cache.relay_info.name[sizeof(g_unified_cache.relay_info.name) - 1] = '\0';
result = strdup(value); // Return dynamically allocated copy
pthread_mutex_unlock(&g_unified_cache.cache_lock);
} else if (strcmp(key, "relay_description") == 0) {
pthread_mutex_lock(&g_unified_cache.cache_lock);
strncpy(g_unified_cache.relay_info.description, value, sizeof(g_unified_cache.relay_info.description) - 1);
g_unified_cache.relay_info.description[sizeof(g_unified_cache.relay_info.description) - 1] = '\0';
result = strdup(value); // Return dynamically allocated copy
pthread_mutex_unlock(&g_unified_cache.cache_lock);
} else if (strcmp(key, "relay_contact") == 0) {
pthread_mutex_lock(&g_unified_cache.cache_lock);
strncpy(g_unified_cache.relay_info.contact, value, sizeof(g_unified_cache.relay_info.contact) - 1);
g_unified_cache.relay_info.contact[sizeof(g_unified_cache.relay_info.contact) - 1] = '\0';
result = strdup(value); // Return dynamically allocated copy
pthread_mutex_unlock(&g_unified_cache.cache_lock);
} else if (strcmp(key, "relay_software") == 0) {
pthread_mutex_lock(&g_unified_cache.cache_lock);
strncpy(g_unified_cache.relay_info.software, value, sizeof(g_unified_cache.relay_info.software) - 1);
g_unified_cache.relay_info.software[sizeof(g_unified_cache.relay_info.software) - 1] = '\0';
result = strdup(value); // Return dynamically allocated copy
pthread_mutex_unlock(&g_unified_cache.cache_lock);
} else if (strcmp(key, "relay_version") == 0) {
pthread_mutex_lock(&g_unified_cache.cache_lock);
strncpy(g_unified_cache.relay_info.version, value, sizeof(g_unified_cache.relay_info.version) - 1);
g_unified_cache.relay_info.version[sizeof(g_unified_cache.relay_info.version) - 1] = '\0';
result = strdup(value); // Return dynamically allocated copy
pthread_mutex_unlock(&g_unified_cache.cache_lock);
} else if (strcmp(key, "supported_nips") == 0) {
pthread_mutex_lock(&g_unified_cache.cache_lock);
strncpy(g_unified_cache.relay_info.supported_nips_str, value, sizeof(g_unified_cache.relay_info.supported_nips_str) - 1);
g_unified_cache.relay_info.supported_nips_str[sizeof(g_unified_cache.relay_info.supported_nips_str) - 1] = '\0';
result = strdup(value); // Return dynamically allocated copy
pthread_mutex_unlock(&g_unified_cache.cache_lock);
} else if (strcmp(key, "language_tags") == 0) {
pthread_mutex_lock(&g_unified_cache.cache_lock);
strncpy(g_unified_cache.relay_info.language_tags_str, value, sizeof(g_unified_cache.relay_info.language_tags_str) - 1);
g_unified_cache.relay_info.language_tags_str[sizeof(g_unified_cache.relay_info.language_tags_str) - 1] = '\0';
result = strdup(value); // Return dynamically allocated copy
pthread_mutex_unlock(&g_unified_cache.cache_lock);
} else if (strcmp(key, "relay_countries") == 0) {
pthread_mutex_lock(&g_unified_cache.cache_lock);
strncpy(g_unified_cache.relay_info.relay_countries_str, value, sizeof(g_unified_cache.relay_info.relay_countries_str) - 1);
g_unified_cache.relay_info.relay_countries_str[sizeof(g_unified_cache.relay_info.relay_countries_str) - 1] = '\0';
result = strdup(value); // Return dynamically allocated copy
pthread_mutex_unlock(&g_unified_cache.cache_lock);
} else if (strcmp(key, "posting_policy") == 0) {
pthread_mutex_lock(&g_unified_cache.cache_lock);
strncpy(g_unified_cache.relay_info.posting_policy, value, sizeof(g_unified_cache.relay_info.posting_policy) - 1);
g_unified_cache.relay_info.posting_policy[sizeof(g_unified_cache.relay_info.posting_policy) - 1] = '\0';
result = strdup(value); // Return dynamically allocated copy
pthread_mutex_unlock(&g_unified_cache.cache_lock);
} else if (strcmp(key, "payments_url") == 0) {
pthread_mutex_lock(&g_unified_cache.cache_lock);
strncpy(g_unified_cache.relay_info.payments_url, value, sizeof(g_unified_cache.relay_info.payments_url) - 1);
g_unified_cache.relay_info.payments_url[sizeof(g_unified_cache.relay_info.payments_url) - 1] = '\0';
result = strdup(value); // Return dynamically allocated copy
pthread_mutex_unlock(&g_unified_cache.cache_lock);
} else {
// For other keys, return a dynamically allocated string to prevent buffer reuse
result = strdup(value);
}
}
}
sqlite3_finalize(stmt);
return result;
}
@@ -3920,12 +3988,17 @@ const char* get_config_value_hybrid(const char* key) {
if (is_config_table_ready()) {
const char* table_value = get_config_value_from_table(key);
if (table_value) {
return table_value;
return table_value; // Already dynamically allocated
}
}
// Fall back to event-based config
return get_config_value(key);
// Fall back to event-based config, but ensure it's dynamically allocated
const char* fallback_value = get_config_value(key);
if (fallback_value) {
return strdup(fallback_value); // Make a copy since fallback might be static
}
return NULL;
}
// Check if config table is ready

View File

@@ -56,6 +56,11 @@ typedef struct {
char version[64];
char privacy_policy[RELAY_URL_MAX_LENGTH];
char terms_of_service[RELAY_URL_MAX_LENGTH];
// Raw string values for parsing into cJSON arrays
char supported_nips_str[CONFIG_VALUE_MAX_LENGTH];
char language_tags_str[CONFIG_VALUE_MAX_LENGTH];
char relay_countries_str[CONFIG_VALUE_MAX_LENGTH];
// Parsed cJSON arrays
cJSON* supported_nips;
cJSON* limitation;
cJSON* retention;

View File

@@ -3,6 +3,7 @@
#include <cjson/cJSON.h>
#include "config.h" // For cli_options_t definition
#include "main.h" // For relay metadata constants
/*
* Default Configuration Event Template
@@ -33,10 +34,16 @@ static const struct {
{"max_connections", "100"},
// NIP-11 Relay Information (relay keys will be populated at runtime)
{"relay_description", "High-performance C Nostr relay with SQLite storage"},
{"relay_contact", ""},
{"relay_software", "https://git.laantungir.net/laantungir/c-relay.git"},
{"relay_version", "v1.0.0"},
{"relay_name", RELAY_NAME},
{"relay_description", RELAY_DESCRIPTION},
{"relay_contact", RELAY_CONTACT},
{"relay_software", RELAY_SOFTWARE},
{"relay_version", RELAY_VERSION},
{"supported_nips", SUPPORTED_NIPS},
{"language_tags", LANGUAGE_TAGS},
{"relay_countries", RELAY_COUNTRIES},
{"posting_policy", POSTING_POLICY},
{"payments_url", PAYMENTS_URL},
// NIP-13 Proof of Work (pow_min_difficulty = 0 means PoW disabled)
{"pow_min_difficulty", "0"},

View File

@@ -348,18 +348,18 @@ int init_database(const char* database_path_override) {
}
if (!has_auth_rules) {
// Add auth_rules table
// Add auth_rules table matching sql_schema.h
const char* create_auth_rules_sql =
"CREATE TABLE IF NOT EXISTS auth_rules ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" rule_type TEXT NOT NULL," // 'pubkey_whitelist', 'pubkey_blacklist', 'hash_blacklist'
" operation TEXT NOT NULL," // 'event', 'event_kind_1', etc.
" rule_target TEXT NOT NULL," // pubkey, hash, or other identifier
" enabled INTEGER DEFAULT 1," // 0 = disabled, 1 = enabled
" priority INTEGER DEFAULT 1000," // Lower numbers = higher priority
" description TEXT," // Optional description
" created_at INTEGER DEFAULT (strftime('%s', 'now')),"
" UNIQUE(rule_type, operation, rule_target)"
" rule_type TEXT NOT NULL CHECK (rule_type IN ('whitelist', 'blacklist', 'rate_limit', 'auth_required')),"
" pattern_type TEXT NOT NULL CHECK (pattern_type IN ('pubkey', 'kind', 'ip', 'global')),"
" pattern_value TEXT,"
" action TEXT NOT NULL CHECK (action IN ('allow', 'deny', 'require_auth', 'rate_limit')),"
" parameters TEXT,"
" active INTEGER NOT NULL DEFAULT 1,"
" created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),"
" updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))"
");";
char* error_msg = NULL;
@@ -373,6 +373,24 @@ int init_database(const char* database_path_override) {
return -1;
}
log_success("Created auth_rules table");
// Add indexes for auth_rules table
const char* create_auth_rules_indexes_sql =
"CREATE INDEX IF NOT EXISTS idx_auth_rules_pattern ON auth_rules(pattern_type, pattern_value);"
"CREATE INDEX IF NOT EXISTS idx_auth_rules_type ON auth_rules(rule_type);"
"CREATE INDEX IF NOT EXISTS idx_auth_rules_active ON auth_rules(active);";
char* index_error_msg = NULL;
int index_rc = sqlite3_exec(g_db, create_auth_rules_indexes_sql, NULL, NULL, &index_error_msg);
if (index_rc != SQLITE_OK) {
char index_error_log[512];
snprintf(index_error_log, sizeof(index_error_log), "Failed to create auth_rules indexes: %s",
index_error_msg ? index_error_msg : "unknown error");
log_error(index_error_log);
if (index_error_msg) sqlite3_free(index_error_msg);
return -1;
}
log_success("Created auth_rules indexes");
} else {
log_info("auth_rules table already exists, skipping creation");
}
@@ -1408,35 +1426,43 @@ int main(int argc, char* argv[]) {
}
// Handle configuration setup after database is initialized
if (cli_options.admin_pubkey_override && strlen(cli_options.admin_pubkey_override) == 64) {
// Admin pubkey provided - populate config table directly
log_info("Populating config table for admin pubkey override after database initialization");
// Always populate defaults directly in config table (abandoning legacy event signing)
log_info("Populating config table with defaults after database initialization");
// Populate default config values in table
if (populate_default_config_values() != 0) {
log_error("Failed to populate default config values");
cleanup_configuration_system();
nostr_cleanup();
close_database();
return 1;
}
// Add pubkeys to config table
if (add_pubkeys_to_config_table() != 0) {
log_error("Failed to add pubkeys to config table");
cleanup_configuration_system();
nostr_cleanup();
close_database();
return 1;
}
log_success("Configuration populated directly in config table after database initialization");
} else {
// Admin private key available - retry storing initial config event
if (retry_store_initial_config_event() != 0) {
log_warning("Failed to store initial config event - will retry later");
}
// Populate default config values in table
if (populate_default_config_values() != 0) {
log_error("Failed to populate default config values");
cleanup_configuration_system();
nostr_cleanup();
close_database();
return 1;
}
// Apply CLI overrides now that database is available
if (cli_options.port_override > 0) {
char port_str[16];
snprintf(port_str, sizeof(port_str), "%d", cli_options.port_override);
if (update_config_in_table("relay_port", port_str) != 0) {
log_error("Failed to update relay port override in config table");
cleanup_configuration_system();
nostr_cleanup();
close_database();
return 1;
}
log_info("Applied port override from command line");
printf(" Port: %d (overriding default)\n", cli_options.port_override);
}
// Add pubkeys to config table
if (add_pubkeys_to_config_table() != 0) {
log_error("Failed to add pubkeys to config table");
cleanup_configuration_system();
nostr_cleanup();
close_database();
return 1;
}
log_success("Configuration populated directly in config table after database initialization");
// Now store the pubkeys in config table since database is available
const char* admin_pubkey = get_admin_pubkey_cached();
@@ -1539,6 +1565,21 @@ int main(int argc, char* argv[]) {
log_warning("No configuration event found in existing database");
}
// Apply CLI overrides for existing relay (port override should work even for existing relays)
if (cli_options.port_override > 0) {
char port_str[16];
snprintf(port_str, sizeof(port_str), "%d", cli_options.port_override);
if (update_config_in_table("relay_port", port_str) != 0) {
log_error("Failed to update relay port override in config table for existing relay");
cleanup_configuration_system();
nostr_cleanup();
close_database();
return 1;
}
log_info("Applied port override from command line for existing relay");
printf(" Port: %d (overriding configured port)\n", cli_options.port_override);
}
// Free memory
free(relay_pubkey);
for (int i = 0; existing_files[i]; i++) {

32
src/main.h Normal file
View File

@@ -0,0 +1,32 @@
/*
* C-Relay Main Header - Version and Metadata Information
*
* This header contains version information and relay metadata that is
* automatically updated by the build system (build_and_push.sh).
*
* The build_and_push.sh script updates VERSION and related macros when
* creating new releases.
*/
#ifndef MAIN_H
#define MAIN_H
// Version information (auto-updated by build_and_push.sh)
#define VERSION "v0.4.2"
#define VERSION_MAJOR 0
#define VERSION_MINOR 4
#define VERSION_PATCH 2
// Relay metadata (authoritative source for NIP-11 information)
#define RELAY_NAME "C-Relay"
#define RELAY_DESCRIPTION "High-performance C Nostr relay with SQLite storage"
#define RELAY_CONTACT ""
#define RELAY_SOFTWARE "https://git.laantungir.net/laantungir/c-relay.git"
#define RELAY_VERSION VERSION // Use the same version as the build
#define SUPPORTED_NIPS "1,2,4,9,11,12,13,15,16,20,22,33,40,42"
#define LANGUAGE_TAGS ""
#define RELAY_COUNTRIES ""
#define POSTING_POLICY ""
#define PAYMENTS_URL ""
#endif /* MAIN_H */

View File

@@ -34,76 +34,213 @@ extern unified_config_cache_t g_unified_cache;
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
// Helper function to parse comma-separated string into cJSON array
cJSON* parse_comma_separated_array(const char* csv_string) {
log_info("parse_comma_separated_array called");
if (!csv_string || strlen(csv_string) == 0) {
log_info("Empty or null csv_string, returning empty array");
return cJSON_CreateArray();
}
log_info("Creating cJSON array");
cJSON* array = cJSON_CreateArray();
if (!array) {
log_info("Failed to create cJSON array");
return NULL;
}
log_info("Duplicating csv_string");
char* csv_copy = strdup(csv_string);
if (!csv_copy) {
log_info("Failed to duplicate csv_string");
cJSON_Delete(array);
return NULL;
}
log_info("Starting token parsing");
char* token = strtok(csv_copy, ",");
while (token) {
log_info("Processing token");
// Trim whitespace
while (*token == ' ') token++;
char* end = token + strlen(token) - 1;
while (end > token && *end == ' ') *end-- = '\0';
if (strlen(token) > 0) {
log_info("Token has content, parsing");
// Try to parse as number first (for supported_nips)
char* endptr;
long num = strtol(token, &endptr, 10);
if (*endptr == '\0') {
log_info("Token is number, adding to array");
// It's a number
cJSON_AddItemToArray(array, cJSON_CreateNumber(num));
} else {
log_info("Token is string, adding to array");
// It's a string
cJSON_AddItemToArray(array, cJSON_CreateString(token));
}
} else {
log_info("Token is empty, skipping");
}
token = strtok(NULL, ",");
}
log_info("Freeing csv_copy");
free(csv_copy);
log_info("Returning parsed array");
return array;
}
// Initialize relay information using configuration system
void init_relay_info() {
log_info("Initializing relay information from configuration...");
// Get all config values first (without holding mutex to avoid deadlock)
// Note: These may be dynamically allocated strings that need to be freed
log_info("Fetching relay configuration values...");
const char* relay_name = get_config_value("relay_name");
log_info("relay_name fetched");
const char* relay_description = get_config_value("relay_description");
log_info("relay_description fetched");
const char* relay_software = get_config_value("relay_software");
log_info("relay_software fetched");
const char* relay_version = get_config_value("relay_version");
log_info("relay_version fetched");
const char* relay_contact = get_config_value("relay_contact");
log_info("relay_contact fetched");
const char* relay_pubkey = get_config_value("relay_pubkey");
log_info("relay_pubkey fetched");
const char* supported_nips_csv = get_config_value("supported_nips");
log_info("supported_nips fetched");
const char* language_tags_csv = get_config_value("language_tags");
log_info("language_tags fetched");
const char* relay_countries_csv = get_config_value("relay_countries");
log_info("relay_countries fetched");
const char* posting_policy = get_config_value("posting_policy");
log_info("posting_policy fetched");
const char* payments_url = get_config_value("payments_url");
log_info("payments_url fetched");
// Get config values for limitations
log_info("Fetching limitation configuration values...");
int max_message_length = get_config_int("max_message_length", 16384);
log_info("max_message_length fetched");
int max_subscriptions_per_client = get_config_int("max_subscriptions_per_client", 20);
log_info("max_subscriptions_per_client fetched");
int max_limit = get_config_int("max_limit", 5000);
log_info("max_limit fetched");
int max_event_tags = get_config_int("max_event_tags", 100);
log_info("max_event_tags fetched");
int max_content_length = get_config_int("max_content_length", 8196);
log_info("max_content_length fetched");
int default_limit = get_config_int("default_limit", 500);
log_info("default_limit fetched");
int admin_enabled = get_config_bool("admin_enabled", 0);
log_info("admin_enabled fetched");
pthread_mutex_lock(&g_unified_cache.cache_lock);
// Update relay information fields
log_info("Storing string values in cache...");
if (relay_name) {
log_info("Storing relay_name");
strncpy(g_unified_cache.relay_info.name, relay_name, sizeof(g_unified_cache.relay_info.name) - 1);
free((char*)relay_name); // Free dynamically allocated string
log_info("relay_name stored and freed");
} else {
log_info("Using default relay_name");
strncpy(g_unified_cache.relay_info.name, "C Nostr Relay", sizeof(g_unified_cache.relay_info.name) - 1);
}
if (relay_description) {
log_info("Storing relay_description");
strncpy(g_unified_cache.relay_info.description, relay_description, sizeof(g_unified_cache.relay_info.description) - 1);
free((char*)relay_description); // Free dynamically allocated string
log_info("relay_description stored and freed");
} else {
log_info("Using default relay_description");
strncpy(g_unified_cache.relay_info.description, "A high-performance Nostr relay implemented in C with SQLite storage", sizeof(g_unified_cache.relay_info.description) - 1);
}
if (relay_software) {
log_info("Storing relay_software");
strncpy(g_unified_cache.relay_info.software, relay_software, sizeof(g_unified_cache.relay_info.software) - 1);
free((char*)relay_software); // Free dynamically allocated string
log_info("relay_software stored and freed");
} else {
log_info("Using default relay_software");
strncpy(g_unified_cache.relay_info.software, "https://git.laantungir.net/laantungir/c-relay.git", sizeof(g_unified_cache.relay_info.software) - 1);
}
if (relay_version) {
log_info("Storing relay_version");
strncpy(g_unified_cache.relay_info.version, relay_version, sizeof(g_unified_cache.relay_info.version) - 1);
free((char*)relay_version); // Free dynamically allocated string
log_info("relay_version stored and freed");
} else {
log_info("Using default relay_version");
strncpy(g_unified_cache.relay_info.version, "0.2.0", sizeof(g_unified_cache.relay_info.version) - 1);
}
if (relay_contact) {
log_info("Storing relay_contact");
strncpy(g_unified_cache.relay_info.contact, relay_contact, sizeof(g_unified_cache.relay_info.contact) - 1);
free((char*)relay_contact); // Free dynamically allocated string
log_info("relay_contact stored and freed");
}
if (relay_pubkey) {
log_info("Storing relay_pubkey");
strncpy(g_unified_cache.relay_info.pubkey, relay_pubkey, sizeof(g_unified_cache.relay_info.pubkey) - 1);
free((char*)relay_pubkey); // Free dynamically allocated string
log_info("relay_pubkey stored and freed");
}
if (posting_policy) {
log_info("Storing posting_policy");
strncpy(g_unified_cache.relay_info.posting_policy, posting_policy, sizeof(g_unified_cache.relay_info.posting_policy) - 1);
free((char*)posting_policy); // Free dynamically allocated string
log_info("posting_policy stored and freed");
}
if (payments_url) {
log_info("Storing payments_url");
strncpy(g_unified_cache.relay_info.payments_url, payments_url, sizeof(g_unified_cache.relay_info.payments_url) - 1);
free((char*)payments_url); // Free dynamically allocated string
log_info("payments_url stored and freed");
}
// Initialize supported NIPs array
g_unified_cache.relay_info.supported_nips = cJSON_CreateArray();
if (g_unified_cache.relay_info.supported_nips) {
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(1)); // NIP-01: Basic protocol
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(9)); // NIP-09: Event deletion
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(11)); // NIP-11: Relay information
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(13)); // NIP-13: Proof of Work
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(15)); // NIP-15: EOSE
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(20)); // NIP-20: Command results
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(40)); // NIP-40: Expiration Timestamp
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(42)); // NIP-42: Authentication
// Initialize supported NIPs array from config
log_info("Initializing supported_nips array");
if (supported_nips_csv) {
log_info("Parsing supported_nips from config");
g_unified_cache.relay_info.supported_nips = parse_comma_separated_array(supported_nips_csv);
log_info("supported_nips parsed successfully");
free((char*)supported_nips_csv); // Free dynamically allocated string
log_info("supported_nips_csv freed");
} else {
log_info("Using default supported_nips");
// Fallback to default supported NIPs
g_unified_cache.relay_info.supported_nips = cJSON_CreateArray();
if (g_unified_cache.relay_info.supported_nips) {
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(1)); // NIP-01: Basic protocol
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(9)); // NIP-09: Event deletion
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(11)); // NIP-11: Relay information
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(13)); // NIP-13: Proof of Work
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(15)); // NIP-15: EOSE
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(20)); // NIP-20: Command results
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(40)); // NIP-40: Expiration Timestamp
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(42)); // NIP-42: Authentication
}
log_info("Default supported_nips created");
}
// Initialize server limitations using configuration
log_info("Initializing server limitations");
g_unified_cache.relay_info.limitation = cJSON_CreateObject();
if (g_unified_cache.relay_info.limitation) {
log_info("Adding limitation fields");
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_message_length", max_message_length);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_subscriptions", max_subscriptions_per_client);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_limit", max_limit);
@@ -117,31 +254,60 @@ void init_relay_info() {
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "created_at_lower_limit", 0);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "created_at_upper_limit", 2147483647);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "default_limit", default_limit);
log_info("Limitation fields added");
} else {
log_info("Failed to create limitation object");
}
// Initialize empty retention policies (can be configured later)
log_info("Initializing retention policies");
g_unified_cache.relay_info.retention = cJSON_CreateArray();
// Initialize language tags - set to global for now
g_unified_cache.relay_info.language_tags = cJSON_CreateArray();
if (g_unified_cache.relay_info.language_tags) {
cJSON_AddItemToArray(g_unified_cache.relay_info.language_tags, cJSON_CreateString("*"));
// Initialize language tags from config
log_info("Initializing language_tags");
if (language_tags_csv) {
log_info("Parsing language_tags from config");
g_unified_cache.relay_info.language_tags = parse_comma_separated_array(language_tags_csv);
log_info("language_tags parsed successfully");
free((char*)language_tags_csv); // Free dynamically allocated string
log_info("language_tags_csv freed");
} else {
log_info("Using default language_tags");
// Fallback to global
g_unified_cache.relay_info.language_tags = cJSON_CreateArray();
if (g_unified_cache.relay_info.language_tags) {
cJSON_AddItemToArray(g_unified_cache.relay_info.language_tags, cJSON_CreateString("*"));
}
}
// Initialize relay countries - set to global for now
g_unified_cache.relay_info.relay_countries = cJSON_CreateArray();
if (g_unified_cache.relay_info.relay_countries) {
cJSON_AddItemToArray(g_unified_cache.relay_info.relay_countries, cJSON_CreateString("*"));
// Initialize relay countries from config
log_info("Initializing relay_countries");
if (relay_countries_csv) {
log_info("Parsing relay_countries from config");
g_unified_cache.relay_info.relay_countries = parse_comma_separated_array(relay_countries_csv);
log_info("relay_countries parsed successfully");
free((char*)relay_countries_csv); // Free dynamically allocated string
log_info("relay_countries_csv freed");
} else {
log_info("Using default relay_countries");
// Fallback to global
g_unified_cache.relay_info.relay_countries = cJSON_CreateArray();
if (g_unified_cache.relay_info.relay_countries) {
cJSON_AddItemToArray(g_unified_cache.relay_info.relay_countries, cJSON_CreateString("*"));
}
}
// Initialize content tags as empty array
log_info("Initializing tags");
g_unified_cache.relay_info.tags = cJSON_CreateArray();
// Initialize fees as empty object (no payment required by default)
log_info("Initializing fees");
g_unified_cache.relay_info.fees = cJSON_CreateObject();
log_info("Unlocking cache mutex");
pthread_mutex_unlock(&g_unified_cache.cache_lock);
log_success("Relay information initialized with default values");
}

View File

@@ -60,6 +60,7 @@ void init_pow_config() {
g_unified_cache.pow_config.enabled = 0;
log_info("PoW validation disabled via configuration");
}
free((char*)pow_mode); // Free dynamically allocated string
} else {
// Default to basic mode
g_unified_cache.pow_config.validation_flags = NOSTR_POW_VALIDATE_BASIC;

View File

@@ -211,13 +211,15 @@ int ginxsom_request_validator_init(const char *db_path, const char *app_name) {
// Initialize NIP-42 challenge manager using unified config
memset(&g_challenge_manager, 0, sizeof(g_challenge_manager));
const char* nip42_timeout = get_config_value("nip42_challenge_timeout");
g_challenge_manager.timeout_seconds = nip42_timeout ? atoi(nip42_timeout) : 600;
if (nip42_timeout) free((char*)nip42_timeout);
const char* nip42_tolerance = get_config_value("nip42_time_tolerance");
g_challenge_manager.time_tolerance_seconds = nip42_tolerance ? atoi(nip42_tolerance) : 300;
if (nip42_tolerance) free((char*)nip42_tolerance);
g_challenge_manager.last_cleanup = time(NULL);
g_validator_initialized = 1;
@@ -232,13 +234,20 @@ int ginxsom_request_validator_init(const char *db_path, const char *app_name) {
int nostr_auth_rules_enabled(void) {
// Use unified cache from config.c
const char* auth_enabled = get_config_value("auth_enabled");
int result = 0;
if (auth_enabled && strcmp(auth_enabled, "true") == 0) {
return 1;
result = 1;
}
if (auth_enabled) free((char*)auth_enabled);
// Also check legacy key
const char* auth_rules_enabled = get_config_value("auth_rules_enabled");
return (auth_rules_enabled && strcmp(auth_rules_enabled, "true") == 0) ? 1 : 0;
if (auth_rules_enabled && strcmp(auth_rules_enabled, "true") == 0) {
result = 1;
}
if (auth_rules_enabled) free((char*)auth_rules_enabled);
return result;
}
///////////////////////////////////////////////////////////////////////////////////////
@@ -344,9 +353,11 @@ int nostr_validate_unified_request(const char* json_string, size_t json_length)
const char* nip42_enabled = get_config_value("nip42_auth_enabled");
if (nip42_enabled && strcmp(nip42_enabled, "false") == 0) {
validator_debug_log("VALIDATOR_DEBUG: STEP 8 FAILED - NIP-42 is disabled\n");
free((char*)nip42_enabled);
cJSON_Delete(event);
return NOSTR_ERROR_NIP42_DISABLED;
}
if (nip42_enabled) free((char*)nip42_enabled);
// TODO: Implement full NIP-42 challenge validation
// For now, accept all valid NIP-42 events

View File

@@ -310,8 +310,51 @@ else
print_failure "Relay failed to start for network test"
fi
# TEST 10: Multiple Startup Attempts (Port Conflict)
print_test_header "Test 10: Port Conflict Handling"
# TEST 10: Port Override with Admin/Relay Key Overrides
print_test_header "Test 10: Port Override with -a/-r Flags"
cleanup_test_files
# Generate test keys (64 hex chars each)
TEST_ADMIN_PUBKEY="1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
TEST_RELAY_PRIVKEY="abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
print_info "Testing port override with -p 9999 -a $TEST_ADMIN_PUBKEY -r $TEST_RELAY_PRIVKEY"
# Start relay with port override and key overrides
timeout 15 $RELAY_BINARY -p 9999 -a $TEST_ADMIN_PUBKEY -r $TEST_RELAY_PRIVKEY > "test_port_override.log" 2>&1 &
relay_pid=$!
sleep 5
if kill -0 $relay_pid 2>/dev/null; then
# Check if relay bound to port 9999 (not default 8888)
if netstat -tln 2>/dev/null | grep -q ":9999"; then
print_success "Relay successfully bound to overridden port 9999"
else
print_failure "Relay not bound to overridden port 9999"
fi
# Check that relay started successfully
if check_relay_startup "test_port_override.log"; then
print_success "Relay startup completed with overrides"
else
print_failure "Relay failed to complete startup with overrides"
fi
# Check that admin keys were NOT generated (since -a was provided)
if ! check_admin_keys "test_port_override.log"; then
print_success "Admin keys not generated (correctly using provided -a key)"
else
print_failure "Admin keys generated despite -a override"
fi
stop_relay_test $relay_pid
else
print_failure "Relay failed to start with port/key overrides"
fi
# TEST 11: Multiple Startup Attempts (Port Conflict)
print_test_header "Test 11: Port Conflict Handling"
relay_pid1=$(start_relay_test "port_conflict_1" 10)
sleep 2
@@ -320,14 +363,14 @@ if kill -0 $relay_pid1 2>/dev/null; then
# Try to start a second relay (should fail due to port conflict)
relay_pid2=$(start_relay_test "port_conflict_2" 5)
sleep 1
if [ "$relay_pid2" = "0" ] || ! kill -0 $relay_pid2 2>/dev/null; then
print_success "Port conflict properly handled (second instance failed to start)"
else
print_failure "Multiple relay instances started (port conflict not handled)"
stop_relay_test $relay_pid2
fi
stop_relay_test $relay_pid1
else
print_failure "First relay instance failed to start"