Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bd7aa5a10 | ||
|
|
361912ec85 | ||
|
|
0de491382e | ||
|
|
3148bbbee7 | ||
|
|
3965ba04d8 | ||
|
|
b96af938bd | ||
|
|
89c8248013 | ||
|
|
d8f477c6cf | ||
|
|
040eeadb13 |
+2
-2
@@ -288,8 +288,8 @@ AUTH RULES MANAGEMENT
|
||||
<!-- Auth Rule Input Section -->
|
||||
<div id="authRuleInputSections" style="display: block;">
|
||||
<div class="input-group">
|
||||
<label for="authRulePubkey">Pubkey (nsec or hex):</label>
|
||||
<input type="text" id="authRulePubkey" placeholder="nsec1... or 64-character hex pubkey">
|
||||
<label for="authRulePubkey">Public Key (npub or hex):</label>
|
||||
<input type="text" id="authRulePubkey" placeholder="npub1... 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
|
||||
|
||||
+25
-11
@@ -523,16 +523,28 @@ async function verifyAdminAccess() {
|
||||
// Show a loading indicator while verifying
|
||||
showAdminVerificationLoading();
|
||||
|
||||
// Send system_status command to verify admin access
|
||||
// Send system_status command to verify admin access.
|
||||
// Under heavy relay load the publish OK may time out even though the relay
|
||||
// received the event — so we catch the error and still wait for the response.
|
||||
try {
|
||||
await sendAdminCommand(['system_command', 'system_status']);
|
||||
console.log('Admin verification command sent');
|
||||
} catch (error) {
|
||||
console.error('Failed to send admin verification command:', error);
|
||||
// Continue with timeout - the command might have been queued
|
||||
// Under heavy load the relay may not send OK in time but still processes
|
||||
// the event. Continue waiting for the response subscription to fire.
|
||||
console.log('Continuing to wait for admin response despite publish error...');
|
||||
}
|
||||
|
||||
// Set timeout for admin verification (5 seconds)
|
||||
// Update status message after 5 seconds to indicate relay is under load
|
||||
setTimeout(() => {
|
||||
const statusEl = document.getElementById('admin-verify-status');
|
||||
if (statusEl && pendingAdminVerification) {
|
||||
statusEl.textContent = 'Relay is under heavy load — still waiting for response...';
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
// Final timeout: 60 seconds — if no response by then, deny access
|
||||
adminVerificationTimeout = setTimeout(() => {
|
||||
if (pendingAdminVerification) {
|
||||
console.log('⛔ Admin verification timeout - user is not admin');
|
||||
@@ -541,7 +553,7 @@ async function verifyAdminAccess() {
|
||||
hideAdminVerificationLoading();
|
||||
showAccessDeniedOverlay();
|
||||
}
|
||||
}, 5000);
|
||||
}, 60000);
|
||||
}
|
||||
|
||||
// Show loading indicator while verifying admin access
|
||||
@@ -556,7 +568,7 @@ function showAdminVerificationLoading() {
|
||||
<div class="admin-verification-content">
|
||||
<div class="spinner"></div>
|
||||
<h3>Verifying Administrator Access...</h3>
|
||||
<p>Please wait while we verify your admin privileges.</p>
|
||||
<p id="admin-verify-status">Waiting for response from relay...</p>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(loadingOverlay);
|
||||
@@ -1847,11 +1859,12 @@ function handleSystemCommandResponse(responseData) {
|
||||
console.log('Command:', responseData.command);
|
||||
console.log('Status:', responseData.status);
|
||||
|
||||
// Handle admin verification via system_status response
|
||||
if (responseData.command === 'system_status' && pendingAdminVerification) {
|
||||
// Handle admin verification via system_status response.
|
||||
// Also handle late responses that arrive after the timeout fired (relay was under load).
|
||||
if (responseData.command === 'system_status' && (pendingAdminVerification || (isLoggedIn && !isAdminVerified))) {
|
||||
console.log('✅ Admin verification successful - received system_status response');
|
||||
|
||||
// Clear the timeout
|
||||
// Clear the timeout if still pending
|
||||
if (adminVerificationTimeout) {
|
||||
clearTimeout(adminVerificationTimeout);
|
||||
adminVerificationTimeout = null;
|
||||
@@ -1860,8 +1873,9 @@ function handleSystemCommandResponse(responseData) {
|
||||
pendingAdminVerification = false;
|
||||
isAdminVerified = true;
|
||||
|
||||
// Hide loading overlay
|
||||
// Hide loading overlay and access denied overlay (in case timeout already showed it)
|
||||
hideAdminVerificationLoading();
|
||||
hideAccessDeniedOverlay();
|
||||
|
||||
// Show admin sections now that we're verified
|
||||
updateAdminSectionsVisibility();
|
||||
@@ -3154,7 +3168,7 @@ function addBlacklistRule() {
|
||||
// Convert nsec or npub to hex if needed
|
||||
const hexPubkey = nsecToHex(inputValue);
|
||||
if (!hexPubkey) {
|
||||
log('Invalid pubkey format. Please enter nsec1..., npub1..., or 64-character hex', 'ERROR');
|
||||
log('Invalid public key format. Please enter npub1... or 64-character hex pubkey', 'ERROR');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3206,7 +3220,7 @@ function addWhitelistRule() {
|
||||
// Convert nsec or npub to hex if needed
|
||||
const hexPubkey = nsecToHex(inputValue);
|
||||
if (!hexPubkey) {
|
||||
log('Invalid pubkey format. Please enter nsec1..., npub1..., or 64-character hex', 'ERROR');
|
||||
log('Invalid public key format. Please enter npub1... or 64-character hex pubkey', 'ERROR');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -952,6 +952,15 @@ static int validate_config_field(const char* key, const char* value, char* error
|
||||
return 0;
|
||||
}
|
||||
|
||||
// NIP-42 auth timeout
|
||||
if (strcmp(key, "nip42_auth_timeout_sec") == 0) {
|
||||
if (!is_valid_positive_integer(value) && strcmp(value, "0") != 0) {
|
||||
snprintf(error_msg, error_size, "invalid nip42_auth_timeout_sec '%s' (must be non-negative integer)", value);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// SQLite performance tuning
|
||||
if (strcmp(key, "sqlite_mmap_size") == 0) {
|
||||
if (!is_valid_positive_integer(value) && strcmp(value, "0") != 0) {
|
||||
|
||||
@@ -83,6 +83,11 @@ static const struct {
|
||||
// IP-based rate limiting or access control (which would require firewall protection anyway)
|
||||
{"trust_proxy_headers", "true"},
|
||||
|
||||
// NIP-42 Authentication Timeout
|
||||
// Seconds after connection before unauthenticated clients are disconnected (0 = disabled)
|
||||
// Prevents unauthenticated connections from accumulating under heavy load
|
||||
{"nip42_auth_timeout_sec", "10"},
|
||||
|
||||
// SQLite Performance Tuning
|
||||
// mmap_size: bytes of database file to memory-map (0 = disabled, 268435456 = 256MB recommended)
|
||||
// Eliminates pread64 syscall overhead for database reads — significant CPU savings under load
|
||||
|
||||
File diff suppressed because one or more lines are too long
+29
-10
@@ -881,6 +881,22 @@ int store_event(cJSON* event) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Fast duplicate check: returns 1 if event ID already exists in DB, 0 if not.
|
||||
// Uses the primary key index — single B-tree lookup, ~10μs.
|
||||
// Call this BEFORE signature verification to skip expensive crypto on duplicates.
|
||||
int event_id_exists_in_db(const char* event_id) {
|
||||
if (!g_db || !event_id || strlen(event_id) != 64) return 0;
|
||||
|
||||
sqlite3_stmt* stmt;
|
||||
const char* sql = "SELECT 1 FROM events WHERE id=? LIMIT 1";
|
||||
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return 0;
|
||||
|
||||
sqlite3_bind_text(stmt, 1, event_id, 64, SQLITE_STATIC);
|
||||
int exists = (sqlite3_step(stmt) == SQLITE_ROW) ? 1 : 0;
|
||||
sqlite3_finalize(stmt);
|
||||
return exists;
|
||||
}
|
||||
|
||||
// Populate event_tags from existing events (run once at startup)
|
||||
int populate_event_tags_from_existing(void) {
|
||||
if (!g_db) return -1;
|
||||
@@ -1552,21 +1568,24 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
|
||||
}
|
||||
}
|
||||
|
||||
// Build EVENT message using string concatenation (much faster than cJSON operations)
|
||||
// Build EVENT message using zero-copy path: allocate with LWS_PRE prefix,
|
||||
// write directly, transfer ownership to queue — no memcpy.
|
||||
// Format: ["EVENT","<sub_id>",<event_json>]
|
||||
size_t sub_id_len = strlen(sub_id);
|
||||
size_t event_json_len = strlen(event_json_str);
|
||||
size_t msg_len = 10 + sub_id_len + 3 + event_json_len + 1; // ["EVENT",""] + sub_id + "," + event_json + ]
|
||||
|
||||
char* msg_str = malloc(msg_len + 1);
|
||||
if (msg_str) {
|
||||
snprintf(msg_str, msg_len + 1, "[\"EVENT\",\"%s\",%s]", sub_id, event_json_str);
|
||||
|
||||
// Use proper message queue system instead of direct lws_write
|
||||
if (queue_message(wsi, pss, msg_str, strlen(msg_str), LWS_WRITE_TEXT) != 0) {
|
||||
size_t msg_len = 10 + sub_id_len + 3 + event_json_len + 1;
|
||||
|
||||
unsigned char* buf = malloc(LWS_PRE + msg_len + 1);
|
||||
if (buf) {
|
||||
char* msg_ptr = (char*)(buf + LWS_PRE);
|
||||
snprintf(msg_ptr, msg_len + 1, "[\"EVENT\",\"%s\",%s]", sub_id, event_json_str);
|
||||
size_t actual_len = strlen(msg_ptr);
|
||||
|
||||
// queue_message_take_ownership takes buf ownership — no memcpy, no free needed here
|
||||
if (queue_message_take_ownership(wsi, pss, buf, actual_len, LWS_WRITE_TEXT) != 0) {
|
||||
DEBUG_ERROR("Failed to queue EVENT message for sub=%s", sub_id);
|
||||
// buf already freed by queue_message_take_ownership on failure
|
||||
}
|
||||
free(msg_str);
|
||||
}
|
||||
|
||||
cJSON_Delete(event);
|
||||
|
||||
+2
-2
@@ -13,8 +13,8 @@
|
||||
// Using CRELAY_ prefix to avoid conflicts with nostr_core_lib VERSION macros
|
||||
#define CRELAY_VERSION_MAJOR 1
|
||||
#define CRELAY_VERSION_MINOR 2
|
||||
#define CRELAY_VERSION_PATCH 9
|
||||
#define CRELAY_VERSION "v1.2.9"
|
||||
#define CRELAY_VERSION_PATCH 18
|
||||
#define CRELAY_VERSION "v1.2.18"
|
||||
|
||||
// Relay metadata (authoritative source for NIP-11 information)
|
||||
#define RELAY_NAME "C-Relay"
|
||||
|
||||
+23
-2
@@ -118,6 +118,8 @@ cJSON* generate_relay_info_json() {
|
||||
int default_limit = get_config_int("default_limit", 500);
|
||||
int min_pow_difficulty = get_config_int("pow_min_difficulty", 0);
|
||||
int admin_enabled = get_config_bool("admin_enabled", 0);
|
||||
int wot_enabled = get_config_int("wot_enabled", 0);
|
||||
int auth_enabled = get_config_bool("auth_enabled", 0);
|
||||
|
||||
// Add basic relay information
|
||||
if (relay_name && strlen(relay_name) > 0) {
|
||||
@@ -209,6 +211,9 @@ cJSON* generate_relay_info_json() {
|
||||
}
|
||||
|
||||
// Add server limitations
|
||||
// restricted_writes is true when WoT or auth is enabled (only trusted pubkeys can write)
|
||||
int restricted_writes = (wot_enabled > 0 || auth_enabled) ? 1 : 0;
|
||||
|
||||
cJSON* limitation = cJSON_CreateObject();
|
||||
if (limitation) {
|
||||
cJSON_AddNumberToObject(limitation, "max_message_length", max_message_length);
|
||||
@@ -218,15 +223,31 @@ cJSON* generate_relay_info_json() {
|
||||
cJSON_AddNumberToObject(limitation, "max_event_tags", max_event_tags);
|
||||
cJSON_AddNumberToObject(limitation, "max_content_length", max_content_length);
|
||||
cJSON_AddNumberToObject(limitation, "min_pow_difficulty", min_pow_difficulty);
|
||||
cJSON_AddBoolToObject(limitation, "auth_required", admin_enabled ? cJSON_True : cJSON_False);
|
||||
cJSON_AddBoolToObject(limitation, "auth_required", auth_enabled ? cJSON_True : cJSON_False);
|
||||
cJSON_AddBoolToObject(limitation, "payment_required", cJSON_False);
|
||||
cJSON_AddBoolToObject(limitation, "restricted_writes", cJSON_False);
|
||||
cJSON_AddBoolToObject(limitation, "restricted_writes", restricted_writes ? cJSON_True : cJSON_False);
|
||||
cJSON_AddNumberToObject(limitation, "created_at_lower_limit", 0);
|
||||
cJSON_AddNumberToObject(limitation, "created_at_upper_limit", 2147483647);
|
||||
cJSON_AddNumberToObject(limitation, "default_limit", default_limit);
|
||||
cJSON_AddItemToObject(info, "limitation", limitation);
|
||||
}
|
||||
|
||||
// Add Web of Trust information when enabled
|
||||
// This informs clients that the relay operates on a trust network
|
||||
if (wot_enabled > 0) {
|
||||
cJSON* wot_info = cJSON_CreateObject();
|
||||
if (wot_info) {
|
||||
cJSON_AddNumberToObject(wot_info, "level", wot_enabled);
|
||||
const char* level_desc = (wot_enabled == 1)
|
||||
? "write-only: only followed pubkeys can publish events"
|
||||
: "full: only followed pubkeys can publish and subscribe";
|
||||
cJSON_AddStringToObject(wot_info, "description", level_desc);
|
||||
cJSON_AddStringToObject(wot_info, "policy",
|
||||
"Events from pubkeys not in the relay operator's Web of Trust are rejected.");
|
||||
cJSON_AddItemToObject(info, "web_of_trust", wot_info);
|
||||
}
|
||||
}
|
||||
|
||||
// Add retention policies (empty array for now)
|
||||
cJSON* retention = cJSON_CreateArray();
|
||||
if (retention) {
|
||||
|
||||
@@ -126,6 +126,9 @@ void handle_nip42_auth_signed_event(struct lws* wsi, struct per_session_data* ps
|
||||
pss->challenge_expires = 0;
|
||||
pss->auth_challenge_sent = 0;
|
||||
pthread_mutex_unlock(&pss->session_lock);
|
||||
|
||||
// Cancel the auth timeout — client has authenticated, keep connection open
|
||||
lws_set_timeout(wsi, NO_PENDING_TIMEOUT, 0);
|
||||
|
||||
send_notice_message(wsi, pss, "NIP-42 authentication successful");
|
||||
} else {
|
||||
|
||||
+10
-8
@@ -876,7 +876,6 @@ int broadcast_event_to_subscriptions(cJSON* event) {
|
||||
// Serialize event once per subscription using pre-serialized event_json if available,
|
||||
// otherwise fall back to cJSON serialization.
|
||||
// Format: ["EVENT","<sub_id>",<event_json>]
|
||||
cJSON* event_id_obj = cJSON_GetObjectItemCaseSensitive(event, "id");
|
||||
const char* event_json_str = NULL;
|
||||
char* event_json_allocated = NULL;
|
||||
|
||||
@@ -895,16 +894,19 @@ int broadcast_event_to_subscriptions(cJSON* event) {
|
||||
size_t event_json_len = strlen(event_json_str);
|
||||
// ["EVENT","<sub_id>",<event_json>]
|
||||
size_t msg_len = 10 + sub_id_len + 3 + event_json_len + 1;
|
||||
char* msg_str = malloc(msg_len + 1);
|
||||
if (msg_str) {
|
||||
snprintf(msg_str, msg_len + 1, "[\"EVENT\",\"%s\",%s]", current_temp->id, event_json_str);
|
||||
size_t actual_len = strlen(msg_str);
|
||||
|
||||
// Zero-copy: allocate with LWS_PRE prefix, write directly, transfer ownership to queue
|
||||
unsigned char* buf = malloc(LWS_PRE + msg_len + 1);
|
||||
if (buf) {
|
||||
char* msg_ptr = (char*)(buf + LWS_PRE);
|
||||
snprintf(msg_ptr, msg_len + 1, "[\"EVENT\",\"%s\",%s]", current_temp->id, event_json_str);
|
||||
size_t actual_len = strlen(msg_ptr);
|
||||
|
||||
DEBUG_TRACE("WS_FRAME_SEND: type=EVENT sub=%s len=%zu", current_temp->id, actual_len);
|
||||
|
||||
// Queue message for proper libwebsockets pattern
|
||||
struct per_session_data* pss = (struct per_session_data*)lws_wsi_user(current_temp->wsi);
|
||||
if (queue_message(current_temp->wsi, pss, msg_str, actual_len, LWS_WRITE_TEXT) == 0) {
|
||||
// queue_message_take_ownership takes buf ownership — no memcpy, no free needed here
|
||||
if (queue_message_take_ownership(current_temp->wsi, pss, buf, actual_len, LWS_WRITE_TEXT) == 0) {
|
||||
broadcasts++;
|
||||
|
||||
// Update events sent counter for this subscription
|
||||
@@ -922,8 +924,8 @@ int broadcast_event_to_subscriptions(cJSON* event) {
|
||||
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
||||
} else {
|
||||
DEBUG_ERROR("Failed to queue EVENT message for sub=%s", current_temp->id);
|
||||
// buf already freed by queue_message_take_ownership on failure
|
||||
}
|
||||
free(msg_str);
|
||||
}
|
||||
}
|
||||
if (event_json_allocated) {
|
||||
|
||||
@@ -64,6 +64,7 @@ int remove_subscription_from_manager(const char* sub_id, struct lws* wsi);
|
||||
// Forward declarations for event handling
|
||||
int handle_event_message(cJSON* event, char* error_message, size_t error_size);
|
||||
int nostr_validate_unified_request(const char* json_string, size_t json_length);
|
||||
int event_id_exists_in_db(const char* event_id);
|
||||
|
||||
// Forward declarations for admin event processing
|
||||
int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size, struct lws* wsi);
|
||||
@@ -194,6 +195,66 @@ int queue_message(struct lws* wsi, struct per_session_data* pss, const char* mes
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zero-copy variant of queue_message. The caller allocates a buffer of
|
||||
* (LWS_PRE + length) bytes, writes the message at (buf + LWS_PRE), then
|
||||
* passes ownership to the queue. The queue will free buf when done.
|
||||
* No memcpy is performed — eliminates one copy per queued message.
|
||||
*
|
||||
* @param wsi WebSocket instance
|
||||
* @param pss Per-session data containing message queue
|
||||
* @param buf Pre-allocated buffer of size (LWS_PRE + length); ownership transferred
|
||||
* @param length Length of message (NOT including LWS_PRE)
|
||||
* @param type LWS_WRITE_* type
|
||||
* @return 0 on success, -1 on error (buf is freed on error)
|
||||
*/
|
||||
int queue_message_take_ownership(struct lws* wsi, struct per_session_data* pss, unsigned char* buf, size_t length, enum lws_write_protocol type) {
|
||||
if (!wsi || !pss || !buf || length == 0) {
|
||||
DEBUG_ERROR("queue_message_take_ownership: invalid parameters");
|
||||
free(buf);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Drop message if queue is full
|
||||
if (pss->message_queue_count >= MAX_MESSAGE_QUEUE_SIZE) {
|
||||
DEBUG_WARN("queue_message_take_ownership: queue full (%d), dropping message",
|
||||
pss->message_queue_count);
|
||||
free(buf);
|
||||
return -1;
|
||||
}
|
||||
|
||||
struct message_queue_node* node = malloc(sizeof(struct message_queue_node));
|
||||
if (!node) {
|
||||
DEBUG_ERROR("queue_message_take_ownership: failed to allocate queue node");
|
||||
free(buf);
|
||||
return -1;
|
||||
}
|
||||
|
||||
node->data = buf; // buf already has LWS_PRE prefix — no copy needed
|
||||
node->length = length;
|
||||
node->type = type;
|
||||
node->next = NULL;
|
||||
|
||||
pthread_mutex_lock(&pss->session_lock);
|
||||
if (!pss->message_queue_head) {
|
||||
pss->message_queue_head = node;
|
||||
pss->message_queue_tail = node;
|
||||
} else {
|
||||
pss->message_queue_tail->next = node;
|
||||
pss->message_queue_tail = node;
|
||||
}
|
||||
pss->message_queue_count++;
|
||||
pthread_mutex_unlock(&pss->session_lock);
|
||||
|
||||
if (!pss->writeable_requested) {
|
||||
pss->writeable_requested = 1;
|
||||
lws_callback_on_writable(wsi);
|
||||
}
|
||||
|
||||
DEBUG_TRACE("Queued message (zero-copy): len=%zu, queue_count=%d", length, pss->message_queue_count);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process message queue when the socket becomes writeable.
|
||||
* This function is called from LWS_CALLBACK_SERVER_WRITEABLE.
|
||||
@@ -484,6 +545,20 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
memset(pss->active_challenge, 0, sizeof(pss->active_challenge));
|
||||
pss->challenge_created = 0;
|
||||
pss->challenge_expires = 0;
|
||||
|
||||
// Set libwebsockets auth timeout: if NIP-42 auth is required and the client
|
||||
// doesn't authenticate within nip42_auth_timeout_sec seconds, lws will close
|
||||
// the connection automatically — even if the client never sends a message.
|
||||
// This prevents idle unauthenticated connections from accumulating.
|
||||
if (pss->nip42_auth_required_events || pss->nip42_auth_required_subscriptions) {
|
||||
int auth_timeout = get_config_int("nip42_auth_timeout_sec", 10);
|
||||
if (auth_timeout > 0) {
|
||||
lws_set_timeout(wsi, PENDING_TIMEOUT_AWAITING_PING, auth_timeout);
|
||||
DEBUG_TRACE("Auth timeout set: %d seconds for unauthenticated connection from %s",
|
||||
auth_timeout, pss->client_ip);
|
||||
}
|
||||
}
|
||||
|
||||
DEBUG_TRACE("WebSocket connection initialization complete");
|
||||
break;
|
||||
|
||||
@@ -609,6 +684,27 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Early duplicate check: skip expensive crypto for events already in DB.
|
||||
// The event id is a 64-char hex string — look it up via primary key index (~10μs).
|
||||
// This must happen AFTER we have the id string but BEFORE signature verification.
|
||||
cJSON* id_obj_dup = cJSON_GetObjectItemCaseSensitive(event, "id");
|
||||
if (id_obj_dup && cJSON_IsString(id_obj_dup)) {
|
||||
const char* event_id_str = cJSON_GetStringValue(id_obj_dup);
|
||||
if (event_id_str && event_id_exists_in_db(event_id_str)) {
|
||||
// Already have this event — send OK true and skip crypto
|
||||
DEBUG_TRACE("Duplicate event %s — skipping signature verification", event_id_str);
|
||||
char ok_msg[128];
|
||||
snprintf(ok_msg, sizeof(ok_msg),
|
||||
"[\"OK\",\"%s\",true,\"duplicate: already have this event\"]",
|
||||
event_id_str);
|
||||
size_t ok_len = strlen(ok_msg);
|
||||
queue_message(wsi, pss, ok_msg, ok_len, LWS_WRITE_TEXT);
|
||||
free(event_json_str);
|
||||
cJSON_Delete(json);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Call unified validator with JSON string
|
||||
size_t event_json_len = strlen(event_json_str);
|
||||
int validation_result = nostr_validate_unified_request(event_json_str, event_json_len);
|
||||
@@ -929,6 +1025,21 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
} else {
|
||||
send_notice_message(wsi, pss, "NIP-42 authentication required for subscriptions");
|
||||
DEBUG_WARN("REQ rejected: NIP-42 authentication required");
|
||||
|
||||
// Auth timeout: close connection if challenge was sent but client
|
||||
// hasn't authenticated within nip42_auth_timeout_sec seconds
|
||||
int auth_timeout = get_config_int("nip42_auth_timeout_sec", 10);
|
||||
if (auth_timeout > 0 && pss->connection_established > 0) {
|
||||
time_t connection_age = time(NULL) - pss->connection_established;
|
||||
if (connection_age >= auth_timeout) {
|
||||
DEBUG_LOG("Closing unauthenticated connection from %s after %ld seconds (timeout=%d)",
|
||||
pss->client_ip, connection_age, auth_timeout);
|
||||
lws_close_reason(wsi, LWS_CLOSE_STATUS_POLICY_VIOLATION,
|
||||
(unsigned char*)"Authentication timeout", 22);
|
||||
cJSON_Delete(json);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
cJSON_Delete(json);
|
||||
// Note: complete_message points to reassembly_buffer, which is managed separately
|
||||
@@ -1344,6 +1455,25 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Early duplicate check: skip expensive crypto for events already in DB.
|
||||
cJSON* id_obj_dup2 = cJSON_GetObjectItemCaseSensitive(event, "id");
|
||||
if (id_obj_dup2 && cJSON_IsString(id_obj_dup2)) {
|
||||
const char* event_id_str2 = cJSON_GetStringValue(id_obj_dup2);
|
||||
if (event_id_str2 && event_id_exists_in_db(event_id_str2)) {
|
||||
DEBUG_TRACE("Duplicate event %s — skipping signature verification", event_id_str2);
|
||||
char ok_msg2[128];
|
||||
snprintf(ok_msg2, sizeof(ok_msg2),
|
||||
"[\"OK\",\"%s\",true,\"duplicate: already have this event\"]",
|
||||
event_id_str2);
|
||||
size_t ok_len2 = strlen(ok_msg2);
|
||||
queue_message(wsi, pss, ok_msg2, ok_len2, LWS_WRITE_TEXT);
|
||||
free(event_json_str);
|
||||
cJSON_Delete(json);
|
||||
free(message);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Call unified validator with JSON string
|
||||
size_t event_json_len = strlen(event_json_str);
|
||||
int validation_result = nostr_validate_unified_request(event_json_str, event_json_len);
|
||||
@@ -1667,6 +1797,22 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
} else {
|
||||
send_notice_message(wsi, pss, "NIP-42 authentication required for subscriptions");
|
||||
DEBUG_WARN("REQ rejected: NIP-42 authentication required");
|
||||
|
||||
// Auth timeout: close connection if challenge was sent but client
|
||||
// hasn't authenticated within nip42_auth_timeout_sec seconds
|
||||
int auth_timeout = get_config_int("nip42_auth_timeout_sec", 10);
|
||||
if (auth_timeout > 0 && pss->connection_established > 0) {
|
||||
time_t connection_age = time(NULL) - pss->connection_established;
|
||||
if (connection_age >= auth_timeout) {
|
||||
DEBUG_LOG("Closing unauthenticated connection from %s after %ld seconds (timeout=%d)",
|
||||
pss->client_ip, connection_age, auth_timeout);
|
||||
lws_close_reason(wsi, LWS_CLOSE_STATUS_POLICY_VIOLATION,
|
||||
(unsigned char*)"Authentication timeout", 22);
|
||||
cJSON_Delete(json);
|
||||
free(message);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
cJSON_Delete(json);
|
||||
free(message);
|
||||
|
||||
@@ -107,6 +107,10 @@ int start_websocket_relay(int port_override, int strict_port);
|
||||
int queue_message(struct lws* wsi, struct per_session_data* pss, const char* message, size_t length, enum lws_write_protocol type);
|
||||
int process_message_queue(struct lws* wsi, struct per_session_data* pss);
|
||||
|
||||
// Zero-copy variant: caller allocates (LWS_PRE + length) bytes, writes message at buf+LWS_PRE,
|
||||
// then passes ownership to the queue. The queue will free buf when done. No memcpy performed.
|
||||
int queue_message_take_ownership(struct lws* wsi, struct per_session_data* pss, unsigned char* buf, size_t length, enum lws_write_protocol type);
|
||||
|
||||
// Auth rules checking function from request_validator.c
|
||||
int check_database_auth_rules(const char *pubkey, const char *operation, const char *resource_hash);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user