Compare commits

..

4 Commits

18 changed files with 345 additions and 214 deletions

3
.gitmodules vendored
View File

@@ -1,3 +1,6 @@
[submodule "nostr_core_lib"]
path = nostr_core_lib
url = https://git.laantungir.net/laantungir/nostr_core_lib.git
[submodule "c_utils_lib"]
path = c_utils_lib
url = ssh://git@git.laantungir.net:2222/laantungir/c_utils_lib.git

View File

@@ -89,9 +89,9 @@ RUN cd nostr_core_lib && \
COPY src/ /build/src/
COPY Makefile /build/Makefile
# Build c-relay with full static linking (only rebuilds when src/ changes)
# Build c-relay with full static linking and debug symbols (only rebuilds when src/ changes)
# Disable fortification to avoid __*_chk symbols that don't exist in MUSL
RUN gcc -static -O2 -Wall -Wextra -std=c99 \
RUN gcc -static -g -O0 -DDEBUG -Wall -Wextra -std=c99 \
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \
-I. -Inostr_core_lib -Inostr_core_lib/nostr_core \
-Inostr_core_lib/cjson -Inostr_core_lib/nostr_websocket \
@@ -103,8 +103,8 @@ RUN gcc -static -O2 -Wall -Wextra -std=c99 \
-lwebsockets -lssl -lcrypto -lsqlite3 -lsecp256k1 \
-lcurl -lz -lpthread -lm -ldl
# Strip binary to reduce size
RUN strip /build/c_relay_static
# DO NOT strip - we need debug symbols for debugging
# RUN strip /build/c_relay_static
# Verify it's truly static
RUN echo "=== Binary Information ===" && \

View File

@@ -218,9 +218,13 @@ button:disabled {
.config-actions-cell {
cursor: pointer;
transition: all 0.2s ease;
text-align: center;
text-align: center !important;
font-weight: bold;
vertical-align: middle;
width: 60px;
min-width: 60px;
max-width: 60px;
padding: 8px 4px;
}
.config-actions-cell:hover {

View File

@@ -86,99 +86,12 @@
</div> <!-- End Main Sections Wrapper -->
<!-- Testing Section -->
<div id="div_config" class="section flex-section" style="display: none;">
<h2>RELAY CONFIGURATION</h2>
<div id="config-display" class="hidden">
<div class="config-table-container">
<table class="config-table" id="config-table">
<thead>
<tr>
<th>Parameter</th>
<th>Value</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="config-table-body">
</tbody>
</table>
</div>
<div class="inline-buttons">
<button type="button" id="fetch-config-btn">REFRESH</button>
</div>
</div>
</div>
<!-- Auth Rules Management - Moved after configuration -->
<div class="section flex-section" id="authRulesSection" style="display: none;">
<div class="section-header">
<h2>AUTH RULES MANAGEMENT</h2>
</div>
<!-- Auth Rules Table -->
<div id="authRulesTableContainer" style="display: none;">
<table class="config-table" id="authRulesTable">
<thead>
<tr>
<th>Rule Type</th>
<th>Pattern Type</th>
<th>Pattern Value</th>
<th>Action</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="authRulesTableBody">
</tbody>
</table>
</div>
<!-- Simplified Auth Rule Input Section -->
<div id="authRuleInputSections" style="display: block;">
<!-- Combined Pubkey Auth Rule Section -->
<div class="input-group">
<label for="authRulePubkey">Pubkey (nsec or hex):</label>
<input type="text" id="authRulePubkey" placeholder="nsec1... or 64-character hex pubkey">
</div>
<div id="whitelistWarning" class="warning-box" style="display: none;">
<strong>⚠️ WARNING:</strong> Adding whitelist rules changes relay behavior to whitelist-only
mode.
Only whitelisted users will be able to interact with the relay.
</div>
<div class="inline-buttons">
<button type="button" id="addWhitelistBtn" onclick="addWhitelistRule()">ADD TO
WHITELIST</button>
<button type="button" id="addBlacklistBtn" onclick="addBlacklistRule()">ADD TO
BLACKLIST</button>
<button type="button" id="refreshAuthRulesBtn">REFRESH</button>
</div>
</div>
</div>
<!-- DATABASE STATISTICS Section -->
<div class="section">
<div class="section flex-section" id="databaseStatisticsSection" style="display: none;">
<div class="section-header">
<h2>DATABASE STATISTICS</h2>
</div>
<!-- Database Overview Table -->
<div class="input-group">
@@ -300,6 +213,90 @@
</div>
</div>
<!-- Testing Section -->
<div id="div_config" class="section flex-section" style="display: none;">
<h2>RELAY CONFIGURATION</h2>
<div id="config-display" class="hidden">
<div class="config-table-container">
<table class="config-table" id="config-table">
<thead>
<tr>
<th>Parameter</th>
<th>Value</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="config-table-body">
</tbody>
</table>
</div>
<div class="inline-buttons">
<button type="button" id="fetch-config-btn">REFRESH</button>
</div>
</div>
</div>
<!-- Auth Rules Management - Moved after configuration -->
<div class="section flex-section" id="authRulesSection" style="display: none;">
<div class="section-header">
<h2>AUTH RULES MANAGEMENT</h2>
</div>
<!-- Auth Rules Table -->
<div id="authRulesTableContainer" style="display: none;">
<table class="config-table" id="authRulesTable">
<thead>
<tr>
<th>Rule Type</th>
<th>Pattern Type</th>
<th>Pattern Value</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="authRulesTableBody">
</tbody>
</table>
</div>
<!-- Simplified Auth Rule Input Section -->
<div id="authRuleInputSections" style="display: block;">
<!-- Combined Pubkey Auth Rule Section -->
<div class="input-group">
<label for="authRulePubkey">Pubkey (nsec or hex):</label>
<input type="text" id="authRulePubkey" placeholder="nsec1... or 64-character hex pubkey">
</div>
<div id="whitelistWarning" class="warning-box" style="display: none;">
<strong>⚠️ WARNING:</strong> Adding whitelist rules changes relay behavior to whitelist-only
mode.
Only whitelisted users will be able to interact with the relay.
</div>
<div class="inline-buttons">
<button type="button" id="addWhitelistBtn" onclick="addWhitelistRule()">ADD TO
WHITELIST</button>
<button type="button" id="addBlacklistBtn" onclick="addBlacklistRule()">ADD TO
BLACKLIST</button>
<button type="button" id="refreshAuthRulesBtn">REFRESH</button>
</div>
</div>
</div>
<!-- NIP-17 DIRECT MESSAGES Section -->
<div class="section" id="nip17DMSection" style="display: none;">
<div class="section-header">

View File

@@ -607,11 +607,13 @@
function updateAdminSectionsVisibility() {
const divConfig = document.getElementById('div_config');
const authRulesSection = document.getElementById('authRulesSection');
const databaseStatisticsSection = document.getElementById('databaseStatisticsSection');
const nip17DMSection = document.getElementById('nip17DMSection');
const shouldShow = isLoggedIn && isRelayConnected;
if (divConfig) divConfig.style.display = shouldShow ? 'block' : 'none';
if (authRulesSection) authRulesSection.style.display = shouldShow ? 'block' : 'none';
if (databaseStatisticsSection) databaseStatisticsSection.style.display = shouldShow ? 'block' : 'none';
if (nip17DMSection) nip17DMSection.style.display = shouldShow ? 'block' : 'none';
}
@@ -806,7 +808,7 @@
// Add to inbox
const timestamp = new Date(event.created_at * 1000).toLocaleString();
addMessageToInbox('received', decryptedContent, timestamp);
addMessageToInbox('received', decryptedContent, timestamp, event.pubkey);
// Log for testing
if (typeof logTestEvent === 'function') {
@@ -843,7 +845,7 @@
// Add to inbox
const timestamp = new Date(event.created_at * 1000).toLocaleString();
addMessageToInbox('received', rumor.content, timestamp);
addMessageToInbox('received', rumor.content, timestamp, rumor.pubkey);
// Log for testing
if (typeof logTestEvent === 'function') {
@@ -1846,7 +1848,6 @@
<td>${rule.rule_type}</td>
<td>${rule.pattern_type || rule.operation || '-'}</td>
<td style="font-family: 'Courier New', monospace; font-size: 12px; word-break: break-all; max-width: 200px;">${rule.pattern_value || rule.rule_target || '-'}</td>
<td>${rule.action || 'allow'}</td>
<td>${rule.enabled !== false ? 'Active' : 'Inactive'}</td>
<td>
<div class="inline-buttons">
@@ -2131,7 +2132,7 @@
const originalShowMainInterface = showMainInterface;
showMainInterface = function () {
originalShowMainInterface();
showAuthRulesSection();
// Removed showAuthRulesSection() call - visibility now handled by updateAdminSectionsVisibility()
};
// Auth rules event handlers
@@ -2157,7 +2158,7 @@
// STREAMLINED AUTH RULE FUNCTIONS
// ================================
// Utility function to convert nsec to hex pubkey
// Utility function to convert nsec to hex pubkey or npub to hex pubkey
function nsecToHex(input) {
if (!input || input.trim().length === 0) {
return null;
@@ -2176,11 +2177,17 @@
if (window.NostrTools && window.NostrTools.nip19 && window.NostrTools.nip19.decode) {
const decoded = window.NostrTools.nip19.decode(trimmed);
if (decoded.type === 'nsec') {
// Convert bytes to hex
const hexPubkey = Array.from(decoded.data)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
return hexPubkey;
// Handle different versions of nostr-tools
if (typeof decoded.data === 'string') {
// v1 style - data is already hex
return decoded.data;
} else {
// v2 style - data is Uint8Array
const hexPubkey = Array.from(decoded.data)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
return hexPubkey;
}
}
}
} catch (error) {
@@ -2189,6 +2196,31 @@
}
}
// If it starts with npub1, try to decode to hex
if (trimmed.startsWith('npub1')) {
try {
if (window.NostrTools && window.NostrTools.nip19 && window.NostrTools.nip19.decode) {
const decoded = window.NostrTools.nip19.decode(trimmed);
if (decoded.type === 'npub') {
// Handle different versions of nostr-tools
if (typeof decoded.data === 'string') {
// v1 style - data is already hex
return decoded.data;
} else {
// v2 style - data is Uint8Array
const hexPubkey = Array.from(decoded.data)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
return hexPubkey;
}
}
}
} catch (error) {
console.error('Failed to decode npub:', error);
return null;
}
}
return null; // Invalid format
}
@@ -2204,10 +2236,10 @@
return;
}
// Convert nsec to hex if needed
// Convert nsec or npub to hex if needed
const hexPubkey = nsecToHex(inputValue);
if (!hexPubkey) {
log('Invalid pubkey format. Please enter nsec1... or 64-character hex', 'ERROR');
log('Invalid pubkey format. Please enter nsec1..., npub1..., or 64-character hex', 'ERROR');
return;
}
@@ -2256,10 +2288,10 @@
return;
}
// Convert nsec to hex if needed
// Convert nsec or npub to hex if needed
const hexPubkey = nsecToHex(inputValue);
if (!hexPubkey) {
log('Invalid pubkey format. Please enter nsec1... or 64-character hex', 'ERROR');
log('Invalid pubkey format. Please enter nsec1..., npub1..., or 64-character hex', 'ERROR');
return;
}
@@ -3063,21 +3095,32 @@
}
// Add message to inbox display
function addMessageToInbox(direction, message, timestamp) {
function addMessageToInbox(direction, message, timestamp, pubkey = null) {
if (!dmInbox) return;
const messageDiv = document.createElement('div');
messageDiv.className = 'log-entry';
const directionColor = direction === 'sent' ? '#007bff' : '#28a745';
// Convert newlines to <br> tags for proper HTML display
const formattedMessage = message.replace(/\n/g, '<br>');
// Add pubkey display for received messages
let pubkeyDisplay = '';
if (pubkey && direction === 'received') {
try {
const npub = window.NostrTools.nip19.npubEncode(pubkey);
pubkeyDisplay = ` <span style="color: #666; font-size: 11px;">(${npub})</span>`;
} catch (error) {
console.error('Failed to encode pubkey to npub:', error);
}
}
messageDiv.innerHTML = `
<span class="log-timestamp">${timestamp}</span>
<span style="color: ${directionColor}; font-weight: bold;">[${direction.toUpperCase()}]</span>
<span style="white-space: pre-wrap;">${formattedMessage}</span>
<span style="white-space: pre-wrap;">${formattedMessage}${pubkeyDisplay}</span>
`;
// Remove the "No messages received yet" placeholder if it exists
@@ -3397,10 +3440,10 @@
data.top_pubkeys.forEach((pubkey, index) => {
const row = document.createElement('tr');
const shortPubkey = pubkey.pubkey ? pubkey.pubkey.substring(0, 16) + '...' : '-';
const npub = pubkey.pubkey ? window.NostrTools.nip19.npubEncode(pubkey.pubkey) : '-';
row.innerHTML = `
<td>${index + 1}</td>
<td style="font-family: 'Courier New', monospace; font-size: 12px;">${shortPubkey}</td>
<td style="font-family: 'Courier New', monospace; font-size: 12px; word-break: break-all;">${npub}</td>
<td>${pubkey.event_count}</td>
<td>${pubkey.percentage}%</td>
`;
@@ -3532,6 +3575,9 @@
// Initialize login/logout button state
updateLoginLogoutButton();
// Ensure admin sections are hidden by default on page load
updateAdminSectionsVisibility();
setTimeout(() => {
initializeApp();
// Enhance SimplePool for testing after initialization

1
c_utils_lib Submodule

Submodule c_utils_lib added at 3fd5d0911a

View File

@@ -1,8 +1,7 @@
#!/bin/bash
# C-Relay Static Binary Deployment Script
# Deploys build/c_relay_static_x86_64 to server via sshlt
# Usage: ./deploy_static.sh [--debug-level=N] [-d=N]
# Deploys build/c_relay_static_x86_64 to server via ssh
set -e
@@ -11,44 +10,6 @@ LOCAL_BINARY="build/c_relay_static_x86_64"
REMOTE_BINARY_PATH="/usr/local/bin/c_relay/c_relay"
SERVICE_NAME="c-relay"
# Default debug level
DEBUG_LEVEL=0
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
--debug-level=*)
DEBUG_LEVEL="${1#*=}"
shift
;;
-d=*)
DEBUG_LEVEL="${1#*=}"
shift
;;
--debug-level)
DEBUG_LEVEL="$2"
shift 2
;;
-d)
DEBUG_LEVEL="$2"
shift 2
;;
*)
echo "Unknown option: $1"
echo "Usage: $0 [--debug-level=N] [-d=N]"
exit 1
;;
esac
done
# Validate debug level
if ! [[ "$DEBUG_LEVEL" =~ ^[0-5]$ ]]; then
echo "Error: Debug level must be 0-5, got: $DEBUG_LEVEL"
exit 1
fi
echo "Deploying with debug level: $DEBUG_LEVEL"
# Create backup
ssh ubuntu@laantungir.com "sudo cp '$REMOTE_BINARY_PATH' '${REMOTE_BINARY_PATH}.backup.$(date +%Y%m%d_%H%M%S)'" 2>/dev/null || true
@@ -60,9 +21,6 @@ ssh ubuntu@laantungir.com "sudo mv '/tmp/c_relay.tmp' '$REMOTE_BINARY_PATH'"
ssh ubuntu@laantungir.com "sudo chown c-relay:c-relay '$REMOTE_BINARY_PATH'"
ssh ubuntu@laantungir.com "sudo chmod +x '$REMOTE_BINARY_PATH'"
# Update systemd service environment variable
ssh ubuntu@laantungir.com "sudo sed -i 's/Environment=DEBUG_LEVEL=.*/Environment=DEBUG_LEVEL=$DEBUG_LEVEL/' /etc/systemd/system/c-relay.service"
# Reload systemd and restart service
ssh ubuntu@laantungir.com "sudo systemctl daemon-reload"
ssh ubuntu@laantungir.com "sudo systemctl restart '$SERVICE_NAME'"

44
notes.txt Normal file
View File

@@ -0,0 +1,44 @@
Task Completed
Perfect! The core dump limit is now set correctly. You can see both:
LimitCORE=infinity
LimitCORESoft=infinity
What happens next:
The relay is now configured to generate core dumps when it crashes
Wait for the next crash to occur (based on your logs, it seems to happen intermittently)
When it crashes, the core dump will be automatically captured by systemd
After the next crash, analyze it:
# List all core dumps (most recent first)
sudo coredumpctl list
# View info about the most recent c-relay crash
sudo coredumpctl info c-relay
# Load the core dump in gdb for detailed analysis
sudo coredumpctl gdb c-relay
Inside gdb, run these commands:
(gdb) bt full # Full backtrace with all variables
(gdb) info threads # Show all threads
(gdb) thread apply all bt # Backtrace for all threads
(gdb) frame 0 # Go to crash frame
(gdb) info locals # Show local variables
(gdb) list # Show source code around crash
The backtrace will show you exactly which line of code caused the segmentation fault and the state of all variables at that moment, which should pinpoint whether it's the subscription management issue, buffer overflow, or something else.
You're all set up now - just wait for the next crash and then run the coredumpctl commands to analyze it.
--------------------------
Even simpler: Use this one-liner
# Start relay and immediately attach gdb
cd /usr/local/bin/c_relay
sudo -u c-relay ./c_relay --debug-level=5 & sleep 2 && sudo gdb -p $(pgrep c_relay)
Once gdb attaches, type continue and wait for the crash. This way the relay starts normally and gdb just monitors it.
Which approach would you like to try?

View File

@@ -1 +1 @@
2614899
2864152

View File

@@ -64,7 +64,7 @@ int process_admin_config_event(cJSON* event, char* error_message, size_t error_s
// Forward declaration for relay info initialization
void init_relay_info(void);
int add_auth_rule_from_config(const char* rule_type, const char* pattern_type,
const char* pattern_value, const char* action);
const char* pattern_value);
int remove_auth_rule_from_config(const char* rule_type, const char* pattern_type,
const char* pattern_value);
int is_config_table_ready(void);
@@ -2067,28 +2067,27 @@ int process_admin_auth_event(cJSON* event, char* error_message, size_t error_siz
// Add auth rule from configuration
int add_auth_rule_from_config(const char* rule_type, const char* pattern_type,
const char* pattern_value, const char* action) {
if (!g_db || !rule_type || !pattern_type || !pattern_value || !action) {
const char* pattern_value) {
if (!g_db || !rule_type || !pattern_type || !pattern_value) {
return -1;
}
const char* sql = "INSERT INTO auth_rules (rule_type, pattern_type, pattern_value, action) "
"VALUES (?, ?, ?, ?)";
const char* sql = "INSERT INTO auth_rules (rule_type, pattern_type, pattern_value) "
"VALUES (?, ?, ?)";
sqlite3_stmt* stmt;
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
return -1;
}
sqlite3_bind_text(stmt, 1, rule_type, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, pattern_type, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 3, pattern_value, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 4, action, -1, SQLITE_STATIC);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
return (rc == SQLITE_DONE) ? 0 : -1;
}
@@ -2725,13 +2724,13 @@ int handle_auth_query_unified(cJSON* event, const char* query_type, char* error_
// Build appropriate SQL query based on query type
if (strcmp(query_type, "all") == 0) {
sql = "SELECT rule_type, pattern_type, pattern_value, action FROM auth_rules ORDER BY rule_type, pattern_type";
sql = "SELECT rule_type, pattern_type, pattern_value FROM auth_rules WHERE active = 1 ORDER BY rule_type, pattern_type";
}
else if (strcmp(query_type, "whitelist") == 0) {
sql = "SELECT rule_type, pattern_type, pattern_value, action FROM auth_rules WHERE rule_type LIKE '%whitelist%' ORDER BY pattern_type";
sql = "SELECT rule_type, pattern_type, pattern_value FROM auth_rules WHERE rule_type LIKE '%whitelist%' AND active = 1 ORDER BY pattern_type";
}
else if (strcmp(query_type, "blacklist") == 0) {
sql = "SELECT rule_type, pattern_type, pattern_value, action FROM auth_rules WHERE rule_type LIKE '%blacklist%' ORDER BY pattern_type";
sql = "SELECT rule_type, pattern_type, pattern_value FROM auth_rules WHERE rule_type LIKE '%blacklist%' AND active = 1 ORDER BY pattern_type";
}
else if (strcmp(query_type, "pattern") == 0) {
// Get pattern value from tags
@@ -2740,7 +2739,7 @@ int handle_auth_query_unified(cJSON* event, const char* query_type, char* error_
snprintf(error_message, error_size, "invalid: pattern query requires pattern value");
return -1;
}
sql = "SELECT rule_type, pattern_type, pattern_value, action FROM auth_rules WHERE pattern_value = ? ORDER BY rule_type, pattern_type";
sql = "SELECT rule_type, pattern_type, pattern_value FROM auth_rules WHERE pattern_value = ? AND active = 1 ORDER BY rule_type, pattern_type";
use_pattern_param = 1;
}
else {
@@ -2775,7 +2774,6 @@ int handle_auth_query_unified(cJSON* event, const char* query_type, char* error_
const char* rule_type = (const char*)sqlite3_column_text(stmt, 0);
const char* pattern_type = (const char*)sqlite3_column_text(stmt, 1);
const char* pattern_value_result = (const char*)sqlite3_column_text(stmt, 2);
const char* action = (const char*)sqlite3_column_text(stmt, 3);
// printf(" %s %s:%s -> %s\n",
// rule_type ? rule_type : "",
@@ -2788,7 +2786,7 @@ int handle_auth_query_unified(cJSON* event, const char* query_type, char* error_
cJSON_AddStringToObject(rule_obj, "rule_type", rule_type ? rule_type : "");
cJSON_AddStringToObject(rule_obj, "pattern_type", pattern_type ? pattern_type : "");
cJSON_AddStringToObject(rule_obj, "pattern_value", pattern_value_result ? pattern_value_result : "");
cJSON_AddStringToObject(rule_obj, "action", action ? action : "allow");
cJSON_AddStringToObject(rule_obj, "action", "allow"); // Simplified: rule_type determines behavior
cJSON_AddItemToArray(results_array, rule_obj);
rule_count++;
@@ -3314,7 +3312,7 @@ int handle_auth_rule_modification_unified(cJSON* event, char* error_message, siz
// Process auth rule: ["blacklist"|"whitelist", "pubkey"|"hash", "value"]
if (strcmp(rule_type, "blacklist") == 0 || strcmp(rule_type, "whitelist") == 0) {
if (add_auth_rule_from_config(rule_type, pattern_type, pattern_value, "allow") == 0) {
if (add_auth_rule_from_config(rule_type, pattern_type, pattern_value) == 0) {
rules_processed++;
// Add processed rule to response array
@@ -3322,7 +3320,7 @@ int handle_auth_rule_modification_unified(cJSON* event, char* error_message, siz
cJSON_AddStringToObject(rule_obj, "rule_type", rule_type);
cJSON_AddStringToObject(rule_obj, "pattern_type", pattern_type);
cJSON_AddStringToObject(rule_obj, "pattern_value", pattern_value);
cJSON_AddStringToObject(rule_obj, "action", "allow");
cJSON_AddStringToObject(rule_obj, "action", "allow"); // Simplified: rule_type determines behavior
cJSON_AddStringToObject(rule_obj, "status", "added");
cJSON_AddItemToArray(processed_rules, rule_obj);
}

View File

@@ -114,7 +114,7 @@ cJSON* build_query_response(const char* query_type, cJSON* results_array, int to
// 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);
const char* pattern_value);
int remove_auth_rule_from_config(const char* rule_type, const char* pattern_type,
const char* pattern_value);

View File

@@ -1086,19 +1086,30 @@ int send_nip17_response(const char* sender_pubkey, const char* response_content,
}
}
// Store the gift wrap in database
// Broadcast FIRST before storing (broadcasting needs the event intact)
// Make a copy for broadcasting to avoid use-after-free issues
cJSON* gift_wrap_copy = cJSON_Duplicate(gift_wraps[0], 1);
if (!gift_wrap_copy) {
cJSON_Delete(gift_wraps[0]);
strncpy(error_message, "NIP-17: Failed to duplicate gift wrap for broadcast", error_size - 1);
return -1;
}
// Broadcast the copy to active subscriptions
broadcast_event_to_subscriptions(gift_wrap_copy);
// Store the original in database
int store_result = store_event(gift_wraps[0]);
// Clean up both copies
cJSON_Delete(gift_wrap_copy);
cJSON_Delete(gift_wraps[0]);
if (store_result != 0) {
cJSON_Delete(gift_wraps[0]);
strncpy(error_message, "NIP-17: Failed to store response gift wrap", error_size - 1);
return -1;
}
// Broadcast the response event to active subscriptions
broadcast_event_to_subscriptions(gift_wraps[0]);
cJSON_Delete(gift_wraps[0]);
return 0;
}

File diff suppressed because one or more lines are too long

View File

@@ -15,6 +15,7 @@
#include "../nostr_core_lib/nostr_core/nip013.h" // NIP-13: Proof of Work
#include "../nostr_core_lib/nostr_core/nostr_common.h"
#include "../nostr_core_lib/nostr_core/utils.h"
#include "debug.h" // C-relay debug system
#include "config.h" // C-relay configuration system
#include <sqlite3.h>
#include <stdio.h>
@@ -531,6 +532,8 @@ int check_database_auth_rules(const char *pubkey, const char *operation __attrib
sqlite3_stmt *stmt = NULL;
int rc;
DEBUG_TRACE("Checking auth rules for pubkey: %s", pubkey);
if (!pubkey) {
return NOSTR_ERROR_INVALID_INPUT;
}
@@ -547,19 +550,21 @@ int check_database_auth_rules(const char *pubkey, const char *operation __attrib
// Step 1: Check pubkey blacklist (highest priority)
const char *blacklist_sql =
"SELECT rule_type, action FROM auth_rules WHERE rule_type = "
"'blacklist' AND pattern_type = 'pubkey' AND pattern_value = ? LIMIT 1";
"SELECT rule_type FROM auth_rules WHERE rule_type = "
"'blacklist' AND pattern_type = 'pubkey' AND pattern_value = ? AND active = 1 LIMIT 1";
DEBUG_TRACE("Blacklist SQL: %s", blacklist_sql);
rc = sqlite3_prepare_v2(db, blacklist_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, pubkey, -1, SQLITE_STATIC);
if (sqlite3_step(stmt) == SQLITE_ROW) {
const char *action = (const char *)sqlite3_column_text(stmt, 1);
int step_result = sqlite3_step(stmt);
DEBUG_TRACE("Blacklist query result: %s", step_result == SQLITE_ROW ? "FOUND" : "NOT_FOUND");
if (step_result == SQLITE_ROW) {
DEBUG_TRACE("BLACKLIST HIT: Denying access for pubkey: %s", pubkey);
// Set specific violation details for status code mapping
strcpy(g_last_rule_violation.violation_type, "pubkey_blacklist");
sprintf(g_last_rule_violation.reason, "Public key blacklisted: %s",
action ? action : "PUBKEY_BLACKLIST");
sprintf(g_last_rule_violation.reason, "Public key blacklisted");
sqlite3_finalize(stmt);
sqlite3_close(db);
@@ -571,19 +576,16 @@ int check_database_auth_rules(const char *pubkey, const char *operation __attrib
// Step 2: Check hash blacklist
if (resource_hash) {
const char *hash_blacklist_sql =
"SELECT rule_type, action FROM auth_rules WHERE rule_type = "
"'blacklist' AND pattern_type = 'hash' AND pattern_value = ? LIMIT 1";
"SELECT rule_type FROM auth_rules WHERE rule_type = "
"'blacklist' AND pattern_type = 'hash' AND pattern_value = ? AND active = 1 LIMIT 1";
rc = sqlite3_prepare_v2(db, hash_blacklist_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, resource_hash, -1, SQLITE_STATIC);
if (sqlite3_step(stmt) == SQLITE_ROW) {
const char *action = (const char *)sqlite3_column_text(stmt, 1);
// Set specific violation details for status code mapping
strcpy(g_last_rule_violation.violation_type, "hash_blacklist");
sprintf(g_last_rule_violation.reason, "File hash blacklisted: %s",
action ? action : "HASH_BLACKLIST");
sprintf(g_last_rule_violation.reason, "File hash blacklisted");
sqlite3_finalize(stmt);
sqlite3_close(db);
@@ -595,8 +597,8 @@ int check_database_auth_rules(const char *pubkey, const char *operation __attrib
// Step 3: Check pubkey whitelist
const char *whitelist_sql =
"SELECT rule_type, action FROM auth_rules WHERE rule_type = "
"'whitelist' AND pattern_type = 'pubkey' AND pattern_value = ? LIMIT 1";
"SELECT rule_type FROM auth_rules WHERE rule_type = "
"'whitelist' AND pattern_type = 'pubkey' AND pattern_value = ? AND active = 1 LIMIT 1";
rc = sqlite3_prepare_v2(db, whitelist_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, pubkey, -1, SQLITE_STATIC);
@@ -612,7 +614,7 @@ int check_database_auth_rules(const char *pubkey, const char *operation __attrib
// Step 4: Check if any whitelist rules exist - if yes, deny by default
const char *whitelist_exists_sql =
"SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'whitelist' "
"AND pattern_type = 'pubkey' LIMIT 1";
"AND pattern_type = 'pubkey' AND active = 1 LIMIT 1";
rc = sqlite3_prepare_v2(db, whitelist_exists_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {

View File

@@ -142,8 +142,6 @@ CREATE TABLE auth_rules (\n\
rule_type TEXT NOT NULL CHECK (rule_type IN ('whitelist', 'blacklist', 'rate_limit', 'auth_required')),\n\
pattern_type TEXT NOT NULL CHECK (pattern_type IN ('pubkey', 'kind', 'ip', 'global')),\n\
pattern_value TEXT,\n\
action TEXT NOT NULL CHECK (action IN ('allow', 'deny', 'require_auth', 'rate_limit')),\n\
parameters TEXT, -- JSON parameters for rate limiting, etc.\n\
active INTEGER NOT NULL DEFAULT 1,\n\
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n\
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))\n\

View File

@@ -622,7 +622,8 @@ int broadcast_event_to_subscriptions(cJSON* event) {
subscription_t* update_sub = g_subscription_manager.active_subscriptions;
while (update_sub) {
if (update_sub->wsi == current_temp->wsi &&
strcmp(update_sub->id, current_temp->id) == 0) {
strcmp(update_sub->id, current_temp->id) == 0 &&
update_sub->active) { // Add active check to prevent use-after-free
update_sub->events_sent++;
break;
}

View File

@@ -516,8 +516,33 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
if (event_kind == 23456) {
if (admin_result != 0) {
char error_result_msg[512];
snprintf(error_result_msg, sizeof(error_result_msg),
"ERROR: Kind %d event processing failed: %s", event_kind, admin_error);
if (strlen(admin_error) > 0) {
// Safely truncate admin_error if too long
size_t max_error_len = sizeof(error_result_msg) - 50; // Leave room for prefix
size_t error_len = strlen(admin_error);
if (error_len > max_error_len) {
error_len = max_error_len;
}
char truncated_error[512];
memcpy(truncated_error, admin_error, error_len);
truncated_error[error_len] = '\0';
// Use a safer approach to avoid truncation warning
size_t prefix_len = snprintf(error_result_msg, sizeof(error_result_msg),
"ERROR: Kind %d event processing failed: ", event_kind);
if (prefix_len < sizeof(error_result_msg)) {
size_t remaining = sizeof(error_result_msg) - prefix_len;
size_t copy_len = strlen(truncated_error);
if (copy_len >= remaining) {
copy_len = remaining - 1;
}
memcpy(error_result_msg + prefix_len, truncated_error, copy_len);
error_result_msg[prefix_len + copy_len] = '\0';
}
} else {
snprintf(error_result_msg, sizeof(error_result_msg),
"ERROR: Kind %d event processing failed", event_kind);
}
DEBUG_ERROR(error_result_msg);
}
}
@@ -870,10 +895,9 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
return 0;
}
// Remove from global manager
remove_subscription_from_manager(subscription_id, wsi);
// Remove from session list if present
// CRITICAL FIX: Remove from session list FIRST (while holding lock)
// to prevent race condition where global manager frees the subscription
// while we're still iterating through the session list
if (pss) {
pthread_mutex_lock(&pss->session_lock);
@@ -891,6 +915,10 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
pthread_mutex_unlock(&pss->session_lock);
}
// Remove from global manager AFTER removing from session list
// This prevents use-after-free when iterating session subscriptions
remove_subscription_from_manager(subscription_id, wsi);
// Subscription closed
} else {
send_notice_message(wsi, "error: missing or invalid subscription ID in CLOSE");

40
tests/post_events.sh Executable file
View File

@@ -0,0 +1,40 @@
#!/bin/bash
# Test script to post kind 1 events to the relay every second
# Cycles through three different secret keys
# Content includes current timestamp
# Array of secret keys to cycle through
SECRET_KEYS=(
"3fdd8227a920c2385559400b2b14e464f22e80df312a73cc7a86e1d7e91d608f"
"a156011cd65b71f84b4a488ac81687f2aed57e490b31c28f58195d787030db60"
"1618aaa21f5bd45c5ffede0d9a60556db67d4a046900e5f66b0bae5c01c801fb"
)
RELAY_URL="ws://localhost:8888"
KEY_INDEX=0
echo "Starting event posting test to $RELAY_URL"
echo "Press Ctrl+C to stop"
while true; do
# Get current timestamp
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S UTC")
# Get current secret key
CURRENT_KEY=${SECRET_KEYS[$KEY_INDEX]}
# Create content with timestamp
CONTENT="Test event at $TIMESTAMP"
echo "[$TIMESTAMP] Posting event with key ${KEY_INDEX}: ${CURRENT_KEY:0:16}..."
# Post event using nak
nak event -c "$CONTENT" --sec "$CURRENT_KEY" "$RELAY_URL"
# Cycle to next key
KEY_INDEX=$(( (KEY_INDEX + 1) % ${#SECRET_KEYS[@]} ))
# Wait 1 second
sleep 1
done