Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b20452fab | ||
|
|
10c19bc243 | ||
|
|
3d7aa2196f | ||
|
|
a416c3f275 | ||
|
|
bba9baabc3 |
+39
-28
@@ -1328,12 +1328,24 @@ async function subscribeToConfiguration() {
|
||||
},
|
||||
onclose(reason) {
|
||||
console.log('Subscription closed:', reason);
|
||||
// Reset subscription state to allow re-subscription
|
||||
isSubscribed = false;
|
||||
isSubscribing = false;
|
||||
isRelayConnected = false;
|
||||
updateConfigStatus(false);
|
||||
log('WebSocket connection closed - subscription state reset', 'WARNING');
|
||||
// Only reset state if the admin relay itself closed (not external relays timing out)
|
||||
// reason is an array of close reasons, one per relay in the subscription
|
||||
const adminUrl = relayConnectionUrl.value.trim();
|
||||
const reasons = Array.isArray(reason) ? reason : [reason];
|
||||
// Check if the admin relay's connection closed (not just external relays timing out)
|
||||
// If all reasons are 'connection timed out', these are external relays, not our relay
|
||||
const allTimeout = reasons.every(r => r === 'connection timed out');
|
||||
if (!allTimeout) {
|
||||
// Admin relay actually closed — reset state
|
||||
isSubscribed = false;
|
||||
isSubscribing = false;
|
||||
isRelayConnected = false;
|
||||
updateConfigStatus(false);
|
||||
log('WebSocket connection closed - subscription state reset', 'WARNING');
|
||||
} else {
|
||||
// External relays timed out — keep admin relay state intact
|
||||
log('External relay connections timed out (admin relay still connected)', 'INFO');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6193,27 +6205,13 @@ async function loadIpBans() {
|
||||
|
||||
// Execute SQL query and handle IP bans response
|
||||
async function executeSqlQueryRaw(query, queryId) {
|
||||
if (!pool || pool.length === 0) {
|
||||
if (!relayPool) {
|
||||
log('Not connected to relay', 'ERROR');
|
||||
return;
|
||||
}
|
||||
|
||||
const relay = pool[0];
|
||||
if (!relay) {
|
||||
log('No relay connection available', 'ERROR');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a kind 23456 event with SQL query
|
||||
const event = {
|
||||
kind: 23456,
|
||||
content: query,
|
||||
tags: [['t', 'sql_query'], ['id', queryId]],
|
||||
created_at: Math.floor(Date.now() / 1000)
|
||||
};
|
||||
|
||||
try {
|
||||
await relay.publish(event);
|
||||
await sendAdminCommand(['sql_query', query, queryId]);
|
||||
log('Loading IP bans...', 'INFO');
|
||||
} catch (error) {
|
||||
log('Failed to load IP bans: ' + error.message, 'ERROR');
|
||||
@@ -6222,7 +6220,20 @@ async function executeSqlQueryRaw(query, queryId) {
|
||||
|
||||
// Handle IP bans SQL response
|
||||
function handleIpBansResponse(responseData) {
|
||||
if (!responseData.results || responseData.results.length === 0) {
|
||||
// Convert rows+columns format to array of objects
|
||||
let results = [];
|
||||
if (responseData.rows && responseData.columns) {
|
||||
const cols = responseData.columns;
|
||||
results = responseData.rows.map(row => {
|
||||
const obj = {};
|
||||
cols.forEach((col, i) => { obj[col] = row[i]; });
|
||||
return obj;
|
||||
});
|
||||
} else if (responseData.results) {
|
||||
results = responseData.results;
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
document.getElementById('ip-bans-tbody').innerHTML = '<tr><td colspan="7" style="text-align: center;">No IP bans found</td></tr>';
|
||||
document.getElementById('ip-bans-total').textContent = '0';
|
||||
document.getElementById('ip-bans-active').textContent = '0';
|
||||
@@ -6231,20 +6242,20 @@ function handleIpBansResponse(responseData) {
|
||||
}
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
let totalIPs = responseData.results.length;
|
||||
let totalIPs = results.length;
|
||||
let currentlyBanned = 0;
|
||||
let totalBans = 0;
|
||||
|
||||
// Filter results based on current filter
|
||||
let filteredResults = responseData.results;
|
||||
let filteredResults = results;
|
||||
if (ipBansFilter === 'banned') {
|
||||
filteredResults = responseData.results.filter(row => row.banned_until > now);
|
||||
filteredResults = results.filter(row => row.banned_until > now);
|
||||
} else if (ipBansFilter === 'expired') {
|
||||
filteredResults = responseData.results.filter(row => row.banned_until <= now && row.banned_until > 0);
|
||||
filteredResults = results.filter(row => row.banned_until <= now && row.banned_until > 0);
|
||||
}
|
||||
|
||||
// Calculate stats
|
||||
responseData.results.forEach(row => {
|
||||
results.forEach(row => {
|
||||
totalBans += parseInt(row.ban_count || 0);
|
||||
if (row.banned_until > now) {
|
||||
currentlyBanned++;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -250,9 +250,34 @@ void ip_ban_save_to_db(sqlite3* db) {
|
||||
DEBUG_TRACE("IP ban table saved: %d entries written to DB", saved);
|
||||
}
|
||||
|
||||
// Check if an IP is in the idle_ban_whitelist config (comma-separated list)
|
||||
static int ip_is_whitelisted(const char* ip) {
|
||||
const char* whitelist = get_config_value("idle_ban_whitelist");
|
||||
if (!whitelist || whitelist[0] == '\0') return 0;
|
||||
|
||||
// Make a mutable copy to tokenize
|
||||
char buf[1024];
|
||||
strncpy(buf, whitelist, sizeof(buf) - 1);
|
||||
buf[sizeof(buf) - 1] = '\0';
|
||||
|
||||
char* token = strtok(buf, ",");
|
||||
while (token) {
|
||||
// Trim leading/trailing spaces
|
||||
while (*token == ' ') token++;
|
||||
char* end = token + strlen(token) - 1;
|
||||
while (end > token && *end == ' ') { *end = '\0'; end--; }
|
||||
if (strcmp(token, ip) == 0) return 1;
|
||||
token = strtok(NULL, ",");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ip_ban_is_banned(const char* ip) {
|
||||
if (!ip || !g_initialized) return 0;
|
||||
|
||||
// Whitelisted IPs are never banned
|
||||
if (ip_is_whitelisted(ip)) return 0;
|
||||
|
||||
pthread_mutex_lock(&g_ban_mutex);
|
||||
int idx = find_slot(ip);
|
||||
if (idx < 0 || g_ban_table[idx].state == IP_BAN_EMPTY) {
|
||||
@@ -359,6 +384,7 @@ void ip_ban_record_failure(const char* ip) {
|
||||
void ip_ban_record_idle_failure(const char* ip) {
|
||||
if (!ip || !g_initialized) return;
|
||||
if (!get_config_bool("idle_ban_enabled", 1)) return;
|
||||
if (ip_is_whitelisted(ip)) return; // Never record idle failures for whitelisted IPs
|
||||
|
||||
int threshold = get_config_int("idle_ban_threshold", 1);
|
||||
int window_sec = get_config_int("idle_ban_window_sec", 30);
|
||||
|
||||
+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 34
|
||||
#define CRELAY_VERSION "v1.2.34"
|
||||
#define CRELAY_VERSION_PATCH 39
|
||||
#define CRELAY_VERSION "v1.2.39"
|
||||
|
||||
// Relay metadata (authoritative source for NIP-11 information)
|
||||
#define RELAY_NAME "C-Relay"
|
||||
|
||||
+5
-1
@@ -634,6 +634,9 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
pss->challenge_created = 0;
|
||||
pss->challenge_expires = 0;
|
||||
|
||||
// Mark as WebSocket connection (not HTTP)
|
||||
pss->is_websocket = 1;
|
||||
|
||||
// Register in global connection list for idle tracking
|
||||
connection_list_add(wsi, pss);
|
||||
|
||||
@@ -2260,7 +2263,8 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
// This catches:
|
||||
// 1. Idle connections that timed out (never sent REQ/EVENT/AUTH)
|
||||
// 2. Early disconnects (client closed before sending REQ/EVENT/AUTH)
|
||||
if (!pss->session_active && strlen(pss->client_ip) > 0) {
|
||||
// Only record for WebSocket connections, not HTTP requests (NIP-11, embedded files)
|
||||
if (!pss->session_active && pss->is_websocket && strlen(pss->client_ip) > 0) {
|
||||
// Use separate idle failure recording (has its own threshold/duration)
|
||||
ip_ban_record_idle_failure(pss->client_ip);
|
||||
DEBUG_LOG("Recording idle/early-disconnect failure for IP %s (connected %ld seconds)",
|
||||
|
||||
@@ -93,6 +93,7 @@ struct per_session_data {
|
||||
// Session activity tracking for idle connection banning
|
||||
int session_active; // 1 if client sent REQ or EVENT, 0 otherwise
|
||||
int idle_timeout_sec; // Timeout value for this session (copied from config)
|
||||
int is_websocket; // 1 if this is a WebSocket connection, 0 for HTTP
|
||||
};
|
||||
|
||||
// NIP-11 HTTP session data structure for managing buffer lifetime
|
||||
|
||||
Reference in New Issue
Block a user