Compare commits

...

2 Commits

Author SHA1 Message Date
Your Name
9f3b3dd773 v0.3.8 - safety push 2025-09-18 10:18:15 -04:00
Your Name
3210b9e752 v0.3.7 - working on cinfig api 2025-09-16 15:52:27 -04:00
10 changed files with 7740 additions and 1717 deletions

View File

@@ -1,5 +1,6 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -10,7 +11,7 @@
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Courier New', monospace;
background-color: white;
@@ -20,7 +21,7 @@
max-width: 1200px;
margin: 0 auto;
}
h1 {
border-bottom: 2px solid black;
padding-bottom: 10px;
@@ -28,7 +29,7 @@
font-weight: normal;
font-size: 24px;
}
h2 {
margin: 30px 0 15px 0;
font-weight: normal;
@@ -36,25 +37,28 @@
padding-left: 10px;
font-size: 16px;
}
.section {
border: 1px solid black;
padding: 20px;
margin-bottom: 20px;
}
.input-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
font-size: 14px;
}
input, textarea, select, button {
input,
textarea,
select,
button {
width: 100%;
padding: 8px;
border: 1px solid black;
@@ -63,7 +67,7 @@
background-color: white;
color: black;
}
button {
background-color: black;
color: white;
@@ -71,65 +75,65 @@
margin: 5px 0;
font-weight: bold;
}
button:hover {
background-color: #333;
}
button:disabled {
background-color: #ccc;
color: #666;
cursor: not-allowed;
}
.status {
padding: 10px;
margin: 10px 0;
border: 1px solid black;
font-weight: bold;
}
.status.connected {
background-color: black;
color: white;
}
.status.disconnected {
background-color: white;
color: black;
}
.status.authenticated {
background-color: black;
color: white;
}
.status.error {
background-color: white;
color: black;
border: 2px solid black;
}
.config-table {
border: 1px solid black;
width: 100%;
border-collapse: collapse;
margin: 10px 0;
}
.config-table th,
.config-table td {
border: 1px solid black;
padding: 8px;
text-align: left;
}
.config-table th {
background-color: black;
color: white;
font-weight: bold;
}
.json-display {
background-color: white;
border: 1px solid black;
@@ -141,7 +145,7 @@
overflow-y: auto;
margin: 10px 0;
}
.log-panel {
height: 200px;
overflow-y: auto;
@@ -150,71 +154,72 @@
font-size: 12px;
background-color: white;
}
.log-entry {
margin-bottom: 5px;
border-bottom: 1px solid #ccc;
padding-bottom: 5px;
}
.log-timestamp {
font-weight: bold;
}
.inline-buttons {
display: flex;
gap: 10px;
}
.inline-buttons button {
flex: 1;
}
.user-info {
padding: 10px;
border: 1px solid black;
margin: 10px 0;
background-color: white;
}
.user-pubkey {
font-family: 'Courier New', monospace;
font-size: 12px;
word-break: break-all;
margin: 5px 0;
}
.hidden {
display: none;
}
#login-section {
text-align: center;
padding: 20px;
}
@media (max-width: 768px) {
body {
padding: 10px;
}
.inline-buttons {
flex-direction: column;
}
h1 {
font-size: 20px;
}
h2 {
font-size: 14px;
}
}
</style>
</head>
<body>
<h1>C-RELAY ADMIN API</h1>
<!-- Testing Section - Always Visible -->
<div class="section">
<h2>DEBUG - TEST FETCH WITHOUT LOGIN</h2>
@@ -225,7 +230,7 @@
<div class="status disconnected" id="relay-status">READY TO FETCH</div>
<button type="button" id="fetch-config-btn">FETCH CONFIGURATION (NO LOGIN)</button>
<div class="status disconnected" id="config-status">NO CONFIGURATION LOADED</div>
<div id="config-display" class="hidden">
<div id="config-view-mode">
<table class="config-table" id="config-table">
@@ -239,7 +244,7 @@
<tbody id="config-table-body">
</tbody>
</table>
<div class="inline-buttons">
<button type="button" id="edit-config-btn">EDIT CONFIGURATION</button>
<button type="button" id="copy-config-btn">COPY CONFIGURATION</button>
@@ -251,13 +256,13 @@
<div id="config-form" class="section">
<!-- Dynamic form will be generated here -->
</div>
<div class="inline-buttons">
<button type="button" id="save-config-btn">SAVE & PUBLISH</button>
<button type="button" id="cancel-edit-btn">CANCEL</button>
</div>
</div>
<div id="config-raw-display">
<h3>Raw Event JSON:</h3>
<div class="json-display" id="raw-config-json"></div>
@@ -273,21 +278,23 @@
<!-- nostr-lite login UI will be injected here -->
</div>
</div>
<!-- Main Interface (hidden until logged in) -->
<div id="main-interface" class="hidden">
<!-- User Info Section -->
<div class="section">
<h2>LOGGED IN USER</h2>
<div class="user-info">
<div><strong>Name:</strong> <span id="user-name">Loading...</span></div>
<div><strong>Public Key:</strong> <div class="user-pubkey" id="user-pubkey">Loading...</div></div>
<div><strong>Public Key:</strong>
<div class="user-pubkey" id="user-pubkey">Loading...</div>
</div>
<div><strong>About:</strong> <span id="user-about">Loading...</span></div>
</div>
<button type="button" id="logout-btn">LOGOUT</button>
</div>
<!-- Command Section -->
<div class="section">
<h2>ADMIN COMMANDS</h2>
@@ -300,20 +307,20 @@
<option value="get_status">Get Status</option>
</select>
</div>
<div class="input-group">
<label for="command-payload">Command Payload (JSON):</label>
<textarea id="command-payload" rows="4" placeholder='{"param": "value"}'></textarea>
</div>
<div class="input-group">
<label>Event Preview:</label>
<div class="json-display" id="event-preview">No event constructed</div>
</div>
<button type="button" id="send-command-btn">SEND COMMAND</button>
</div>
<!-- Log Section -->
<div class="section">
<h2>EVENT LOG</h2>
@@ -324,7 +331,7 @@
</div>
<button type="button" id="clear-log-btn">CLEAR LOG</button>
</div>
</div>
<!-- Load the official nostr-tools bundle first -->
@@ -342,10 +349,10 @@
<script>
// Global error handler to prevent page refreshes
window.addEventListener('error', function(e) {
window.addEventListener('error', function (e) {
console.error('Global error caught:', e.error);
console.error('Error message:', e.message);
console.error('Error filename:', e.filename);
@@ -354,7 +361,7 @@
return true; // Prevent page refresh
});
window.addEventListener('unhandledrejection', function(e) {
window.addEventListener('unhandledrejection', function (e) {
console.error('Unhandled promise rejection:', e.reason);
e.preventDefault(); // Prevent default browser error handling
return true; // Prevent page refresh
@@ -399,10 +406,10 @@
function log(message, type = 'INFO') {
const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
const logMessage = `${timestamp} [${type}]: ${message}`;
// Always log to browser console so we don't lose logs on refresh
console.log(logMessage);
// Also log to UI if elements exist
if (logPanel) {
const logEntry = document.createElement('div');
@@ -418,35 +425,33 @@
try {
await window.NOSTR_LOGIN_LITE.init({
theme: 'default',
darkMode: false,
relays: ['wss://relay.damus.io', 'wss://nos.lol'],
methods: {
extension: true,
local: true,
readonly: true,
connect: true, // Enables "Nostr Connect" (NIP-46)
remote: true, // Also needed for "Nostr Connect" compatibility
otp: true // Enables "DM/OTP"
connect: true,
remote: true,
otp: true
},
floatingTab: {
enabled: true,
hPosition: 0.98, // 80% from left
vPosition: 0.00, // Top of page
hPosition: 1, // 0.0-1.0 or '95%' from left
vPosition: 0, // 0.0-1.0 or '50%' from top
appearance: {
style: 'minimal',
theme: 'auto',
icon: '',
text: 'Login',
iconOnly: false
style: 'pill', // 'pill', 'square', 'circle', 'minimal'
icon: '', // Clean display without icon placeholders
text: 'Login'
},
behavior: {
hideWhenAuthenticated: false,
showUserInfo: true,
autoSlide: false,
persistent: false
}
},
debug: true
autoSlide: true
},
getUserInfo: true, // Enable profile fetching
getUserRelay: [ // Specific relays for profile fetching
'wss://relay.laantungir.net'
]
}
});
nlLite = window.NOSTR_LOGIN_LITE;
@@ -462,26 +467,26 @@
// Handle authentication events
function handleAuthEvent(event) {
const {pubkey, method, error} = event.detail;
const { pubkey, method, error } = event.detail;
if (method && pubkey) {
userPubkey = pubkey;
isLoggedIn = true;
console.log(`Login successful! Method: ${method}`);
console.log(`Public key: ${pubkey}`);
showMainInterface();
loadUserProfile();
// Automatically fetch configuration after login
setTimeout(() => {
fetchConfiguration().catch(error => {
console.log('Auto-fetch configuration failed: ' + error.message);
});
}, 1000);
console.log('Login successful. Auto-fetching configuration...');
} else if (error) {
console.log(`Authentication error: ${error}`);
}
@@ -506,7 +511,7 @@
// Create a SimplePool instance
relayPool = new window.NostrTools.SimplePool();
const relays = ['wss://relay.laantungir.net'];
// Get profile event (kind 0) for the user
const events = await relayPool.querySync(relays, {
kinds: [0],
@@ -549,7 +554,7 @@
const relays = ['wss://relay.laantungir.net'];
relayPool = null;
}
// Clean up configuration WebSocket
if (configWebSocket) {
console.log('Closing configuration WebSocket...');
@@ -557,22 +562,22 @@
configWebSocket = null;
subscriptionId = null;
}
await nlLite.logout();
userPubkey = null;
isLoggedIn = false;
currentConfig = null;
// Reset UI
mainInterface.classList.add('hidden');
loginSection.classList.remove('hidden');
updateConfigStatus(false);
relayStatus.textContent = 'READY TO FETCH';
relayStatus.className = 'status disconnected';
console.log('Logged out successfully');
} catch (error) {
console.log('Logout failed: ' + error.message);
}
@@ -603,11 +608,11 @@
async function subscribeToConfiguration() {
try {
console.log('=== STARTING DIRECT WEBSOCKET CONFIGURATION SUBSCRIPTION ===');
if (!isLoggedIn) {
console.log('WARNING: Not logged in, but proceeding with subscription test');
}
const url = relayUrl.value.trim();
if (!url) {
console.error('Please enter a relay URL');
@@ -653,10 +658,10 @@
reject(new Error('Connection timeout'));
}, 10000); // 10 second timeout
configWebSocket.onopen = function(event) {
configWebSocket.onopen = function (event) {
console.log('WebSocket connection established');
clearTimeout(timeoutId);
relayStatus.textContent = 'CONNECTED - SUBSCRIBING...';
relayStatus.className = 'status connected';
@@ -683,7 +688,7 @@
resolve(true);
};
configWebSocket.onmessage = function(event) {
configWebSocket.onmessage = function (event) {
try {
const message = JSON.parse(event.data);
console.log('Received message:', message);
@@ -704,12 +709,12 @@
relayStatus.textContent = 'SUBSCRIBED - LIVE UPDATES';
relayStatus.className = 'status connected';
} else if (messageType === "EOSE" && subId === subscriptionId) {
clearTimeout(timeoutId);
console.log('EOSE received - End of stored events');
console.log('Current config after EOSE:', currentConfig);
if (!currentConfig) {
console.log('No configuration events were received');
configStatus.textContent = 'NO CONFIGURATION EVENTS FOUND';
@@ -720,22 +725,22 @@
relayStatus.textContent = 'SUBSCRIBED - LIVE UPDATES';
relayStatus.className = 'status connected';
}
} else if (messageType === "NOTICE") {
console.log('Received NOTICE:', eventData || message[1]);
} else if (messageType === "OK") {
console.log('Received OK response:', message);
}
}
} catch (parseError) {
console.error('Error parsing message:', parseError);
console.log('Raw message data:', event.data);
}
};
configWebSocket.onerror = function(error) {
configWebSocket.onerror = function (error) {
clearTimeout(timeoutId);
console.error('WebSocket error:', error);
console.error('WebSocket URL that failed:', urlsToTry[0]);
@@ -750,7 +755,7 @@
reject(new Error(`WebSocket connection error to ${urlsToTry[0]}`));
};
configWebSocket.onclose = function(event) {
configWebSocket.onclose = function (event) {
clearTimeout(timeoutId);
console.log('WebSocket connection closed:', event.code, event.reason);
relayStatus.textContent = 'CONNECTION CLOSED';
@@ -764,7 +769,7 @@
console.error('Configuration subscription failed:', error.message);
console.error('Configuration subscription failed:', error);
console.error('Error stack:', error.stack);
relayStatus.textContent = 'SUBSCRIPTION FAILED';
relayStatus.className = 'status error';
return false;
@@ -780,12 +785,12 @@
try {
console.log('=== DISPLAYING CONFIGURATION EVENT ===');
console.log('Event received for display:', event);
currentConfig = event;
// Clear existing table
configTableBody.innerHTML = '';
// Display basic event info
const basicInfo = [
['Event ID', event.id],
@@ -794,14 +799,14 @@
['Kind', event.kind],
['Content', event.content]
];
console.log(`Adding ${basicInfo.length} basic info rows`);
basicInfo.forEach(([key, value]) => {
const row = document.createElement('tr');
row.innerHTML = `<td>${key}</td><td>${value}</td><td>-</td>`;
configTableBody.appendChild(row);
});
// Display tags
console.log(`Processing ${event.tags.length} tags`);
event.tags.forEach(tag => {
@@ -811,13 +816,13 @@
configTableBody.appendChild(row);
}
});
// Display raw JSON
rawConfigJson.textContent = JSON.stringify(event, null, 2);
console.log('Configuration display completed successfully');
updateConfigStatus(true);
} catch (error) {
console.error('Error in displayConfiguration:', error.message);
console.error('Display configuration error:', error);
@@ -832,7 +837,7 @@
}
configForm.innerHTML = '';
// Define field types and validation for different config parameters
const fieldTypes = {
'auth_enabled': 'boolean',
@@ -897,16 +902,16 @@
Object.entries(configData).forEach(([key, value]) => {
const fieldType = fieldTypes[key] || 'text';
const description = descriptions[key] || key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
const fieldGroup = document.createElement('div');
fieldGroup.className = 'input-group';
const label = document.createElement('label');
label.textContent = description;
label.setAttribute('for', `config-${key}`);
let input;
if (fieldType === 'boolean') {
input = document.createElement('select');
input.innerHTML = `
@@ -923,15 +928,15 @@
input.type = 'text';
input.value = value;
}
input.id = `config-${key}`;
input.name = key;
// Make relay_pubkey read-only
if (key === 'relay_pubkey' || key === 'd') {
input.disabled = true;
}
fieldGroup.appendChild(label);
fieldGroup.appendChild(input);
configForm.appendChild(fieldGroup);
@@ -945,7 +950,7 @@
console.log('No configuration loaded to edit');
return;
}
generateConfigForm(currentConfig);
configViewMode.classList.add('hidden');
configEditMode.classList.remove('hidden');
@@ -972,18 +977,18 @@
try {
console.log('Building new configuration event...');
// Collect form data
const formData = new FormData();
const formInputs = configForm.querySelectorAll('input, select');
const newTags = [];
// Preserve the 'd' tag (relay identifier) from original event
const dTag = currentConfig.tags.find(tag => tag[0] === 'd');
if (dTag) {
newTags.push(dTag);
}
// Add updated configuration tags
formInputs.forEach(input => {
if (!input.disabled && input.name) {
@@ -1000,11 +1005,11 @@
content: currentConfig.content || 'C Nostr Relay Configuration'
};
console.log('Signing event with nostr-lite...');
// Sign the event using nostr-lite
const signedEvent = await nlLite.signEvent(newEvent);
console.log('Signing event with window.nostr...');
// Sign the event using window.nostr (NIP-07 interface)
const signedEvent = await window.nostr.signEvent(newEvent);
if (!signedEvent || !signedEvent.sig) {
throw new Error('Event signing failed - no signature returned');
}
@@ -1032,7 +1037,7 @@
// Create a new WebSocket connection for publishing
const publishWs = new WebSocket(url);
return new Promise((resolve, reject) => {
let timeoutId = setTimeout(() => {
console.error('Publish timeout');
@@ -1040,31 +1045,31 @@
reject(new Error('Publish timeout'));
}, 10000);
publishWs.onopen = function() {
publishWs.onopen = function () {
console.log('Publish WebSocket connected, sending event...');
// Send EVENT message
const eventMessage = ["EVENT", signedEvent];
console.log('Sending EVENT message:', JSON.stringify(eventMessage));
publishWs.send(JSON.stringify(eventMessage));
};
publishWs.onmessage = function(event) {
publishWs.onmessage = function (event) {
try {
const message = JSON.parse(event.data);
console.log('Publish response:', message);
if (Array.isArray(message)) {
const [messageType, eventId, success, errorMsg] = message;
if (messageType === "OK") {
clearTimeout(timeoutId);
publishWs.close();
if (success) {
console.log('Configuration published successfully!');
console.log('The updated configuration should appear automatically via subscription');
// Exit edit mode
exitEditMode();
resolve(true);
@@ -1079,14 +1084,14 @@
}
};
publishWs.onerror = function(error) {
publishWs.onerror = function (error) {
clearTimeout(timeoutId);
console.error('Publish WebSocket error:', error);
publishWs.close();
reject(new Error('Publish WebSocket error'));
};
publishWs.onclose = function() {
publishWs.onclose = function () {
clearTimeout(timeoutId);
};
});
@@ -1101,12 +1106,12 @@
function updateEventPreview() {
const type = commandType.value;
const payload = commandPayload.value.trim();
if (!type || !userPubkey) {
eventPreview.textContent = 'No event constructed';
return;
}
const event = {
kind: 1,
pubkey: userPubkey,
@@ -1119,13 +1124,13 @@
id: 'EVENT_ID_PLACEHOLDER',
sig: 'SIGNATURE_PLACEHOLDER'
};
eventPreview.textContent = JSON.stringify(event, null, 2);
}
// Event handlers
logoutBtn.addEventListener('click', logout);
fetchConfigBtn.addEventListener('click', function(e) {
fetchConfigBtn.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
fetchConfiguration().catch(error => {
@@ -1133,7 +1138,7 @@
});
});
copyConfigBtn.addEventListener('click', function(e) {
copyConfigBtn.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
if (currentConfig) {
@@ -1143,13 +1148,13 @@
}
});
editConfigBtn.addEventListener('click', function(e) {
editConfigBtn.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
enterEditMode();
});
saveConfigBtn.addEventListener('click', function(e) {
saveConfigBtn.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
saveConfiguration().catch(error => {
@@ -1157,7 +1162,7 @@
});
});
cancelEditBtn.addEventListener('click', function(e) {
cancelEditBtn.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
exitEditMode();
@@ -1166,7 +1171,7 @@
commandType.addEventListener('change', updateEventPreview);
commandPayload.addEventListener('input', updateEventPreview);
sendCommandBtn.addEventListener('click', function(e) {
sendCommandBtn.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
const type = commandType.value;
@@ -1174,11 +1179,11 @@
console.log('Please select a command type');
return;
}
console.log(`Command sending not yet implemented: ${type}`);
});
clearLogBtn.addEventListener('click', function(e) {
clearLogBtn.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
logPanel.innerHTML = '<div class="log-entry"><span class="log-timestamp">SYSTEM:</span> Log cleared.</div>';
@@ -1191,4 +1196,5 @@
});
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1 @@
3327716
295261

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@
#include <sqlite3.h>
#include <cjson/cJSON.h>
#include <time.h>
#include <pthread.h>
// Configuration constants
#define CONFIG_VALUE_MAX_LENGTH 1024
@@ -23,14 +24,71 @@
// Database path for event-based config
extern char g_database_path[512];
// Configuration manager structure
// Unified configuration cache structure (consolidates all caching systems)
typedef struct {
sqlite3* db;
char relay_pubkey[65];
// Critical keys (frequently accessed)
char admin_pubkey[65];
time_t last_config_check;
char config_file_path[512]; // Temporary for compatibility
} config_manager_t;
char relay_pubkey[65];
// Auth config (from request_validator)
int auth_required;
long max_file_size;
int admin_enabled;
int nip42_mode;
int nip42_challenge_timeout;
int nip42_time_tolerance;
// Static buffer for config values (replaces static buffers in get_config_value functions)
char temp_buffer[CONFIG_VALUE_MAX_LENGTH];
// NIP-11 relay information (migrated from g_relay_info in main.c)
struct {
char name[RELAY_NAME_MAX_LENGTH];
char description[RELAY_DESCRIPTION_MAX_LENGTH];
char banner[RELAY_URL_MAX_LENGTH];
char icon[RELAY_URL_MAX_LENGTH];
char pubkey[RELAY_PUBKEY_MAX_LENGTH];
char contact[RELAY_CONTACT_MAX_LENGTH];
char software[RELAY_URL_MAX_LENGTH];
char version[64];
char privacy_policy[RELAY_URL_MAX_LENGTH];
char terms_of_service[RELAY_URL_MAX_LENGTH];
cJSON* supported_nips;
cJSON* limitation;
cJSON* retention;
cJSON* relay_countries;
cJSON* language_tags;
cJSON* tags;
char posting_policy[RELAY_URL_MAX_LENGTH];
cJSON* fees;
char payments_url[RELAY_URL_MAX_LENGTH];
} relay_info;
// NIP-13 PoW configuration (migrated from g_pow_config in main.c)
struct {
int enabled;
int min_pow_difficulty;
int validation_flags;
int require_nonce_tag;
int reject_lower_targets;
int strict_format;
int anti_spam_mode;
} pow_config;
// NIP-40 Expiration configuration (migrated from g_expiration_config in main.c)
struct {
int enabled;
int strict_mode;
int filter_responses;
int delete_expired;
long grace_period;
} expiration_config;
// Cache management
time_t cache_expires;
int cache_valid;
pthread_mutex_t cache_lock;
} unified_config_cache_t;
// Command line options structure for first-time startup
typedef struct {
@@ -39,8 +97,8 @@ typedef struct {
char relay_privkey_override[65]; // Empty string = not set, 64-char hex = override
} cli_options_t;
// Global configuration manager
extern config_manager_t g_config_manager;
// Global unified configuration cache
extern unified_config_cache_t g_unified_cache;
// Core configuration functions (temporary compatibility)
int init_configuration_system(const char* config_dir_override, const char* config_file_override);
@@ -90,4 +148,53 @@ int parse_auth_required_kinds(const char* kinds_str, int* kinds_array, int max_k
int is_nip42_auth_required_for_kind(int event_kind);
int is_nip42_auth_globally_required(void);
// ================================
// NEW ADMIN API FUNCTIONS
// ================================
// Config table management functions (config table created via embedded schema)
const char* get_config_value_from_table(const char* key);
int set_config_value_in_table(const char* key, const char* value, const char* data_type,
const char* description, const char* category, int requires_restart);
int update_config_in_table(const char* key, const char* value);
int populate_default_config_values(void);
int add_pubkeys_to_config_table(void);
// Admin event processing functions
int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size);
int process_admin_config_event(cJSON* event, char* error_message, size_t error_size);
int process_admin_auth_event(cJSON* event, char* error_message, size_t error_size);
// Auth rules management functions
int add_auth_rule_from_config(const char* rule_type, const char* pattern_type,
const char* pattern_value, const char* action);
int remove_auth_rule_from_config(const char* rule_type, const char* pattern_type,
const char* pattern_value);
// Unified configuration cache management
void force_config_cache_refresh(void);
const char* get_admin_pubkey_cached(void);
const char* get_relay_pubkey_cached(void);
void invalidate_config_cache(void);
int reload_config_from_table(void);
// Hybrid config access functions
const char* get_config_value_hybrid(const char* key);
int is_config_table_ready(void);
// Migration support functions
int initialize_config_system_with_migration(void);
int migrate_config_from_events_to_table(void);
int populate_config_table_from_event(const cJSON* event);
// Startup configuration processing functions
int process_startup_config_event(const cJSON* event);
int process_startup_config_event_with_fallback(const cJSON* event);
// Dynamic event generation functions for WebSocket configuration fetching
cJSON* generate_config_event_from_table(void);
int req_filter_requests_config_events(const cJSON* filter);
cJSON* generate_synthetic_config_event_for_subscription(const char* sub_id, const cJSON* filters);
char* generate_config_event_json(void);
#endif /* CONFIG_H */

View File

@@ -68,8 +68,8 @@ struct relay_info {
char payments_url[RELAY_URL_MAX_LENGTH];
};
// Global relay information instance
static struct relay_info g_relay_info = {0};
// Global relay information instance moved to unified cache
// static struct relay_info g_relay_info = {0}; // REMOVED - now in g_unified_cache.relay_info
// NIP-13 PoW configuration structure
struct pow_config {
@@ -227,6 +227,9 @@ int nostr_validate_unified_request(const char* json_string, size_t json_length);
// Forward declaration for configuration event handling (kind 33334)
int handle_configuration_event(cJSON* event, char* error_message, size_t error_size);
// Forward declaration for admin event processing (kinds 33334 and 33335)
int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size);
// Forward declaration for NOTICE message support
void send_notice_message(struct lws* wsi, const char* message);
@@ -695,7 +698,12 @@ int broadcast_event_to_subscriptions(cJSON* event) {
}
// Check if event is expired and should not be broadcast (NIP-40)
if (g_expiration_config.enabled && g_expiration_config.filter_responses) {
pthread_mutex_lock(&g_unified_cache.cache_lock);
int expiration_enabled = g_unified_cache.expiration_config.enabled;
int filter_responses = g_unified_cache.expiration_config.filter_responses;
pthread_mutex_unlock(&g_unified_cache.cache_lock);
if (expiration_enabled && filter_responses) {
time_t current_time = time(NULL);
if (is_event_expired(event, current_time)) {
char debug_msg[256];
@@ -1477,131 +1485,147 @@ int mark_event_as_deleted(const char* event_id, const char* deletion_event_id, c
// Initialize relay information using configuration system
void init_relay_info() {
// Load relay information from configuration system
// Get all config values first (without holding mutex to avoid deadlock)
const char* relay_name = get_config_value("relay_name");
if (relay_name) {
strncpy(g_relay_info.name, relay_name, sizeof(g_relay_info.name) - 1);
} else {
strncpy(g_relay_info.name, "C Nostr Relay", sizeof(g_relay_info.name) - 1);
}
const char* relay_description = get_config_value("relay_description");
if (relay_description) {
strncpy(g_relay_info.description, relay_description, sizeof(g_relay_info.description) - 1);
} else {
strncpy(g_relay_info.description, "A high-performance Nostr relay implemented in C with SQLite storage", sizeof(g_relay_info.description) - 1);
}
const char* relay_software = get_config_value("relay_software");
if (relay_software) {
strncpy(g_relay_info.software, relay_software, sizeof(g_relay_info.software) - 1);
} else {
strncpy(g_relay_info.software, "https://git.laantungir.net/laantungir/c-relay.git", sizeof(g_relay_info.software) - 1);
}
const char* relay_version = get_config_value("relay_version");
if (relay_version) {
strncpy(g_relay_info.version, relay_version, sizeof(g_relay_info.version) - 1);
} else {
strncpy(g_relay_info.version, "0.2.0", sizeof(g_relay_info.version) - 1);
}
// Load optional fields
const char* relay_contact = get_config_value("relay_contact");
if (relay_contact) {
strncpy(g_relay_info.contact, relay_contact, sizeof(g_relay_info.contact) - 1);
const char* relay_pubkey = get_config_value("relay_pubkey");
// Get config values for limitations
int max_message_length = get_config_int("max_message_length", 16384);
int max_subscriptions_per_client = get_config_int("max_subscriptions_per_client", 20);
int max_limit = get_config_int("max_limit", 5000);
int max_event_tags = get_config_int("max_event_tags", 100);
int max_content_length = get_config_int("max_content_length", 8196);
int default_limit = get_config_int("default_limit", 500);
int admin_enabled = get_config_bool("admin_enabled", 0);
pthread_mutex_lock(&g_unified_cache.cache_lock);
// Update relay information fields
if (relay_name) {
strncpy(g_unified_cache.relay_info.name, relay_name, sizeof(g_unified_cache.relay_info.name) - 1);
} else {
strncpy(g_unified_cache.relay_info.name, "C Nostr Relay", sizeof(g_unified_cache.relay_info.name) - 1);
}
if (relay_description) {
strncpy(g_unified_cache.relay_info.description, relay_description, sizeof(g_unified_cache.relay_info.description) - 1);
} else {
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) {
strncpy(g_unified_cache.relay_info.software, relay_software, sizeof(g_unified_cache.relay_info.software) - 1);
} else {
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) {
strncpy(g_unified_cache.relay_info.version, relay_version, sizeof(g_unified_cache.relay_info.version) - 1);
} else {
strncpy(g_unified_cache.relay_info.version, "0.2.0", sizeof(g_unified_cache.relay_info.version) - 1);
}
if (relay_contact) {
strncpy(g_unified_cache.relay_info.contact, relay_contact, sizeof(g_unified_cache.relay_info.contact) - 1);
}
const char* relay_pubkey = get_config_value("relay_pubkey");
if (relay_pubkey) {
strncpy(g_relay_info.pubkey, relay_pubkey, sizeof(g_relay_info.pubkey) - 1);
strncpy(g_unified_cache.relay_info.pubkey, relay_pubkey, sizeof(g_unified_cache.relay_info.pubkey) - 1);
}
// Initialize supported NIPs array
g_relay_info.supported_nips = cJSON_CreateArray();
if (g_relay_info.supported_nips) {
cJSON_AddItemToArray(g_relay_info.supported_nips, cJSON_CreateNumber(1)); // NIP-01: Basic protocol
cJSON_AddItemToArray(g_relay_info.supported_nips, cJSON_CreateNumber(9)); // NIP-09: Event deletion
cJSON_AddItemToArray(g_relay_info.supported_nips, cJSON_CreateNumber(11)); // NIP-11: Relay information
cJSON_AddItemToArray(g_relay_info.supported_nips, cJSON_CreateNumber(13)); // NIP-13: Proof of Work
cJSON_AddItemToArray(g_relay_info.supported_nips, cJSON_CreateNumber(15)); // NIP-15: EOSE
cJSON_AddItemToArray(g_relay_info.supported_nips, cJSON_CreateNumber(20)); // NIP-20: Command results
cJSON_AddItemToArray(g_relay_info.supported_nips, cJSON_CreateNumber(40)); // NIP-40: Expiration Timestamp
cJSON_AddItemToArray(g_relay_info.supported_nips, cJSON_CreateNumber(42)); // NIP-42: Authentication
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 server limitations using configuration
g_relay_info.limitation = cJSON_CreateObject();
if (g_relay_info.limitation) {
cJSON_AddNumberToObject(g_relay_info.limitation, "max_message_length", get_config_int("max_message_length", 16384));
cJSON_AddNumberToObject(g_relay_info.limitation, "max_subscriptions", get_config_int("max_subscriptions_per_client", 20));
cJSON_AddNumberToObject(g_relay_info.limitation, "max_limit", get_config_int("max_limit", 5000));
cJSON_AddNumberToObject(g_relay_info.limitation, "max_subid_length", SUBSCRIPTION_ID_MAX_LENGTH);
cJSON_AddNumberToObject(g_relay_info.limitation, "max_event_tags", get_config_int("max_event_tags", 100));
cJSON_AddNumberToObject(g_relay_info.limitation, "max_content_length", get_config_int("max_content_length", 8196));
cJSON_AddNumberToObject(g_relay_info.limitation, "min_pow_difficulty", g_pow_config.min_pow_difficulty);
cJSON_AddBoolToObject(g_relay_info.limitation, "auth_required", get_config_bool("admin_enabled", 0) ? cJSON_True : cJSON_False);
cJSON_AddBoolToObject(g_relay_info.limitation, "payment_required", cJSON_False);
cJSON_AddBoolToObject(g_relay_info.limitation, "restricted_writes", cJSON_False);
cJSON_AddNumberToObject(g_relay_info.limitation, "created_at_lower_limit", 0);
cJSON_AddNumberToObject(g_relay_info.limitation, "created_at_upper_limit", 2147483647);
cJSON_AddNumberToObject(g_relay_info.limitation, "default_limit", get_config_int("default_limit", 500));
g_unified_cache.relay_info.limitation = cJSON_CreateObject();
if (g_unified_cache.relay_info.limitation) {
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);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_subid_length", SUBSCRIPTION_ID_MAX_LENGTH);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_event_tags", max_event_tags);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_content_length", max_content_length);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "min_pow_difficulty", g_unified_cache.pow_config.min_pow_difficulty);
cJSON_AddBoolToObject(g_unified_cache.relay_info.limitation, "auth_required", admin_enabled ? cJSON_True : cJSON_False);
cJSON_AddBoolToObject(g_unified_cache.relay_info.limitation, "payment_required", cJSON_False);
cJSON_AddBoolToObject(g_unified_cache.relay_info.limitation, "restricted_writes", cJSON_False);
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);
}
// Initialize empty retention policies (can be configured later)
g_relay_info.retention = cJSON_CreateArray();
g_unified_cache.relay_info.retention = cJSON_CreateArray();
// Initialize language tags - set to global for now
g_relay_info.language_tags = cJSON_CreateArray();
if (g_relay_info.language_tags) {
cJSON_AddItemToArray(g_relay_info.language_tags, cJSON_CreateString("*"));
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_relay_info.relay_countries = cJSON_CreateArray();
if (g_relay_info.relay_countries) {
cJSON_AddItemToArray(g_relay_info.relay_countries, cJSON_CreateString("*"));
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
g_relay_info.tags = cJSON_CreateArray();
g_unified_cache.relay_info.tags = cJSON_CreateArray();
// Initialize fees as empty object (no payment required by default)
g_relay_info.fees = cJSON_CreateObject();
g_unified_cache.relay_info.fees = cJSON_CreateObject();
pthread_mutex_unlock(&g_unified_cache.cache_lock);
log_success("Relay information initialized with default values");
}
// Clean up relay information JSON objects
void cleanup_relay_info() {
if (g_relay_info.supported_nips) {
cJSON_Delete(g_relay_info.supported_nips);
g_relay_info.supported_nips = NULL;
pthread_mutex_lock(&g_unified_cache.cache_lock);
if (g_unified_cache.relay_info.supported_nips) {
cJSON_Delete(g_unified_cache.relay_info.supported_nips);
g_unified_cache.relay_info.supported_nips = NULL;
}
if (g_relay_info.limitation) {
cJSON_Delete(g_relay_info.limitation);
g_relay_info.limitation = NULL;
if (g_unified_cache.relay_info.limitation) {
cJSON_Delete(g_unified_cache.relay_info.limitation);
g_unified_cache.relay_info.limitation = NULL;
}
if (g_relay_info.retention) {
cJSON_Delete(g_relay_info.retention);
g_relay_info.retention = NULL;
if (g_unified_cache.relay_info.retention) {
cJSON_Delete(g_unified_cache.relay_info.retention);
g_unified_cache.relay_info.retention = NULL;
}
if (g_relay_info.language_tags) {
cJSON_Delete(g_relay_info.language_tags);
g_relay_info.language_tags = NULL;
if (g_unified_cache.relay_info.language_tags) {
cJSON_Delete(g_unified_cache.relay_info.language_tags);
g_unified_cache.relay_info.language_tags = NULL;
}
if (g_relay_info.relay_countries) {
cJSON_Delete(g_relay_info.relay_countries);
g_relay_info.relay_countries = NULL;
if (g_unified_cache.relay_info.relay_countries) {
cJSON_Delete(g_unified_cache.relay_info.relay_countries);
g_unified_cache.relay_info.relay_countries = NULL;
}
if (g_relay_info.tags) {
cJSON_Delete(g_relay_info.tags);
g_relay_info.tags = NULL;
if (g_unified_cache.relay_info.tags) {
cJSON_Delete(g_unified_cache.relay_info.tags);
g_unified_cache.relay_info.tags = NULL;
}
if (g_relay_info.fees) {
cJSON_Delete(g_relay_info.fees);
g_relay_info.fees = NULL;
if (g_unified_cache.relay_info.fees) {
cJSON_Delete(g_unified_cache.relay_info.fees);
g_unified_cache.relay_info.fees = NULL;
}
pthread_mutex_unlock(&g_unified_cache.cache_lock);
}
// Generate NIP-11 compliant JSON document
@@ -1612,79 +1636,83 @@ cJSON* generate_relay_info_json() {
return NULL;
}
pthread_mutex_lock(&g_unified_cache.cache_lock);
// Add basic relay information
if (strlen(g_relay_info.name) > 0) {
cJSON_AddStringToObject(info, "name", g_relay_info.name);
if (strlen(g_unified_cache.relay_info.name) > 0) {
cJSON_AddStringToObject(info, "name", g_unified_cache.relay_info.name);
}
if (strlen(g_relay_info.description) > 0) {
cJSON_AddStringToObject(info, "description", g_relay_info.description);
if (strlen(g_unified_cache.relay_info.description) > 0) {
cJSON_AddStringToObject(info, "description", g_unified_cache.relay_info.description);
}
if (strlen(g_relay_info.banner) > 0) {
cJSON_AddStringToObject(info, "banner", g_relay_info.banner);
if (strlen(g_unified_cache.relay_info.banner) > 0) {
cJSON_AddStringToObject(info, "banner", g_unified_cache.relay_info.banner);
}
if (strlen(g_relay_info.icon) > 0) {
cJSON_AddStringToObject(info, "icon", g_relay_info.icon);
if (strlen(g_unified_cache.relay_info.icon) > 0) {
cJSON_AddStringToObject(info, "icon", g_unified_cache.relay_info.icon);
}
if (strlen(g_relay_info.pubkey) > 0) {
cJSON_AddStringToObject(info, "pubkey", g_relay_info.pubkey);
if (strlen(g_unified_cache.relay_info.pubkey) > 0) {
cJSON_AddStringToObject(info, "pubkey", g_unified_cache.relay_info.pubkey);
}
if (strlen(g_relay_info.contact) > 0) {
cJSON_AddStringToObject(info, "contact", g_relay_info.contact);
if (strlen(g_unified_cache.relay_info.contact) > 0) {
cJSON_AddStringToObject(info, "contact", g_unified_cache.relay_info.contact);
}
// Add supported NIPs
if (g_relay_info.supported_nips) {
cJSON_AddItemToObject(info, "supported_nips", cJSON_Duplicate(g_relay_info.supported_nips, 1));
if (g_unified_cache.relay_info.supported_nips) {
cJSON_AddItemToObject(info, "supported_nips", cJSON_Duplicate(g_unified_cache.relay_info.supported_nips, 1));
}
// Add software information
if (strlen(g_relay_info.software) > 0) {
cJSON_AddStringToObject(info, "software", g_relay_info.software);
if (strlen(g_unified_cache.relay_info.software) > 0) {
cJSON_AddStringToObject(info, "software", g_unified_cache.relay_info.software);
}
if (strlen(g_relay_info.version) > 0) {
cJSON_AddStringToObject(info, "version", g_relay_info.version);
if (strlen(g_unified_cache.relay_info.version) > 0) {
cJSON_AddStringToObject(info, "version", g_unified_cache.relay_info.version);
}
// Add policies
if (strlen(g_relay_info.privacy_policy) > 0) {
cJSON_AddStringToObject(info, "privacy_policy", g_relay_info.privacy_policy);
if (strlen(g_unified_cache.relay_info.privacy_policy) > 0) {
cJSON_AddStringToObject(info, "privacy_policy", g_unified_cache.relay_info.privacy_policy);
}
if (strlen(g_relay_info.terms_of_service) > 0) {
cJSON_AddStringToObject(info, "terms_of_service", g_relay_info.terms_of_service);
if (strlen(g_unified_cache.relay_info.terms_of_service) > 0) {
cJSON_AddStringToObject(info, "terms_of_service", g_unified_cache.relay_info.terms_of_service);
}
if (strlen(g_relay_info.posting_policy) > 0) {
cJSON_AddStringToObject(info, "posting_policy", g_relay_info.posting_policy);
if (strlen(g_unified_cache.relay_info.posting_policy) > 0) {
cJSON_AddStringToObject(info, "posting_policy", g_unified_cache.relay_info.posting_policy);
}
// Add server limitations
if (g_relay_info.limitation) {
cJSON_AddItemToObject(info, "limitation", cJSON_Duplicate(g_relay_info.limitation, 1));
if (g_unified_cache.relay_info.limitation) {
cJSON_AddItemToObject(info, "limitation", cJSON_Duplicate(g_unified_cache.relay_info.limitation, 1));
}
// Add retention policies if configured
if (g_relay_info.retention && cJSON_GetArraySize(g_relay_info.retention) > 0) {
cJSON_AddItemToObject(info, "retention", cJSON_Duplicate(g_relay_info.retention, 1));
if (g_unified_cache.relay_info.retention && cJSON_GetArraySize(g_unified_cache.relay_info.retention) > 0) {
cJSON_AddItemToObject(info, "retention", cJSON_Duplicate(g_unified_cache.relay_info.retention, 1));
}
// Add geographical and language information
if (g_relay_info.relay_countries) {
cJSON_AddItemToObject(info, "relay_countries", cJSON_Duplicate(g_relay_info.relay_countries, 1));
if (g_unified_cache.relay_info.relay_countries) {
cJSON_AddItemToObject(info, "relay_countries", cJSON_Duplicate(g_unified_cache.relay_info.relay_countries, 1));
}
if (g_relay_info.language_tags) {
cJSON_AddItemToObject(info, "language_tags", cJSON_Duplicate(g_relay_info.language_tags, 1));
if (g_unified_cache.relay_info.language_tags) {
cJSON_AddItemToObject(info, "language_tags", cJSON_Duplicate(g_unified_cache.relay_info.language_tags, 1));
}
if (g_relay_info.tags && cJSON_GetArraySize(g_relay_info.tags) > 0) {
cJSON_AddItemToObject(info, "tags", cJSON_Duplicate(g_relay_info.tags, 1));
if (g_unified_cache.relay_info.tags && cJSON_GetArraySize(g_unified_cache.relay_info.tags) > 0) {
cJSON_AddItemToObject(info, "tags", cJSON_Duplicate(g_unified_cache.relay_info.tags, 1));
}
// Add payment information if configured
if (strlen(g_relay_info.payments_url) > 0) {
cJSON_AddStringToObject(info, "payments_url", g_relay_info.payments_url);
if (strlen(g_unified_cache.relay_info.payments_url) > 0) {
cJSON_AddStringToObject(info, "payments_url", g_unified_cache.relay_info.payments_url);
}
if (g_relay_info.fees && cJSON_GetObjectItem(g_relay_info.fees, "admission")) {
cJSON_AddItemToObject(info, "fees", cJSON_Duplicate(g_relay_info.fees, 1));
if (g_unified_cache.relay_info.fees && cJSON_GetObjectItem(g_unified_cache.relay_info.fees, "admission")) {
cJSON_AddItemToObject(info, "fees", cJSON_Duplicate(g_unified_cache.relay_info.fees, 1));
}
pthread_mutex_unlock(&g_unified_cache.cache_lock);
return info;
}
@@ -1862,34 +1890,40 @@ int handle_nip11_http_request(struct lws* wsi, const char* accept_header) {
void init_pow_config() {
log_info("Initializing NIP-13 Proof of Work configuration");
// Load PoW settings from configuration system
g_pow_config.enabled = get_config_bool("pow_enabled", 1);
g_pow_config.min_pow_difficulty = get_config_int("pow_min_difficulty", 0);
// Get PoW mode from configuration
// Get all config values first (without holding mutex to avoid deadlock)
int pow_enabled = get_config_bool("pow_enabled", 1);
int pow_min_difficulty = get_config_int("pow_min_difficulty", 0);
const char* pow_mode = get_config_value("pow_mode");
pthread_mutex_lock(&g_unified_cache.cache_lock);
// Load PoW settings from configuration system
g_unified_cache.pow_config.enabled = pow_enabled;
g_unified_cache.pow_config.min_pow_difficulty = pow_min_difficulty;
// Configure PoW mode
if (pow_mode) {
if (strcmp(pow_mode, "strict") == 0) {
g_pow_config.validation_flags = NOSTR_POW_VALIDATE_ANTI_SPAM | NOSTR_POW_STRICT_FORMAT;
g_pow_config.require_nonce_tag = 1;
g_pow_config.reject_lower_targets = 1;
g_pow_config.strict_format = 1;
g_pow_config.anti_spam_mode = 1;
g_unified_cache.pow_config.validation_flags = NOSTR_POW_VALIDATE_ANTI_SPAM | NOSTR_POW_STRICT_FORMAT;
g_unified_cache.pow_config.require_nonce_tag = 1;
g_unified_cache.pow_config.reject_lower_targets = 1;
g_unified_cache.pow_config.strict_format = 1;
g_unified_cache.pow_config.anti_spam_mode = 1;
log_info("PoW configured in strict anti-spam mode");
} else if (strcmp(pow_mode, "full") == 0) {
g_pow_config.validation_flags = NOSTR_POW_VALIDATE_FULL;
g_pow_config.require_nonce_tag = 1;
g_unified_cache.pow_config.validation_flags = NOSTR_POW_VALIDATE_FULL;
g_unified_cache.pow_config.require_nonce_tag = 1;
log_info("PoW configured in full validation mode");
} else if (strcmp(pow_mode, "basic") == 0) {
g_pow_config.validation_flags = NOSTR_POW_VALIDATE_BASIC;
g_unified_cache.pow_config.validation_flags = NOSTR_POW_VALIDATE_BASIC;
log_info("PoW configured in basic validation mode");
} else if (strcmp(pow_mode, "disabled") == 0) {
g_pow_config.enabled = 0;
g_unified_cache.pow_config.enabled = 0;
log_info("PoW validation disabled via configuration");
}
} else {
// Default to basic mode
g_pow_config.validation_flags = NOSTR_POW_VALIDATE_BASIC;
g_unified_cache.pow_config.validation_flags = NOSTR_POW_VALIDATE_BASIC;
log_info("PoW configured in basic validation mode (default)");
}
@@ -1897,17 +1931,25 @@ void init_pow_config() {
char config_msg[512];
snprintf(config_msg, sizeof(config_msg),
"PoW Configuration: enabled=%s, min_difficulty=%d, validation_flags=0x%x, mode=%s",
g_pow_config.enabled ? "true" : "false",
g_pow_config.min_pow_difficulty,
g_pow_config.validation_flags,
g_pow_config.anti_spam_mode ? "anti-spam" :
(g_pow_config.validation_flags & NOSTR_POW_VALIDATE_FULL) ? "full" : "basic");
g_unified_cache.pow_config.enabled ? "true" : "false",
g_unified_cache.pow_config.min_pow_difficulty,
g_unified_cache.pow_config.validation_flags,
g_unified_cache.pow_config.anti_spam_mode ? "anti-spam" :
(g_unified_cache.pow_config.validation_flags & NOSTR_POW_VALIDATE_FULL) ? "full" : "basic");
log_info(config_msg);
pthread_mutex_unlock(&g_unified_cache.cache_lock);
}
// Validate event Proof of Work according to NIP-13
int validate_event_pow(cJSON* event, char* error_message, size_t error_size) {
if (!g_pow_config.enabled) {
pthread_mutex_lock(&g_unified_cache.cache_lock);
int enabled = g_unified_cache.pow_config.enabled;
int min_pow_difficulty = g_unified_cache.pow_config.min_pow_difficulty;
int validation_flags = g_unified_cache.pow_config.validation_flags;
pthread_mutex_unlock(&g_unified_cache.cache_lock);
if (!enabled) {
return 0; // PoW validation disabled
}
@@ -1918,7 +1960,7 @@ int validate_event_pow(cJSON* event, char* error_message, size_t error_size) {
// If min_pow_difficulty is 0, only validate events that have nonce tags
// This allows events without PoW when difficulty requirement is 0
if (g_pow_config.min_pow_difficulty == 0) {
if (min_pow_difficulty == 0) {
cJSON* tags = cJSON_GetObjectItem(event, "tags");
int has_nonce_tag = 0;
@@ -1946,8 +1988,8 @@ int validate_event_pow(cJSON* event, char* error_message, size_t error_size) {
// Perform PoW validation using nostr_core_lib
nostr_pow_result_t pow_result;
int validation_result = nostr_validate_pow(event, g_pow_config.min_pow_difficulty,
g_pow_config.validation_flags, &pow_result);
int validation_result = nostr_validate_pow(event, min_pow_difficulty,
validation_flags, &pow_result);
if (validation_result != NOSTR_SUCCESS) {
// Handle specific error cases with appropriate messages
@@ -1955,12 +1997,12 @@ int validate_event_pow(cJSON* event, char* error_message, size_t error_size) {
case NOSTR_ERROR_NIP13_INSUFFICIENT:
snprintf(error_message, error_size,
"pow: insufficient difficulty: %d < %d",
pow_result.actual_difficulty, g_pow_config.min_pow_difficulty);
pow_result.actual_difficulty, min_pow_difficulty);
log_warning("Event rejected: insufficient PoW difficulty");
break;
case NOSTR_ERROR_NIP13_NO_NONCE_TAG:
// This should not happen with min_difficulty=0 after our check above
if (g_pow_config.min_pow_difficulty > 0) {
if (min_pow_difficulty > 0) {
snprintf(error_message, error_size, "pow: missing required nonce tag");
log_warning("Event rejected: missing nonce tag");
} else {
@@ -1974,7 +2016,7 @@ int validate_event_pow(cJSON* event, char* error_message, size_t error_size) {
case NOSTR_ERROR_NIP13_TARGET_MISMATCH:
snprintf(error_message, error_size,
"pow: committed target (%d) lower than minimum (%d)",
pow_result.committed_target, g_pow_config.min_pow_difficulty);
pow_result.committed_target, min_pow_difficulty);
log_warning("Event rejected: committed target too low (anti-spam protection)");
break;
case NOSTR_ERROR_NIP13_CALCULATION:
@@ -1994,7 +2036,7 @@ int validate_event_pow(cJSON* event, char* error_message, size_t error_size) {
}
// Log successful PoW validation (only if minimum difficulty is required)
if (g_pow_config.min_pow_difficulty > 0 || pow_result.has_nonce_tag) {
if (min_pow_difficulty > 0 || pow_result.has_nonce_tag) {
char debug_msg[256];
snprintf(debug_msg, sizeof(debug_msg),
"PoW validated: difficulty=%d, target=%d, nonce=%llu%s",
@@ -2018,28 +2060,39 @@ int validate_event_pow(cJSON* event, char* error_message, size_t error_size) {
void init_expiration_config() {
log_info("Initializing NIP-40 Expiration Timestamp configuration");
// Get all config values first (without holding mutex to avoid deadlock)
int expiration_enabled = get_config_bool("expiration_enabled", 1);
int expiration_strict = get_config_bool("expiration_strict", 1);
int expiration_filter = get_config_bool("expiration_filter", 1);
int expiration_delete = get_config_bool("expiration_delete", 0);
long expiration_grace_period = get_config_int("expiration_grace_period", 1);
pthread_mutex_lock(&g_unified_cache.cache_lock);
// Load expiration settings from configuration system
g_expiration_config.enabled = get_config_bool("expiration_enabled", 1);
g_expiration_config.strict_mode = get_config_bool("expiration_strict", 1);
g_expiration_config.filter_responses = get_config_bool("expiration_filter", 1);
g_expiration_config.delete_expired = get_config_bool("expiration_delete", 0);
g_expiration_config.grace_period = get_config_int("expiration_grace_period", 1);
g_unified_cache.expiration_config.enabled = expiration_enabled;
g_unified_cache.expiration_config.strict_mode = expiration_strict;
g_unified_cache.expiration_config.filter_responses = expiration_filter;
g_unified_cache.expiration_config.delete_expired = expiration_delete;
g_unified_cache.expiration_config.grace_period = expiration_grace_period;
// Validate grace period bounds
if (g_expiration_config.grace_period < 0 || g_expiration_config.grace_period > 86400) {
if (g_unified_cache.expiration_config.grace_period < 0 || g_unified_cache.expiration_config.grace_period > 86400) {
log_warning("Invalid grace period, using default of 300 seconds");
g_expiration_config.grace_period = 300;
g_unified_cache.expiration_config.grace_period = 300;
}
// Log final configuration
char config_msg[512];
snprintf(config_msg, sizeof(config_msg),
"Expiration Configuration: enabled=%s, strict_mode=%s, filter_responses=%s, grace_period=%ld seconds",
g_expiration_config.enabled ? "true" : "false",
g_expiration_config.strict_mode ? "true" : "false",
g_expiration_config.filter_responses ? "true" : "false",
g_expiration_config.grace_period);
g_unified_cache.expiration_config.enabled ? "true" : "false",
g_unified_cache.expiration_config.strict_mode ? "true" : "false",
g_unified_cache.expiration_config.filter_responses ? "true" : "false",
g_unified_cache.expiration_config.grace_period);
log_info(config_msg);
pthread_mutex_unlock(&g_unified_cache.cache_lock);
}
// Extract expiration timestamp from event tags
@@ -2109,12 +2162,22 @@ int is_event_expired(cJSON* event, time_t current_time) {
}
// Check if current time exceeds expiration + grace period
return (current_time > (expiration_ts + g_expiration_config.grace_period));
pthread_mutex_lock(&g_unified_cache.cache_lock);
long grace_period = g_unified_cache.expiration_config.grace_period;
pthread_mutex_unlock(&g_unified_cache.cache_lock);
return (current_time > (expiration_ts + grace_period));
}
// Validate event expiration according to NIP-40
int validate_event_expiration(cJSON* event, char* error_message, size_t error_size) {
if (!g_expiration_config.enabled) {
pthread_mutex_lock(&g_unified_cache.cache_lock);
int enabled = g_unified_cache.expiration_config.enabled;
int strict_mode = g_unified_cache.expiration_config.strict_mode;
long grace_period = g_unified_cache.expiration_config.grace_period;
pthread_mutex_unlock(&g_unified_cache.cache_lock);
if (!enabled) {
return 0; // Expiration validation disabled
}
@@ -2126,13 +2189,13 @@ int validate_event_expiration(cJSON* event, char* error_message, size_t error_si
// Check if event is expired
time_t current_time = time(NULL);
if (is_event_expired(event, current_time)) {
if (g_expiration_config.strict_mode) {
if (strict_mode) {
cJSON* tags = cJSON_GetObjectItem(event, "tags");
long expiration_ts = extract_expiration_timestamp(tags);
snprintf(error_message, error_size,
"invalid: event expired (expiration=%ld, current=%ld, grace=%ld)",
expiration_ts, (long)current_time, g_expiration_config.grace_period);
expiration_ts, (long)current_time, grace_period);
log_warning("Event rejected: expired timestamp");
return -1;
} else {
@@ -2628,6 +2691,54 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
return 0;
}
// Check for kind 33334 configuration event requests BEFORE creating subscription
int config_events_sent = 0;
int has_config_request = 0;
// Check if any filter requests kind 33334 (configuration events)
for (int i = 0; i < cJSON_GetArraySize(filters); i++) {
cJSON* filter = cJSON_GetArrayItem(filters, i);
if (filter && cJSON_IsObject(filter)) {
if (req_filter_requests_config_events(filter)) {
has_config_request = 1;
// Generate synthetic config event for this subscription
cJSON* filters_array = cJSON_CreateArray();
cJSON_AddItemToArray(filters_array, cJSON_Duplicate(filter, 1));
cJSON* event_msg = generate_synthetic_config_event_for_subscription(sub_id, filters_array);
if (event_msg) {
char* msg_str = cJSON_Print(event_msg);
if (msg_str) {
size_t msg_len = strlen(msg_str);
unsigned char* buf = malloc(LWS_PRE + msg_len);
if (buf) {
memcpy(buf + LWS_PRE, msg_str, msg_len);
lws_write(wsi, buf + LWS_PRE, msg_len, LWS_WRITE_TEXT);
config_events_sent++;
free(buf);
}
free(msg_str);
}
cJSON_Delete(event_msg);
}
cJSON_Delete(filters_array);
char debug_msg[256];
snprintf(debug_msg, sizeof(debug_msg),
"Generated %d synthetic config events for subscription %s",
config_events_sent, sub_id);
log_info(debug_msg);
break; // Only generate once per subscription
}
}
}
// If only config events were requested, we can return early after sending EOSE
// But still create the subscription for future config updates
// Check session subscription limits
if (pss && pss->subscription_count >= g_subscription_manager.max_subscriptions_per_client) {
log_error("Maximum subscriptions per client exceeded");
@@ -2651,14 +2762,14 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
}
cJSON_Delete(closed_msg);
return 0;
return has_config_request ? config_events_sent : 0;
}
// Create persistent subscription
subscription_t* subscription = create_subscription(sub_id, wsi, filters, pss ? pss->client_ip : "unknown");
if (!subscription) {
log_error("Failed to create subscription");
return 0;
return has_config_request ? config_events_sent : 0;
}
// Add to global manager
@@ -2685,7 +2796,7 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
}
cJSON_Delete(closed_msg);
return 0;
return has_config_request ? config_events_sent : 0;
}
// Add to session's subscription list (if session data available)
@@ -2697,7 +2808,7 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
pthread_mutex_unlock(&pss->session_lock);
}
int events_sent = 0;
int events_sent = config_events_sent; // Start with synthetic config events
// Process each filter in the array
for (int i = 0; i < cJSON_GetArraySize(filters); i++) {
@@ -2844,7 +2955,12 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
cJSON_AddItemToObject(event, "tags", tags);
// Check expiration filtering (NIP-40) at application level
if (g_expiration_config.enabled && g_expiration_config.filter_responses) {
pthread_mutex_lock(&g_unified_cache.cache_lock);
int expiration_enabled = g_unified_cache.expiration_config.enabled;
int filter_responses = g_unified_cache.expiration_config.filter_responses;
pthread_mutex_unlock(&g_unified_cache.cache_lock);
if (expiration_enabled && filter_responses) {
time_t current_time = time(NULL);
if (is_event_expired(event, current_time)) {
// Skip this expired event
@@ -2955,8 +3071,13 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
// Get real client IP address
char client_ip[CLIENT_IP_MAX_LENGTH];
lws_get_peer_simple(wsi, client_ip, sizeof(client_ip));
strncpy(pss->client_ip, client_ip, CLIENT_IP_MAX_LENGTH - 1);
pss->client_ip[CLIENT_IP_MAX_LENGTH - 1] = '\0';
// Ensure client_ip is null-terminated and copy safely
client_ip[CLIENT_IP_MAX_LENGTH - 1] = '\0';
size_t ip_len = strlen(client_ip);
size_t copy_len = (ip_len < CLIENT_IP_MAX_LENGTH - 1) ? ip_len : CLIENT_IP_MAX_LENGTH - 1;
memcpy(pss->client_ip, client_ip, copy_len);
pss->client_ip[copy_len] = '\0';
// Initialize NIP-42 authentication state
pss->authenticated = 0;
@@ -3092,17 +3213,50 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
// Cleanup event JSON string
free(event_json_str);
// Store event in database and broadcast to subscriptions
// Check for admin events (kinds 33334 and 33335) and intercept them
if (result == 0) {
// Store the event in the database first
if (store_event(event) != 0) {
log_error("Failed to store event in database");
result = -1;
strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1);
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
if (kind_obj && cJSON_IsNumber(kind_obj)) {
int event_kind = (int)cJSON_GetNumberValue(kind_obj);
if (event_kind == 33334 || event_kind == 33335) {
// This is an admin event - process it through the admin API instead of normal storage
log_info("Admin event detected, processing through admin API");
char admin_error[512] = {0};
if (process_admin_event_in_config(event, admin_error, sizeof(admin_error)) != 0) {
log_error("Failed to process admin event through admin API");
result = -1;
size_t error_len = strlen(admin_error);
size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1;
memcpy(error_message, admin_error, copy_len);
error_message[copy_len] = '\0';
} else {
log_success("Admin event processed successfully through admin API");
// Admin events are processed by the admin API, not broadcast to subscriptions
}
} else {
// Regular event - store in database and broadcast
if (store_event(event) != 0) {
log_error("Failed to store event in database");
result = -1;
strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1);
} else {
log_info("Event stored successfully in database");
// Broadcast event to matching persistent subscriptions
broadcast_event_to_subscriptions(event);
}
}
} else {
log_info("Event stored successfully in database");
// Broadcast event to matching persistent subscriptions
broadcast_event_to_subscriptions(event);
// Event without valid kind - try normal storage
if (store_event(event) != 0) {
log_error("Failed to store event in database");
result = -1;
strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1);
} else {
log_info("Event stored successfully in database");
broadcast_event_to_subscriptions(event);
}
}
}
@@ -3651,10 +3805,29 @@ int main(int argc, char* argv[]) {
return 1;
}
// Systematically add pubkeys to config table
if (add_pubkeys_to_config_table() != 0) {
log_warning("Failed to add pubkeys to config table systematically");
} else {
log_success("Pubkeys added to config table systematically");
}
// Retry storing the configuration event now that database is initialized
if (retry_store_initial_config_event() != 0) {
log_warning("Failed to store initial configuration event after database init");
}
// Now store the pubkeys in config table since database is available
const char* admin_pubkey = get_admin_pubkey_cached();
const char* relay_pubkey_from_cache = get_relay_pubkey_cached();
if (admin_pubkey && strlen(admin_pubkey) == 64) {
set_config_value_in_table("admin_pubkey", admin_pubkey, "string", "Administrator public key", "authentication", 0);
log_success("Admin pubkey stored in config table for first-time startup");
}
if (relay_pubkey_from_cache && strlen(relay_pubkey_from_cache) == 64) {
set_config_value_in_table("relay_pubkey", relay_pubkey_from_cache, "string", "Relay public key", "relay", 0);
log_success("Relay pubkey stored in config table for first-time startup");
}
} else {
log_info("Existing relay detected");
@@ -3724,6 +3897,21 @@ int main(int argc, char* argv[]) {
log_warning("Failed to apply configuration from database");
} else {
log_success("Configuration loaded from database");
// Extract admin pubkey from the config event and store in config table for unified cache access
cJSON* pubkey_obj = cJSON_GetObjectItem(config_event, "pubkey");
const char* admin_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : NULL;
// Store both admin and relay pubkeys in config table for unified cache
if (admin_pubkey && strlen(admin_pubkey) == 64) {
set_config_value_in_table("admin_pubkey", admin_pubkey, "string", "Administrator public key", "authentication", 0);
log_info("Admin pubkey stored in config table for existing relay");
}
if (relay_pubkey && strlen(relay_pubkey) == 64) {
set_config_value_in_table("relay_pubkey", relay_pubkey, "string", "Relay public key", "relay", 0);
log_info("Relay pubkey stored in config table for existing relay");
}
}
cJSON_Delete(config_event);
} else {

View File

@@ -132,24 +132,11 @@ typedef struct {
int time_tolerance_seconds;
} nip42_challenge_manager_t;
// Cached configuration structure
typedef struct {
int auth_required; // Whether authentication is required
long max_file_size; // Maximum file size in bytes
int admin_enabled; // Whether admin interface is enabled
char admin_pubkey[65]; // Admin public key
int nip42_mode; // NIP-42 authentication mode
int nip42_challenge_timeout; // NIP-42 challenge timeout in seconds
int nip42_time_tolerance; // NIP-42 time tolerance in seconds
time_t cache_expires; // When cache expires
int cache_valid; // Whether cache is valid
} auth_config_cache_t;
//=============================================================================
// GLOBAL STATE
//=============================================================================
static auth_config_cache_t g_auth_cache = {0};
// No longer using local auth cache - using unified cache from config.c
static nip42_challenge_manager_t g_challenge_manager = {0};
static int g_validator_initialized = 0;
@@ -222,15 +209,15 @@ int ginxsom_request_validator_init(const char *db_path, const char *app_name) {
return result;
}
// Initialize NIP-42 challenge manager
// Initialize NIP-42 challenge manager using unified config
memset(&g_challenge_manager, 0, sizeof(g_challenge_manager));
g_challenge_manager.timeout_seconds =
g_auth_cache.nip42_challenge_timeout > 0
? g_auth_cache.nip42_challenge_timeout
: 600;
g_challenge_manager.time_tolerance_seconds =
g_auth_cache.nip42_time_tolerance > 0 ? g_auth_cache.nip42_time_tolerance
: 300;
const char* nip42_timeout = get_config_value("nip42_challenge_timeout");
g_challenge_manager.timeout_seconds = nip42_timeout ? atoi(nip42_timeout) : 600;
const char* nip42_tolerance = get_config_value("nip42_time_tolerance");
g_challenge_manager.time_tolerance_seconds = nip42_tolerance ? atoi(nip42_tolerance) : 300;
g_challenge_manager.last_cleanup = time(NULL);
g_validator_initialized = 1;
@@ -243,12 +230,15 @@ int ginxsom_request_validator_init(const char *db_path, const char *app_name) {
* Check if authentication rules are enabled
*/
int nostr_auth_rules_enabled(void) {
// Reload config if cache expired
if (!g_auth_cache.cache_valid || time(NULL) > g_auth_cache.cache_expires) {
reload_auth_config();
// Use unified cache from config.c
const char* auth_enabled = get_config_value("auth_enabled");
if (auth_enabled && strcmp(auth_enabled, "true") == 0) {
return 1;
}
return g_auth_cache.auth_required;
// 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;
}
///////////////////////////////////////////////////////////////////////////////////////
@@ -306,14 +296,12 @@ int nostr_validate_unified_request(const char* json_string, size_t json_length)
int event_kind = (int)cJSON_GetNumberValue(kind);
// 5. Reload config if needed
if (!g_auth_cache.cache_valid || time(NULL) > g_auth_cache.cache_expires) {
reload_auth_config();
}
// 5. Check configuration using unified cache
int auth_required = nostr_auth_rules_enabled();
char config_msg[256];
sprintf(config_msg, "VALIDATOR_DEBUG: STEP 5 PASSED - Event kind: %d, auth_required: %d\n",
event_kind, g_auth_cache.auth_required);
event_kind, auth_required);
validator_debug_log(config_msg);
/////////////////////////////////////////////////////////////////////
@@ -352,7 +340,9 @@ int nostr_validate_unified_request(const char* json_string, size_t json_length)
if (event_kind == 22242) {
validator_debug_log("VALIDATOR_DEBUG: STEP 8 - Processing NIP-42 challenge response\n");
if (g_auth_cache.nip42_mode == 0) {
// Check NIP-42 mode using unified cache
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");
cJSON_Delete(event);
return NOSTR_ERROR_NIP42_DISABLED;
@@ -370,7 +360,7 @@ int nostr_validate_unified_request(const char* json_string, size_t json_length)
/////////////////////////////////////////////////////////////////////
// 9. Check if authentication rules are enabled
if (!g_auth_cache.auth_required) {
if (!auth_required) {
validator_debug_log("VALIDATOR_DEBUG: STEP 9 - Authentication disabled, skipping database auth rules\n");
} else {
// 10. Check database authentication rules (only if auth enabled)
@@ -404,17 +394,23 @@ int nostr_validate_unified_request(const char* json_string, size_t json_length)
/////////////////////////////////////////////////////////////////////
// 11. NIP-13 Proof of Work validation
if (g_pow_config.enabled && g_pow_config.min_pow_difficulty > 0) {
pthread_mutex_lock(&g_unified_cache.cache_lock);
int pow_enabled = g_unified_cache.pow_config.enabled;
int pow_min_difficulty = g_unified_cache.pow_config.min_pow_difficulty;
int pow_validation_flags = g_unified_cache.pow_config.validation_flags;
pthread_mutex_unlock(&g_unified_cache.cache_lock);
if (pow_enabled && pow_min_difficulty > 0) {
validator_debug_log("VALIDATOR_DEBUG: STEP 11 - Validating NIP-13 Proof of Work\n");
nostr_pow_result_t pow_result;
int pow_validation_result = nostr_validate_pow(event, g_pow_config.min_pow_difficulty,
g_pow_config.validation_flags, &pow_result);
int pow_validation_result = nostr_validate_pow(event, pow_min_difficulty,
pow_validation_flags, &pow_result);
if (pow_validation_result != NOSTR_SUCCESS) {
char pow_msg[256];
sprintf(pow_msg, "VALIDATOR_DEBUG: STEP 11 FAILED - PoW validation failed (error=%d, difficulty=%d/%d)\n",
pow_validation_result, pow_result.actual_difficulty, g_pow_config.min_pow_difficulty);
pow_validation_result, pow_result.actual_difficulty, pow_min_difficulty);
validator_debug_log(pow_msg);
cJSON_Delete(event);
return pow_validation_result;
@@ -553,7 +549,6 @@ void nostr_request_validator_clear_violation(void) {
*/
void ginxsom_request_validator_cleanup(void) {
g_validator_initialized = 0;
memset(&g_auth_cache, 0, sizeof(g_auth_cache));
nostr_request_validator_clear_violation();
}
@@ -573,145 +568,22 @@ void nostr_request_result_free_file_data(nostr_request_result_t *result) {
// HELPER FUNCTIONS
//=============================================================================
/**
* Get cache timeout from environment variable or default
*/
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
}
/**
* Force cache refresh - invalidates current cache
* Force cache refresh - use unified cache system
*/
void nostr_request_validator_force_cache_refresh(void) {
g_auth_cache.cache_valid = 0;
g_auth_cache.cache_expires = 0;
validator_debug_log("VALIDATOR: Cache forcibly invalidated\n");
// Use unified cache refresh from config.c
force_config_cache_refresh();
validator_debug_log("VALIDATOR: Cache forcibly invalidated via unified cache\n");
}
/**
* Reload authentication configuration from unified config table
* This function is no longer needed - configuration is handled by unified cache
*/
static int reload_auth_config(void) {
sqlite3 *db = NULL;
sqlite3_stmt *stmt = NULL;
int rc;
// Clear cache
memset(&g_auth_cache, 0, sizeof(g_auth_cache));
// Open database using global database path
if (strlen(g_database_path) == 0) {
validator_debug_log("VALIDATOR: No database path available\n");
// Use defaults
g_auth_cache.auth_required = 0;
g_auth_cache.max_file_size = 104857600; // 100MB
g_auth_cache.admin_enabled = 0;
g_auth_cache.nip42_mode = 1; // Optional
int cache_timeout = get_cache_timeout();
g_auth_cache.cache_expires = time(NULL) + cache_timeout;
g_auth_cache.cache_valid = 1;
return NOSTR_SUCCESS;
}
rc = sqlite3_open_v2(g_database_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc != SQLITE_OK) {
validator_debug_log("VALIDATOR: Could not open database\n");
// Use defaults
g_auth_cache.auth_required = 0;
g_auth_cache.max_file_size = 104857600; // 100MB
g_auth_cache.admin_enabled = 0;
g_auth_cache.nip42_mode = 1; // Optional
int cache_timeout = get_cache_timeout();
g_auth_cache.cache_expires = time(NULL) + cache_timeout;
g_auth_cache.cache_valid = 1;
return NOSTR_SUCCESS;
}
// Load configuration values from unified config table
const char *config_sql =
"SELECT key, value FROM config WHERE key IN ('require_auth', "
"'auth_rules_enabled', 'max_file_size', 'admin_enabled', 'admin_pubkey', "
"'nip42_require_auth', 'nip42_challenge_timeout', "
"'nip42_time_tolerance')";
rc = sqlite3_prepare_v2(db, config_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
while (sqlite3_step(stmt) == SQLITE_ROW) {
const char *key = (const char *)sqlite3_column_text(stmt, 0);
const char *value = (const char *)sqlite3_column_text(stmt, 1);
if (!key || !value)
continue;
if (strcmp(key, "require_auth") == 0) {
g_auth_cache.auth_required = (strcmp(value, "true") == 0) ? 1 : 0;
} else if (strcmp(key, "auth_rules_enabled") == 0) {
// Override auth_required with auth_rules_enabled if present (higher
// priority)
g_auth_cache.auth_required = (strcmp(value, "true") == 0) ? 1 : 0;
} else if (strcmp(key, "max_file_size") == 0) {
g_auth_cache.max_file_size = atol(value);
} else if (strcmp(key, "admin_enabled") == 0) {
g_auth_cache.admin_enabled = (strcmp(value, "true") == 0) ? 1 : 0;
} else if (strcmp(key, "admin_pubkey") == 0) {
strncpy(g_auth_cache.admin_pubkey, value,
sizeof(g_auth_cache.admin_pubkey) - 1);
} else if (strcmp(key, "nip42_require_auth") == 0) {
if (strcmp(value, "false") == 0) {
g_auth_cache.nip42_mode = 0; // Disabled
} else if (strcmp(value, "required") == 0) {
g_auth_cache.nip42_mode = 2; // Required
} else if (strcmp(value, "true") == 0) {
g_auth_cache.nip42_mode = 1; // Optional/Enabled
} else {
g_auth_cache.nip42_mode = 1; // Default to Optional/Enabled
}
} else if (strcmp(key, "nip42_challenge_timeout") == 0) {
g_auth_cache.nip42_challenge_timeout = atoi(value);
} else if (strcmp(key, "nip42_time_tolerance") == 0) {
g_auth_cache.nip42_time_tolerance = atoi(value);
}
}
sqlite3_finalize(stmt);
}
sqlite3_close(db);
// Set cache expiration with environment variable support
int cache_timeout = get_cache_timeout();
g_auth_cache.cache_expires = time(NULL) + cache_timeout;
g_auth_cache.cache_valid = 1;
// Set defaults for missing values
if (g_auth_cache.max_file_size == 0) {
g_auth_cache.max_file_size = 104857600; // 100MB
}
// Debug logging
fprintf(stderr,
"VALIDATOR: Configuration loaded from unified config table - "
"auth_required: %d, max_file_size: %ld, nip42_mode: %d, "
"cache_timeout: %d\n",
g_auth_cache.auth_required, g_auth_cache.max_file_size,
g_auth_cache.nip42_mode, cache_timeout);
fprintf(stderr,
"VALIDATOR: NIP-42 mode details - nip42_mode=%d (0=disabled, "
"1=optional/enabled, 2=required)\n",
g_auth_cache.nip42_mode);
// Configuration is now handled by the unified cache in config.c
validator_debug_log("VALIDATOR: Using unified cache system for configuration\n");
return NOSTR_SUCCESS;
}

View File

@@ -1,12 +1,12 @@
/* Embedded SQL Schema for C Nostr Relay
* Generated from db/schema.sql - Do not edit manually
* Schema Version: 6
* Schema Version: 7
*/
#ifndef SQL_SCHEMA_H
#define SQL_SCHEMA_H
/* Schema version constant */
#define EMBEDDED_SCHEMA_VERSION "6"
#define EMBEDDED_SCHEMA_VERSION "7"
/* Embedded SQL schema as C string literal */
static const char* const EMBEDDED_SCHEMA_SQL =
@@ -15,7 +15,7 @@ static const char* const EMBEDDED_SCHEMA_SQL =
-- Event-based configuration system using kind 33334 Nostr events\n\
\n\
-- Schema version tracking\n\
PRAGMA user_version = 6;\n\
PRAGMA user_version = 7;\n\
\n\
-- Enable foreign key support\n\
PRAGMA foreign_keys = ON;\n\
@@ -58,8 +58,8 @@ CREATE TABLE schema_info (\n\
\n\
-- Insert schema metadata\n\
INSERT INTO schema_info (key, value) VALUES\n\
('version', '6'),\n\
('description', 'Event-based Nostr relay schema with secure relay private key storage'),\n\
('version', '7'),\n\
('description', 'Hybrid Nostr relay schema with event-based and table-based configuration'),\n\
('created_at', strftime('%s', 'now'));\n\
\n\
-- Helper views for common queries\n\
@@ -154,6 +154,60 @@ CREATE INDEX idx_auth_rules_pattern ON auth_rules(pattern_type, pattern_value);\
CREATE INDEX idx_auth_rules_type ON auth_rules(rule_type);\n\
CREATE INDEX idx_auth_rules_active ON auth_rules(active);\n\
\n\
-- Configuration Table for Table-Based Config Management\n\
-- Hybrid system supporting both event-based and table-based configuration\n\
CREATE TABLE config (\n\
key TEXT PRIMARY KEY,\n\
value TEXT NOT NULL,\n\
data_type TEXT NOT NULL CHECK (data_type IN ('string', 'integer', 'boolean', 'json')),\n\
description TEXT,\n\
category TEXT DEFAULT 'general',\n\
requires_restart INTEGER DEFAULT 0,\n\
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n\
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))\n\
);\n\
\n\
-- Indexes for config table performance\n\
CREATE INDEX idx_config_category ON config(category);\n\
CREATE INDEX idx_config_restart ON config(requires_restart);\n\
CREATE INDEX idx_config_updated ON config(updated_at DESC);\n\
\n\
-- Trigger to update config timestamp on changes\n\
CREATE TRIGGER update_config_timestamp\n\
AFTER UPDATE ON config\n\
FOR EACH ROW\n\
BEGIN\n\
UPDATE config SET updated_at = strftime('%s', 'now') WHERE key = NEW.key;\n\
END;\n\
\n\
-- Insert default configuration values\n\
INSERT INTO config (key, value, data_type, description, category, requires_restart) VALUES\n\
('relay_description', 'A C Nostr Relay', 'string', 'Relay description', 'general', 0),\n\
('relay_contact', '', 'string', 'Relay contact information', 'general', 0),\n\
('relay_software', 'https://github.com/laanwj/c-relay', 'string', 'Relay software URL', 'general', 0),\n\
('relay_version', '1.0.0', 'string', 'Relay version', 'general', 0),\n\
('relay_port', '8888', 'integer', 'Relay port number', 'network', 1),\n\
('max_connections', '1000', 'integer', 'Maximum concurrent connections', 'network', 1),\n\
('auth_enabled', 'false', 'boolean', 'Enable NIP-42 authentication', 'auth', 0),\n\
('nip42_auth_required_events', 'false', 'boolean', 'Require auth for event publishing', 'auth', 0),\n\
('nip42_auth_required_subscriptions', 'false', 'boolean', 'Require auth for subscriptions', 'auth', 0),\n\
('nip42_auth_required_kinds', '[]', 'json', 'Event kinds requiring authentication', 'auth', 0),\n\
('nip42_challenge_expiration', '600', 'integer', 'Auth challenge expiration seconds', 'auth', 0),\n\
('pow_min_difficulty', '0', 'integer', 'Minimum proof-of-work difficulty', 'validation', 0),\n\
('pow_mode', 'optional', 'string', 'Proof-of-work mode', 'validation', 0),\n\
('nip40_expiration_enabled', 'true', 'boolean', 'Enable event expiration', 'validation', 0),\n\
('nip40_expiration_strict', 'false', 'boolean', 'Strict expiration mode', 'validation', 0),\n\
('nip40_expiration_filter', 'true', 'boolean', 'Filter expired events in queries', 'validation', 0),\n\
('nip40_expiration_grace_period', '60', 'integer', 'Expiration grace period seconds', 'validation', 0),\n\
('max_subscriptions_per_client', '25', 'integer', 'Maximum subscriptions per client', 'limits', 0),\n\
('max_total_subscriptions', '1000', 'integer', 'Maximum total subscriptions', 'limits', 0),\n\
('max_filters_per_subscription', '10', 'integer', 'Maximum filters per subscription', 'limits', 0),\n\
('max_event_tags', '2000', 'integer', 'Maximum tags per event', 'limits', 0),\n\
('max_content_length', '100000', 'integer', 'Maximum event content length', 'limits', 0),\n\
('max_message_length', '131072', 'integer', 'Maximum WebSocket message length', 'limits', 0),\n\
('default_limit', '100', 'integer', 'Default query limit', 'limits', 0),\n\
('max_limit', '5000', 'integer', 'Maximum query limit', 'limits', 0);\n\
\n\
-- Persistent Subscriptions Logging Tables (Phase 2)\n\
-- Optional database logging for subscription analytics and debugging\n\
\n\

View File

@@ -1,191 +0,0 @@
#!/usr/bin/env node
// Import the nostr-tools bundle
const fs = require('fs');
const path = require('path');
const { TextEncoder, TextDecoder } = require('util');
// Load nostr.bundle.js
const bundlePath = path.join(__dirname, 'api', 'nostr.bundle.js');
if (!fs.existsSync(bundlePath)) {
console.error('nostr.bundle.js not found at:', bundlePath);
process.exit(1);
}
// Read and eval the bundle to get NostrTools
const bundleCode = fs.readFileSync(bundlePath, 'utf8');
const vm = require('vm');
// Create a more complete browser-like context
const context = {
window: {},
global: {},
console: console,
setTimeout: setTimeout,
setInterval: setInterval,
clearTimeout: clearTimeout,
clearInterval: clearInterval,
Buffer: Buffer,
process: process,
require: require,
module: module,
exports: exports,
__dirname: __dirname,
__filename: __filename,
TextEncoder: TextEncoder,
TextDecoder: TextDecoder,
crypto: require('crypto'),
atob: (str) => Buffer.from(str, 'base64').toString('binary'),
btoa: (str) => Buffer.from(str, 'binary').toString('base64'),
fetch: require('https').get // Basic polyfill, might need adjustment
};
// Add common browser globals to window
context.window.TextEncoder = TextEncoder;
context.window.TextDecoder = TextDecoder;
context.window.crypto = context.crypto;
context.window.atob = context.atob;
context.window.btoa = context.btoa;
context.window.console = console;
context.window.setTimeout = setTimeout;
context.window.setInterval = setInterval;
context.window.clearTimeout = clearTimeout;
context.window.clearInterval = clearInterval;
// Execute bundle in context
vm.createContext(context);
try {
vm.runInContext(bundleCode, context);
} catch (error) {
console.error('Error loading nostr bundle:', error.message);
process.exit(1);
}
// Debug what's available in the context
console.log('Bundle loaded, checking available objects...');
console.log('context.window keys:', Object.keys(context.window));
console.log('context.global keys:', Object.keys(context.global));
// Try different ways to access NostrTools
let NostrTools = context.window.NostrTools || context.NostrTools || context.global.NostrTools;
// If still not found, look for other possible exports
if (!NostrTools) {
console.log('Looking for alternative exports...');
// Check if it's under a different name
const windowKeys = Object.keys(context.window);
const possibleExports = windowKeys.filter(key =>
key.toLowerCase().includes('nostr') ||
key.toLowerCase().includes('tools') ||
typeof context.window[key] === 'object'
);
console.log('Possible nostr-related exports:', possibleExports);
// Try the first one that looks promising
if (possibleExports.length > 0) {
NostrTools = context.window[possibleExports[0]];
console.log(`Trying ${possibleExports[0]}:`, typeof NostrTools);
}
}
if (!NostrTools) {
console.error('NostrTools not found in bundle');
console.error('Bundle might not be compatible with Node.js or needs different loading approach');
process.exit(1);
}
console.log('NostrTools loaded successfully');
console.log('Available methods:', Object.keys(NostrTools));
async function testRelay() {
const relayUrl = 'ws://127.0.0.1:8888';
try {
console.log('\n=== Testing Relay Connection ===');
console.log('Relay URL:', relayUrl);
// Create SimplePool
const pool = new NostrTools.SimplePool();
console.log('SimplePool created');
// Test 1: Query for kind 1 events
console.log('\n--- Test 1: Kind 1 Events ---');
const kind1Events = await pool.querySync([relayUrl], {
kinds: [1],
limit: 5
});
console.log(`Found ${kind1Events.length} kind 1 events`);
kind1Events.forEach((event, index) => {
console.log(`Event ${index + 1}:`, {
id: event.id,
kind: event.kind,
pubkey: event.pubkey.substring(0, 16) + '...',
created_at: new Date(event.created_at * 1000).toISOString(),
content: event.content.substring(0, 50) + (event.content.length > 50 ? '...' : '')
});
});
// Test 2: Query for kind 33334 events (configuration)
console.log('\n--- Test 2: Kind 33334 Events (Configuration) ---');
const configEvents = await pool.querySync([relayUrl], {
kinds: [33334],
limit: 10
});
console.log(`Found ${configEvents.length} kind 33334 events`);
configEvents.forEach((event, index) => {
console.log(`Config Event ${index + 1}:`, {
id: event.id,
kind: event.kind,
pubkey: event.pubkey.substring(0, 16) + '...',
created_at: new Date(event.created_at * 1000).toISOString(),
tags: event.tags.length,
content: event.content
});
// Show some tags
if (event.tags.length > 0) {
console.log(' Sample tags:');
event.tags.slice(0, 5).forEach(tag => {
console.log(` ${tag[0]}: ${tag[1] || ''}`);
});
}
});
// Test 3: Query for any events
console.log('\n--- Test 3: Any Events (limit 3) ---');
const anyEvents = await pool.querySync([relayUrl], {
limit: 3
});
console.log(`Found ${anyEvents.length} total events`);
anyEvents.forEach((event, index) => {
console.log(`Event ${index + 1}:`, {
id: event.id,
kind: event.kind,
pubkey: event.pubkey.substring(0, 16) + '...',
created_at: new Date(event.created_at * 1000).toISOString()
});
});
// Clean up
pool.close([relayUrl]);
console.log('\n=== Test Complete ===');
} catch (error) {
console.error('Relay test failed:', error.message);
console.error('Stack:', error.stack);
}
}
// Run the test
testRelay().then(() => {
console.log('Test finished');
process.exit(0);
}).catch((error) => {
console.error('Test failed:', error);
process.exit(1);
});