@@ -616,13 +616,12 @@
@@ -975,6 +974,7 @@
// Step 4: Update UI
updateRelayConnectionStatus('connected');
+ updateAdminSectionsVisibility();
// Step 5: Relay URL updated
@@ -1034,6 +1034,7 @@
// Update UI
updateRelayConnectionStatus('disconnected');
hideRelayInfo();
+ updateAdminSectionsVisibility();
log('Disconnected from relay', 'INFO');
@@ -1280,7 +1281,7 @@
loginSection.classList.remove('hidden');
updateConfigStatus(false);
updateLoginLogoutButton();
- hideAuthRulesSection();
+ updateAdminSectionsVisibility();
console.log('Logout event handled successfully');
}
@@ -1298,11 +1299,22 @@
}
}
+ // Update visibility of admin sections based on login and relay connection status
+ function updateAdminSectionsVisibility() {
+ const divConfig = document.getElementById('div_config');
+ const authRulesSection = document.getElementById('authRulesSection');
+ const shouldShow = isLoggedIn && isRelayConnected;
+
+ if (divConfig) divConfig.style.display = shouldShow ? 'block' : 'none';
+ if (authRulesSection) authRulesSection.style.display = shouldShow ? 'block' : 'none';
+ }
+
// Show main interface after login
function showMainInterface() {
loginSection.classList.add('hidden');
// mainInterface.classList.remove('hidden');
updateLoginLogoutButton();
+ updateAdminSectionsVisibility();
}
// Load user profile using nostr-tools pool
diff --git a/relay.pid b/relay.pid
index 322df05..e89f8a7 100644
--- a/relay.pid
+++ b/relay.pid
@@ -1 +1 @@
-1412551
+1487904
diff --git a/src/main.c b/src/main.c
index 5430435..d06b8b1 100644
--- a/src/main.c
+++ b/src/main.c
@@ -21,6 +21,8 @@
#include "../nostr_core_lib/nostr_core/nip013.h" // NIP-13: Proof of Work
#include "config.h" // Configuration management system
#include "sql_schema.h" // Embedded database schema
+#include "websockets.h" // WebSocket protocol implementation
+#include "subscriptions.h" // Subscription management system
// Forward declarations for unified request validator
int nostr_validate_unified_request(const char* json_string, size_t json_length);
@@ -42,8 +44,8 @@ int nostr_nip42_verify_auth_event(cJSON *event, const char *challenge_id,
// Global state
sqlite3* g_db = NULL; // Non-static so config.c can access it
-static int g_server_running = 1;
-static struct lws_context *ws_context = NULL;
+int g_server_running = 1; // Non-static so websockets.c can access it
+struct lws_context *ws_context = NULL; // Non-static so websockets.c can access it
// NIP-11 relay information structure
struct relay_info {
@@ -71,45 +73,12 @@ struct relay_info {
// 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 {
- int enabled; // 0 = disabled, 1 = enabled
- int min_pow_difficulty; // Minimum required difficulty (0 = no requirement)
- int validation_flags; // Bitflags for validation options
- int require_nonce_tag; // 1 = require nonce tag presence
- int reject_lower_targets; // 1 = reject if committed < actual difficulty
- int strict_format; // 1 = enforce strict nonce tag format
- int anti_spam_mode; // 1 = full anti-spam validation
-};
-// Global PoW configuration instance
-struct pow_config g_pow_config = {
- .enabled = 1, // Enable PoW validation by default
- .min_pow_difficulty = 0, // No minimum difficulty by default
- .validation_flags = NOSTR_POW_VALIDATE_BASIC,
- .require_nonce_tag = 0, // Don't require nonce tags by default
- .reject_lower_targets = 0, // Allow lower committed targets by default
- .strict_format = 0, // Relaxed format validation by default
- .anti_spam_mode = 0 // Basic validation by default
-};
+// NIP-40 Expiration configuration (now in nip040.c)
+extern struct expiration_config g_expiration_config;
-// NIP-40 Expiration configuration structure
-struct expiration_config {
- int enabled; // 0 = disabled, 1 = enabled
- int strict_mode; // 1 = reject expired events on submission
- int filter_responses; // 1 = filter expired events from responses
- int delete_expired; // 1 = delete expired events from DB (future feature)
- long grace_period; // Grace period in seconds for clock skew
-};
-
-// Global expiration configuration instance
-struct expiration_config g_expiration_config = {
- .enabled = 1, // Enable expiration handling by default
- .strict_mode = 1, // Reject expired events on submission by default
- .filter_responses = 1, // Filter expired events from responses by default
- .delete_expired = 0, // Don't delete by default (keep for audit)
- .grace_period = 1 // 1 second grace period for testing (was 300)
-};
+// Global subscription manager instance (defined in websockets.c)
+extern subscription_manager_t g_subscription_manager;
/////////////////////////////////////////////////////////////////////////////////////////
@@ -118,86 +87,10 @@ struct expiration_config g_expiration_config = {
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
-// Forward declarations
-typedef struct subscription_filter subscription_filter_t;
-typedef struct subscription subscription_t;
-typedef struct subscription_manager subscription_manager_t;
-// Subscription filter structure
-struct subscription_filter {
- // Filter criteria (all optional)
- cJSON* kinds; // Array of event kinds [1,2,3]
- cJSON* authors; // Array of author pubkeys
- cJSON* ids; // Array of event IDs
- long since; // Unix timestamp (0 = not set)
- long until; // Unix timestamp (0 = not set)
- int limit; // Result limit (0 = no limit)
- cJSON* tag_filters; // Object with tag filters: {"#e": ["id1"], "#p": ["pubkey1"]}
-
- // Linked list for multiple filters per subscription
- struct subscription_filter* next;
-};
-// Active subscription structure
-struct subscription {
- char id[SUBSCRIPTION_ID_MAX_LENGTH]; // Subscription ID
- struct lws* wsi; // WebSocket connection handle
- subscription_filter_t* filters; // Linked list of filters (OR'd together)
- time_t created_at; // When subscription was created
- int events_sent; // Counter for sent events
- int active; // 1 = active, 0 = closed
-
- // Client info for logging
- char client_ip[CLIENT_IP_MAX_LENGTH]; // Client IP address
-
- // Linked list pointers
- struct subscription* next; // Next subscription globally
- struct subscription* session_next; // Next subscription for this session
-};
-// Enhanced per-session data with subscription management and NIP-42 authentication
-struct per_session_data {
- int authenticated;
- subscription_t* subscriptions; // Head of this session's subscription list
- pthread_mutex_t session_lock; // Per-session thread safety
- char client_ip[CLIENT_IP_MAX_LENGTH]; // Client IP for logging
- int subscription_count; // Number of subscriptions for this session
-
- // NIP-42 Authentication State
- char authenticated_pubkey[65]; // Authenticated public key (64 hex + null)
- char active_challenge[65]; // Current challenge for this session (64 hex + null)
- time_t challenge_created; // When challenge was created
- time_t challenge_expires; // Challenge expiration time
- int nip42_auth_required_events; // Whether NIP-42 auth is required for EVENT submission
- int nip42_auth_required_subscriptions; // Whether NIP-42 auth is required for REQ operations
- int auth_challenge_sent; // Whether challenge has been sent (0/1)
-};
-// Global subscription manager
-struct subscription_manager {
- subscription_t* active_subscriptions; // Head of global subscription list
- pthread_mutex_t subscriptions_lock; // Global thread safety
- int total_subscriptions; // Current count
-
- // Configuration
- int max_subscriptions_per_client; // Default: 20
- int max_total_subscriptions; // Default: 5000
-
- // Statistics
- uint64_t total_created; // Lifetime subscription count
- uint64_t total_events_broadcast; // Lifetime event broadcast count
-};
-
-// Global subscription manager instance
-static subscription_manager_t g_subscription_manager = {
- .active_subscriptions = NULL,
- .subscriptions_lock = PTHREAD_MUTEX_INITIALIZER,
- .total_subscriptions = 0,
- .max_subscriptions_per_client = MAX_SUBSCRIPTIONS_PER_CLIENT, // Will be updated from config
- .max_total_subscriptions = MAX_TOTAL_SUBSCRIPTIONS, // Will be updated from config
- .total_created = 0,
- .total_events_broadcast = 0
-};
// Forward declarations for logging functions
void log_info(const char* message);
@@ -253,708 +146,21 @@ void cleanup_relay_info();
cJSON* generate_relay_info_json();
int handle_nip11_http_request(struct lws* wsi, const char* accept_header);
-// Forward declarations for NIP-13 PoW validation
+// Forward declaration for WebSocket relay server
+int start_websocket_relay(int port_override, int strict_port);
+
+
+// Forward declarations for NIP-13 PoW handling (now in nip013.c)
void init_pow_config();
int validate_event_pow(cJSON* event, char* error_message, size_t error_size);
-// Forward declarations for NIP-40 expiration handling
+// Forward declarations for NIP-40 expiration handling (now in nip040.c)
void init_expiration_config();
long extract_expiration_timestamp(cJSON* tags);
int is_event_expired(cJSON* event, time_t current_time);
int validate_event_expiration(cJSON* event, char* error_message, size_t error_size);
-/////////////////////////////////////////////////////////////////////////////////////////
-/////////////////////////////////////////////////////////////////////////////////////////
-// PERSISTENT SUBSCRIPTIONS SYSTEM
-/////////////////////////////////////////////////////////////////////////////////////////
-/////////////////////////////////////////////////////////////////////////////////////////
-
-// Create a subscription filter from cJSON filter object
-subscription_filter_t* create_subscription_filter(cJSON* filter_json) {
- if (!filter_json || !cJSON_IsObject(filter_json)) {
- return NULL;
- }
-
- subscription_filter_t* filter = calloc(1, sizeof(subscription_filter_t));
- if (!filter) {
- return NULL;
- }
-
- // Copy filter criteria
- cJSON* kinds = cJSON_GetObjectItem(filter_json, "kinds");
- if (kinds && cJSON_IsArray(kinds)) {
- filter->kinds = cJSON_Duplicate(kinds, 1);
- }
-
- cJSON* authors = cJSON_GetObjectItem(filter_json, "authors");
- if (authors && cJSON_IsArray(authors)) {
- filter->authors = cJSON_Duplicate(authors, 1);
- }
-
- cJSON* ids = cJSON_GetObjectItem(filter_json, "ids");
- if (ids && cJSON_IsArray(ids)) {
- filter->ids = cJSON_Duplicate(ids, 1);
- }
-
- cJSON* since = cJSON_GetObjectItem(filter_json, "since");
- if (since && cJSON_IsNumber(since)) {
- filter->since = (long)cJSON_GetNumberValue(since);
- }
-
- cJSON* until = cJSON_GetObjectItem(filter_json, "until");
- if (until && cJSON_IsNumber(until)) {
- filter->until = (long)cJSON_GetNumberValue(until);
- }
-
- cJSON* limit = cJSON_GetObjectItem(filter_json, "limit");
- if (limit && cJSON_IsNumber(limit)) {
- filter->limit = (int)cJSON_GetNumberValue(limit);
- }
-
- // Handle tag filters (e.g., {"#e": ["id1"], "#p": ["pubkey1"]})
- cJSON* item = NULL;
- cJSON_ArrayForEach(item, filter_json) {
- if (item->string && strlen(item->string) >= 2 && item->string[0] == '#') {
- if (!filter->tag_filters) {
- filter->tag_filters = cJSON_CreateObject();
- }
- if (filter->tag_filters) {
- cJSON_AddItemToObject(filter->tag_filters, item->string, cJSON_Duplicate(item, 1));
- }
- }
- }
-
- return filter;
-}
-
-// Free a subscription filter
-void free_subscription_filter(subscription_filter_t* filter) {
- if (!filter) return;
-
- if (filter->kinds) cJSON_Delete(filter->kinds);
- if (filter->authors) cJSON_Delete(filter->authors);
- if (filter->ids) cJSON_Delete(filter->ids);
- if (filter->tag_filters) cJSON_Delete(filter->tag_filters);
-
- if (filter->next) {
- free_subscription_filter(filter->next);
- }
-
- free(filter);
-}
-
-// Create a new subscription
-subscription_t* create_subscription(const char* sub_id, struct lws* wsi, cJSON* filters_array, const char* client_ip) {
- if (!sub_id || !wsi || !filters_array) {
- return NULL;
- }
-
- subscription_t* sub = calloc(1, sizeof(subscription_t));
- if (!sub) {
- return NULL;
- }
-
- // Copy subscription ID (truncate if too long)
- strncpy(sub->id, sub_id, SUBSCRIPTION_ID_MAX_LENGTH - 1);
- sub->id[SUBSCRIPTION_ID_MAX_LENGTH - 1] = '\0';
-
- // Set WebSocket connection
- sub->wsi = wsi;
-
- // Set client IP
- if (client_ip) {
- strncpy(sub->client_ip, client_ip, CLIENT_IP_MAX_LENGTH - 1);
- sub->client_ip[CLIENT_IP_MAX_LENGTH - 1] = '\0';
- }
-
- // Set timestamps and state
- sub->created_at = time(NULL);
- sub->events_sent = 0;
- sub->active = 1;
-
- // Convert filters array to linked list
- subscription_filter_t* filter_tail = NULL;
- int filter_count = 0;
-
- if (cJSON_IsArray(filters_array)) {
- cJSON* filter_json = NULL;
- cJSON_ArrayForEach(filter_json, filters_array) {
- if (filter_count >= MAX_FILTERS_PER_SUBSCRIPTION) {
- log_warning("Maximum filters per subscription exceeded, ignoring excess filters");
- break;
- }
-
- subscription_filter_t* filter = create_subscription_filter(filter_json);
- if (filter) {
- if (!sub->filters) {
- sub->filters = filter;
- filter_tail = filter;
- } else {
- filter_tail->next = filter;
- filter_tail = filter;
- }
- filter_count++;
- }
- }
- }
-
- if (filter_count == 0) {
- log_error("No valid filters found for subscription");
- free(sub);
- return NULL;
- }
-
- return sub;
-}
-
-// Free a subscription
-void free_subscription(subscription_t* sub) {
- if (!sub) return;
-
- if (sub->filters) {
- free_subscription_filter(sub->filters);
- }
-
- free(sub);
-}
-
-// Add subscription to global manager (thread-safe)
-int add_subscription_to_manager(subscription_t* sub) {
- if (!sub) return -1;
-
- pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
-
- // Check global limits
- if (g_subscription_manager.total_subscriptions >= g_subscription_manager.max_total_subscriptions) {
- pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
- log_error("Maximum total subscriptions reached");
- return -1;
- }
-
- // Add to global list
- sub->next = g_subscription_manager.active_subscriptions;
- g_subscription_manager.active_subscriptions = sub;
- g_subscription_manager.total_subscriptions++;
- g_subscription_manager.total_created++;
-
- pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
-
- // Log subscription creation to database
- log_subscription_created(sub);
-
- char debug_msg[256];
- snprintf(debug_msg, sizeof(debug_msg), "Added subscription '%s' (total: %d)",
- sub->id, g_subscription_manager.total_subscriptions);
- log_info(debug_msg);
-
- return 0;
-}
-
-// Remove subscription from global manager (thread-safe)
-int remove_subscription_from_manager(const char* sub_id, struct lws* wsi) {
- if (!sub_id) return -1;
-
- pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
-
- subscription_t** current = &g_subscription_manager.active_subscriptions;
-
- while (*current) {
- subscription_t* sub = *current;
-
- // Match by ID and WebSocket connection
- if (strcmp(sub->id, sub_id) == 0 && (!wsi || sub->wsi == wsi)) {
- // Remove from list
- *current = sub->next;
- g_subscription_manager.total_subscriptions--;
-
- pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
-
- // Log subscription closure to database
- log_subscription_closed(sub_id, sub->client_ip, "closed");
-
- // Update events sent counter before freeing
- update_subscription_events_sent(sub_id, sub->events_sent);
-
- char debug_msg[256];
- snprintf(debug_msg, sizeof(debug_msg), "Removed subscription '%s' (total: %d)",
- sub_id, g_subscription_manager.total_subscriptions);
- log_info(debug_msg);
-
- free_subscription(sub);
- return 0;
- }
-
- current = &(sub->next);
- }
-
- pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
-
- char debug_msg[256];
- snprintf(debug_msg, sizeof(debug_msg), "Subscription '%s' not found for removal", sub_id);
- log_warning(debug_msg);
-
- return -1;
-}
-
-// Check if an event matches a subscription filter
-int event_matches_filter(cJSON* event, subscription_filter_t* filter) {
- if (!event || !filter) {
- return 0;
- }
-
- // Check kinds filter
- if (filter->kinds && cJSON_IsArray(filter->kinds)) {
- cJSON* event_kind = cJSON_GetObjectItem(event, "kind");
- if (!event_kind || !cJSON_IsNumber(event_kind)) {
- return 0;
- }
-
- int event_kind_val = (int)cJSON_GetNumberValue(event_kind);
- int kind_match = 0;
-
- cJSON* kind_item = NULL;
- cJSON_ArrayForEach(kind_item, filter->kinds) {
- if (cJSON_IsNumber(kind_item) && (int)cJSON_GetNumberValue(kind_item) == event_kind_val) {
- kind_match = 1;
- break;
- }
- }
-
- if (!kind_match) {
- return 0;
- }
- }
-
- // Check authors filter
- if (filter->authors && cJSON_IsArray(filter->authors)) {
- cJSON* event_pubkey = cJSON_GetObjectItem(event, "pubkey");
- if (!event_pubkey || !cJSON_IsString(event_pubkey)) {
- return 0;
- }
-
- const char* event_pubkey_str = cJSON_GetStringValue(event_pubkey);
- int author_match = 0;
-
- cJSON* author_item = NULL;
- cJSON_ArrayForEach(author_item, filter->authors) {
- if (cJSON_IsString(author_item)) {
- const char* author_str = cJSON_GetStringValue(author_item);
- // Support prefix matching (partial pubkeys)
- if (strncmp(event_pubkey_str, author_str, strlen(author_str)) == 0) {
- author_match = 1;
- break;
- }
- }
- }
-
- if (!author_match) {
- return 0;
- }
- }
-
- // Check IDs filter
- if (filter->ids && cJSON_IsArray(filter->ids)) {
- cJSON* event_id = cJSON_GetObjectItem(event, "id");
- if (!event_id || !cJSON_IsString(event_id)) {
- return 0;
- }
-
- const char* event_id_str = cJSON_GetStringValue(event_id);
- int id_match = 0;
-
- cJSON* id_item = NULL;
- cJSON_ArrayForEach(id_item, filter->ids) {
- if (cJSON_IsString(id_item)) {
- const char* id_str = cJSON_GetStringValue(id_item);
- // Support prefix matching (partial IDs)
- if (strncmp(event_id_str, id_str, strlen(id_str)) == 0) {
- id_match = 1;
- break;
- }
- }
- }
-
- if (!id_match) {
- return 0;
- }
- }
-
- // Check since filter
- if (filter->since > 0) {
- cJSON* event_created_at = cJSON_GetObjectItem(event, "created_at");
- if (!event_created_at || !cJSON_IsNumber(event_created_at)) {
- return 0;
- }
-
- long event_timestamp = (long)cJSON_GetNumberValue(event_created_at);
- if (event_timestamp < filter->since) {
- return 0;
- }
- }
-
- // Check until filter
- if (filter->until > 0) {
- cJSON* event_created_at = cJSON_GetObjectItem(event, "created_at");
- if (!event_created_at || !cJSON_IsNumber(event_created_at)) {
- return 0;
- }
-
- long event_timestamp = (long)cJSON_GetNumberValue(event_created_at);
- if (event_timestamp > filter->until) {
- return 0;
- }
- }
-
- // Check tag filters (e.g., #e, #p tags)
- if (filter->tag_filters && cJSON_IsObject(filter->tag_filters)) {
- cJSON* event_tags = cJSON_GetObjectItem(event, "tags");
- if (!event_tags || !cJSON_IsArray(event_tags)) {
- return 0; // Event has no tags but filter requires tags
- }
-
- // Check each tag filter
- cJSON* tag_filter = NULL;
- cJSON_ArrayForEach(tag_filter, filter->tag_filters) {
- if (!tag_filter->string || strlen(tag_filter->string) < 2 || tag_filter->string[0] != '#') {
- continue; // Invalid tag filter
- }
-
- const char* tag_name = tag_filter->string + 1; // Skip the '#'
-
- if (!cJSON_IsArray(tag_filter)) {
- continue; // Tag filter must be an array
- }
-
- int tag_match = 0;
-
- // Search through event tags for matching tag name and value
- cJSON* event_tag = NULL;
- cJSON_ArrayForEach(event_tag, event_tags) {
- if (!cJSON_IsArray(event_tag) || cJSON_GetArraySize(event_tag) < 2) {
- continue; // Invalid tag format
- }
-
- cJSON* event_tag_name = cJSON_GetArrayItem(event_tag, 0);
- cJSON* event_tag_value = cJSON_GetArrayItem(event_tag, 1);
-
- if (!cJSON_IsString(event_tag_name) || !cJSON_IsString(event_tag_value)) {
- continue;
- }
-
- // Check if tag name matches
- if (strcmp(cJSON_GetStringValue(event_tag_name), tag_name) == 0) {
- const char* event_tag_value_str = cJSON_GetStringValue(event_tag_value);
-
- // Check if any of the filter values match this tag value
- cJSON* filter_value = NULL;
- cJSON_ArrayForEach(filter_value, tag_filter) {
- if (cJSON_IsString(filter_value)) {
- const char* filter_value_str = cJSON_GetStringValue(filter_value);
- // Support prefix matching for tag values
- if (strncmp(event_tag_value_str, filter_value_str, strlen(filter_value_str)) == 0) {
- tag_match = 1;
- break;
- }
- }
- }
-
- if (tag_match) {
- break;
- }
- }
- }
-
- if (!tag_match) {
- return 0; // This tag filter didn't match, so the event doesn't match
- }
- }
- }
-
- return 1; // All filters passed
-}
-
-// Check if an event matches any filter in a subscription (filters are OR'd together)
-int event_matches_subscription(cJSON* event, subscription_t* subscription) {
- if (!event || !subscription || !subscription->filters) {
- return 0;
- }
-
- subscription_filter_t* filter = subscription->filters;
- while (filter) {
- if (event_matches_filter(event, filter)) {
- return 1; // Match found (OR logic)
- }
- filter = filter->next;
- }
-
- return 0; // No filters matched
-}
-
-// Broadcast event to all matching subscriptions (thread-safe)
-int broadcast_event_to_subscriptions(cJSON* event) {
- if (!event) {
- return 0;
- }
-
- // Check if event is expired and should not be broadcast (NIP-40)
- 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];
- cJSON* event_id_obj = cJSON_GetObjectItem(event, "id");
- const char* event_id = event_id_obj ? cJSON_GetStringValue(event_id_obj) : "unknown";
- snprintf(debug_msg, sizeof(debug_msg), "Skipping broadcast of expired event: %.16s", event_id);
- log_info(debug_msg);
- return 0; // Don't broadcast expired events
- }
- }
-
- int broadcasts = 0;
-
- pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
-
- subscription_t* sub = g_subscription_manager.active_subscriptions;
- while (sub) {
- if (sub->active && event_matches_subscription(event, sub)) {
- // Create EVENT message for this subscription
- cJSON* event_msg = cJSON_CreateArray();
- cJSON_AddItemToArray(event_msg, cJSON_CreateString("EVENT"));
- cJSON_AddItemToArray(event_msg, cJSON_CreateString(sub->id));
- cJSON_AddItemToArray(event_msg, cJSON_Duplicate(event, 1));
-
- 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);
-
- // Send to WebSocket connection
- int write_result = lws_write(sub->wsi, buf + LWS_PRE, msg_len, LWS_WRITE_TEXT);
- if (write_result >= 0) {
- sub->events_sent++;
- broadcasts++;
-
- // Log event broadcast to database (optional - can be disabled for performance)
- cJSON* event_id_obj = cJSON_GetObjectItem(event, "id");
- if (event_id_obj && cJSON_IsString(event_id_obj)) {
- log_event_broadcast(cJSON_GetStringValue(event_id_obj), sub->id, sub->client_ip);
- }
- }
-
- free(buf);
- }
- free(msg_str);
- }
-
- cJSON_Delete(event_msg);
- }
-
- sub = sub->next;
- }
-
- // Update global statistics
- g_subscription_manager.total_events_broadcast += broadcasts;
-
- pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
-
- if (broadcasts > 0) {
- char debug_msg[256];
- snprintf(debug_msg, sizeof(debug_msg), "Broadcasted event to %d subscriptions", broadcasts);
- log_info(debug_msg);
- }
-
- return broadcasts;
-}
-
-
-/////////////////////////////////////////////////////////////////////////////////////////
-/////////////////////////////////////////////////////////////////////////////////////////
-// SUBSCRIPTION DATABASE LOGGING
-/////////////////////////////////////////////////////////////////////////////////////////
-/////////////////////////////////////////////////////////////////////////////////////////
-
-// Log subscription creation to database
-void log_subscription_created(const subscription_t* sub) {
- if (!g_db || !sub) return;
-
- // Create filter JSON for logging
- char* filter_json = NULL;
- if (sub->filters) {
- cJSON* filters_array = cJSON_CreateArray();
- subscription_filter_t* filter = sub->filters;
-
- while (filter) {
- cJSON* filter_obj = cJSON_CreateObject();
-
- if (filter->kinds) {
- cJSON_AddItemToObject(filter_obj, "kinds", cJSON_Duplicate(filter->kinds, 1));
- }
- if (filter->authors) {
- cJSON_AddItemToObject(filter_obj, "authors", cJSON_Duplicate(filter->authors, 1));
- }
- if (filter->ids) {
- cJSON_AddItemToObject(filter_obj, "ids", cJSON_Duplicate(filter->ids, 1));
- }
- if (filter->since > 0) {
- cJSON_AddNumberToObject(filter_obj, "since", filter->since);
- }
- if (filter->until > 0) {
- cJSON_AddNumberToObject(filter_obj, "until", filter->until);
- }
- if (filter->limit > 0) {
- cJSON_AddNumberToObject(filter_obj, "limit", filter->limit);
- }
- if (filter->tag_filters) {
- cJSON* tags_obj = cJSON_Duplicate(filter->tag_filters, 1);
- cJSON* item = NULL;
- cJSON_ArrayForEach(item, tags_obj) {
- if (item->string) {
- cJSON_AddItemToObject(filter_obj, item->string, cJSON_Duplicate(item, 1));
- }
- }
- cJSON_Delete(tags_obj);
- }
-
- cJSON_AddItemToArray(filters_array, filter_obj);
- filter = filter->next;
- }
-
- filter_json = cJSON_Print(filters_array);
- cJSON_Delete(filters_array);
- }
-
- const char* sql =
- "INSERT INTO subscription_events (subscription_id, client_ip, event_type, filter_json) "
- "VALUES (?, ?, 'created', ?)";
-
- sqlite3_stmt* stmt;
- int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
- if (rc == SQLITE_OK) {
- sqlite3_bind_text(stmt, 1, sub->id, -1, SQLITE_STATIC);
- sqlite3_bind_text(stmt, 2, sub->client_ip, -1, SQLITE_STATIC);
- sqlite3_bind_text(stmt, 3, filter_json ? filter_json : "[]", -1, SQLITE_TRANSIENT);
-
- sqlite3_step(stmt);
- sqlite3_finalize(stmt);
- }
-
- if (filter_json) free(filter_json);
-}
-
-// Log subscription closure to database
-void log_subscription_closed(const char* sub_id, const char* client_ip, const char* reason) {
- (void)reason; // Mark as intentionally unused
- if (!g_db || !sub_id) return;
-
- const char* sql =
- "INSERT INTO subscription_events (subscription_id, client_ip, event_type) "
- "VALUES (?, ?, 'closed')";
-
- sqlite3_stmt* stmt;
- int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
- if (rc == SQLITE_OK) {
- sqlite3_bind_text(stmt, 1, sub_id, -1, SQLITE_STATIC);
- sqlite3_bind_text(stmt, 2, client_ip ? client_ip : "unknown", -1, SQLITE_STATIC);
-
- sqlite3_step(stmt);
- sqlite3_finalize(stmt);
- }
-
- // Update the corresponding 'created' entry with end time and events sent
- const char* update_sql =
- "UPDATE subscription_events "
- "SET ended_at = strftime('%s', 'now') "
- "WHERE subscription_id = ? AND event_type = 'created' AND ended_at IS NULL";
-
- rc = sqlite3_prepare_v2(g_db, update_sql, -1, &stmt, NULL);
- if (rc == SQLITE_OK) {
- sqlite3_bind_text(stmt, 1, sub_id, -1, SQLITE_STATIC);
- sqlite3_step(stmt);
- sqlite3_finalize(stmt);
- }
-}
-
-// Log subscription disconnection to database
-void log_subscription_disconnected(const char* client_ip) {
- if (!g_db || !client_ip) return;
-
- // Mark all active subscriptions for this client as disconnected
- const char* sql =
- "UPDATE subscription_events "
- "SET ended_at = strftime('%s', 'now') "
- "WHERE client_ip = ? AND event_type = 'created' AND ended_at IS NULL";
-
- sqlite3_stmt* stmt;
- int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
- if (rc == SQLITE_OK) {
- sqlite3_bind_text(stmt, 1, client_ip, -1, SQLITE_STATIC);
- int changes = sqlite3_changes(g_db);
- sqlite3_step(stmt);
- sqlite3_finalize(stmt);
-
- if (changes > 0) {
- // Log a disconnection event
- const char* insert_sql =
- "INSERT INTO subscription_events (subscription_id, client_ip, event_type) "
- "VALUES ('disconnect', ?, 'disconnected')";
-
- rc = sqlite3_prepare_v2(g_db, insert_sql, -1, &stmt, NULL);
- if (rc == SQLITE_OK) {
- sqlite3_bind_text(stmt, 1, client_ip, -1, SQLITE_STATIC);
- sqlite3_step(stmt);
- sqlite3_finalize(stmt);
- }
- }
- }
-}
-
-// Log event broadcast to database (optional, can be resource intensive)
-void log_event_broadcast(const char* event_id, const char* sub_id, const char* client_ip) {
- if (!g_db || !event_id || !sub_id || !client_ip) return;
-
- const char* sql =
- "INSERT INTO event_broadcasts (event_id, subscription_id, client_ip) "
- "VALUES (?, ?, ?)";
-
- sqlite3_stmt* stmt;
- int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
- if (rc == SQLITE_OK) {
- sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_STATIC);
- sqlite3_bind_text(stmt, 2, sub_id, -1, SQLITE_STATIC);
- sqlite3_bind_text(stmt, 3, client_ip, -1, SQLITE_STATIC);
-
- sqlite3_step(stmt);
- sqlite3_finalize(stmt);
- }
-}
-
-// Update events sent counter for a subscription
-void update_subscription_events_sent(const char* sub_id, int events_sent) {
- if (!g_db || !sub_id) return;
-
- const char* sql =
- "UPDATE subscription_events "
- "SET events_sent = ? "
- "WHERE subscription_id = ? AND event_type = 'created'";
-
- sqlite3_stmt* stmt;
- int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
- if (rc == SQLITE_OK) {
- sqlite3_bind_int(stmt, 1, events_sent);
- sqlite3_bind_text(stmt, 2, sub_id, -1, SQLITE_STATIC);
-
- sqlite3_step(stmt);
- sqlite3_finalize(stmt);
- }
-}
-
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
// LOGGING FUNCTIONS
@@ -1047,1189 +253,6 @@ void send_notice_message(struct lws* wsi, const char* message) {
cJSON_Delete(notice_msg);
}
-/////////////////////////////////////////////////////////////////////////////////////////
-/////////////////////////////////////////////////////////////////////////////////////////
-// NIP-42 AUTHENTICATION FUNCTIONS
-/////////////////////////////////////////////////////////////////////////////////////////
-/////////////////////////////////////////////////////////////////////////////////////////
-
-// Send NIP-42 authentication challenge to client
-void send_nip42_auth_challenge(struct lws* wsi, struct per_session_data* pss) {
- if (!wsi || !pss) return;
-
- // Generate challenge using existing request_validator function
- char challenge[65];
- if (nostr_nip42_generate_challenge(challenge, sizeof(challenge)) != 0) {
- log_error("Failed to generate NIP-42 challenge");
- send_notice_message(wsi, "Authentication temporarily unavailable");
- return;
- }
-
- // Store challenge in session
- pthread_mutex_lock(&pss->session_lock);
- strncpy(pss->active_challenge, challenge, sizeof(pss->active_challenge) - 1);
- pss->active_challenge[sizeof(pss->active_challenge) - 1] = '\0';
- pss->challenge_created = time(NULL);
- pss->challenge_expires = pss->challenge_created + 600; // 10 minutes
- pss->auth_challenge_sent = 1;
- pthread_mutex_unlock(&pss->session_lock);
-
- // Send AUTH challenge message: ["AUTH",
]
- cJSON* auth_msg = cJSON_CreateArray();
- cJSON_AddItemToArray(auth_msg, cJSON_CreateString("AUTH"));
- cJSON_AddItemToArray(auth_msg, cJSON_CreateString(challenge));
-
- char* msg_str = cJSON_Print(auth_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);
- free(buf);
- }
- free(msg_str);
- }
- cJSON_Delete(auth_msg);
-
- char debug_msg[128];
- snprintf(debug_msg, sizeof(debug_msg), "NIP-42 auth challenge sent: %.16s...", challenge);
- log_info(debug_msg);
-}
-
-// Handle NIP-42 signed authentication event from client
-void handle_nip42_auth_signed_event(struct lws* wsi, struct per_session_data* pss, cJSON* auth_event) {
- if (!wsi || !pss || !auth_event) return;
-
- // Serialize event for validation
- char* event_json = cJSON_Print(auth_event);
- if (!event_json) {
- send_notice_message(wsi, "Invalid authentication event format");
- return;
- }
-
- pthread_mutex_lock(&pss->session_lock);
- char challenge_copy[65];
- strncpy(challenge_copy, pss->active_challenge, sizeof(challenge_copy) - 1);
- challenge_copy[sizeof(challenge_copy) - 1] = '\0';
- time_t challenge_expires = pss->challenge_expires;
- pthread_mutex_unlock(&pss->session_lock);
-
- // Check if challenge has expired
- time_t current_time = time(NULL);
- if (current_time > challenge_expires) {
- free(event_json);
- send_notice_message(wsi, "Authentication challenge expired, please retry");
- log_warning("NIP-42 authentication failed: challenge expired");
- return;
- }
-
- // Verify authentication using existing request_validator function
- // Note: nostr_nip42_verify_auth_event doesn't extract pubkey, we need to do that separately
- int result = nostr_nip42_verify_auth_event(auth_event, challenge_copy,
- "ws://localhost:8888", 600); // 10 minutes tolerance
-
- char authenticated_pubkey[65] = {0};
- if (result == 0) {
- // Extract pubkey from the auth event
- cJSON* pubkey_json = cJSON_GetObjectItem(auth_event, "pubkey");
- if (pubkey_json && cJSON_IsString(pubkey_json)) {
- const char* pubkey_str = cJSON_GetStringValue(pubkey_json);
- if (pubkey_str && strlen(pubkey_str) == 64) {
- strncpy(authenticated_pubkey, pubkey_str, sizeof(authenticated_pubkey) - 1);
- authenticated_pubkey[sizeof(authenticated_pubkey) - 1] = '\0';
- } else {
- result = -1; // Invalid pubkey format
- }
- } else {
- result = -1; // Missing pubkey
- }
- }
-
- free(event_json);
-
- if (result == 0) {
- // Authentication successful
- pthread_mutex_lock(&pss->session_lock);
- pss->authenticated = 1;
- strncpy(pss->authenticated_pubkey, authenticated_pubkey, sizeof(pss->authenticated_pubkey) - 1);
- pss->authenticated_pubkey[sizeof(pss->authenticated_pubkey) - 1] = '\0';
- // Clear challenge
- memset(pss->active_challenge, 0, sizeof(pss->active_challenge));
- pss->challenge_expires = 0;
- pss->auth_challenge_sent = 0;
- pthread_mutex_unlock(&pss->session_lock);
-
- char success_msg[256];
- snprintf(success_msg, sizeof(success_msg),
- "NIP-42 authentication successful for pubkey: %.16s...", authenticated_pubkey);
- log_success(success_msg);
-
- send_notice_message(wsi, "NIP-42 authentication successful");
- } else {
- // Authentication failed
- char error_msg[256];
- snprintf(error_msg, sizeof(error_msg),
- "NIP-42 authentication failed (error code: %d)", result);
- log_warning(error_msg);
-
- send_notice_message(wsi, "NIP-42 authentication failed - invalid signature or challenge");
- }
-}
-
-// Handle challenge response (not typically used in NIP-42, but included for completeness)
-void handle_nip42_auth_challenge_response(struct lws* wsi, struct per_session_data* pss, const char* challenge) {
- (void)wsi; (void)pss; (void)challenge; // Mark as intentionally unused
-
- // NIP-42 doesn't typically use challenge responses from client to server
- // This is reserved for potential future use or protocol extensions
- log_warning("Received unexpected challenge response from client (not part of standard NIP-42 flow)");
- send_notice_message(wsi, "Challenge responses are not supported - please send signed authentication event");
-}
-
-/////////////////////////////////////////////////////////////////////////////////////////
-/////////////////////////////////////////////////////////////////////////////////////////
-// NIP-09 EVENT DELETION REQUEST HANDLING
-/////////////////////////////////////////////////////////////////////////////////////////
-/////////////////////////////////////////////////////////////////////////////////////////
-
-// Handle NIP-09 deletion request event (kind 5)
-int handle_deletion_request(cJSON* event, char* error_message, size_t error_size) {
- if (!event) {
- snprintf(error_message, error_size, "invalid: null deletion request");
- return -1;
- }
-
- // Extract event details
- cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
- cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
- cJSON* created_at_obj = cJSON_GetObjectItem(event, "created_at");
- cJSON* tags_obj = cJSON_GetObjectItem(event, "tags");
- cJSON* content_obj = cJSON_GetObjectItem(event, "content");
- cJSON* event_id_obj = cJSON_GetObjectItem(event, "id");
-
- if (!kind_obj || !pubkey_obj || !created_at_obj || !tags_obj || !event_id_obj) {
- snprintf(error_message, error_size, "invalid: incomplete deletion request");
- return -1;
- }
-
- int kind = (int)cJSON_GetNumberValue(kind_obj);
- if (kind != 5) {
- snprintf(error_message, error_size, "invalid: not a deletion request");
- return -1;
- }
-
- const char* requester_pubkey = cJSON_GetStringValue(pubkey_obj);
- // Extract deletion event ID and reason (for potential logging)
- const char* deletion_event_id = cJSON_GetStringValue(event_id_obj);
- const char* reason = content_obj ? cJSON_GetStringValue(content_obj) : "";
- (void)deletion_event_id; // Mark as intentionally unused for now
- (void)reason; // Mark as intentionally unused for now
- long deletion_timestamp = (long)cJSON_GetNumberValue(created_at_obj);
-
- if (!cJSON_IsArray(tags_obj)) {
- snprintf(error_message, error_size, "invalid: deletion request tags must be an array");
- return -1;
- }
-
- // Collect event IDs and addresses from tags
- cJSON* event_ids = cJSON_CreateArray();
- cJSON* addresses = cJSON_CreateArray();
- cJSON* kinds_to_delete = cJSON_CreateArray();
-
- int deletion_targets_found = 0;
-
- cJSON* tag = NULL;
- cJSON_ArrayForEach(tag, tags_obj) {
- if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
- continue;
- }
-
- cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
- cJSON* tag_value = cJSON_GetArrayItem(tag, 1);
-
- if (!cJSON_IsString(tag_name) || !cJSON_IsString(tag_value)) {
- continue;
- }
-
- const char* name = cJSON_GetStringValue(tag_name);
- const char* value = cJSON_GetStringValue(tag_value);
-
- if (strcmp(name, "e") == 0) {
- // Event ID reference
- cJSON_AddItemToArray(event_ids, cJSON_CreateString(value));
- deletion_targets_found++;
- } else if (strcmp(name, "a") == 0) {
- // Addressable event reference (kind:pubkey:d-identifier)
- cJSON_AddItemToArray(addresses, cJSON_CreateString(value));
- deletion_targets_found++;
- } else if (strcmp(name, "k") == 0) {
- // Kind hint - store for validation but not required
- int kind_hint = atoi(value);
- if (kind_hint > 0) {
- cJSON_AddItemToArray(kinds_to_delete, cJSON_CreateNumber(kind_hint));
- }
- }
- }
-
- if (deletion_targets_found == 0) {
- cJSON_Delete(event_ids);
- cJSON_Delete(addresses);
- cJSON_Delete(kinds_to_delete);
- snprintf(error_message, error_size, "invalid: deletion request must contain 'e' or 'a' tags");
- return -1;
- }
-
- int deleted_count = 0;
-
- // Process event ID deletions
- if (cJSON_GetArraySize(event_ids) > 0) {
- int result = delete_events_by_id(requester_pubkey, event_ids);
- if (result > 0) {
- deleted_count += result;
- }
- }
-
- // Process addressable event deletions
- if (cJSON_GetArraySize(addresses) > 0) {
- int result = delete_events_by_address(requester_pubkey, addresses, deletion_timestamp);
- if (result > 0) {
- deleted_count += result;
- }
- }
-
- // Clean up
- cJSON_Delete(event_ids);
- cJSON_Delete(addresses);
- cJSON_Delete(kinds_to_delete);
-
- // Store the deletion request itself (it should be kept according to NIP-09)
- if (store_event(event) != 0) {
- log_warning("Failed to store deletion request event");
- }
-
- char debug_msg[256];
- snprintf(debug_msg, sizeof(debug_msg), "Deletion request processed: %d events deleted", deleted_count);
- log_info(debug_msg);
-
- error_message[0] = '\0'; // Success - empty error message
- return 0;
-}
-
-// Delete events by ID (with pubkey authorization)
-int delete_events_by_id(const char* requester_pubkey, cJSON* event_ids) {
- if (!g_db || !requester_pubkey || !event_ids || !cJSON_IsArray(event_ids)) {
- return 0;
- }
-
- int deleted_count = 0;
-
- cJSON* event_id = NULL;
- cJSON_ArrayForEach(event_id, event_ids) {
- if (!cJSON_IsString(event_id)) {
- continue;
- }
-
- const char* id = cJSON_GetStringValue(event_id);
-
- // First check if event exists and if requester is authorized
- const char* check_sql = "SELECT pubkey FROM events WHERE id = ?";
- sqlite3_stmt* check_stmt;
-
- int rc = sqlite3_prepare_v2(g_db, check_sql, -1, &check_stmt, NULL);
- if (rc != SQLITE_OK) {
- continue;
- }
-
- sqlite3_bind_text(check_stmt, 1, id, -1, SQLITE_STATIC);
-
- if (sqlite3_step(check_stmt) == SQLITE_ROW) {
- const char* event_pubkey = (char*)sqlite3_column_text(check_stmt, 0);
-
- // Only delete if the requester is the author
- if (event_pubkey && strcmp(event_pubkey, requester_pubkey) == 0) {
- sqlite3_finalize(check_stmt);
-
- // Delete the event
- const char* delete_sql = "DELETE FROM events WHERE id = ? AND pubkey = ?";
- sqlite3_stmt* delete_stmt;
-
- rc = sqlite3_prepare_v2(g_db, delete_sql, -1, &delete_stmt, NULL);
- if (rc == SQLITE_OK) {
- sqlite3_bind_text(delete_stmt, 1, id, -1, SQLITE_STATIC);
- sqlite3_bind_text(delete_stmt, 2, requester_pubkey, -1, SQLITE_STATIC);
-
- if (sqlite3_step(delete_stmt) == SQLITE_DONE && sqlite3_changes(g_db) > 0) {
- deleted_count++;
-
- char debug_msg[128];
- snprintf(debug_msg, sizeof(debug_msg), "Deleted event by ID: %.16s...", id);
- log_info(debug_msg);
- }
- sqlite3_finalize(delete_stmt);
- }
- } else {
- sqlite3_finalize(check_stmt);
- char warning_msg[128];
- snprintf(warning_msg, sizeof(warning_msg), "Unauthorized deletion attempt for event: %.16s...", id);
- log_warning(warning_msg);
- }
- } else {
- sqlite3_finalize(check_stmt);
- char debug_msg[128];
- snprintf(debug_msg, sizeof(debug_msg), "Event not found for deletion: %.16s...", id);
- log_info(debug_msg);
- }
- }
-
- return deleted_count;
-}
-
-// Delete events by addressable reference (kind:pubkey:d-identifier)
-int delete_events_by_address(const char* requester_pubkey, cJSON* addresses, long deletion_timestamp) {
- if (!g_db || !requester_pubkey || !addresses || !cJSON_IsArray(addresses)) {
- return 0;
- }
-
- int deleted_count = 0;
-
- cJSON* address = NULL;
- cJSON_ArrayForEach(address, addresses) {
- if (!cJSON_IsString(address)) {
- continue;
- }
-
- const char* addr = cJSON_GetStringValue(address);
-
- // Parse address format: kind:pubkey:d-identifier
- char* addr_copy = strdup(addr);
- if (!addr_copy) continue;
-
- char* kind_str = strtok(addr_copy, ":");
- char* pubkey_str = strtok(NULL, ":");
- char* d_identifier = strtok(NULL, ":");
-
- if (!kind_str || !pubkey_str) {
- free(addr_copy);
- continue;
- }
-
- int kind = atoi(kind_str);
-
- // Only delete if the requester is the author
- if (strcmp(pubkey_str, requester_pubkey) != 0) {
- free(addr_copy);
- char warning_msg[128];
- snprintf(warning_msg, sizeof(warning_msg), "Unauthorized deletion attempt for address: %.32s...", addr);
- log_warning(warning_msg);
- continue;
- }
-
- // Build deletion query based on whether we have d-identifier
- const char* delete_sql;
- sqlite3_stmt* delete_stmt;
-
- if (d_identifier && strlen(d_identifier) > 0) {
- // Delete specific addressable event with d-tag
- delete_sql = "DELETE FROM events WHERE kind = ? AND pubkey = ? AND created_at <= ? "
- "AND json_extract(tags, '$[*]') LIKE '%[\"d\",\"' || ? || '\"]%'";
- } else {
- // Delete all events of this kind by this author up to deletion timestamp
- delete_sql = "DELETE FROM events WHERE kind = ? AND pubkey = ? AND created_at <= ?";
- }
-
- int rc = sqlite3_prepare_v2(g_db, delete_sql, -1, &delete_stmt, NULL);
- if (rc == SQLITE_OK) {
- sqlite3_bind_int(delete_stmt, 1, kind);
- sqlite3_bind_text(delete_stmt, 2, requester_pubkey, -1, SQLITE_STATIC);
- sqlite3_bind_int64(delete_stmt, 3, deletion_timestamp);
-
- if (d_identifier && strlen(d_identifier) > 0) {
- sqlite3_bind_text(delete_stmt, 4, d_identifier, -1, SQLITE_STATIC);
- }
-
- if (sqlite3_step(delete_stmt) == SQLITE_DONE) {
- int changes = sqlite3_changes(g_db);
- if (changes > 0) {
- deleted_count += changes;
-
- char debug_msg[128];
- snprintf(debug_msg, sizeof(debug_msg), "Deleted %d events by address: %.32s...", changes, addr);
- log_info(debug_msg);
- }
- }
- sqlite3_finalize(delete_stmt);
- }
-
- free(addr_copy);
- }
-
- return deleted_count;
-}
-
-// Mark event as deleted (alternative to hard deletion - not used in current implementation)
-int mark_event_as_deleted(const char* event_id, const char* deletion_event_id, const char* reason) {
- (void)event_id; (void)deletion_event_id; (void)reason; // Suppress unused warnings
-
- // This function could be used if we wanted to implement soft deletion
- // For now, NIP-09 implementation uses hard deletion as specified
-
- return 0;
-}
-
-/////////////////////////////////////////////////////////////////////////////////////////
-/////////////////////////////////////////////////////////////////////////////////////////
-// NIP-11 RELAY INFORMATION DOCUMENT
-/////////////////////////////////////////////////////////////////////////////////////////
-/////////////////////////////////////////////////////////////////////////////////////////
-
-// Initialize relay information using configuration system
-void init_relay_info() {
- // Get all config values first (without holding mutex to avoid deadlock)
- const char* relay_name = get_config_value("relay_name");
- const char* relay_description = get_config_value("relay_description");
- const char* relay_software = get_config_value("relay_software");
- const char* relay_version = get_config_value("relay_version");
- const char* relay_contact = get_config_value("relay_contact");
- 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);
- }
-
- if (relay_pubkey) {
- strncpy(g_unified_cache.relay_info.pubkey, relay_pubkey, sizeof(g_unified_cache.relay_info.pubkey) - 1);
- }
-
- // Initialize supported NIPs array
- g_unified_cache.relay_info.supported_nips = cJSON_CreateArray();
- if (g_unified_cache.relay_info.supported_nips) {
- cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(1)); // NIP-01: Basic protocol
- cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(9)); // NIP-09: Event deletion
- cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(11)); // NIP-11: Relay information
- cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(13)); // NIP-13: Proof of Work
- cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(15)); // NIP-15: EOSE
- cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(20)); // NIP-20: Command results
- cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(40)); // NIP-40: Expiration Timestamp
- cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(42)); // NIP-42: Authentication
- }
-
- // Initialize server limitations using configuration
- 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_unified_cache.relay_info.retention = cJSON_CreateArray();
-
- // Initialize language tags - set to global for now
- g_unified_cache.relay_info.language_tags = cJSON_CreateArray();
- if (g_unified_cache.relay_info.language_tags) {
- cJSON_AddItemToArray(g_unified_cache.relay_info.language_tags, cJSON_CreateString("*"));
- }
-
- // Initialize relay countries - set to global for now
- g_unified_cache.relay_info.relay_countries = cJSON_CreateArray();
- if (g_unified_cache.relay_info.relay_countries) {
- cJSON_AddItemToArray(g_unified_cache.relay_info.relay_countries, cJSON_CreateString("*"));
- }
-
- // Initialize content tags as empty array
- g_unified_cache.relay_info.tags = cJSON_CreateArray();
-
- // Initialize fees as empty object (no payment required by default)
- 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() {
- 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_unified_cache.relay_info.limitation) {
- cJSON_Delete(g_unified_cache.relay_info.limitation);
- g_unified_cache.relay_info.limitation = 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_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_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_unified_cache.relay_info.tags) {
- cJSON_Delete(g_unified_cache.relay_info.tags);
- g_unified_cache.relay_info.tags = 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
-cJSON* generate_relay_info_json() {
- cJSON* info = cJSON_CreateObject();
- if (!info) {
- log_error("Failed to create relay info JSON object");
- return NULL;
- }
-
- pthread_mutex_lock(&g_unified_cache.cache_lock);
-
- // Add basic relay information
- if (strlen(g_unified_cache.relay_info.name) > 0) {
- cJSON_AddStringToObject(info, "name", g_unified_cache.relay_info.name);
- }
- if (strlen(g_unified_cache.relay_info.description) > 0) {
- cJSON_AddStringToObject(info, "description", g_unified_cache.relay_info.description);
- }
- if (strlen(g_unified_cache.relay_info.banner) > 0) {
- cJSON_AddStringToObject(info, "banner", g_unified_cache.relay_info.banner);
- }
- if (strlen(g_unified_cache.relay_info.icon) > 0) {
- cJSON_AddStringToObject(info, "icon", g_unified_cache.relay_info.icon);
- }
- if (strlen(g_unified_cache.relay_info.pubkey) > 0) {
- cJSON_AddStringToObject(info, "pubkey", g_unified_cache.relay_info.pubkey);
- }
- if (strlen(g_unified_cache.relay_info.contact) > 0) {
- cJSON_AddStringToObject(info, "contact", g_unified_cache.relay_info.contact);
- }
-
- // Add supported NIPs
- 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_unified_cache.relay_info.software) > 0) {
- cJSON_AddStringToObject(info, "software", g_unified_cache.relay_info.software);
- }
- if (strlen(g_unified_cache.relay_info.version) > 0) {
- cJSON_AddStringToObject(info, "version", g_unified_cache.relay_info.version);
- }
-
- // Add policies
- 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_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_unified_cache.relay_info.posting_policy) > 0) {
- cJSON_AddStringToObject(info, "posting_policy", g_unified_cache.relay_info.posting_policy);
- }
-
- // Add server limitations
- 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_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_unified_cache.relay_info.relay_countries) {
- cJSON_AddItemToObject(info, "relay_countries", cJSON_Duplicate(g_unified_cache.relay_info.relay_countries, 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_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_unified_cache.relay_info.payments_url) > 0) {
- cJSON_AddStringToObject(info, "payments_url", g_unified_cache.relay_info.payments_url);
- }
- 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;
-}
-
-// NIP-11 HTTP session data structure for managing buffer lifetime
-struct nip11_session_data {
- char* json_buffer;
- size_t json_length;
- int headers_sent;
- int body_sent;
-};
-
-// Handle NIP-11 HTTP request with proper asynchronous buffer management
-int handle_nip11_http_request(struct lws* wsi, const char* accept_header) {
- log_info("Handling NIP-11 relay information request");
-
- // Check if client accepts application/nostr+json
- int accepts_nostr_json = 0;
- if (accept_header) {
- if (strstr(accept_header, "application/nostr+json") != NULL) {
- accepts_nostr_json = 1;
- }
- }
-
- if (!accepts_nostr_json) {
- log_warning("HTTP request without proper Accept header for NIP-11");
- // Return 406 Not Acceptable
- unsigned char buf[LWS_PRE + 256];
- unsigned char *p = &buf[LWS_PRE];
- unsigned char *start = p;
- unsigned char *end = &buf[sizeof(buf) - 1];
-
- if (lws_add_http_header_status(wsi, HTTP_STATUS_NOT_ACCEPTABLE, &p, end)) {
- return -1;
- }
- if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE, (unsigned char*)"text/plain", 10, &p, end)) {
- return -1;
- }
- if (lws_add_http_header_content_length(wsi, 0, &p, end)) {
- return -1;
- }
- if (lws_finalize_http_header(wsi, &p, end)) {
- return -1;
- }
- lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS);
- return -1; // Close connection
- }
-
- // Generate relay information JSON
- cJSON* info_json = generate_relay_info_json();
- if (!info_json) {
- log_error("Failed to generate relay info JSON");
- unsigned char buf[LWS_PRE + 256];
- unsigned char *p = &buf[LWS_PRE];
- unsigned char *start = p;
- unsigned char *end = &buf[sizeof(buf) - 1];
-
- if (lws_add_http_header_status(wsi, HTTP_STATUS_INTERNAL_SERVER_ERROR, &p, end)) {
- return -1;
- }
- if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE, (unsigned char*)"text/plain", 10, &p, end)) {
- return -1;
- }
- if (lws_add_http_header_content_length(wsi, 0, &p, end)) {
- return -1;
- }
- if (lws_finalize_http_header(wsi, &p, end)) {
- return -1;
- }
- lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS);
- return -1;
- }
-
- char* json_string = cJSON_Print(info_json);
- cJSON_Delete(info_json);
-
- if (!json_string) {
- log_error("Failed to serialize relay info JSON");
- unsigned char buf[LWS_PRE + 256];
- unsigned char *p = &buf[LWS_PRE];
- unsigned char *start = p;
- unsigned char *end = &buf[sizeof(buf) - 1];
-
- if (lws_add_http_header_status(wsi, HTTP_STATUS_INTERNAL_SERVER_ERROR, &p, end)) {
- return -1;
- }
- if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE, (unsigned char*)"text/plain", 10, &p, end)) {
- return -1;
- }
- if (lws_add_http_header_content_length(wsi, 0, &p, end)) {
- return -1;
- }
- if (lws_finalize_http_header(wsi, &p, end)) {
- return -1;
- }
- lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS);
- return -1;
- }
-
- size_t json_len = strlen(json_string);
-
- // Allocate session data to manage buffer lifetime across callbacks
- struct nip11_session_data* session_data = malloc(sizeof(struct nip11_session_data));
- if (!session_data) {
- log_error("Failed to allocate NIP-11 session data");
- free(json_string);
- return -1;
- }
-
- // Store JSON buffer in session data for asynchronous handling
- session_data->json_buffer = json_string;
- session_data->json_length = json_len;
- session_data->headers_sent = 0;
- session_data->body_sent = 0;
-
- // Store session data in WSI user data for callback access
- lws_set_wsi_user(wsi, session_data);
-
- // Prepare HTTP response with CORS headers
- unsigned char buf[LWS_PRE + 1024];
- unsigned char *p = &buf[LWS_PRE];
- unsigned char *start = p;
- unsigned char *end = &buf[sizeof(buf) - 1];
-
- // Add status
- if (lws_add_http_header_status(wsi, HTTP_STATUS_OK, &p, end)) {
- free(session_data->json_buffer);
- free(session_data);
- return -1;
- }
-
- // Add content type
- if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE,
- (unsigned char*)"application/nostr+json", 22, &p, end)) {
- free(session_data->json_buffer);
- free(session_data);
- return -1;
- }
-
- // Add content length
- if (lws_add_http_header_content_length(wsi, json_len, &p, end)) {
- free(session_data->json_buffer);
- free(session_data);
- return -1;
- }
-
- // Add CORS headers as required by NIP-11
- if (lws_add_http_header_by_name(wsi, (unsigned char*)"access-control-allow-origin:",
- (unsigned char*)"*", 1, &p, end)) {
- free(session_data->json_buffer);
- free(session_data);
- return -1;
- }
- if (lws_add_http_header_by_name(wsi, (unsigned char*)"access-control-allow-headers:",
- (unsigned char*)"content-type, accept", 20, &p, end)) {
- free(session_data->json_buffer);
- free(session_data);
- return -1;
- }
- if (lws_add_http_header_by_name(wsi, (unsigned char*)"access-control-allow-methods:",
- (unsigned char*)"GET, OPTIONS", 12, &p, end)) {
- free(session_data->json_buffer);
- free(session_data);
- return -1;
- }
-
- // Finalize headers
- if (lws_finalize_http_header(wsi, &p, end)) {
- free(session_data->json_buffer);
- free(session_data);
- return -1;
- }
-
- // Write headers
- if (lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS) < 0) {
- free(session_data->json_buffer);
- free(session_data);
- return -1;
- }
-
- session_data->headers_sent = 1;
-
- // Request callback for body transmission
- lws_callback_on_writable(wsi);
-
- log_success("NIP-11 headers sent, body transmission scheduled");
- return 0;
-}
-
-/////////////////////////////////////////////////////////////////////////////////////////
-/////////////////////////////////////////////////////////////////////////////////////////
-// NIP-13 PROOF OF WORK VALIDATION
-/////////////////////////////////////////////////////////////////////////////////////////
-/////////////////////////////////////////////////////////////////////////////////////////
-
-// Initialize PoW configuration using configuration system
-void init_pow_config() {
- log_info("Initializing NIP-13 Proof of Work 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_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_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_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_unified_cache.pow_config.enabled = 0;
- log_info("PoW validation disabled via configuration");
- }
- } else {
- // Default to basic mode
- g_unified_cache.pow_config.validation_flags = NOSTR_POW_VALIDATE_BASIC;
- log_info("PoW configured in basic validation mode (default)");
- }
-
- // Log final configuration
- char config_msg[512];
- snprintf(config_msg, sizeof(config_msg),
- "PoW Configuration: enabled=%s, min_difficulty=%d, validation_flags=0x%x, mode=%s",
- 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) {
- 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
- }
-
- if (!event) {
- snprintf(error_message, error_size, "pow: null event");
- return NOSTR_ERROR_INVALID_INPUT;
- }
-
- // If min_pow_difficulty is 0, only validate events that have nonce tags
- // This allows events without PoW when difficulty requirement is 0
- if (min_pow_difficulty == 0) {
- cJSON* tags = cJSON_GetObjectItem(event, "tags");
- int has_nonce_tag = 0;
-
- if (tags && cJSON_IsArray(tags)) {
- cJSON* tag = NULL;
- cJSON_ArrayForEach(tag, tags) {
- if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
- cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
- if (cJSON_IsString(tag_name)) {
- const char* name = cJSON_GetStringValue(tag_name);
- if (name && strcmp(name, "nonce") == 0) {
- has_nonce_tag = 1;
- break;
- }
- }
- }
- }
- }
-
- // If no minimum difficulty required and no nonce tag, skip PoW validation
- if (!has_nonce_tag) {
- return 0; // Accept event without PoW when min_difficulty=0
- }
- }
-
- // Perform PoW validation using nostr_core_lib
- nostr_pow_result_t 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
- switch (validation_result) {
- case NOSTR_ERROR_NIP13_INSUFFICIENT:
- snprintf(error_message, error_size,
- "pow: insufficient difficulty: %d < %d",
- 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 (min_pow_difficulty > 0) {
- snprintf(error_message, error_size, "pow: missing required nonce tag");
- log_warning("Event rejected: missing nonce tag");
- } else {
- return 0; // Allow when min_difficulty=0
- }
- break;
- case NOSTR_ERROR_NIP13_INVALID_NONCE_TAG:
- snprintf(error_message, error_size, "pow: invalid nonce tag format");
- log_warning("Event rejected: invalid nonce tag format");
- break;
- case NOSTR_ERROR_NIP13_TARGET_MISMATCH:
- snprintf(error_message, error_size,
- "pow: committed target (%d) lower than minimum (%d)",
- pow_result.committed_target, min_pow_difficulty);
- log_warning("Event rejected: committed target too low (anti-spam protection)");
- break;
- case NOSTR_ERROR_NIP13_CALCULATION:
- snprintf(error_message, error_size, "pow: difficulty calculation failed");
- log_error("PoW difficulty calculation error");
- break;
- case NOSTR_ERROR_EVENT_INVALID_ID:
- snprintf(error_message, error_size, "pow: invalid event ID format");
- log_warning("Event rejected: invalid event ID for PoW calculation");
- break;
- default:
- snprintf(error_message, error_size, "pow: validation failed - %s",
- strlen(pow_result.error_detail) > 0 ? pow_result.error_detail : "unknown error");
- log_warning("Event rejected: PoW validation failed");
- }
- return validation_result;
- }
-
- // Log successful PoW validation (only if minimum difficulty is required)
- 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",
- pow_result.actual_difficulty,
- pow_result.committed_target,
- (unsigned long long)pow_result.nonce_value,
- pow_result.has_nonce_tag ? "" : " (no nonce tag)");
- log_info(debug_msg);
- }
-
- return 0; // Success
-}
-
-/////////////////////////////////////////////////////////////////////////////////////////
-/////////////////////////////////////////////////////////////////////////////////////////
-// NIP-40 EXPIRATION TIMESTAMP HANDLING
-/////////////////////////////////////////////////////////////////////////////////////////
-/////////////////////////////////////////////////////////////////////////////////////////
-
-// Initialize expiration configuration using configuration system
-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_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_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_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_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
-long extract_expiration_timestamp(cJSON* tags) {
- if (!tags || !cJSON_IsArray(tags)) {
- return 0; // No expiration
- }
-
- cJSON* tag = NULL;
- cJSON_ArrayForEach(tag, tags) {
- if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
- cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
- cJSON* tag_value = cJSON_GetArrayItem(tag, 1);
-
- if (cJSON_IsString(tag_name) && cJSON_IsString(tag_value)) {
- const char* name = cJSON_GetStringValue(tag_name);
- const char* value = cJSON_GetStringValue(tag_value);
-
- if (name && value && strcmp(name, "expiration") == 0) {
- // Validate that the string contains only digits (and optional leading whitespace)
- const char* p = value;
-
- // Skip leading whitespace
- while (*p == ' ' || *p == '\t') p++;
-
- // Check if we have at least one digit
- if (*p == '\0') {
- continue; // Empty or whitespace-only string, ignore this tag
- }
-
- // Validate that all remaining characters are digits
- const char* digit_start = p;
- while (*p >= '0' && *p <= '9') p++;
-
- // If we didn't consume the entire string or found no digits, it's malformed
- if (*p != '\0' || p == digit_start) {
- char debug_msg[256];
- snprintf(debug_msg, sizeof(debug_msg),
- "Ignoring malformed expiration tag value: '%.32s'", value);
- log_warning(debug_msg);
- continue; // Ignore malformed expiration tag
- }
-
- long expiration_ts = atol(value);
- if (expiration_ts > 0) {
- return expiration_ts;
- }
- }
- }
- }
- }
-
- return 0; // No valid expiration tag found
-}
-
-// Check if event is currently expired
-int is_event_expired(cJSON* event, time_t current_time) {
- if (!event) {
- return 0; // Invalid event, not expired
- }
-
- cJSON* tags = cJSON_GetObjectItem(event, "tags");
- long expiration_ts = extract_expiration_timestamp(tags);
-
- if (expiration_ts == 0) {
- return 0; // No expiration timestamp, not expired
- }
-
- // Check if current time exceeds expiration + 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) {
- 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
- }
-
- if (!event) {
- snprintf(error_message, error_size, "expiration: null event");
- return -1;
- }
-
- // Check if event is expired
- time_t current_time = time(NULL);
- if (is_event_expired(event, current_time)) {
- 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, grace_period);
- log_warning("Event rejected: expired timestamp");
- return -1;
- } else {
- // In non-strict mode, log but allow expired events
- char debug_msg[256];
- snprintf(debug_msg, sizeof(debug_msg),
- "Accepting expired event (strict_mode disabled)");
- log_info(debug_msg);
- }
- }
-
- return 0; // Success
-}
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
@@ -2839,8 +862,8 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
continue;
}
- // Build SQL query based on filter
- char sql[1024] = "SELECT id, pubkey, created_at, kind, content, sig, tags FROM events WHERE 1=1";
+ // Build SQL query based on filter - exclude ephemeral events (kinds 20000-29999) from historical queries
+ char sql[1024] = "SELECT id, pubkey, created_at, kind, content, sig, tags FROM events WHERE 1=1 AND kind < 20000";
char* sql_ptr = sql + strlen(sql);
int remaining = sizeof(sql) - strlen(sql);
@@ -2883,7 +906,7 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
snprintf(sql_ptr, remaining, " AND pubkey IN (");
sql_ptr += strlen(sql_ptr);
remaining = sizeof(sql) - strlen(sql);
-
+
for (int a = 0; a < author_count; a++) {
cJSON* author = cJSON_GetArrayItem(authors, a);
if (cJSON_IsString(author)) {
@@ -2902,7 +925,35 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
remaining = sizeof(sql) - strlen(sql);
}
}
-
+
+ // Handle ids filter
+ cJSON* ids = cJSON_GetObjectItem(filter, "ids");
+ if (ids && cJSON_IsArray(ids)) {
+ int id_count = cJSON_GetArraySize(ids);
+ if (id_count > 0) {
+ snprintf(sql_ptr, remaining, " AND id IN (");
+ sql_ptr += strlen(sql_ptr);
+ remaining = sizeof(sql) - strlen(sql);
+
+ for (int i = 0; i < id_count; i++) {
+ cJSON* id = cJSON_GetArrayItem(ids, i);
+ if (cJSON_IsString(id)) {
+ if (i > 0) {
+ snprintf(sql_ptr, remaining, ",");
+ sql_ptr++;
+ remaining--;
+ }
+ snprintf(sql_ptr, remaining, "'%s'", cJSON_GetStringValue(id));
+ sql_ptr += strlen(sql_ptr);
+ remaining = sizeof(sql) - strlen(sql);
+ }
+ }
+ snprintf(sql_ptr, remaining, ")");
+ sql_ptr += strlen(sql_ptr);
+ remaining = sizeof(sql) - strlen(sql);
+ }
+ }
+
// Handle since filter
cJSON* since = cJSON_GetObjectItem(filter, "since");
if (since && cJSON_IsNumber(since)) {
@@ -3137,771 +1188,6 @@ int is_authorized_admin_event(cJSON* event, char* error_buffer, size_t error_buf
-
-/////////////////////////////////////////////////////////////////////////////////////////
-/////////////////////////////////////////////////////////////////////////////////////////
-// WEBSOCKET PROTOCOL
-/////////////////////////////////////////////////////////////////////////////////////////
-/////////////////////////////////////////////////////////////////////////////////////////
-
-// WebSocket callback function for Nostr relay protocol
-static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reason,
- void *user, void *in, size_t len) {
- struct per_session_data *pss = (struct per_session_data *)user;
-
- switch (reason) {
- case LWS_CALLBACK_HTTP:
- // Handle NIP-11 relay information requests (HTTP GET to root path)
- {
- char *requested_uri = (char *)in;
- log_info("HTTP request received");
-
- // Check if this is a GET request to the root path
- if (strcmp(requested_uri, "/") == 0) {
- // Get Accept header
- char accept_header[256] = {0};
- int header_len = lws_hdr_copy(wsi, accept_header, sizeof(accept_header) - 1, WSI_TOKEN_HTTP_ACCEPT);
-
- if (header_len > 0) {
- accept_header[header_len] = '\0';
-
- // Handle NIP-11 request
- if (handle_nip11_http_request(wsi, accept_header) == 0) {
- return 0; // Successfully handled
- }
- } else {
- log_warning("HTTP request without Accept header");
- }
-
- // Return 404 for other requests
- lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL);
- return -1;
- }
-
- // Return 404 for non-root paths
- lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL);
- return -1;
- }
-
- case LWS_CALLBACK_HTTP_WRITEABLE:
- // Handle NIP-11 HTTP body transmission with proper buffer management
- {
- struct nip11_session_data* session_data = (struct nip11_session_data*)lws_wsi_user(wsi);
- if (session_data && session_data->headers_sent && !session_data->body_sent) {
- // Allocate buffer for JSON body transmission
- unsigned char *json_buf = malloc(LWS_PRE + session_data->json_length);
- if (!json_buf) {
- log_error("Failed to allocate buffer for NIP-11 body transmission");
- // Clean up session data
- free(session_data->json_buffer);
- free(session_data);
- lws_set_wsi_user(wsi, NULL);
- return -1;
- }
-
- // Copy JSON data to buffer
- memcpy(json_buf + LWS_PRE, session_data->json_buffer, session_data->json_length);
-
- // Write JSON body
- int write_result = lws_write(wsi, json_buf + LWS_PRE, session_data->json_length, LWS_WRITE_HTTP);
-
- // Free the transmission buffer immediately (it's been copied by libwebsockets)
- free(json_buf);
-
- if (write_result < 0) {
- log_error("Failed to write NIP-11 JSON body");
- // Clean up session data
- free(session_data->json_buffer);
- free(session_data);
- lws_set_wsi_user(wsi, NULL);
- return -1;
- }
-
- // Mark body as sent and clean up session data
- session_data->body_sent = 1;
- free(session_data->json_buffer);
- free(session_data);
- lws_set_wsi_user(wsi, NULL);
-
- log_success("NIP-11 relay information served successfully");
- return 0; // Close connection after successful transmission
- }
- }
- break;
-
- case LWS_CALLBACK_ESTABLISHED:
- log_info("WebSocket connection established");
- memset(pss, 0, sizeof(*pss));
- pthread_mutex_init(&pss->session_lock, NULL);
-
- // Get real client IP address
- char client_ip[CLIENT_IP_MAX_LENGTH];
- lws_get_peer_simple(wsi, client_ip, sizeof(client_ip));
-
- // 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;
- pss->nip42_auth_required_events = get_config_bool("nip42_auth_required_events", 0);
- pss->nip42_auth_required_subscriptions = get_config_bool("nip42_auth_required_subscriptions", 0);
- pss->auth_challenge_sent = 0;
- memset(pss->authenticated_pubkey, 0, sizeof(pss->authenticated_pubkey));
- memset(pss->active_challenge, 0, sizeof(pss->active_challenge));
- pss->challenge_created = 0;
- pss->challenge_expires = 0;
- break;
-
- case LWS_CALLBACK_RECEIVE:
- if (len > 0) {
- char *message = malloc(len + 1);
- if (message) {
- memcpy(message, in, len);
- message[len] = '\0';
-
- // Parse JSON message (this is the normal program flow)
- cJSON* json = cJSON_Parse(message);
- if (json && cJSON_IsArray(json)) {
- // Log the complete parsed JSON message once
- char* complete_message = cJSON_Print(json);
- if (complete_message) {
- char debug_msg[2048];
- snprintf(debug_msg, sizeof(debug_msg),
- "Received complete WebSocket message: %s", complete_message);
- log_info(debug_msg);
- free(complete_message);
- }
- // Get message type
- cJSON* type = cJSON_GetArrayItem(json, 0);
- if (type && cJSON_IsString(type)) {
- const char* msg_type = cJSON_GetStringValue(type);
-
- if (strcmp(msg_type, "EVENT") == 0) {
- // Extract event for kind-specific NIP-42 authentication check
- cJSON* event_obj = cJSON_GetArrayItem(json, 1);
- if (event_obj && cJSON_IsObject(event_obj)) {
- // Extract event kind for kind-specific NIP-42 authentication check
- cJSON* kind_obj = cJSON_GetObjectItem(event_obj, "kind");
- int event_kind = kind_obj && cJSON_IsNumber(kind_obj) ? (int)cJSON_GetNumberValue(kind_obj) : -1;
-
- // Extract pubkey and event ID for debugging
- cJSON* pubkey_obj = cJSON_GetObjectItem(event_obj, "pubkey");
- cJSON* id_obj = cJSON_GetObjectItem(event_obj, "id");
- const char* event_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : "unknown";
- const char* event_id = id_obj ? cJSON_GetStringValue(id_obj) : "unknown";
-
- char debug_event_msg[512];
- snprintf(debug_event_msg, sizeof(debug_event_msg),
- "DEBUG EVENT: Processing kind %d event from pubkey %.16s... ID %.16s...",
- event_kind, event_pubkey, event_id);
- log_info(debug_event_msg);
-
- // Check if NIP-42 authentication is required for this event kind or globally
- int auth_required = is_nip42_auth_globally_required() || is_nip42_auth_required_for_kind(event_kind);
-
- char debug_auth_msg[256];
- snprintf(debug_auth_msg, sizeof(debug_auth_msg),
- "DEBUG AUTH: auth_required=%d, pss->authenticated=%d, event_kind=%d",
- auth_required, pss ? pss->authenticated : -1, event_kind);
- log_info(debug_auth_msg);
-
- if (pss && auth_required && !pss->authenticated) {
- if (!pss->auth_challenge_sent) {
- log_info("DEBUG AUTH: Sending NIP-42 authentication challenge");
- send_nip42_auth_challenge(wsi, pss);
- } else {
- char auth_msg[256];
- if (event_kind == 4 || event_kind == 14) {
- snprintf(auth_msg, sizeof(auth_msg),
- "NIP-42 authentication required for direct message events (kind %d)", event_kind);
- } else {
- snprintf(auth_msg, sizeof(auth_msg),
- "NIP-42 authentication required for event kind %d", event_kind);
- }
- send_notice_message(wsi, auth_msg);
- log_warning("Event rejected: NIP-42 authentication required for kind");
- char debug_msg[128];
- snprintf(debug_msg, sizeof(debug_msg), "Auth required for kind %d", event_kind);
- log_info(debug_msg);
- }
- cJSON_Delete(json);
- free(message);
- return 0;
- }
- }
-
- // Handle EVENT message
- cJSON* event = cJSON_GetArrayItem(json, 1);
- if (event && cJSON_IsObject(event)) {
- // Extract event JSON string for unified validator
- char *event_json_str = cJSON_Print(event);
- if (!event_json_str) {
- log_error("Failed to serialize event JSON for validation");
- cJSON* error_response = cJSON_CreateArray();
- cJSON_AddItemToArray(error_response, cJSON_CreateString("OK"));
- cJSON_AddItemToArray(error_response, cJSON_CreateString("unknown"));
- cJSON_AddItemToArray(error_response, cJSON_CreateBool(0));
- cJSON_AddItemToArray(error_response, cJSON_CreateString("error: failed to process event"));
-
- char *error_str = cJSON_Print(error_response);
- if (error_str) {
- size_t error_len = strlen(error_str);
- unsigned char *buf = malloc(LWS_PRE + error_len);
- if (buf) {
- memcpy(buf + LWS_PRE, error_str, error_len);
- lws_write(wsi, buf + LWS_PRE, error_len, LWS_WRITE_TEXT);
- free(buf);
- }
- free(error_str);
- }
- cJSON_Delete(error_response);
- return 0;
- }
-
- log_info("DEBUG VALIDATION: Starting unified validator");
-
- // 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);
-
- // Map validation result to old result format (0 = success, -1 = failure)
- int result = (validation_result == NOSTR_SUCCESS) ? 0 : -1;
-
- char debug_validation_msg[256];
- snprintf(debug_validation_msg, sizeof(debug_validation_msg),
- "DEBUG VALIDATION: validation_result=%d, result=%d", validation_result, result);
- log_info(debug_validation_msg);
-
- // Generate error message based on validation result
- char error_message[512] = {0};
- if (result != 0) {
- switch (validation_result) {
- case NOSTR_ERROR_INVALID_INPUT:
- strncpy(error_message, "invalid: malformed event structure", sizeof(error_message) - 1);
- break;
- case NOSTR_ERROR_EVENT_INVALID_SIGNATURE:
- strncpy(error_message, "invalid: signature verification failed", sizeof(error_message) - 1);
- break;
- case NOSTR_ERROR_EVENT_INVALID_ID:
- strncpy(error_message, "invalid: event id verification failed", sizeof(error_message) - 1);
- break;
- case NOSTR_ERROR_EVENT_INVALID_PUBKEY:
- strncpy(error_message, "invalid: invalid pubkey format", sizeof(error_message) - 1);
- break;
- case -103: // NOSTR_ERROR_EVENT_EXPIRED
- strncpy(error_message, "rejected: event expired", sizeof(error_message) - 1);
- break;
- case -102: // NOSTR_ERROR_NIP42_DISABLED
- strncpy(error_message, "auth-required: NIP-42 authentication required", sizeof(error_message) - 1);
- break;
- case -101: // NOSTR_ERROR_AUTH_REQUIRED
- strncpy(error_message, "blocked: pubkey not authorized", sizeof(error_message) - 1);
- break;
- default:
- strncpy(error_message, "error: validation failed", sizeof(error_message) - 1);
- break;
- }
- char debug_error_msg[256];
- snprintf(debug_error_msg, sizeof(debug_error_msg),
- "DEBUG VALIDATION ERROR: %s", error_message);
- log_warning(debug_error_msg);
- } else {
- log_info("DEBUG VALIDATION: Event validated successfully using unified validator");
- }
-
- // Cleanup event JSON string
- free(event_json_str);
-
- // Check for admin events (kind 23456) and intercept them
- if (result == 0) {
- cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
- if (kind_obj && cJSON_IsNumber(kind_obj)) {
- int event_kind = (int)cJSON_GetNumberValue(kind_obj);
-
- log_info("DEBUG ADMIN: Checking if admin event processing is needed");
-
- // Log reception of Kind 23456 events
- if (event_kind == 23456) {
- char* event_json_debug = cJSON_Print(event);
- char debug_received_msg[1024];
- snprintf(debug_received_msg, sizeof(debug_received_msg),
- "RECEIVED Kind %d event: %s", event_kind,
- event_json_debug ? event_json_debug : "Failed to serialize");
- log_info(debug_received_msg);
-
- if (event_json_debug) {
- free(event_json_debug);
- }
- }
-
- if (event_kind == 23456) {
- // Enhanced admin event security - check authorization first
- log_info("DEBUG ADMIN: Admin event detected, checking authorization");
-
- char auth_error[512] = {0};
- int auth_result = is_authorized_admin_event(event, auth_error, sizeof(auth_error));
-
- if (auth_result != 0) {
- // Authorization failed - log and reject
- log_warning("DEBUG ADMIN: Admin event authorization failed");
- result = -1;
- size_t error_len = strlen(auth_error);
- size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1;
- memcpy(error_message, auth_error, copy_len);
- error_message[copy_len] = '\0';
-
- char debug_auth_error_msg[600];
- snprintf(debug_auth_error_msg, sizeof(debug_auth_error_msg),
- "DEBUG ADMIN AUTH ERROR: %.400s", auth_error);
- log_warning(debug_auth_error_msg);
- } else {
- // Authorization successful - process through admin API
- log_info("DEBUG ADMIN: Admin event authorized, processing through admin API");
-
- char admin_error[512] = {0};
- int admin_result = process_admin_event_in_config(event, admin_error, sizeof(admin_error), wsi);
-
- char debug_admin_msg[256];
- snprintf(debug_admin_msg, sizeof(debug_admin_msg),
- "DEBUG ADMIN: process_admin_event_in_config returned %d", admin_result);
- log_info(debug_admin_msg);
-
- // Log results for Kind 23456 events
- if (event_kind == 23456) {
- if (admin_result == 0) {
- char success_result_msg[256];
- snprintf(success_result_msg, sizeof(success_result_msg),
- "SUCCESS: Kind %d event processed successfully", event_kind);
- log_success(success_result_msg);
- } else {
- char error_result_msg[512];
- snprintf(error_result_msg, sizeof(error_result_msg),
- "ERROR: Kind %d event processing failed: %s", event_kind, admin_error);
- log_error(error_result_msg);
- }
- }
-
- if (admin_result != 0) {
- log_error("DEBUG ADMIN: 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';
-
- char debug_admin_error_msg[600];
- snprintf(debug_admin_error_msg, sizeof(debug_admin_error_msg),
- "DEBUG ADMIN ERROR: %.400s", admin_error);
- log_error(debug_admin_error_msg);
- } else {
- log_success("DEBUG ADMIN: 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
- log_info("DEBUG STORAGE: Regular event - storing in database");
- if (store_event(event) != 0) {
- log_error("DEBUG STORAGE: Failed to store event in database");
- result = -1;
- strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1);
- } else {
- log_info("DEBUG STORAGE: Event stored successfully in database");
- // Broadcast event to matching persistent subscriptions
- int broadcast_count = broadcast_event_to_subscriptions(event);
- char debug_broadcast_msg[128];
- snprintf(debug_broadcast_msg, sizeof(debug_broadcast_msg),
- "DEBUG BROADCAST: Event broadcast to %d subscriptions", broadcast_count);
- log_info(debug_broadcast_msg);
- }
- }
- } else {
- // Event without valid kind - try normal storage
- log_warning("DEBUG STORAGE: Event without valid kind - trying normal storage");
- if (store_event(event) != 0) {
- log_error("DEBUG STORAGE: Failed to store event without kind in database");
- result = -1;
- strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1);
- } else {
- log_info("DEBUG STORAGE: Event without kind stored successfully in database");
- broadcast_event_to_subscriptions(event);
- }
- }
- }
-
- // Send OK response
- cJSON* event_id = cJSON_GetObjectItem(event, "id");
- if (event_id && cJSON_IsString(event_id)) {
- cJSON* response = cJSON_CreateArray();
- cJSON_AddItemToArray(response, cJSON_CreateString("OK"));
- cJSON_AddItemToArray(response, cJSON_CreateString(cJSON_GetStringValue(event_id)));
- cJSON_AddItemToArray(response, cJSON_CreateBool(result == 0));
- cJSON_AddItemToArray(response, cJSON_CreateString(strlen(error_message) > 0 ? error_message : ""));
-
- // TODO: REPLACE - Remove wasteful cJSON_Print conversion
- char *response_str = cJSON_Print(response);
- if (response_str) {
- char debug_response_msg[512];
- snprintf(debug_response_msg, sizeof(debug_response_msg),
- "DEBUG RESPONSE: Sending OK response: %s", response_str);
- log_info(debug_response_msg);
-
- size_t response_len = strlen(response_str);
- unsigned char *buf = malloc(LWS_PRE + response_len);
- if (buf) {
- memcpy(buf + LWS_PRE, response_str, response_len);
- int write_result = lws_write(wsi, buf + LWS_PRE, response_len, LWS_WRITE_TEXT);
-
- char debug_write_msg[128];
- snprintf(debug_write_msg, sizeof(debug_write_msg),
- "DEBUG RESPONSE: lws_write returned %d", write_result);
- log_info(debug_write_msg);
-
- free(buf);
- }
- free(response_str);
- }
- cJSON_Delete(response);
- }
- }
- } else if (strcmp(msg_type, "REQ") == 0) {
- // Check NIP-42 authentication for REQ subscriptions if required
- if (pss && pss->nip42_auth_required_subscriptions && !pss->authenticated) {
- if (!pss->auth_challenge_sent) {
- send_nip42_auth_challenge(wsi, pss);
- } else {
- send_notice_message(wsi, "NIP-42 authentication required for subscriptions");
- log_warning("REQ rejected: NIP-42 authentication required");
- }
- cJSON_Delete(json);
- free(message);
- return 0;
- }
-
- // Handle REQ message
- cJSON* sub_id = cJSON_GetArrayItem(json, 1);
-
- if (sub_id && cJSON_IsString(sub_id)) {
- const char* subscription_id = cJSON_GetStringValue(sub_id);
-
- // Create array of filter objects from position 2 onwards
- cJSON* filters = cJSON_CreateArray();
- int json_size = cJSON_GetArraySize(json);
- for (int i = 2; i < json_size; i++) {
- cJSON* filter = cJSON_GetArrayItem(json, i);
- if (filter) {
- cJSON_AddItemToArray(filters, cJSON_Duplicate(filter, 1));
- }
- }
-
- handle_req_message(subscription_id, filters, wsi, pss);
-
- // Clean up the filters array we created
- cJSON_Delete(filters);
-
- // Send EOSE (End of Stored Events)
- cJSON* eose_response = cJSON_CreateArray();
- cJSON_AddItemToArray(eose_response, cJSON_CreateString("EOSE"));
- cJSON_AddItemToArray(eose_response, cJSON_CreateString(subscription_id));
-
- char *eose_str = cJSON_Print(eose_response);
- if (eose_str) {
- size_t eose_len = strlen(eose_str);
- unsigned char *buf = malloc(LWS_PRE + eose_len);
- if (buf) {
- memcpy(buf + LWS_PRE, eose_str, eose_len);
- lws_write(wsi, buf + LWS_PRE, eose_len, LWS_WRITE_TEXT);
- free(buf);
- }
- free(eose_str);
- }
- cJSON_Delete(eose_response);
- }
- } else if (strcmp(msg_type, "CLOSE") == 0) {
- // Handle CLOSE message
- cJSON* sub_id = cJSON_GetArrayItem(json, 1);
- if (sub_id && cJSON_IsString(sub_id)) {
- const char* subscription_id = cJSON_GetStringValue(sub_id);
-
- // Remove from global manager
- remove_subscription_from_manager(subscription_id, wsi);
-
- // Remove from session list if present
- if (pss) {
- pthread_mutex_lock(&pss->session_lock);
-
- subscription_t** current = &pss->subscriptions;
- while (*current) {
- if (strcmp((*current)->id, subscription_id) == 0) {
- subscription_t* to_remove = *current;
- *current = to_remove->session_next;
- pss->subscription_count--;
- break;
- }
- current = &((*current)->session_next);
- }
-
- pthread_mutex_unlock(&pss->session_lock);
- }
-
- char debug_msg[256];
- snprintf(debug_msg, sizeof(debug_msg), "Closed subscription: %s", subscription_id);
- log_info(debug_msg);
- }
- } else if (strcmp(msg_type, "AUTH") == 0) {
- // Handle NIP-42 AUTH message
- if (cJSON_GetArraySize(json) >= 2) {
- cJSON* auth_payload = cJSON_GetArrayItem(json, 1);
-
- if (cJSON_IsString(auth_payload)) {
- // AUTH challenge response: ["AUTH", ] (unusual)
- handle_nip42_auth_challenge_response(wsi, pss, cJSON_GetStringValue(auth_payload));
- } else if (cJSON_IsObject(auth_payload)) {
- // AUTH signed event: ["AUTH", ] (standard NIP-42)
- handle_nip42_auth_signed_event(wsi, pss, auth_payload);
- } else {
- send_notice_message(wsi, "Invalid AUTH message format");
- log_warning("Received AUTH message with invalid payload type");
- }
- } else {
- send_notice_message(wsi, "AUTH message requires payload");
- log_warning("Received AUTH message without payload");
- }
- } else {
- // Unknown message type
- char unknown_msg[128];
- snprintf(unknown_msg, sizeof(unknown_msg), "Unknown message type: %.32s", msg_type);
- log_warning(unknown_msg);
- send_notice_message(wsi, "Unknown message type");
- }
- }
- }
-
- if (json) cJSON_Delete(json);
- free(message);
- }
- }
- break;
-
- case LWS_CALLBACK_CLOSED:
- log_info("WebSocket connection closed");
-
- // Clean up session subscriptions
- if (pss) {
- pthread_mutex_lock(&pss->session_lock);
-
- subscription_t* sub = pss->subscriptions;
- while (sub) {
- subscription_t* next = sub->session_next;
- remove_subscription_from_manager(sub->id, wsi);
- sub = next;
- }
-
- pss->subscriptions = NULL;
- pss->subscription_count = 0;
-
- pthread_mutex_unlock(&pss->session_lock);
- pthread_mutex_destroy(&pss->session_lock);
- }
- break;
-
- default:
- break;
- }
-
- return 0;
-}
-
-// WebSocket protocol definition
-static struct lws_protocols protocols[] = {
- {
- "nostr-relay-protocol",
- nostr_relay_callback,
- sizeof(struct per_session_data),
- 4096, // rx buffer size
- 0, NULL, 0
- },
- { NULL, NULL, 0, 0, 0, NULL, 0 } // terminator
-};
-
-// Check if a port is available for binding
-int check_port_available(int port) {
- int sockfd;
- struct sockaddr_in addr;
- int result;
- int reuse = 1;
-
- // Create a socket
- sockfd = socket(AF_INET, SOCK_STREAM, 0);
- if (sockfd < 0) {
- return 0; // Cannot create socket, assume port unavailable
- }
-
- // Set SO_REUSEADDR to allow binding to ports in TIME_WAIT state
- // This matches libwebsockets behavior and prevents false unavailability
- if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) {
- close(sockfd);
- return 0; // Failed to set socket option
- }
-
- // Set up the address structure
- memset(&addr, 0, sizeof(addr));
- addr.sin_family = AF_INET;
- addr.sin_addr.s_addr = INADDR_ANY;
- addr.sin_port = htons(port);
-
- // Try to bind to the port
- result = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
-
- // Close the socket
- close(sockfd);
-
- // Return 1 if bind succeeded (port available), 0 if failed (port in use)
- return (result == 0) ? 1 : 0;
-}
-
-// Start libwebsockets-based WebSocket Nostr relay server
-int start_websocket_relay(int port_override, int strict_port) {
- struct lws_context_creation_info info;
-
- log_info("Starting libwebsockets-based Nostr relay server...");
-
- memset(&info, 0, sizeof(info));
- // Use port override if provided, otherwise use configuration
- int configured_port = (port_override > 0) ? port_override : get_config_int("relay_port", DEFAULT_PORT);
- int actual_port = configured_port;
- int port_attempts = 0;
- const int max_port_attempts = 10; // Increased from 5 to 10
-
- // Minimal libwebsockets configuration
- info.protocols = protocols;
- info.gid = -1;
- info.uid = -1;
- info.options = LWS_SERVER_OPTION_VALIDATE_UTF8;
-
- // Remove interface restrictions - let system choose
- // info.vhost_name = NULL;
- // info.iface = NULL;
-
- // Increase max connections for relay usage
- info.max_http_header_pool = 16;
- info.timeout_secs = 10;
-
- // Max payload size for Nostr events
- info.max_http_header_data = 4096;
-
- // Find an available port with pre-checking (or fail immediately in strict mode)
- while (port_attempts < (strict_port ? 1 : max_port_attempts)) {
- char attempt_msg[256];
- snprintf(attempt_msg, sizeof(attempt_msg), "Checking port availability: %d", actual_port);
- log_info(attempt_msg);
-
- // Pre-check if port is available
- if (!check_port_available(actual_port)) {
- port_attempts++;
- if (strict_port) {
- char error_msg[256];
- snprintf(error_msg, sizeof(error_msg),
- "Strict port mode: port %d is not available", actual_port);
- log_error(error_msg);
- return -1;
- } else if (port_attempts < max_port_attempts) {
- char retry_msg[256];
- snprintf(retry_msg, sizeof(retry_msg), "Port %d is in use, trying port %d (attempt %d/%d)",
- actual_port, actual_port + 1, port_attempts + 1, max_port_attempts);
- log_warning(retry_msg);
- actual_port++;
- continue;
- } else {
- char error_msg[512];
- snprintf(error_msg, sizeof(error_msg),
- "Failed to find available port after %d attempts (tried ports %d-%d)",
- max_port_attempts, configured_port, actual_port);
- log_error(error_msg);
- return -1;
- }
- }
-
- // Port appears available, try creating libwebsockets context
- info.port = actual_port;
-
- char binding_msg[256];
- snprintf(binding_msg, sizeof(binding_msg), "Attempting to bind libwebsockets to port %d", actual_port);
- log_info(binding_msg);
-
- ws_context = lws_create_context(&info);
- if (ws_context) {
- // Success! Port binding worked
- break;
- }
-
- // libwebsockets failed even though port check passed
- // This could be due to timing or different socket options
- int errno_saved = errno;
- char lws_error_msg[256];
- snprintf(lws_error_msg, sizeof(lws_error_msg),
- "libwebsockets failed to bind to port %d (errno: %d)", actual_port, errno_saved);
- log_warning(lws_error_msg);
-
- port_attempts++;
- if (strict_port) {
- char error_msg[256];
- snprintf(error_msg, sizeof(error_msg),
- "Strict port mode: failed to bind to port %d", actual_port);
- log_error(error_msg);
- break;
- } else if (port_attempts < max_port_attempts) {
- actual_port++;
- continue;
- }
-
- // If we get here, we've exhausted attempts
- break;
- }
-
- if (!ws_context) {
- char error_msg[512];
- snprintf(error_msg, sizeof(error_msg),
- "Failed to create libwebsockets context after %d attempts. Last attempted port: %d",
- port_attempts, actual_port);
- log_error(error_msg);
- perror("libwebsockets creation error");
- return -1;
- }
-
- char startup_msg[256];
- if (actual_port != configured_port) {
- snprintf(startup_msg, sizeof(startup_msg),
- "WebSocket relay started on ws://127.0.0.1:%d (configured port %d was unavailable)",
- actual_port, configured_port);
- log_warning(startup_msg);
- } else {
- snprintf(startup_msg, sizeof(startup_msg), "WebSocket relay started on ws://127.0.0.1:%d", actual_port);
- }
- log_success(startup_msg);
-
- // Main event loop with proper signal handling
- while (g_server_running) {
- int result = lws_service(ws_context, 1000);
-
- if (result < 0) {
- log_error("libwebsockets service error");
- break;
- }
- }
-
- log_info("Shutting down WebSocket server...");
- lws_context_destroy(ws_context);
- ws_context = NULL;
-
- log_success("WebSocket relay shut down cleanly");
- return 0;
-}
-
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
// MAIN PROGRAM
diff --git a/src/nip009.c b/src/nip009.c
new file mode 100644
index 0000000..c393385
--- /dev/null
+++ b/src/nip009.c
@@ -0,0 +1,313 @@
+#define _GNU_SOURCE
+
+/////////////////////////////////////////////////////////////////////////////////////////
+/////////////////////////////////////////////////////////////////////////////////////////
+// NIP-09 EVENT DELETION REQUEST HANDLING
+/////////////////////////////////////////////////////////////////////////////////////////
+/////////////////////////////////////////////////////////////////////////////////////////
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+// Forward declarations for logging functions
+void log_warning(const char* message);
+void log_info(const char* message);
+
+// Forward declaration for database functions
+int store_event(cJSON* event);
+
+// Forward declarations for deletion functions
+int delete_events_by_id(const char* requester_pubkey, cJSON* event_ids);
+int delete_events_by_address(const char* requester_pubkey, cJSON* addresses, long deletion_timestamp);
+
+// Global database variable
+extern sqlite3* g_db;
+
+
+// Handle NIP-09 deletion request event (kind 5)
+int handle_deletion_request(cJSON* event, char* error_message, size_t error_size) {
+ if (!event) {
+ snprintf(error_message, error_size, "invalid: null deletion request");
+ return -1;
+ }
+
+ // Extract event details
+ cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
+ cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
+ cJSON* created_at_obj = cJSON_GetObjectItem(event, "created_at");
+ cJSON* tags_obj = cJSON_GetObjectItem(event, "tags");
+ cJSON* content_obj = cJSON_GetObjectItem(event, "content");
+ cJSON* event_id_obj = cJSON_GetObjectItem(event, "id");
+
+ if (!kind_obj || !pubkey_obj || !created_at_obj || !tags_obj || !event_id_obj) {
+ snprintf(error_message, error_size, "invalid: incomplete deletion request");
+ return -1;
+ }
+
+ int kind = (int)cJSON_GetNumberValue(kind_obj);
+ if (kind != 5) {
+ snprintf(error_message, error_size, "invalid: not a deletion request");
+ return -1;
+ }
+
+ const char* requester_pubkey = cJSON_GetStringValue(pubkey_obj);
+ // Extract deletion event ID and reason (for potential logging)
+ const char* deletion_event_id = cJSON_GetStringValue(event_id_obj);
+ const char* reason = content_obj ? cJSON_GetStringValue(content_obj) : "";
+ (void)deletion_event_id; // Mark as intentionally unused for now
+ (void)reason; // Mark as intentionally unused for now
+ long deletion_timestamp = (long)cJSON_GetNumberValue(created_at_obj);
+
+ if (!cJSON_IsArray(tags_obj)) {
+ snprintf(error_message, error_size, "invalid: deletion request tags must be an array");
+ return -1;
+ }
+
+ // Collect event IDs and addresses from tags
+ cJSON* event_ids = cJSON_CreateArray();
+ cJSON* addresses = cJSON_CreateArray();
+ cJSON* kinds_to_delete = cJSON_CreateArray();
+
+ int deletion_targets_found = 0;
+
+ cJSON* tag = NULL;
+ cJSON_ArrayForEach(tag, tags_obj) {
+ if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
+ continue;
+ }
+
+ cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
+ cJSON* tag_value = cJSON_GetArrayItem(tag, 1);
+
+ if (!cJSON_IsString(tag_name) || !cJSON_IsString(tag_value)) {
+ continue;
+ }
+
+ const char* name = cJSON_GetStringValue(tag_name);
+ const char* value = cJSON_GetStringValue(tag_value);
+
+ if (strcmp(name, "e") == 0) {
+ // Event ID reference
+ cJSON_AddItemToArray(event_ids, cJSON_CreateString(value));
+ deletion_targets_found++;
+ } else if (strcmp(name, "a") == 0) {
+ // Addressable event reference (kind:pubkey:d-identifier)
+ cJSON_AddItemToArray(addresses, cJSON_CreateString(value));
+ deletion_targets_found++;
+ } else if (strcmp(name, "k") == 0) {
+ // Kind hint - store for validation but not required
+ int kind_hint = atoi(value);
+ if (kind_hint > 0) {
+ cJSON_AddItemToArray(kinds_to_delete, cJSON_CreateNumber(kind_hint));
+ }
+ }
+ }
+
+ if (deletion_targets_found == 0) {
+ cJSON_Delete(event_ids);
+ cJSON_Delete(addresses);
+ cJSON_Delete(kinds_to_delete);
+ snprintf(error_message, error_size, "invalid: deletion request must contain 'e' or 'a' tags");
+ return -1;
+ }
+
+ int deleted_count = 0;
+
+ // Process event ID deletions
+ if (cJSON_GetArraySize(event_ids) > 0) {
+ int result = delete_events_by_id(requester_pubkey, event_ids);
+ if (result > 0) {
+ deleted_count += result;
+ }
+ }
+
+ // Process addressable event deletions
+ if (cJSON_GetArraySize(addresses) > 0) {
+ int result = delete_events_by_address(requester_pubkey, addresses, deletion_timestamp);
+ if (result > 0) {
+ deleted_count += result;
+ }
+ }
+
+ // Clean up
+ cJSON_Delete(event_ids);
+ cJSON_Delete(addresses);
+ cJSON_Delete(kinds_to_delete);
+
+ // Store the deletion request itself (it should be kept according to NIP-09)
+ if (store_event(event) != 0) {
+ log_warning("Failed to store deletion request event");
+ }
+
+ char debug_msg[256];
+ snprintf(debug_msg, sizeof(debug_msg), "Deletion request processed: %d events deleted", deleted_count);
+ log_info(debug_msg);
+
+ error_message[0] = '\0'; // Success - empty error message
+ return 0;
+}
+
+// Delete events by ID (with pubkey authorization)
+int delete_events_by_id(const char* requester_pubkey, cJSON* event_ids) {
+ if (!g_db || !requester_pubkey || !event_ids || !cJSON_IsArray(event_ids)) {
+ return 0;
+ }
+
+ int deleted_count = 0;
+
+ cJSON* event_id = NULL;
+ cJSON_ArrayForEach(event_id, event_ids) {
+ if (!cJSON_IsString(event_id)) {
+ continue;
+ }
+
+ const char* id = cJSON_GetStringValue(event_id);
+
+ // First check if event exists and if requester is authorized
+ const char* check_sql = "SELECT pubkey FROM events WHERE id = ?";
+ sqlite3_stmt* check_stmt;
+
+ int rc = sqlite3_prepare_v2(g_db, check_sql, -1, &check_stmt, NULL);
+ if (rc != SQLITE_OK) {
+ continue;
+ }
+
+ sqlite3_bind_text(check_stmt, 1, id, -1, SQLITE_STATIC);
+
+ if (sqlite3_step(check_stmt) == SQLITE_ROW) {
+ const char* event_pubkey = (char*)sqlite3_column_text(check_stmt, 0);
+
+ // Only delete if the requester is the author
+ if (event_pubkey && strcmp(event_pubkey, requester_pubkey) == 0) {
+ sqlite3_finalize(check_stmt);
+
+ // Delete the event
+ const char* delete_sql = "DELETE FROM events WHERE id = ? AND pubkey = ?";
+ sqlite3_stmt* delete_stmt;
+
+ rc = sqlite3_prepare_v2(g_db, delete_sql, -1, &delete_stmt, NULL);
+ if (rc == SQLITE_OK) {
+ sqlite3_bind_text(delete_stmt, 1, id, -1, SQLITE_STATIC);
+ sqlite3_bind_text(delete_stmt, 2, requester_pubkey, -1, SQLITE_STATIC);
+
+ if (sqlite3_step(delete_stmt) == SQLITE_DONE && sqlite3_changes(g_db) > 0) {
+ deleted_count++;
+
+ char debug_msg[128];
+ snprintf(debug_msg, sizeof(debug_msg), "Deleted event by ID: %.16s...", id);
+ log_info(debug_msg);
+ }
+ sqlite3_finalize(delete_stmt);
+ }
+ } else {
+ sqlite3_finalize(check_stmt);
+ char warning_msg[128];
+ snprintf(warning_msg, sizeof(warning_msg), "Unauthorized deletion attempt for event: %.16s...", id);
+ log_warning(warning_msg);
+ }
+ } else {
+ sqlite3_finalize(check_stmt);
+ char debug_msg[128];
+ snprintf(debug_msg, sizeof(debug_msg), "Event not found for deletion: %.16s...", id);
+ log_info(debug_msg);
+ }
+ }
+
+ return deleted_count;
+}
+
+// Delete events by addressable reference (kind:pubkey:d-identifier)
+int delete_events_by_address(const char* requester_pubkey, cJSON* addresses, long deletion_timestamp) {
+ if (!g_db || !requester_pubkey || !addresses || !cJSON_IsArray(addresses)) {
+ return 0;
+ }
+
+ int deleted_count = 0;
+
+ cJSON* address = NULL;
+ cJSON_ArrayForEach(address, addresses) {
+ if (!cJSON_IsString(address)) {
+ continue;
+ }
+
+ const char* addr = cJSON_GetStringValue(address);
+
+ // Parse address format: kind:pubkey:d-identifier
+ char* addr_copy = strdup(addr);
+ if (!addr_copy) continue;
+
+ char* kind_str = strtok(addr_copy, ":");
+ char* pubkey_str = strtok(NULL, ":");
+ char* d_identifier = strtok(NULL, ":");
+
+ if (!kind_str || !pubkey_str) {
+ free(addr_copy);
+ continue;
+ }
+
+ int kind = atoi(kind_str);
+
+ // Only delete if the requester is the author
+ if (strcmp(pubkey_str, requester_pubkey) != 0) {
+ free(addr_copy);
+ char warning_msg[128];
+ snprintf(warning_msg, sizeof(warning_msg), "Unauthorized deletion attempt for address: %.32s...", addr);
+ log_warning(warning_msg);
+ continue;
+ }
+
+ // Build deletion query based on whether we have d-identifier
+ const char* delete_sql;
+ sqlite3_stmt* delete_stmt;
+
+ if (d_identifier && strlen(d_identifier) > 0) {
+ // Delete specific addressable event with d-tag
+ delete_sql = "DELETE FROM events WHERE kind = ? AND pubkey = ? AND created_at <= ? "
+ "AND json_extract(tags, '$[*]') LIKE '%[\"d\",\"' || ? || '\"]%'";
+ } else {
+ // Delete all events of this kind by this author up to deletion timestamp
+ delete_sql = "DELETE FROM events WHERE kind = ? AND pubkey = ? AND created_at <= ?";
+ }
+
+ int rc = sqlite3_prepare_v2(g_db, delete_sql, -1, &delete_stmt, NULL);
+ if (rc == SQLITE_OK) {
+ sqlite3_bind_int(delete_stmt, 1, kind);
+ sqlite3_bind_text(delete_stmt, 2, requester_pubkey, -1, SQLITE_STATIC);
+ sqlite3_bind_int64(delete_stmt, 3, deletion_timestamp);
+
+ if (d_identifier && strlen(d_identifier) > 0) {
+ sqlite3_bind_text(delete_stmt, 4, d_identifier, -1, SQLITE_STATIC);
+ }
+
+ if (sqlite3_step(delete_stmt) == SQLITE_DONE) {
+ int changes = sqlite3_changes(g_db);
+ if (changes > 0) {
+ deleted_count += changes;
+
+ char debug_msg[128];
+ snprintf(debug_msg, sizeof(debug_msg), "Deleted %d events by address: %.32s...", changes, addr);
+ log_info(debug_msg);
+ }
+ }
+ sqlite3_finalize(delete_stmt);
+ }
+
+ free(addr_copy);
+ }
+
+ return deleted_count;
+}
+
+// Mark event as deleted (alternative to hard deletion - not used in current implementation)
+int mark_event_as_deleted(const char* event_id, const char* deletion_event_id, const char* reason) {
+ (void)event_id; (void)deletion_event_id; (void)reason; // Suppress unused warnings
+
+ // This function could be used if we wanted to implement soft deletion
+ // For now, NIP-09 implementation uses hard deletion as specified
+
+ return 0;
+}
diff --git a/src/nip011.c b/src/nip011.c
new file mode 100644
index 0000000..9fb6035
--- /dev/null
+++ b/src/nip011.c
@@ -0,0 +1,454 @@
+// NIP-11 Relay Information Document module
+#define _GNU_SOURCE
+#include
+#include
+#include
+#include
+#include
+#include "../nostr_core_lib/cjson/cJSON.h"
+#include "config.h"
+
+// Forward declarations for logging functions
+void log_info(const char* message);
+void log_success(const char* message);
+void log_error(const char* message);
+void log_warning(const char* message);
+
+// Forward declarations for configuration functions
+const char* get_config_value(const char* key);
+int get_config_int(const char* key, int default_value);
+int get_config_bool(const char* key, int default_value);
+
+// Forward declarations for global cache access
+extern unified_config_cache_t g_unified_cache;
+
+// Forward declarations for constants (defined in config.h and other headers)
+#define HTTP_STATUS_OK 200
+#define HTTP_STATUS_NOT_ACCEPTABLE 406
+#define HTTP_STATUS_INTERNAL_SERVER_ERROR 500
+
+
+/////////////////////////////////////////////////////////////////////////////////////////
+/////////////////////////////////////////////////////////////////////////////////////////
+// NIP-11 RELAY INFORMATION DOCUMENT
+/////////////////////////////////////////////////////////////////////////////////////////
+/////////////////////////////////////////////////////////////////////////////////////////
+
+// Initialize relay information using configuration system
+void init_relay_info() {
+ // Get all config values first (without holding mutex to avoid deadlock)
+ const char* relay_name = get_config_value("relay_name");
+ const char* relay_description = get_config_value("relay_description");
+ const char* relay_software = get_config_value("relay_software");
+ const char* relay_version = get_config_value("relay_version");
+ const char* relay_contact = get_config_value("relay_contact");
+ 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);
+ }
+
+ if (relay_pubkey) {
+ strncpy(g_unified_cache.relay_info.pubkey, relay_pubkey, sizeof(g_unified_cache.relay_info.pubkey) - 1);
+ }
+
+ // Initialize supported NIPs array
+ g_unified_cache.relay_info.supported_nips = cJSON_CreateArray();
+ if (g_unified_cache.relay_info.supported_nips) {
+ cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(1)); // NIP-01: Basic protocol
+ cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(9)); // NIP-09: Event deletion
+ cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(11)); // NIP-11: Relay information
+ cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(13)); // NIP-13: Proof of Work
+ cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(15)); // NIP-15: EOSE
+ cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(20)); // NIP-20: Command results
+ cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(40)); // NIP-40: Expiration Timestamp
+ cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(42)); // NIP-42: Authentication
+ }
+
+ // Initialize server limitations using configuration
+ 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_unified_cache.relay_info.retention = cJSON_CreateArray();
+
+ // Initialize language tags - set to global for now
+ g_unified_cache.relay_info.language_tags = cJSON_CreateArray();
+ if (g_unified_cache.relay_info.language_tags) {
+ cJSON_AddItemToArray(g_unified_cache.relay_info.language_tags, cJSON_CreateString("*"));
+ }
+
+ // Initialize relay countries - set to global for now
+ g_unified_cache.relay_info.relay_countries = cJSON_CreateArray();
+ if (g_unified_cache.relay_info.relay_countries) {
+ cJSON_AddItemToArray(g_unified_cache.relay_info.relay_countries, cJSON_CreateString("*"));
+ }
+
+ // Initialize content tags as empty array
+ g_unified_cache.relay_info.tags = cJSON_CreateArray();
+
+ // Initialize fees as empty object (no payment required by default)
+ 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() {
+ 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_unified_cache.relay_info.limitation) {
+ cJSON_Delete(g_unified_cache.relay_info.limitation);
+ g_unified_cache.relay_info.limitation = 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_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_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_unified_cache.relay_info.tags) {
+ cJSON_Delete(g_unified_cache.relay_info.tags);
+ g_unified_cache.relay_info.tags = 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
+cJSON* generate_relay_info_json() {
+ cJSON* info = cJSON_CreateObject();
+ if (!info) {
+ log_error("Failed to create relay info JSON object");
+ return NULL;
+ }
+
+ pthread_mutex_lock(&g_unified_cache.cache_lock);
+
+ // Add basic relay information
+ if (strlen(g_unified_cache.relay_info.name) > 0) {
+ cJSON_AddStringToObject(info, "name", g_unified_cache.relay_info.name);
+ }
+ if (strlen(g_unified_cache.relay_info.description) > 0) {
+ cJSON_AddStringToObject(info, "description", g_unified_cache.relay_info.description);
+ }
+ if (strlen(g_unified_cache.relay_info.banner) > 0) {
+ cJSON_AddStringToObject(info, "banner", g_unified_cache.relay_info.banner);
+ }
+ if (strlen(g_unified_cache.relay_info.icon) > 0) {
+ cJSON_AddStringToObject(info, "icon", g_unified_cache.relay_info.icon);
+ }
+ if (strlen(g_unified_cache.relay_info.pubkey) > 0) {
+ cJSON_AddStringToObject(info, "pubkey", g_unified_cache.relay_info.pubkey);
+ }
+ if (strlen(g_unified_cache.relay_info.contact) > 0) {
+ cJSON_AddStringToObject(info, "contact", g_unified_cache.relay_info.contact);
+ }
+
+ // Add supported NIPs
+ 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_unified_cache.relay_info.software) > 0) {
+ cJSON_AddStringToObject(info, "software", g_unified_cache.relay_info.software);
+ }
+ if (strlen(g_unified_cache.relay_info.version) > 0) {
+ cJSON_AddStringToObject(info, "version", g_unified_cache.relay_info.version);
+ }
+
+ // Add policies
+ 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_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_unified_cache.relay_info.posting_policy) > 0) {
+ cJSON_AddStringToObject(info, "posting_policy", g_unified_cache.relay_info.posting_policy);
+ }
+
+ // Add server limitations
+ 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_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_unified_cache.relay_info.relay_countries) {
+ cJSON_AddItemToObject(info, "relay_countries", cJSON_Duplicate(g_unified_cache.relay_info.relay_countries, 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_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_unified_cache.relay_info.payments_url) > 0) {
+ cJSON_AddStringToObject(info, "payments_url", g_unified_cache.relay_info.payments_url);
+ }
+ 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;
+}
+
+// NIP-11 HTTP session data structure for managing buffer lifetime
+struct nip11_session_data {
+ char* json_buffer;
+ size_t json_length;
+ int headers_sent;
+ int body_sent;
+};
+
+// Handle NIP-11 HTTP request with proper asynchronous buffer management
+int handle_nip11_http_request(struct lws* wsi, const char* accept_header) {
+ log_info("Handling NIP-11 relay information request");
+
+ // Check if client accepts application/nostr+json
+ int accepts_nostr_json = 0;
+ if (accept_header) {
+ if (strstr(accept_header, "application/nostr+json") != NULL) {
+ accepts_nostr_json = 1;
+ }
+ }
+
+ if (!accepts_nostr_json) {
+ log_warning("HTTP request without proper Accept header for NIP-11");
+ // Return 406 Not Acceptable
+ unsigned char buf[LWS_PRE + 256];
+ unsigned char *p = &buf[LWS_PRE];
+ unsigned char *start = p;
+ unsigned char *end = &buf[sizeof(buf) - 1];
+
+ if (lws_add_http_header_status(wsi, HTTP_STATUS_NOT_ACCEPTABLE, &p, end)) {
+ return -1;
+ }
+ if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE, (unsigned char*)"text/plain", 10, &p, end)) {
+ return -1;
+ }
+ if (lws_add_http_header_content_length(wsi, 0, &p, end)) {
+ return -1;
+ }
+ if (lws_finalize_http_header(wsi, &p, end)) {
+ return -1;
+ }
+ lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS);
+ return -1; // Close connection
+ }
+
+ // Generate relay information JSON
+ cJSON* info_json = generate_relay_info_json();
+ if (!info_json) {
+ log_error("Failed to generate relay info JSON");
+ unsigned char buf[LWS_PRE + 256];
+ unsigned char *p = &buf[LWS_PRE];
+ unsigned char *start = p;
+ unsigned char *end = &buf[sizeof(buf) - 1];
+
+ if (lws_add_http_header_status(wsi, HTTP_STATUS_INTERNAL_SERVER_ERROR, &p, end)) {
+ return -1;
+ }
+ if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE, (unsigned char*)"text/plain", 10, &p, end)) {
+ return -1;
+ }
+ if (lws_add_http_header_content_length(wsi, 0, &p, end)) {
+ return -1;
+ }
+ if (lws_finalize_http_header(wsi, &p, end)) {
+ return -1;
+ }
+ lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS);
+ return -1;
+ }
+
+ char* json_string = cJSON_Print(info_json);
+ cJSON_Delete(info_json);
+
+ if (!json_string) {
+ log_error("Failed to serialize relay info JSON");
+ unsigned char buf[LWS_PRE + 256];
+ unsigned char *p = &buf[LWS_PRE];
+ unsigned char *start = p;
+ unsigned char *end = &buf[sizeof(buf) - 1];
+
+ if (lws_add_http_header_status(wsi, HTTP_STATUS_INTERNAL_SERVER_ERROR, &p, end)) {
+ return -1;
+ }
+ if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE, (unsigned char*)"text/plain", 10, &p, end)) {
+ return -1;
+ }
+ if (lws_add_http_header_content_length(wsi, 0, &p, end)) {
+ return -1;
+ }
+ if (lws_finalize_http_header(wsi, &p, end)) {
+ return -1;
+ }
+ lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS);
+ return -1;
+ }
+
+ size_t json_len = strlen(json_string);
+
+ // Allocate session data to manage buffer lifetime across callbacks
+ struct nip11_session_data* session_data = malloc(sizeof(struct nip11_session_data));
+ if (!session_data) {
+ log_error("Failed to allocate NIP-11 session data");
+ free(json_string);
+ return -1;
+ }
+
+ // Store JSON buffer in session data for asynchronous handling
+ session_data->json_buffer = json_string;
+ session_data->json_length = json_len;
+ session_data->headers_sent = 0;
+ session_data->body_sent = 0;
+
+ // Store session data in WSI user data for callback access
+ lws_set_wsi_user(wsi, session_data);
+
+ // Prepare HTTP response with CORS headers
+ unsigned char buf[LWS_PRE + 1024];
+ unsigned char *p = &buf[LWS_PRE];
+ unsigned char *start = p;
+ unsigned char *end = &buf[sizeof(buf) - 1];
+
+ // Add status
+ if (lws_add_http_header_status(wsi, HTTP_STATUS_OK, &p, end)) {
+ free(session_data->json_buffer);
+ free(session_data);
+ return -1;
+ }
+
+ // Add content type
+ if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE,
+ (unsigned char*)"application/nostr+json", 22, &p, end)) {
+ free(session_data->json_buffer);
+ free(session_data);
+ return -1;
+ }
+
+ // Add content length
+ if (lws_add_http_header_content_length(wsi, json_len, &p, end)) {
+ free(session_data->json_buffer);
+ free(session_data);
+ return -1;
+ }
+
+ // Add CORS headers as required by NIP-11
+ if (lws_add_http_header_by_name(wsi, (unsigned char*)"access-control-allow-origin:",
+ (unsigned char*)"*", 1, &p, end)) {
+ free(session_data->json_buffer);
+ free(session_data);
+ return -1;
+ }
+ if (lws_add_http_header_by_name(wsi, (unsigned char*)"access-control-allow-headers:",
+ (unsigned char*)"content-type, accept", 20, &p, end)) {
+ free(session_data->json_buffer);
+ free(session_data);
+ return -1;
+ }
+ if (lws_add_http_header_by_name(wsi, (unsigned char*)"access-control-allow-methods:",
+ (unsigned char*)"GET, OPTIONS", 12, &p, end)) {
+ free(session_data->json_buffer);
+ free(session_data);
+ return -1;
+ }
+
+ // Finalize headers
+ if (lws_finalize_http_header(wsi, &p, end)) {
+ free(session_data->json_buffer);
+ free(session_data);
+ return -1;
+ }
+
+ // Write headers
+ if (lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS) < 0) {
+ free(session_data->json_buffer);
+ free(session_data);
+ return -1;
+ }
+
+ session_data->headers_sent = 1;
+
+ // Request callback for body transmission
+ lws_callback_on_writable(wsi);
+
+ log_success("NIP-11 headers sent, body transmission scheduled");
+ return 0;
+}
+
diff --git a/src/nip013.c b/src/nip013.c
new file mode 100644
index 0000000..a8b91e0
--- /dev/null
+++ b/src/nip013.c
@@ -0,0 +1,190 @@
+// NIP-13 Proof of Work validation module
+#include
+#include
+#include
+#include
+#include "../nostr_core_lib/cjson/cJSON.h"
+#include "../nostr_core_lib/nostr_core/nostr_core.h"
+#include "../nostr_core_lib/nostr_core/nip013.h"
+#include "config.h"
+
+// Forward declarations for logging functions
+void log_info(const char* message);
+void log_success(const char* message);
+void log_error(const char* message);
+void log_warning(const char* message);
+
+// NIP-13 PoW configuration structure
+struct pow_config {
+ int enabled; // 0 = disabled, 1 = enabled
+ int min_pow_difficulty; // Minimum required difficulty (0 = no requirement)
+ int validation_flags; // Bitflags for validation options
+ int require_nonce_tag; // 1 = require nonce tag presence
+ int reject_lower_targets; // 1 = reject if committed < actual difficulty
+ int strict_format; // 1 = enforce strict nonce tag format
+ int anti_spam_mode; // 1 = full anti-spam validation
+};
+
+// Initialize PoW configuration using configuration system
+void init_pow_config() {
+ log_info("Initializing NIP-13 Proof of Work 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_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_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_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_unified_cache.pow_config.enabled = 0;
+ log_info("PoW validation disabled via configuration");
+ }
+ } else {
+ // Default to basic mode
+ g_unified_cache.pow_config.validation_flags = NOSTR_POW_VALIDATE_BASIC;
+ log_info("PoW configured in basic validation mode (default)");
+ }
+
+ // Log final configuration
+ char config_msg[512];
+ snprintf(config_msg, sizeof(config_msg),
+ "PoW Configuration: enabled=%s, min_difficulty=%d, validation_flags=0x%x, mode=%s",
+ 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) {
+ 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
+ }
+
+ if (!event) {
+ snprintf(error_message, error_size, "pow: null event");
+ return NOSTR_ERROR_INVALID_INPUT;
+ }
+
+ // If min_pow_difficulty is 0, only validate events that have nonce tags
+ // This allows events without PoW when difficulty requirement is 0
+ if (min_pow_difficulty == 0) {
+ cJSON* tags = cJSON_GetObjectItem(event, "tags");
+ int has_nonce_tag = 0;
+
+ if (tags && cJSON_IsArray(tags)) {
+ cJSON* tag = NULL;
+ cJSON_ArrayForEach(tag, tags) {
+ if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
+ cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
+ if (cJSON_IsString(tag_name)) {
+ const char* name = cJSON_GetStringValue(tag_name);
+ if (name && strcmp(name, "nonce") == 0) {
+ has_nonce_tag = 1;
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ // If no minimum difficulty required and no nonce tag, skip PoW validation
+ if (!has_nonce_tag) {
+ return 0; // Accept event without PoW when min_difficulty=0
+ }
+ }
+
+ // Perform PoW validation using nostr_core_lib
+ nostr_pow_result_t 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
+ switch (validation_result) {
+ case NOSTR_ERROR_NIP13_INSUFFICIENT:
+ snprintf(error_message, error_size,
+ "pow: insufficient difficulty: %d < %d",
+ 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 (min_pow_difficulty > 0) {
+ snprintf(error_message, error_size, "pow: missing required nonce tag");
+ log_warning("Event rejected: missing nonce tag");
+ } else {
+ return 0; // Allow when min_difficulty=0
+ }
+ break;
+ case NOSTR_ERROR_NIP13_INVALID_NONCE_TAG:
+ snprintf(error_message, error_size, "pow: invalid nonce tag format");
+ log_warning("Event rejected: invalid nonce tag format");
+ break;
+ case NOSTR_ERROR_NIP13_TARGET_MISMATCH:
+ snprintf(error_message, error_size,
+ "pow: committed target (%d) lower than minimum (%d)",
+ pow_result.committed_target, min_pow_difficulty);
+ log_warning("Event rejected: committed target too low (anti-spam protection)");
+ break;
+ case NOSTR_ERROR_NIP13_CALCULATION:
+ snprintf(error_message, error_size, "pow: difficulty calculation failed");
+ log_error("PoW difficulty calculation error");
+ break;
+ case NOSTR_ERROR_EVENT_INVALID_ID:
+ snprintf(error_message, error_size, "pow: invalid event ID format");
+ log_warning("Event rejected: invalid event ID for PoW calculation");
+ break;
+ default:
+ snprintf(error_message, error_size, "pow: validation failed - %s",
+ strlen(pow_result.error_detail) > 0 ? pow_result.error_detail : "unknown error");
+ log_warning("Event rejected: PoW validation failed");
+ }
+ return validation_result;
+ }
+
+ // Log successful PoW validation (only if minimum difficulty is required)
+ 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",
+ pow_result.actual_difficulty,
+ pow_result.committed_target,
+ (unsigned long long)pow_result.nonce_value,
+ pow_result.has_nonce_tag ? "" : " (no nonce tag)");
+ log_info(debug_msg);
+ }
+
+ return 0; // Success
+}
\ No newline at end of file
diff --git a/src/nip040.c b/src/nip040.c
new file mode 100644
index 0000000..8bc8e32
--- /dev/null
+++ b/src/nip040.c
@@ -0,0 +1,173 @@
+#define _GNU_SOURCE
+#include
+#include
+#include
+#include
+
+// Include nostr_core_lib for cJSON
+#include "../nostr_core_lib/cjson/cJSON.h"
+
+// Configuration management system
+#include "config.h"
+
+// NIP-40 Expiration configuration structure
+struct expiration_config {
+ int enabled; // 0 = disabled, 1 = enabled
+ int strict_mode; // 1 = reject expired events on submission
+ int filter_responses; // 1 = filter expired events from responses
+ int delete_expired; // 1 = delete expired events from DB (future feature)
+ long grace_period; // Grace period in seconds for clock skew
+};
+
+// Global expiration configuration instance
+struct expiration_config g_expiration_config = {
+ .enabled = 1, // Enable expiration handling by default
+ .strict_mode = 1, // Reject expired events on submission by default
+ .filter_responses = 1, // Filter expired events from responses by default
+ .delete_expired = 0, // Don't delete by default (keep for audit)
+ .grace_period = 1 // 1 second grace period for testing (was 300)
+};
+
+// Forward declarations for logging functions
+void log_info(const char* message);
+void log_warning(const char* message);
+
+// Initialize expiration configuration using configuration system
+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);
+
+ // Load expiration settings from configuration system
+ g_expiration_config.enabled = expiration_enabled;
+ g_expiration_config.strict_mode = expiration_strict;
+ g_expiration_config.filter_responses = expiration_filter;
+ g_expiration_config.delete_expired = expiration_delete;
+ g_expiration_config.grace_period = expiration_grace_period;
+
+ // Validate grace period bounds
+ if (g_expiration_config.grace_period < 0 || g_expiration_config.grace_period > 86400) {
+ log_warning("Invalid grace period, using default of 300 seconds");
+ g_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);
+ log_info(config_msg);
+}
+
+// Extract expiration timestamp from event tags
+long extract_expiration_timestamp(cJSON* tags) {
+ if (!tags || !cJSON_IsArray(tags)) {
+ return 0; // No expiration
+ }
+
+ cJSON* tag = NULL;
+ cJSON_ArrayForEach(tag, tags) {
+ if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
+ cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
+ cJSON* tag_value = cJSON_GetArrayItem(tag, 1);
+
+ if (cJSON_IsString(tag_name) && cJSON_IsString(tag_value)) {
+ const char* name = cJSON_GetStringValue(tag_name);
+ const char* value = cJSON_GetStringValue(tag_value);
+
+ if (name && value && strcmp(name, "expiration") == 0) {
+ // Validate that the string contains only digits (and optional leading whitespace)
+ const char* p = value;
+
+ // Skip leading whitespace
+ while (*p == ' ' || *p == '\t') p++;
+
+ // Check if we have at least one digit
+ if (*p == '\0') {
+ continue; // Empty or whitespace-only string, ignore this tag
+ }
+
+ // Validate that all remaining characters are digits
+ const char* digit_start = p;
+ while (*p >= '0' && *p <= '9') p++;
+
+ // If we didn't consume the entire string or found no digits, it's malformed
+ if (*p != '\0' || p == digit_start) {
+ char debug_msg[256];
+ snprintf(debug_msg, sizeof(debug_msg),
+ "Ignoring malformed expiration tag value: '%.32s'", value);
+ log_warning(debug_msg);
+ continue; // Ignore malformed expiration tag
+ }
+
+ long expiration_ts = atol(value);
+ if (expiration_ts > 0) {
+ return expiration_ts;
+ }
+ }
+ }
+ }
+ }
+
+ return 0; // No valid expiration tag found
+}
+
+// Check if event is currently expired
+int is_event_expired(cJSON* event, time_t current_time) {
+ if (!event) {
+ return 0; // Invalid event, not expired
+ }
+
+ cJSON* tags = cJSON_GetObjectItem(event, "tags");
+ long expiration_ts = extract_expiration_timestamp(tags);
+
+ if (expiration_ts == 0) {
+ return 0; // No expiration timestamp, not expired
+ }
+
+ // Check if current time exceeds expiration + grace period
+ return (current_time > (expiration_ts + g_expiration_config.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) {
+ return 0; // Expiration validation disabled
+ }
+
+ if (!event) {
+ snprintf(error_message, error_size, "expiration: null event");
+ return -1;
+ }
+
+ // Check if event is expired
+ time_t current_time = time(NULL);
+ if (is_event_expired(event, current_time)) {
+ if (g_expiration_config.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);
+ log_warning("Event rejected: expired timestamp");
+ return -1;
+ } else {
+ // In non-strict mode, log but allow expired events
+ char debug_msg[256];
+ snprintf(debug_msg, sizeof(debug_msg),
+ "Accepting expired event (strict_mode disabled)");
+ log_info(debug_msg);
+ }
+ }
+
+ return 0; // Success
+}
\ No newline at end of file
diff --git a/src/nip042.c b/src/nip042.c
new file mode 100644
index 0000000..e8177c8
--- /dev/null
+++ b/src/nip042.c
@@ -0,0 +1,180 @@
+#define _GNU_SOURCE
+
+/////////////////////////////////////////////////////////////////////////////////////////
+/////////////////////////////////////////////////////////////////////////////////////////
+// NIP-42 AUTHENTICATION FUNCTIONS
+/////////////////////////////////////////////////////////////////////////////////////////
+/////////////////////////////////////////////////////////////////////////////////////////
+#include
+#include
+#include
+#include
+#include
+#include
+
+// Forward declarations for logging functions
+void log_error(const char* message);
+void log_info(const char* message);
+void log_warning(const char* message);
+void log_success(const char* message);
+
+// Forward declaration for notice message function
+void send_notice_message(struct lws* wsi, const char* message);
+
+// Forward declarations for NIP-42 functions from request_validator.c
+int nostr_nip42_generate_challenge(char *challenge_buffer, size_t buffer_size);
+int nostr_nip42_verify_auth_event(cJSON *event, const char *challenge_id,
+ const char *relay_url, int time_tolerance_seconds);
+
+// Forward declaration for per_session_data struct (defined in main.c)
+struct per_session_data {
+ int authenticated;
+ void* subscriptions; // Head of this session's subscription list
+ pthread_mutex_t session_lock; // Per-session thread safety
+ char client_ip[41]; // Client IP for logging
+ int subscription_count; // Number of subscriptions for this session
+
+ // NIP-42 Authentication State
+ char authenticated_pubkey[65]; // Authenticated public key (64 hex + null)
+ char active_challenge[65]; // Current challenge for this session (64 hex + null)
+ time_t challenge_created; // When challenge was created
+ time_t challenge_expires; // Challenge expiration time
+ int nip42_auth_required_events; // Whether NIP-42 auth is required for EVENT submission
+ int nip42_auth_required_subscriptions; // Whether NIP-42 auth is required for REQ operations
+ int auth_challenge_sent; // Whether challenge has been sent (0/1)
+};
+
+
+// Send NIP-42 authentication challenge to client
+void send_nip42_auth_challenge(struct lws* wsi, struct per_session_data* pss) {
+ if (!wsi || !pss) return;
+
+ // Generate challenge using existing request_validator function
+ char challenge[65];
+ if (nostr_nip42_generate_challenge(challenge, sizeof(challenge)) != 0) {
+ log_error("Failed to generate NIP-42 challenge");
+ send_notice_message(wsi, "Authentication temporarily unavailable");
+ return;
+ }
+
+ // Store challenge in session
+ pthread_mutex_lock(&pss->session_lock);
+ strncpy(pss->active_challenge, challenge, sizeof(pss->active_challenge) - 1);
+ pss->active_challenge[sizeof(pss->active_challenge) - 1] = '\0';
+ pss->challenge_created = time(NULL);
+ pss->challenge_expires = pss->challenge_created + 600; // 10 minutes
+ pss->auth_challenge_sent = 1;
+ pthread_mutex_unlock(&pss->session_lock);
+
+ // Send AUTH challenge message: ["AUTH", ]
+ cJSON* auth_msg = cJSON_CreateArray();
+ cJSON_AddItemToArray(auth_msg, cJSON_CreateString("AUTH"));
+ cJSON_AddItemToArray(auth_msg, cJSON_CreateString(challenge));
+
+ char* msg_str = cJSON_Print(auth_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);
+ free(buf);
+ }
+ free(msg_str);
+ }
+ cJSON_Delete(auth_msg);
+
+ char debug_msg[128];
+ snprintf(debug_msg, sizeof(debug_msg), "NIP-42 auth challenge sent: %.16s...", challenge);
+ log_info(debug_msg);
+}
+
+// Handle NIP-42 signed authentication event from client
+void handle_nip42_auth_signed_event(struct lws* wsi, struct per_session_data* pss, cJSON* auth_event) {
+ if (!wsi || !pss || !auth_event) return;
+
+ // Serialize event for validation
+ char* event_json = cJSON_Print(auth_event);
+ if (!event_json) {
+ send_notice_message(wsi, "Invalid authentication event format");
+ return;
+ }
+
+ pthread_mutex_lock(&pss->session_lock);
+ char challenge_copy[65];
+ strncpy(challenge_copy, pss->active_challenge, sizeof(challenge_copy) - 1);
+ challenge_copy[sizeof(challenge_copy) - 1] = '\0';
+ time_t challenge_expires = pss->challenge_expires;
+ pthread_mutex_unlock(&pss->session_lock);
+
+ // Check if challenge has expired
+ time_t current_time = time(NULL);
+ if (current_time > challenge_expires) {
+ free(event_json);
+ send_notice_message(wsi, "Authentication challenge expired, please retry");
+ log_warning("NIP-42 authentication failed: challenge expired");
+ return;
+ }
+
+ // Verify authentication using existing request_validator function
+ // Note: nostr_nip42_verify_auth_event doesn't extract pubkey, we need to do that separately
+ int result = nostr_nip42_verify_auth_event(auth_event, challenge_copy,
+ "ws://localhost:8888", 600); // 10 minutes tolerance
+
+ char authenticated_pubkey[65] = {0};
+ if (result == 0) {
+ // Extract pubkey from the auth event
+ cJSON* pubkey_json = cJSON_GetObjectItem(auth_event, "pubkey");
+ if (pubkey_json && cJSON_IsString(pubkey_json)) {
+ const char* pubkey_str = cJSON_GetStringValue(pubkey_json);
+ if (pubkey_str && strlen(pubkey_str) == 64) {
+ strncpy(authenticated_pubkey, pubkey_str, sizeof(authenticated_pubkey) - 1);
+ authenticated_pubkey[sizeof(authenticated_pubkey) - 1] = '\0';
+ } else {
+ result = -1; // Invalid pubkey format
+ }
+ } else {
+ result = -1; // Missing pubkey
+ }
+ }
+
+ free(event_json);
+
+ if (result == 0) {
+ // Authentication successful
+ pthread_mutex_lock(&pss->session_lock);
+ pss->authenticated = 1;
+ strncpy(pss->authenticated_pubkey, authenticated_pubkey, sizeof(pss->authenticated_pubkey) - 1);
+ pss->authenticated_pubkey[sizeof(pss->authenticated_pubkey) - 1] = '\0';
+ // Clear challenge
+ memset(pss->active_challenge, 0, sizeof(pss->active_challenge));
+ pss->challenge_expires = 0;
+ pss->auth_challenge_sent = 0;
+ pthread_mutex_unlock(&pss->session_lock);
+
+ char success_msg[256];
+ snprintf(success_msg, sizeof(success_msg),
+ "NIP-42 authentication successful for pubkey: %.16s...", authenticated_pubkey);
+ log_success(success_msg);
+
+ send_notice_message(wsi, "NIP-42 authentication successful");
+ } else {
+ // Authentication failed
+ char error_msg[256];
+ snprintf(error_msg, sizeof(error_msg),
+ "NIP-42 authentication failed (error code: %d)", result);
+ log_warning(error_msg);
+
+ send_notice_message(wsi, "NIP-42 authentication failed - invalid signature or challenge");
+ }
+}
+
+// Handle challenge response (not typically used in NIP-42, but included for completeness)
+void handle_nip42_auth_challenge_response(struct lws* wsi, struct per_session_data* pss, const char* challenge) {
+ (void)wsi; (void)pss; (void)challenge; // Mark as intentionally unused
+
+ // NIP-42 doesn't typically use challenge responses from client to server
+ // This is reserved for potential future use or protocol extensions
+ log_warning("Received unexpected challenge response from client (not part of standard NIP-42 flow)");
+ send_notice_message(wsi, "Challenge responses are not supported - please send signed authentication event");
+}
diff --git a/src/request_validator.c b/src/request_validator.c
index c9d01e8..68803ab 100644
--- a/src/request_validator.c
+++ b/src/request_validator.c
@@ -169,8 +169,8 @@ static void validator_debug_log(const char *message) {
static int reload_auth_config(void);
// Removed unused forward declarations for functions that are no longer called
-static int check_database_auth_rules(const char *pubkey, const char *operation,
- const char *resource_hash);
+int check_database_auth_rules(const char *pubkey, const char *operation,
+ const char *resource_hash);
void nostr_request_validator_clear_violation(void);
// NIP-42 challenge management functions
@@ -595,8 +595,8 @@ static int reload_auth_config(void) {
* Check database authentication rules for the request
* Implements the 6-step rule evaluation engine from AUTH_API.md
*/
-static int check_database_auth_rules(const char *pubkey, const char *operation,
- const char *resource_hash) {
+int check_database_auth_rules(const char *pubkey, const char *operation,
+ const char *resource_hash) {
sqlite3 *db = NULL;
sqlite3_stmt *stmt = NULL;
int rc;
diff --git a/src/subscriptions.c b/src/subscriptions.c
new file mode 100644
index 0000000..2509bb8
--- /dev/null
+++ b/src/subscriptions.c
@@ -0,0 +1,723 @@
+#define _GNU_SOURCE
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "subscriptions.h"
+
+// Forward declarations for logging functions
+void log_info(const char* message);
+void log_error(const char* message);
+void log_warning(const char* message);
+
+// Forward declarations for configuration functions
+const char* get_config_value(const char* key);
+
+// Forward declarations for NIP-40 expiration functions
+int is_event_expired(cJSON* event, time_t current_time);
+
+// Global database variable
+extern sqlite3* g_db;
+
+// Global unified cache
+extern unified_config_cache_t g_unified_cache;
+
+// Global subscription manager
+extern subscription_manager_t g_subscription_manager;
+
+
+/////////////////////////////////////////////////////////////////////////////////////////
+/////////////////////////////////////////////////////////////////////////////////////////
+// PERSISTENT SUBSCRIPTIONS SYSTEM
+/////////////////////////////////////////////////////////////////////////////////////////
+/////////////////////////////////////////////////////////////////////////////////////////
+
+// Create a subscription filter from cJSON filter object
+subscription_filter_t* create_subscription_filter(cJSON* filter_json) {
+ if (!filter_json || !cJSON_IsObject(filter_json)) {
+ return NULL;
+ }
+
+ subscription_filter_t* filter = calloc(1, sizeof(subscription_filter_t));
+ if (!filter) {
+ return NULL;
+ }
+
+ // Copy filter criteria
+ cJSON* kinds = cJSON_GetObjectItem(filter_json, "kinds");
+ if (kinds && cJSON_IsArray(kinds)) {
+ filter->kinds = cJSON_Duplicate(kinds, 1);
+ }
+
+ cJSON* authors = cJSON_GetObjectItem(filter_json, "authors");
+ if (authors && cJSON_IsArray(authors)) {
+ filter->authors = cJSON_Duplicate(authors, 1);
+ }
+
+ cJSON* ids = cJSON_GetObjectItem(filter_json, "ids");
+ if (ids && cJSON_IsArray(ids)) {
+ filter->ids = cJSON_Duplicate(ids, 1);
+ }
+
+ cJSON* since = cJSON_GetObjectItem(filter_json, "since");
+ if (since && cJSON_IsNumber(since)) {
+ filter->since = (long)cJSON_GetNumberValue(since);
+ }
+
+ cJSON* until = cJSON_GetObjectItem(filter_json, "until");
+ if (until && cJSON_IsNumber(until)) {
+ filter->until = (long)cJSON_GetNumberValue(until);
+ }
+
+ cJSON* limit = cJSON_GetObjectItem(filter_json, "limit");
+ if (limit && cJSON_IsNumber(limit)) {
+ filter->limit = (int)cJSON_GetNumberValue(limit);
+ }
+
+ // Handle tag filters (e.g., {"#e": ["id1"], "#p": ["pubkey1"]})
+ cJSON* item = NULL;
+ cJSON_ArrayForEach(item, filter_json) {
+ if (item->string && strlen(item->string) >= 2 && item->string[0] == '#') {
+ if (!filter->tag_filters) {
+ filter->tag_filters = cJSON_CreateObject();
+ }
+ if (filter->tag_filters) {
+ cJSON_AddItemToObject(filter->tag_filters, item->string, cJSON_Duplicate(item, 1));
+ }
+ }
+ }
+
+ return filter;
+}
+
+// Free a subscription filter
+void free_subscription_filter(subscription_filter_t* filter) {
+ if (!filter) return;
+
+ if (filter->kinds) cJSON_Delete(filter->kinds);
+ if (filter->authors) cJSON_Delete(filter->authors);
+ if (filter->ids) cJSON_Delete(filter->ids);
+ if (filter->tag_filters) cJSON_Delete(filter->tag_filters);
+
+ if (filter->next) {
+ free_subscription_filter(filter->next);
+ }
+
+ free(filter);
+}
+
+// Create a new subscription
+subscription_t* create_subscription(const char* sub_id, struct lws* wsi, cJSON* filters_array, const char* client_ip) {
+ if (!sub_id || !wsi || !filters_array) {
+ return NULL;
+ }
+
+ subscription_t* sub = calloc(1, sizeof(subscription_t));
+ if (!sub) {
+ return NULL;
+ }
+
+ // Copy subscription ID (truncate if too long)
+ strncpy(sub->id, sub_id, SUBSCRIPTION_ID_MAX_LENGTH - 1);
+ sub->id[SUBSCRIPTION_ID_MAX_LENGTH - 1] = '\0';
+
+ // Set WebSocket connection
+ sub->wsi = wsi;
+
+ // Set client IP
+ if (client_ip) {
+ strncpy(sub->client_ip, client_ip, CLIENT_IP_MAX_LENGTH - 1);
+ sub->client_ip[CLIENT_IP_MAX_LENGTH - 1] = '\0';
+ }
+
+ // Set timestamps and state
+ sub->created_at = time(NULL);
+ sub->events_sent = 0;
+ sub->active = 1;
+
+ // Convert filters array to linked list
+ subscription_filter_t* filter_tail = NULL;
+ int filter_count = 0;
+
+ if (cJSON_IsArray(filters_array)) {
+ cJSON* filter_json = NULL;
+ cJSON_ArrayForEach(filter_json, filters_array) {
+ if (filter_count >= MAX_FILTERS_PER_SUBSCRIPTION) {
+ log_warning("Maximum filters per subscription exceeded, ignoring excess filters");
+ break;
+ }
+
+ subscription_filter_t* filter = create_subscription_filter(filter_json);
+ if (filter) {
+ if (!sub->filters) {
+ sub->filters = filter;
+ filter_tail = filter;
+ } else {
+ filter_tail->next = filter;
+ filter_tail = filter;
+ }
+ filter_count++;
+ }
+ }
+ }
+
+ if (filter_count == 0) {
+ log_error("No valid filters found for subscription");
+ free(sub);
+ return NULL;
+ }
+
+ return sub;
+}
+
+// Free a subscription
+void free_subscription(subscription_t* sub) {
+ if (!sub) return;
+
+ if (sub->filters) {
+ free_subscription_filter(sub->filters);
+ }
+
+ free(sub);
+}
+
+// Add subscription to global manager (thread-safe)
+int add_subscription_to_manager(subscription_t* sub) {
+ if (!sub) return -1;
+
+ pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
+
+ // Check global limits
+ if (g_subscription_manager.total_subscriptions >= g_subscription_manager.max_total_subscriptions) {
+ pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
+ log_error("Maximum total subscriptions reached");
+ return -1;
+ }
+
+ // Add to global list
+ sub->next = g_subscription_manager.active_subscriptions;
+ g_subscription_manager.active_subscriptions = sub;
+ g_subscription_manager.total_subscriptions++;
+ g_subscription_manager.total_created++;
+
+ pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
+
+ // Log subscription creation to database
+ log_subscription_created(sub);
+
+ char debug_msg[256];
+ snprintf(debug_msg, sizeof(debug_msg), "Added subscription '%s' (total: %d)",
+ sub->id, g_subscription_manager.total_subscriptions);
+ log_info(debug_msg);
+
+ return 0;
+}
+
+// Remove subscription from global manager (thread-safe)
+int remove_subscription_from_manager(const char* sub_id, struct lws* wsi) {
+ if (!sub_id) return -1;
+
+ pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
+
+ subscription_t** current = &g_subscription_manager.active_subscriptions;
+
+ while (*current) {
+ subscription_t* sub = *current;
+
+ // Match by ID and WebSocket connection
+ if (strcmp(sub->id, sub_id) == 0 && (!wsi || sub->wsi == wsi)) {
+ // Remove from list
+ *current = sub->next;
+ g_subscription_manager.total_subscriptions--;
+
+ pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
+
+ // Log subscription closure to database
+ log_subscription_closed(sub_id, sub->client_ip, "closed");
+
+ // Update events sent counter before freeing
+ update_subscription_events_sent(sub_id, sub->events_sent);
+
+ char debug_msg[256];
+ snprintf(debug_msg, sizeof(debug_msg), "Removed subscription '%s' (total: %d)",
+ sub_id, g_subscription_manager.total_subscriptions);
+ log_info(debug_msg);
+
+ free_subscription(sub);
+ return 0;
+ }
+
+ current = &(sub->next);
+ }
+
+ pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
+
+ char debug_msg[256];
+ snprintf(debug_msg, sizeof(debug_msg), "Subscription '%s' not found for removal", sub_id);
+ log_warning(debug_msg);
+
+ return -1;
+}
+
+// Check if an event matches a subscription filter
+int event_matches_filter(cJSON* event, subscription_filter_t* filter) {
+ if (!event || !filter) {
+ return 0;
+ }
+
+ // Check kinds filter
+ if (filter->kinds && cJSON_IsArray(filter->kinds)) {
+ cJSON* event_kind = cJSON_GetObjectItem(event, "kind");
+ if (!event_kind || !cJSON_IsNumber(event_kind)) {
+ return 0;
+ }
+
+ int event_kind_val = (int)cJSON_GetNumberValue(event_kind);
+ int kind_match = 0;
+
+ cJSON* kind_item = NULL;
+ cJSON_ArrayForEach(kind_item, filter->kinds) {
+ if (cJSON_IsNumber(kind_item) && (int)cJSON_GetNumberValue(kind_item) == event_kind_val) {
+ kind_match = 1;
+ break;
+ }
+ }
+
+ if (!kind_match) {
+ return 0;
+ }
+ }
+
+ // Check authors filter
+ if (filter->authors && cJSON_IsArray(filter->authors)) {
+ cJSON* event_pubkey = cJSON_GetObjectItem(event, "pubkey");
+ if (!event_pubkey || !cJSON_IsString(event_pubkey)) {
+ return 0;
+ }
+
+ const char* event_pubkey_str = cJSON_GetStringValue(event_pubkey);
+ int author_match = 0;
+
+ cJSON* author_item = NULL;
+ cJSON_ArrayForEach(author_item, filter->authors) {
+ if (cJSON_IsString(author_item)) {
+ const char* author_str = cJSON_GetStringValue(author_item);
+ // Support prefix matching (partial pubkeys)
+ if (strncmp(event_pubkey_str, author_str, strlen(author_str)) == 0) {
+ author_match = 1;
+ break;
+ }
+ }
+ }
+
+ if (!author_match) {
+ return 0;
+ }
+ }
+
+ // Check IDs filter
+ if (filter->ids && cJSON_IsArray(filter->ids)) {
+ cJSON* event_id = cJSON_GetObjectItem(event, "id");
+ if (!event_id || !cJSON_IsString(event_id)) {
+ return 0;
+ }
+
+ const char* event_id_str = cJSON_GetStringValue(event_id);
+ int id_match = 0;
+
+ cJSON* id_item = NULL;
+ cJSON_ArrayForEach(id_item, filter->ids) {
+ if (cJSON_IsString(id_item)) {
+ const char* id_str = cJSON_GetStringValue(id_item);
+ // Support prefix matching (partial IDs)
+ if (strncmp(event_id_str, id_str, strlen(id_str)) == 0) {
+ id_match = 1;
+ break;
+ }
+ }
+ }
+
+ if (!id_match) {
+ return 0;
+ }
+ }
+
+ // Check since filter
+ if (filter->since > 0) {
+ cJSON* event_created_at = cJSON_GetObjectItem(event, "created_at");
+ if (!event_created_at || !cJSON_IsNumber(event_created_at)) {
+ return 0;
+ }
+
+ long event_timestamp = (long)cJSON_GetNumberValue(event_created_at);
+ if (event_timestamp < filter->since) {
+ return 0;
+ }
+ }
+
+ // Check until filter
+ if (filter->until > 0) {
+ cJSON* event_created_at = cJSON_GetObjectItem(event, "created_at");
+ if (!event_created_at || !cJSON_IsNumber(event_created_at)) {
+ return 0;
+ }
+
+ long event_timestamp = (long)cJSON_GetNumberValue(event_created_at);
+ if (event_timestamp > filter->until) {
+ return 0;
+ }
+ }
+
+ // Check tag filters (e.g., #e, #p tags)
+ if (filter->tag_filters && cJSON_IsObject(filter->tag_filters)) {
+ cJSON* event_tags = cJSON_GetObjectItem(event, "tags");
+ if (!event_tags || !cJSON_IsArray(event_tags)) {
+ return 0; // Event has no tags but filter requires tags
+ }
+
+ // Check each tag filter
+ cJSON* tag_filter = NULL;
+ cJSON_ArrayForEach(tag_filter, filter->tag_filters) {
+ if (!tag_filter->string || strlen(tag_filter->string) < 2 || tag_filter->string[0] != '#') {
+ continue; // Invalid tag filter
+ }
+
+ const char* tag_name = tag_filter->string + 1; // Skip the '#'
+
+ if (!cJSON_IsArray(tag_filter)) {
+ continue; // Tag filter must be an array
+ }
+
+ int tag_match = 0;
+
+ // Search through event tags for matching tag name and value
+ cJSON* event_tag = NULL;
+ cJSON_ArrayForEach(event_tag, event_tags) {
+ if (!cJSON_IsArray(event_tag) || cJSON_GetArraySize(event_tag) < 2) {
+ continue; // Invalid tag format
+ }
+
+ cJSON* event_tag_name = cJSON_GetArrayItem(event_tag, 0);
+ cJSON* event_tag_value = cJSON_GetArrayItem(event_tag, 1);
+
+ if (!cJSON_IsString(event_tag_name) || !cJSON_IsString(event_tag_value)) {
+ continue;
+ }
+
+ // Check if tag name matches
+ if (strcmp(cJSON_GetStringValue(event_tag_name), tag_name) == 0) {
+ const char* event_tag_value_str = cJSON_GetStringValue(event_tag_value);
+
+ // Check if any of the filter values match this tag value
+ cJSON* filter_value = NULL;
+ cJSON_ArrayForEach(filter_value, tag_filter) {
+ if (cJSON_IsString(filter_value)) {
+ const char* filter_value_str = cJSON_GetStringValue(filter_value);
+ // Support prefix matching for tag values
+ if (strncmp(event_tag_value_str, filter_value_str, strlen(filter_value_str)) == 0) {
+ tag_match = 1;
+ break;
+ }
+ }
+ }
+
+ if (tag_match) {
+ break;
+ }
+ }
+ }
+
+ if (!tag_match) {
+ return 0; // This tag filter didn't match, so the event doesn't match
+ }
+ }
+ }
+
+ return 1; // All filters passed
+}
+
+// Check if an event matches any filter in a subscription (filters are OR'd together)
+int event_matches_subscription(cJSON* event, subscription_t* subscription) {
+ if (!event || !subscription || !subscription->filters) {
+ return 0;
+ }
+
+ subscription_filter_t* filter = subscription->filters;
+ while (filter) {
+ if (event_matches_filter(event, filter)) {
+ return 1; // Match found (OR logic)
+ }
+ filter = filter->next;
+ }
+
+ return 0; // No filters matched
+}
+
+// Broadcast event to all matching subscriptions (thread-safe)
+int broadcast_event_to_subscriptions(cJSON* event) {
+ if (!event) {
+ return 0;
+ }
+
+ // Check if event is expired and should not be broadcast (NIP-40)
+ 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];
+ cJSON* event_id_obj = cJSON_GetObjectItem(event, "id");
+ const char* event_id = event_id_obj ? cJSON_GetStringValue(event_id_obj) : "unknown";
+ snprintf(debug_msg, sizeof(debug_msg), "Skipping broadcast of expired event: %.16s", event_id);
+ log_info(debug_msg);
+ return 0; // Don't broadcast expired events
+ }
+ }
+
+ int broadcasts = 0;
+
+ pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
+
+ subscription_t* sub = g_subscription_manager.active_subscriptions;
+ while (sub) {
+ if (sub->active && event_matches_subscription(event, sub)) {
+ // Create EVENT message for this subscription
+ cJSON* event_msg = cJSON_CreateArray();
+ cJSON_AddItemToArray(event_msg, cJSON_CreateString("EVENT"));
+ cJSON_AddItemToArray(event_msg, cJSON_CreateString(sub->id));
+ cJSON_AddItemToArray(event_msg, cJSON_Duplicate(event, 1));
+
+ 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);
+
+ // Send to WebSocket connection
+ int write_result = lws_write(sub->wsi, buf + LWS_PRE, msg_len, LWS_WRITE_TEXT);
+ if (write_result >= 0) {
+ sub->events_sent++;
+ broadcasts++;
+
+ // Log event broadcast to database (optional - can be disabled for performance)
+ cJSON* event_id_obj = cJSON_GetObjectItem(event, "id");
+ if (event_id_obj && cJSON_IsString(event_id_obj)) {
+ log_event_broadcast(cJSON_GetStringValue(event_id_obj), sub->id, sub->client_ip);
+ }
+ }
+
+ free(buf);
+ }
+ free(msg_str);
+ }
+
+ cJSON_Delete(event_msg);
+ }
+
+ sub = sub->next;
+ }
+
+ // Update global statistics
+ g_subscription_manager.total_events_broadcast += broadcasts;
+
+ pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
+
+ if (broadcasts > 0) {
+ char debug_msg[256];
+ snprintf(debug_msg, sizeof(debug_msg), "Broadcasted event to %d subscriptions", broadcasts);
+ log_info(debug_msg);
+ }
+
+ return broadcasts;
+}
+
+
+/////////////////////////////////////////////////////////////////////////////////////////
+/////////////////////////////////////////////////////////////////////////////////////////
+// SUBSCRIPTION DATABASE LOGGING
+/////////////////////////////////////////////////////////////////////////////////////////
+/////////////////////////////////////////////////////////////////////////////////////////
+
+// Log subscription creation to database
+void log_subscription_created(const subscription_t* sub) {
+ if (!g_db || !sub) return;
+
+ // Create filter JSON for logging
+ char* filter_json = NULL;
+ if (sub->filters) {
+ cJSON* filters_array = cJSON_CreateArray();
+ subscription_filter_t* filter = sub->filters;
+
+ while (filter) {
+ cJSON* filter_obj = cJSON_CreateObject();
+
+ if (filter->kinds) {
+ cJSON_AddItemToObject(filter_obj, "kinds", cJSON_Duplicate(filter->kinds, 1));
+ }
+ if (filter->authors) {
+ cJSON_AddItemToObject(filter_obj, "authors", cJSON_Duplicate(filter->authors, 1));
+ }
+ if (filter->ids) {
+ cJSON_AddItemToObject(filter_obj, "ids", cJSON_Duplicate(filter->ids, 1));
+ }
+ if (filter->since > 0) {
+ cJSON_AddNumberToObject(filter_obj, "since", filter->since);
+ }
+ if (filter->until > 0) {
+ cJSON_AddNumberToObject(filter_obj, "until", filter->until);
+ }
+ if (filter->limit > 0) {
+ cJSON_AddNumberToObject(filter_obj, "limit", filter->limit);
+ }
+ if (filter->tag_filters) {
+ cJSON* tags_obj = cJSON_Duplicate(filter->tag_filters, 1);
+ cJSON* item = NULL;
+ cJSON_ArrayForEach(item, tags_obj) {
+ if (item->string) {
+ cJSON_AddItemToObject(filter_obj, item->string, cJSON_Duplicate(item, 1));
+ }
+ }
+ cJSON_Delete(tags_obj);
+ }
+
+ cJSON_AddItemToArray(filters_array, filter_obj);
+ filter = filter->next;
+ }
+
+ filter_json = cJSON_Print(filters_array);
+ cJSON_Delete(filters_array);
+ }
+
+ const char* sql =
+ "INSERT INTO subscription_events (subscription_id, client_ip, event_type, filter_json) "
+ "VALUES (?, ?, 'created', ?)";
+
+ sqlite3_stmt* stmt;
+ int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
+ if (rc == SQLITE_OK) {
+ sqlite3_bind_text(stmt, 1, sub->id, -1, SQLITE_STATIC);
+ sqlite3_bind_text(stmt, 2, sub->client_ip, -1, SQLITE_STATIC);
+ sqlite3_bind_text(stmt, 3, filter_json ? filter_json : "[]", -1, SQLITE_TRANSIENT);
+
+ sqlite3_step(stmt);
+ sqlite3_finalize(stmt);
+ }
+
+ if (filter_json) free(filter_json);
+}
+
+// Log subscription closure to database
+void log_subscription_closed(const char* sub_id, const char* client_ip, const char* reason) {
+ (void)reason; // Mark as intentionally unused
+ if (!g_db || !sub_id) return;
+
+ const char* sql =
+ "INSERT INTO subscription_events (subscription_id, client_ip, event_type) "
+ "VALUES (?, ?, 'closed')";
+
+ sqlite3_stmt* stmt;
+ int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
+ if (rc == SQLITE_OK) {
+ sqlite3_bind_text(stmt, 1, sub_id, -1, SQLITE_STATIC);
+ sqlite3_bind_text(stmt, 2, client_ip ? client_ip : "unknown", -1, SQLITE_STATIC);
+
+ sqlite3_step(stmt);
+ sqlite3_finalize(stmt);
+ }
+
+ // Update the corresponding 'created' entry with end time and events sent
+ const char* update_sql =
+ "UPDATE subscription_events "
+ "SET ended_at = strftime('%s', 'now') "
+ "WHERE subscription_id = ? AND event_type = 'created' AND ended_at IS NULL";
+
+ rc = sqlite3_prepare_v2(g_db, update_sql, -1, &stmt, NULL);
+ if (rc == SQLITE_OK) {
+ sqlite3_bind_text(stmt, 1, sub_id, -1, SQLITE_STATIC);
+ sqlite3_step(stmt);
+ sqlite3_finalize(stmt);
+ }
+}
+
+// Log subscription disconnection to database
+void log_subscription_disconnected(const char* client_ip) {
+ if (!g_db || !client_ip) return;
+
+ // Mark all active subscriptions for this client as disconnected
+ const char* sql =
+ "UPDATE subscription_events "
+ "SET ended_at = strftime('%s', 'now') "
+ "WHERE client_ip = ? AND event_type = 'created' AND ended_at IS NULL";
+
+ sqlite3_stmt* stmt;
+ int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
+ if (rc == SQLITE_OK) {
+ sqlite3_bind_text(stmt, 1, client_ip, -1, SQLITE_STATIC);
+ int changes = sqlite3_changes(g_db);
+ sqlite3_step(stmt);
+ sqlite3_finalize(stmt);
+
+ if (changes > 0) {
+ // Log a disconnection event
+ const char* insert_sql =
+ "INSERT INTO subscription_events (subscription_id, client_ip, event_type) "
+ "VALUES ('disconnect', ?, 'disconnected')";
+
+ rc = sqlite3_prepare_v2(g_db, insert_sql, -1, &stmt, NULL);
+ if (rc == SQLITE_OK) {
+ sqlite3_bind_text(stmt, 1, client_ip, -1, SQLITE_STATIC);
+ sqlite3_step(stmt);
+ sqlite3_finalize(stmt);
+ }
+ }
+ }
+}
+
+// Log event broadcast to database (optional, can be resource intensive)
+void log_event_broadcast(const char* event_id, const char* sub_id, const char* client_ip) {
+ if (!g_db || !event_id || !sub_id || !client_ip) return;
+
+ const char* sql =
+ "INSERT INTO event_broadcasts (event_id, subscription_id, client_ip) "
+ "VALUES (?, ?, ?)";
+
+ sqlite3_stmt* stmt;
+ int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
+ if (rc == SQLITE_OK) {
+ sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_STATIC);
+ sqlite3_bind_text(stmt, 2, sub_id, -1, SQLITE_STATIC);
+ sqlite3_bind_text(stmt, 3, client_ip, -1, SQLITE_STATIC);
+
+ sqlite3_step(stmt);
+ sqlite3_finalize(stmt);
+ }
+}
+
+// Update events sent counter for a subscription
+void update_subscription_events_sent(const char* sub_id, int events_sent) {
+ if (!g_db || !sub_id) return;
+
+ const char* sql =
+ "UPDATE subscription_events "
+ "SET events_sent = ? "
+ "WHERE subscription_id = ? AND event_type = 'created'";
+
+ sqlite3_stmt* stmt;
+ int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
+ if (rc == SQLITE_OK) {
+ sqlite3_bind_int(stmt, 1, events_sent);
+ sqlite3_bind_text(stmt, 2, sub_id, -1, SQLITE_STATIC);
+
+ sqlite3_step(stmt);
+ sqlite3_finalize(stmt);
+ }
+}
diff --git a/src/subscriptions.h b/src/subscriptions.h
new file mode 100644
index 0000000..11a7ba3
--- /dev/null
+++ b/src/subscriptions.h
@@ -0,0 +1,91 @@
+// Subscription system structures and functions for C-Relay
+// This header defines subscription management functionality
+
+#ifndef SUBSCRIPTIONS_H
+#define SUBSCRIPTIONS_H
+
+#include
+#include
+#include
+#include "../nostr_core_lib/cjson/cJSON.h"
+#include "config.h" // For CLIENT_IP_MAX_LENGTH
+
+// Forward declaration for libwebsockets struct
+struct lws;
+
+// Constants
+#define SUBSCRIPTION_ID_MAX_LENGTH 64
+#define MAX_FILTERS_PER_SUBSCRIPTION 10
+#define MAX_TOTAL_SUBSCRIPTIONS 5000
+
+// Forward declarations for typedefs
+typedef struct subscription_filter subscription_filter_t;
+typedef struct subscription subscription_t;
+typedef struct subscription_manager subscription_manager_t;
+
+// Subscription filter structure
+struct subscription_filter {
+ // Filter criteria (all optional)
+ cJSON* kinds; // Array of event kinds [1,2,3]
+ cJSON* authors; // Array of author pubkeys
+ cJSON* ids; // Array of event IDs
+ long since; // Unix timestamp (0 = not set)
+ long until; // Unix timestamp (0 = not set)
+ int limit; // Result limit (0 = no limit)
+ cJSON* tag_filters; // Object with tag filters: {"#e": ["id1"], "#p": ["pubkey1"]}
+
+ // Linked list for multiple filters per subscription
+ struct subscription_filter* next;
+};
+
+// Active subscription structure
+struct subscription {
+ char id[SUBSCRIPTION_ID_MAX_LENGTH]; // Subscription ID
+ struct lws* wsi; // WebSocket connection handle
+ subscription_filter_t* filters; // Linked list of filters (OR'd together)
+ time_t created_at; // When subscription was created
+ int events_sent; // Counter for sent events
+ int active; // 1 = active, 0 = closed
+
+ // Client info for logging
+ char client_ip[CLIENT_IP_MAX_LENGTH]; // Client IP address
+
+ // Linked list pointers
+ struct subscription* next; // Next subscription globally
+ struct subscription* session_next; // Next subscription for this session
+};
+
+// Global subscription manager
+struct subscription_manager {
+ subscription_t* active_subscriptions; // Head of global subscription list
+ pthread_mutex_t subscriptions_lock; // Global thread safety
+ int total_subscriptions; // Current count
+
+ // Configuration
+ int max_subscriptions_per_client; // Default: 20
+ int max_total_subscriptions; // Default: 5000
+
+ // Statistics
+ uint64_t total_created; // Lifetime subscription count
+ uint64_t total_events_broadcast; // Lifetime event broadcast count
+};
+
+// Function declarations
+subscription_filter_t* create_subscription_filter(cJSON* filter_json);
+void free_subscription_filter(subscription_filter_t* filter);
+subscription_t* create_subscription(const char* sub_id, struct lws* wsi, cJSON* filters_array, const char* client_ip);
+void free_subscription(subscription_t* sub);
+int add_subscription_to_manager(subscription_t* sub);
+int remove_subscription_from_manager(const char* sub_id, struct lws* wsi);
+int event_matches_filter(cJSON* event, subscription_filter_t* filter);
+int event_matches_subscription(cJSON* event, subscription_t* subscription);
+int broadcast_event_to_subscriptions(cJSON* event);
+
+// Database logging functions
+void log_subscription_created(const subscription_t* sub);
+void log_subscription_closed(const char* sub_id, const char* client_ip, const char* reason);
+void log_subscription_disconnected(const char* client_ip);
+void log_event_broadcast(const char* event_id, const char* sub_id, const char* client_ip);
+void update_subscription_events_sent(const char* sub_id, int events_sent);
+
+#endif // SUBSCRIPTIONS_H
\ No newline at end of file
diff --git a/src/websockets.c b/src/websockets.c
new file mode 100644
index 0000000..048c935
--- /dev/null
+++ b/src/websockets.c
@@ -0,0 +1,901 @@
+// Define _GNU_SOURCE to ensure all POSIX features are available
+#define _GNU_SOURCE
+
+// Includes
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+// Include libwebsockets after pthread.h to ensure pthread_rwlock_t is defined
+#include
+#include
+#include
+#include
+#include
+
+// Include nostr_core_lib for Nostr functionality
+#include "../nostr_core_lib/cjson/cJSON.h"
+#include "../nostr_core_lib/nostr_core/nostr_core.h"
+#include "../nostr_core_lib/nostr_core/nip013.h" // NIP-13: Proof of Work
+#include "config.h" // Configuration management system
+#include "sql_schema.h" // Embedded database schema
+#include "websockets.h" // WebSocket structures and constants
+#include "subscriptions.h" // Subscription structures and functions
+
+// Forward declarations for logging functions
+void log_info(const char* message);
+void log_success(const char* message);
+void log_error(const char* message);
+void log_warning(const char* message);
+
+// Forward declarations for configuration functions
+const char* get_config_value(const char* key);
+int get_config_int(const char* key, int default_value);
+int get_config_bool(const char* key, int default_value);
+// Forward declarations for NIP-42 authentication functions
+int is_nip42_auth_globally_required(void);
+int is_nip42_auth_required_for_kind(int kind);
+void send_nip42_auth_challenge(struct lws* wsi, struct per_session_data* pss);
+void handle_nip42_auth_signed_event(struct lws* wsi, struct per_session_data* pss, cJSON* auth_event);
+void handle_nip42_auth_challenge_response(struct lws* wsi, struct per_session_data* pss, const char* challenge);
+
+// Forward declarations for NIP-11 relay information handling
+int handle_nip11_http_request(struct lws* wsi, const char* accept_header);
+
+// Forward declarations for database functions
+int store_event(cJSON* event);
+
+// Forward declarations for subscription management
+int broadcast_event_to_subscriptions(cJSON* event);
+int add_subscription_to_manager(struct subscription* sub);
+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);
+
+// Forward declarations for admin event processing
+int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size, struct lws* wsi);
+int is_authorized_admin_event(cJSON* event, char* error_message, size_t error_size);
+
+// Forward declarations for NIP-09 deletion request handling
+int handle_deletion_request(cJSON* event, char* error_message, size_t error_size);
+
+// Forward declarations for NIP-13 PoW handling
+int validate_event_pow(cJSON* event, char* error_message, size_t error_size);
+
+// Forward declarations for NIP-40 expiration handling
+int is_event_expired(cJSON* event, time_t current_time);
+
+// Forward declarations for subscription handling
+int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, struct per_session_data *pss);
+
+// Forward declarations for NOTICE message support
+void send_notice_message(struct lws* wsi, const char* message);
+
+// Forward declarations for unified cache access
+extern unified_config_cache_t g_unified_cache;
+
+// Forward declarations for global state
+extern sqlite3* g_db;
+extern int g_server_running;
+extern struct lws_context *ws_context;
+
+// Global subscription manager
+struct subscription_manager g_subscription_manager;
+
+
+
+/////////////////////////////////////////////////////////////////////////////////////////
+/////////////////////////////////////////////////////////////////////////////////////////
+// WEBSOCKET PROTOCOL
+/////////////////////////////////////////////////////////////////////////////////////////
+/////////////////////////////////////////////////////////////////////////////////////////
+
+// WebSocket callback function for Nostr relay protocol
+static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reason,
+ void *user, void *in, size_t len) {
+ struct per_session_data *pss = (struct per_session_data *)user;
+
+ switch (reason) {
+ case LWS_CALLBACK_HTTP:
+ // Handle NIP-11 relay information requests (HTTP GET to root path)
+ {
+ char *requested_uri = (char *)in;
+ log_info("HTTP request received");
+
+ // Check if this is a GET request to the root path
+ if (strcmp(requested_uri, "/") == 0) {
+ // Get Accept header
+ char accept_header[256] = {0};
+ int header_len = lws_hdr_copy(wsi, accept_header, sizeof(accept_header) - 1, WSI_TOKEN_HTTP_ACCEPT);
+
+ if (header_len > 0) {
+ accept_header[header_len] = '\0';
+
+ // Handle NIP-11 request
+ if (handle_nip11_http_request(wsi, accept_header) == 0) {
+ return 0; // Successfully handled
+ }
+ } else {
+ log_warning("HTTP request without Accept header");
+ }
+
+ // Return 404 for other requests
+ lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL);
+ return -1;
+ }
+
+ // Return 404 for non-root paths
+ lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL);
+ return -1;
+ }
+
+ case LWS_CALLBACK_HTTP_WRITEABLE:
+ // Handle NIP-11 HTTP body transmission with proper buffer management
+ {
+ struct nip11_session_data* session_data = (struct nip11_session_data*)lws_wsi_user(wsi);
+ if (session_data && session_data->headers_sent && !session_data->body_sent) {
+ // Allocate buffer for JSON body transmission
+ unsigned char *json_buf = malloc(LWS_PRE + session_data->json_length);
+ if (!json_buf) {
+ log_error("Failed to allocate buffer for NIP-11 body transmission");
+ // Clean up session data
+ free(session_data->json_buffer);
+ free(session_data);
+ lws_set_wsi_user(wsi, NULL);
+ return -1;
+ }
+
+ // Copy JSON data to buffer
+ memcpy(json_buf + LWS_PRE, session_data->json_buffer, session_data->json_length);
+
+ // Write JSON body
+ int write_result = lws_write(wsi, json_buf + LWS_PRE, session_data->json_length, LWS_WRITE_HTTP);
+
+ // Free the transmission buffer immediately (it's been copied by libwebsockets)
+ free(json_buf);
+
+ if (write_result < 0) {
+ log_error("Failed to write NIP-11 JSON body");
+ // Clean up session data
+ free(session_data->json_buffer);
+ free(session_data);
+ lws_set_wsi_user(wsi, NULL);
+ return -1;
+ }
+
+ // Mark body as sent and clean up session data
+ session_data->body_sent = 1;
+ free(session_data->json_buffer);
+ free(session_data);
+ lws_set_wsi_user(wsi, NULL);
+
+ log_success("NIP-11 relay information served successfully");
+ return 0; // Close connection after successful transmission
+ }
+ }
+ break;
+
+ case LWS_CALLBACK_ESTABLISHED:
+ log_info("WebSocket connection established");
+ memset(pss, 0, sizeof(*pss));
+ pthread_mutex_init(&pss->session_lock, NULL);
+
+ // Get real client IP address
+ char client_ip[CLIENT_IP_MAX_LENGTH];
+ lws_get_peer_simple(wsi, client_ip, sizeof(client_ip));
+
+ // 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;
+ pss->nip42_auth_required_events = get_config_bool("nip42_auth_required_events", 0);
+ pss->nip42_auth_required_subscriptions = get_config_bool("nip42_auth_required_subscriptions", 0);
+ pss->auth_challenge_sent = 0;
+ memset(pss->authenticated_pubkey, 0, sizeof(pss->authenticated_pubkey));
+ memset(pss->active_challenge, 0, sizeof(pss->active_challenge));
+ pss->challenge_created = 0;
+ pss->challenge_expires = 0;
+ break;
+
+ case LWS_CALLBACK_RECEIVE:
+ if (len > 0) {
+ char *message = malloc(len + 1);
+ if (message) {
+ memcpy(message, in, len);
+ message[len] = '\0';
+
+ // Parse JSON message (this is the normal program flow)
+ cJSON* json = cJSON_Parse(message);
+ if (json && cJSON_IsArray(json)) {
+ // Log the complete parsed JSON message once
+ char* complete_message = cJSON_Print(json);
+ if (complete_message) {
+ char debug_msg[2048];
+ snprintf(debug_msg, sizeof(debug_msg),
+ "Received complete WebSocket message: %s", complete_message);
+ log_info(debug_msg);
+ free(complete_message);
+ }
+ // Get message type
+ cJSON* type = cJSON_GetArrayItem(json, 0);
+ if (type && cJSON_IsString(type)) {
+ const char* msg_type = cJSON_GetStringValue(type);
+
+ if (strcmp(msg_type, "EVENT") == 0) {
+ // Extract event for kind-specific NIP-42 authentication check
+ cJSON* event_obj = cJSON_GetArrayItem(json, 1);
+ if (event_obj && cJSON_IsObject(event_obj)) {
+ // Extract event kind for kind-specific NIP-42 authentication check
+ cJSON* kind_obj = cJSON_GetObjectItem(event_obj, "kind");
+ int event_kind = kind_obj && cJSON_IsNumber(kind_obj) ? (int)cJSON_GetNumberValue(kind_obj) : -1;
+
+ // Extract pubkey and event ID for debugging
+ cJSON* pubkey_obj = cJSON_GetObjectItem(event_obj, "pubkey");
+ cJSON* id_obj = cJSON_GetObjectItem(event_obj, "id");
+ const char* event_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : "unknown";
+ const char* event_id = id_obj ? cJSON_GetStringValue(id_obj) : "unknown";
+
+ char debug_event_msg[512];
+ snprintf(debug_event_msg, sizeof(debug_event_msg),
+ "DEBUG EVENT: Processing kind %d event from pubkey %.16s... ID %.16s...",
+ event_kind, event_pubkey, event_id);
+ log_info(debug_event_msg);
+
+ // Check if NIP-42 authentication is required for this event kind or globally
+ int auth_required = is_nip42_auth_globally_required() || is_nip42_auth_required_for_kind(event_kind);
+
+ char debug_auth_msg[256];
+ snprintf(debug_auth_msg, sizeof(debug_auth_msg),
+ "DEBUG AUTH: auth_required=%d, pss->authenticated=%d, event_kind=%d",
+ auth_required, pss ? pss->authenticated : -1, event_kind);
+ log_info(debug_auth_msg);
+
+ if (pss && auth_required && !pss->authenticated) {
+ if (!pss->auth_challenge_sent) {
+ log_info("DEBUG AUTH: Sending NIP-42 authentication challenge");
+ send_nip42_auth_challenge(wsi, pss);
+ } else {
+ char auth_msg[256];
+ if (event_kind == 4 || event_kind == 14) {
+ snprintf(auth_msg, sizeof(auth_msg),
+ "NIP-42 authentication required for direct message events (kind %d)", event_kind);
+ } else {
+ snprintf(auth_msg, sizeof(auth_msg),
+ "NIP-42 authentication required for event kind %d", event_kind);
+ }
+ send_notice_message(wsi, auth_msg);
+ log_warning("Event rejected: NIP-42 authentication required for kind");
+ char debug_msg[128];
+ snprintf(debug_msg, sizeof(debug_msg), "Auth required for kind %d", event_kind);
+ log_info(debug_msg);
+ }
+ cJSON_Delete(json);
+ free(message);
+ return 0;
+ }
+
+ // Check blacklist/whitelist rules regardless of NIP-42 auth settings
+ // Blacklist should always be enforced
+ if (event_pubkey) {
+ // Forward declaration for auth rules checking function
+ extern int check_database_auth_rules(const char *pubkey, const char *operation, const char *resource_hash);
+
+ int auth_rules_result = check_database_auth_rules(event_pubkey, "event", NULL);
+ if (auth_rules_result != 0) { // 0 = NOSTR_SUCCESS, non-zero = blocked
+ char auth_rules_msg[256];
+ if (auth_rules_result == -101) { // NOSTR_ERROR_AUTH_REQUIRED
+ snprintf(auth_rules_msg, sizeof(auth_rules_msg),
+ "blocked: pubkey not authorized (blacklist/whitelist violation)");
+ } else {
+ snprintf(auth_rules_msg, sizeof(auth_rules_msg),
+ "blocked: authorization check failed (error %d)", auth_rules_result);
+ }
+ send_notice_message(wsi, auth_rules_msg);
+ log_warning("Event rejected: blacklist/whitelist violation");
+
+ // Send OK response with false status
+ cJSON* response = cJSON_CreateArray();
+ cJSON_AddItemToArray(response, cJSON_CreateString("OK"));
+ cJSON_AddItemToArray(response, cJSON_CreateString(event_id));
+ cJSON_AddItemToArray(response, cJSON_CreateBool(0)); // false = rejected
+ cJSON_AddItemToArray(response, cJSON_CreateString(auth_rules_msg));
+
+ char *response_str = cJSON_Print(response);
+ if (response_str) {
+ size_t response_len = strlen(response_str);
+ unsigned char *buf = malloc(LWS_PRE + response_len);
+ if (buf) {
+ memcpy(buf + LWS_PRE, response_str, response_len);
+ lws_write(wsi, buf + LWS_PRE, response_len, LWS_WRITE_TEXT);
+ free(buf);
+ }
+ free(response_str);
+ }
+ cJSON_Delete(response);
+
+ cJSON_Delete(json);
+ free(message);
+ return 0;
+ }
+ }
+ }
+
+ // Handle EVENT message
+ cJSON* event = cJSON_GetArrayItem(json, 1);
+ if (event && cJSON_IsObject(event)) {
+ // Extract event JSON string for unified validator
+ char *event_json_str = cJSON_Print(event);
+ if (!event_json_str) {
+ log_error("Failed to serialize event JSON for validation");
+ cJSON* error_response = cJSON_CreateArray();
+ cJSON_AddItemToArray(error_response, cJSON_CreateString("OK"));
+ cJSON_AddItemToArray(error_response, cJSON_CreateString("unknown"));
+ cJSON_AddItemToArray(error_response, cJSON_CreateBool(0));
+ cJSON_AddItemToArray(error_response, cJSON_CreateString("error: failed to process event"));
+
+ char *error_str = cJSON_Print(error_response);
+ if (error_str) {
+ size_t error_len = strlen(error_str);
+ unsigned char *buf = malloc(LWS_PRE + error_len);
+ if (buf) {
+ memcpy(buf + LWS_PRE, error_str, error_len);
+ lws_write(wsi, buf + LWS_PRE, error_len, LWS_WRITE_TEXT);
+ free(buf);
+ }
+ free(error_str);
+ }
+ cJSON_Delete(error_response);
+ return 0;
+ }
+
+ log_info("DEBUG VALIDATION: Starting unified validator");
+
+ // 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);
+
+ // Map validation result to old result format (0 = success, -1 = failure)
+ int result = (validation_result == NOSTR_SUCCESS) ? 0 : -1;
+
+ char debug_validation_msg[256];
+ snprintf(debug_validation_msg, sizeof(debug_validation_msg),
+ "DEBUG VALIDATION: validation_result=%d, result=%d", validation_result, result);
+ log_info(debug_validation_msg);
+
+ // Generate error message based on validation result
+ char error_message[512] = {0};
+ if (result != 0) {
+ switch (validation_result) {
+ case NOSTR_ERROR_INVALID_INPUT:
+ strncpy(error_message, "invalid: malformed event structure", sizeof(error_message) - 1);
+ break;
+ case NOSTR_ERROR_EVENT_INVALID_SIGNATURE:
+ strncpy(error_message, "invalid: signature verification failed", sizeof(error_message) - 1);
+ break;
+ case NOSTR_ERROR_EVENT_INVALID_ID:
+ strncpy(error_message, "invalid: event id verification failed", sizeof(error_message) - 1);
+ break;
+ case NOSTR_ERROR_EVENT_INVALID_PUBKEY:
+ strncpy(error_message, "invalid: invalid pubkey format", sizeof(error_message) - 1);
+ break;
+ case -103: // NOSTR_ERROR_EVENT_EXPIRED
+ strncpy(error_message, "rejected: event expired", sizeof(error_message) - 1);
+ break;
+ case -102: // NOSTR_ERROR_NIP42_DISABLED
+ strncpy(error_message, "auth-required: NIP-42 authentication required", sizeof(error_message) - 1);
+ break;
+ case -101: // NOSTR_ERROR_AUTH_REQUIRED
+ strncpy(error_message, "blocked: pubkey not authorized", sizeof(error_message) - 1);
+ break;
+ default:
+ strncpy(error_message, "error: validation failed", sizeof(error_message) - 1);
+ break;
+ }
+ char debug_error_msg[256];
+ snprintf(debug_error_msg, sizeof(debug_error_msg),
+ "DEBUG VALIDATION ERROR: %s", error_message);
+ log_warning(debug_error_msg);
+ } else {
+ log_info("DEBUG VALIDATION: Event validated successfully using unified validator");
+ }
+
+ // Cleanup event JSON string
+ free(event_json_str);
+
+ // Check for admin events (kind 23456) and intercept them
+ if (result == 0) {
+ cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
+ if (kind_obj && cJSON_IsNumber(kind_obj)) {
+ int event_kind = (int)cJSON_GetNumberValue(kind_obj);
+
+ log_info("DEBUG ADMIN: Checking if admin event processing is needed");
+
+ // Log reception of Kind 23456 events
+ if (event_kind == 23456) {
+ char* event_json_debug = cJSON_Print(event);
+ char debug_received_msg[1024];
+ snprintf(debug_received_msg, sizeof(debug_received_msg),
+ "RECEIVED Kind %d event: %s", event_kind,
+ event_json_debug ? event_json_debug : "Failed to serialize");
+ log_info(debug_received_msg);
+
+ if (event_json_debug) {
+ free(event_json_debug);
+ }
+ }
+
+ if (event_kind == 23456) {
+ // Enhanced admin event security - check authorization first
+ log_info("DEBUG ADMIN: Admin event detected, checking authorization");
+
+ char auth_error[512] = {0};
+ int auth_result = is_authorized_admin_event(event, auth_error, sizeof(auth_error));
+
+ if (auth_result != 0) {
+ // Authorization failed - log and reject
+ log_warning("DEBUG ADMIN: Admin event authorization failed");
+ result = -1;
+ size_t error_len = strlen(auth_error);
+ size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1;
+ memcpy(error_message, auth_error, copy_len);
+ error_message[copy_len] = '\0';
+
+ char debug_auth_error_msg[600];
+ snprintf(debug_auth_error_msg, sizeof(debug_auth_error_msg),
+ "DEBUG ADMIN AUTH ERROR: %.400s", auth_error);
+ log_warning(debug_auth_error_msg);
+ } else {
+ // Authorization successful - process through admin API
+ log_info("DEBUG ADMIN: Admin event authorized, processing through admin API");
+
+ char admin_error[512] = {0};
+ int admin_result = process_admin_event_in_config(event, admin_error, sizeof(admin_error), wsi);
+
+ char debug_admin_msg[256];
+ snprintf(debug_admin_msg, sizeof(debug_admin_msg),
+ "DEBUG ADMIN: process_admin_event_in_config returned %d", admin_result);
+ log_info(debug_admin_msg);
+
+ // Log results for Kind 23456 events
+ if (event_kind == 23456) {
+ if (admin_result == 0) {
+ char success_result_msg[256];
+ snprintf(success_result_msg, sizeof(success_result_msg),
+ "SUCCESS: Kind %d event processed successfully", event_kind);
+ log_success(success_result_msg);
+ } else {
+ char error_result_msg[512];
+ snprintf(error_result_msg, sizeof(error_result_msg),
+ "ERROR: Kind %d event processing failed: %s", event_kind, admin_error);
+ log_error(error_result_msg);
+ }
+ }
+
+ if (admin_result != 0) {
+ log_error("DEBUG ADMIN: 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';
+
+ char debug_admin_error_msg[600];
+ snprintf(debug_admin_error_msg, sizeof(debug_admin_error_msg),
+ "DEBUG ADMIN ERROR: %.400s", admin_error);
+ log_error(debug_admin_error_msg);
+ } else {
+ log_success("DEBUG ADMIN: 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
+ log_info("DEBUG STORAGE: Regular event - storing in database");
+ if (store_event(event) != 0) {
+ log_error("DEBUG STORAGE: Failed to store event in database");
+ result = -1;
+ strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1);
+ } else {
+ log_info("DEBUG STORAGE: Event stored successfully in database");
+ // Broadcast event to matching persistent subscriptions
+ int broadcast_count = broadcast_event_to_subscriptions(event);
+ char debug_broadcast_msg[128];
+ snprintf(debug_broadcast_msg, sizeof(debug_broadcast_msg),
+ "DEBUG BROADCAST: Event broadcast to %d subscriptions", broadcast_count);
+ log_info(debug_broadcast_msg);
+ }
+ }
+ } else {
+ // Event without valid kind - try normal storage
+ log_warning("DEBUG STORAGE: Event without valid kind - trying normal storage");
+ if (store_event(event) != 0) {
+ log_error("DEBUG STORAGE: Failed to store event without kind in database");
+ result = -1;
+ strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1);
+ } else {
+ log_info("DEBUG STORAGE: Event without kind stored successfully in database");
+ broadcast_event_to_subscriptions(event);
+ }
+ }
+ }
+
+ // Send OK response
+ cJSON* event_id = cJSON_GetObjectItem(event, "id");
+ if (event_id && cJSON_IsString(event_id)) {
+ cJSON* response = cJSON_CreateArray();
+ cJSON_AddItemToArray(response, cJSON_CreateString("OK"));
+ cJSON_AddItemToArray(response, cJSON_CreateString(cJSON_GetStringValue(event_id)));
+ cJSON_AddItemToArray(response, cJSON_CreateBool(result == 0));
+ cJSON_AddItemToArray(response, cJSON_CreateString(strlen(error_message) > 0 ? error_message : ""));
+
+ // TODO: REPLACE - Remove wasteful cJSON_Print conversion
+ char *response_str = cJSON_Print(response);
+ if (response_str) {
+ char debug_response_msg[512];
+ snprintf(debug_response_msg, sizeof(debug_response_msg),
+ "DEBUG RESPONSE: Sending OK response: %s", response_str);
+ log_info(debug_response_msg);
+
+ size_t response_len = strlen(response_str);
+ unsigned char *buf = malloc(LWS_PRE + response_len);
+ if (buf) {
+ memcpy(buf + LWS_PRE, response_str, response_len);
+ int write_result = lws_write(wsi, buf + LWS_PRE, response_len, LWS_WRITE_TEXT);
+
+ char debug_write_msg[128];
+ snprintf(debug_write_msg, sizeof(debug_write_msg),
+ "DEBUG RESPONSE: lws_write returned %d", write_result);
+ log_info(debug_write_msg);
+
+ free(buf);
+ }
+ free(response_str);
+ }
+ cJSON_Delete(response);
+ }
+ }
+ } else if (strcmp(msg_type, "REQ") == 0) {
+ // Check NIP-42 authentication for REQ subscriptions if required
+ if (pss && pss->nip42_auth_required_subscriptions && !pss->authenticated) {
+ if (!pss->auth_challenge_sent) {
+ send_nip42_auth_challenge(wsi, pss);
+ } else {
+ send_notice_message(wsi, "NIP-42 authentication required for subscriptions");
+ log_warning("REQ rejected: NIP-42 authentication required");
+ }
+ cJSON_Delete(json);
+ free(message);
+ return 0;
+ }
+
+ // Handle REQ message
+ cJSON* sub_id = cJSON_GetArrayItem(json, 1);
+
+ if (sub_id && cJSON_IsString(sub_id)) {
+ const char* subscription_id = cJSON_GetStringValue(sub_id);
+
+ // Create array of filter objects from position 2 onwards
+ cJSON* filters = cJSON_CreateArray();
+ int json_size = cJSON_GetArraySize(json);
+ for (int i = 2; i < json_size; i++) {
+ cJSON* filter = cJSON_GetArrayItem(json, i);
+ if (filter) {
+ cJSON_AddItemToArray(filters, cJSON_Duplicate(filter, 1));
+ }
+ }
+
+ handle_req_message(subscription_id, filters, wsi, pss);
+
+ // Clean up the filters array we created
+ cJSON_Delete(filters);
+
+ // Send EOSE (End of Stored Events)
+ cJSON* eose_response = cJSON_CreateArray();
+ cJSON_AddItemToArray(eose_response, cJSON_CreateString("EOSE"));
+ cJSON_AddItemToArray(eose_response, cJSON_CreateString(subscription_id));
+
+ char *eose_str = cJSON_Print(eose_response);
+ if (eose_str) {
+ size_t eose_len = strlen(eose_str);
+ unsigned char *buf = malloc(LWS_PRE + eose_len);
+ if (buf) {
+ memcpy(buf + LWS_PRE, eose_str, eose_len);
+ lws_write(wsi, buf + LWS_PRE, eose_len, LWS_WRITE_TEXT);
+ free(buf);
+ }
+ free(eose_str);
+ }
+ cJSON_Delete(eose_response);
+ }
+ } else if (strcmp(msg_type, "CLOSE") == 0) {
+ // Handle CLOSE message
+ cJSON* sub_id = cJSON_GetArrayItem(json, 1);
+ if (sub_id && cJSON_IsString(sub_id)) {
+ const char* subscription_id = cJSON_GetStringValue(sub_id);
+
+ // Remove from global manager
+ remove_subscription_from_manager(subscription_id, wsi);
+
+ // Remove from session list if present
+ if (pss) {
+ pthread_mutex_lock(&pss->session_lock);
+
+ struct subscription** current = &pss->subscriptions;
+ while (*current) {
+ if (strcmp((*current)->id, subscription_id) == 0) {
+ struct subscription* to_remove = *current;
+ *current = to_remove->session_next;
+ pss->subscription_count--;
+ break;
+ }
+ current = &((*current)->session_next);
+ }
+
+ pthread_mutex_unlock(&pss->session_lock);
+ }
+
+ char debug_msg[256];
+ snprintf(debug_msg, sizeof(debug_msg), "Closed subscription: %s", subscription_id);
+ log_info(debug_msg);
+ }
+ } else if (strcmp(msg_type, "AUTH") == 0) {
+ // Handle NIP-42 AUTH message
+ if (cJSON_GetArraySize(json) >= 2) {
+ cJSON* auth_payload = cJSON_GetArrayItem(json, 1);
+
+ if (cJSON_IsString(auth_payload)) {
+ // AUTH challenge response: ["AUTH", ] (unusual)
+ handle_nip42_auth_challenge_response(wsi, pss, cJSON_GetStringValue(auth_payload));
+ } else if (cJSON_IsObject(auth_payload)) {
+ // AUTH signed event: ["AUTH", ] (standard NIP-42)
+ handle_nip42_auth_signed_event(wsi, pss, auth_payload);
+ } else {
+ send_notice_message(wsi, "Invalid AUTH message format");
+ log_warning("Received AUTH message with invalid payload type");
+ }
+ } else {
+ send_notice_message(wsi, "AUTH message requires payload");
+ log_warning("Received AUTH message without payload");
+ }
+ } else {
+ // Unknown message type
+ char unknown_msg[128];
+ snprintf(unknown_msg, sizeof(unknown_msg), "Unknown message type: %.32s", msg_type);
+ log_warning(unknown_msg);
+ send_notice_message(wsi, "Unknown message type");
+ }
+ }
+ }
+
+ if (json) cJSON_Delete(json);
+ free(message);
+ }
+ }
+ break;
+
+ case LWS_CALLBACK_CLOSED:
+ log_info("WebSocket connection closed");
+
+ // Clean up session subscriptions
+ if (pss) {
+ pthread_mutex_lock(&pss->session_lock);
+
+ struct subscription* sub = pss->subscriptions;
+ while (sub) {
+ struct subscription* next = sub->session_next;
+ remove_subscription_from_manager(sub->id, wsi);
+ sub = next;
+ }
+
+ pss->subscriptions = NULL;
+ pss->subscription_count = 0;
+
+ pthread_mutex_unlock(&pss->session_lock);
+ pthread_mutex_destroy(&pss->session_lock);
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ return 0;
+}
+
+// WebSocket protocol definition
+static struct lws_protocols protocols[] = {
+ {
+ "nostr-relay-protocol",
+ nostr_relay_callback,
+ sizeof(struct per_session_data),
+ 4096, // rx buffer size
+ 0, NULL, 0
+ },
+ { NULL, NULL, 0, 0, 0, NULL, 0 } // terminator
+};
+
+// Check if a port is available for binding
+int check_port_available(int port) {
+ int sockfd;
+ struct sockaddr_in addr;
+ int result;
+ int reuse = 1;
+
+ // Create a socket
+ sockfd = socket(AF_INET, SOCK_STREAM, 0);
+ if (sockfd < 0) {
+ return 0; // Cannot create socket, assume port unavailable
+ }
+
+ // Set SO_REUSEADDR to allow binding to ports in TIME_WAIT state
+ // This matches libwebsockets behavior and prevents false unavailability
+ if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) {
+ close(sockfd);
+ return 0; // Failed to set socket option
+ }
+
+ // Set up the address structure
+ memset(&addr, 0, sizeof(addr));
+ addr.sin_family = AF_INET;
+ addr.sin_addr.s_addr = INADDR_ANY;
+ addr.sin_port = htons(port);
+
+ // Try to bind to the port
+ result = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
+
+ // Close the socket
+ close(sockfd);
+
+ // Return 1 if bind succeeded (port available), 0 if failed (port in use)
+ return (result == 0) ? 1 : 0;
+}
+
+// Start libwebsockets-based WebSocket Nostr relay server
+int start_websocket_relay(int port_override, int strict_port) {
+ struct lws_context_creation_info info;
+
+ log_info("Starting libwebsockets-based Nostr relay server...");
+
+ memset(&info, 0, sizeof(info));
+ // Use port override if provided, otherwise use configuration
+ int configured_port = (port_override > 0) ? port_override : get_config_int("relay_port", DEFAULT_PORT);
+ int actual_port = configured_port;
+ int port_attempts = 0;
+ const int max_port_attempts = 10; // Increased from 5 to 10
+
+ // Minimal libwebsockets configuration
+ info.protocols = protocols;
+ info.gid = -1;
+ info.uid = -1;
+ info.options = LWS_SERVER_OPTION_VALIDATE_UTF8;
+
+ // Remove interface restrictions - let system choose
+ // info.vhost_name = NULL;
+ // info.iface = NULL;
+
+ // Increase max connections for relay usage
+ info.max_http_header_pool = 16;
+ info.timeout_secs = 10;
+
+ // Max payload size for Nostr events
+ info.max_http_header_data = 4096;
+
+ // Find an available port with pre-checking (or fail immediately in strict mode)
+ while (port_attempts < (strict_port ? 1 : max_port_attempts)) {
+ char attempt_msg[256];
+ snprintf(attempt_msg, sizeof(attempt_msg), "Checking port availability: %d", actual_port);
+ log_info(attempt_msg);
+
+ // Pre-check if port is available
+ if (!check_port_available(actual_port)) {
+ port_attempts++;
+ if (strict_port) {
+ char error_msg[256];
+ snprintf(error_msg, sizeof(error_msg),
+ "Strict port mode: port %d is not available", actual_port);
+ log_error(error_msg);
+ return -1;
+ } else if (port_attempts < max_port_attempts) {
+ char retry_msg[256];
+ snprintf(retry_msg, sizeof(retry_msg), "Port %d is in use, trying port %d (attempt %d/%d)",
+ actual_port, actual_port + 1, port_attempts + 1, max_port_attempts);
+ log_warning(retry_msg);
+ actual_port++;
+ continue;
+ } else {
+ char error_msg[512];
+ snprintf(error_msg, sizeof(error_msg),
+ "Failed to find available port after %d attempts (tried ports %d-%d)",
+ max_port_attempts, configured_port, actual_port);
+ log_error(error_msg);
+ return -1;
+ }
+ }
+
+ // Port appears available, try creating libwebsockets context
+ info.port = actual_port;
+
+ char binding_msg[256];
+ snprintf(binding_msg, sizeof(binding_msg), "Attempting to bind libwebsockets to port %d", actual_port);
+ log_info(binding_msg);
+
+ ws_context = lws_create_context(&info);
+ if (ws_context) {
+ // Success! Port binding worked
+ break;
+ }
+
+ // libwebsockets failed even though port check passed
+ // This could be due to timing or different socket options
+ int errno_saved = errno;
+ char lws_error_msg[256];
+ snprintf(lws_error_msg, sizeof(lws_error_msg),
+ "libwebsockets failed to bind to port %d (errno: %d)", actual_port, errno_saved);
+ log_warning(lws_error_msg);
+
+ port_attempts++;
+ if (strict_port) {
+ char error_msg[256];
+ snprintf(error_msg, sizeof(error_msg),
+ "Strict port mode: failed to bind to port %d", actual_port);
+ log_error(error_msg);
+ break;
+ } else if (port_attempts < max_port_attempts) {
+ actual_port++;
+ continue;
+ }
+
+ // If we get here, we've exhausted attempts
+ break;
+ }
+
+ if (!ws_context) {
+ char error_msg[512];
+ snprintf(error_msg, sizeof(error_msg),
+ "Failed to create libwebsockets context after %d attempts. Last attempted port: %d",
+ port_attempts, actual_port);
+ log_error(error_msg);
+ perror("libwebsockets creation error");
+ return -1;
+ }
+
+ char startup_msg[256];
+ if (actual_port != configured_port) {
+ snprintf(startup_msg, sizeof(startup_msg),
+ "WebSocket relay started on ws://127.0.0.1:%d (configured port %d was unavailable)",
+ actual_port, configured_port);
+ log_warning(startup_msg);
+ } else {
+ snprintf(startup_msg, sizeof(startup_msg), "WebSocket relay started on ws://127.0.0.1:%d", actual_port);
+ }
+ log_success(startup_msg);
+
+ // Main event loop with proper signal handling
+ while (g_server_running) {
+ int result = lws_service(ws_context, 1000);
+
+ if (result < 0) {
+ log_error("libwebsockets service error");
+ break;
+ }
+ }
+
+ log_info("Shutting down WebSocket server...");
+ lws_context_destroy(ws_context);
+ ws_context = NULL;
+
+ log_success("WebSocket relay shut down cleanly");
+ return 0;
+}
diff --git a/src/websockets.h b/src/websockets.h
new file mode 100644
index 0000000..9978fdc
--- /dev/null
+++ b/src/websockets.h
@@ -0,0 +1,49 @@
+// WebSocket protocol structures and constants for C-Relay
+// This header defines structures shared between main.c and websockets.c
+
+#ifndef WEBSOCKETS_H
+#define WEBSOCKETS_H
+
+#include
+#include
+#include
+#include "../nostr_core_lib/cjson/cJSON.h"
+#include "config.h" // For CLIENT_IP_MAX_LENGTH and MAX_SUBSCRIPTIONS_PER_CLIENT
+
+// Constants
+#define CHALLENGE_MAX_LENGTH 128
+#define AUTHENTICATED_PUBKEY_MAX_LENGTH 65 // 64 hex + null
+
+// Enhanced per-session data with subscription management and NIP-42 authentication
+struct per_session_data {
+ int authenticated;
+ struct subscription* subscriptions; // Head of this session's subscription list
+ pthread_mutex_t session_lock; // Per-session thread safety
+ char client_ip[CLIENT_IP_MAX_LENGTH]; // Client IP for logging
+ int subscription_count; // Number of subscriptions for this session
+
+ // NIP-42 Authentication State
+ char authenticated_pubkey[65]; // Authenticated public key (64 hex + null)
+ char active_challenge[65]; // Current challenge for this session (64 hex + null)
+ time_t challenge_created; // When challenge was created
+ time_t challenge_expires; // Challenge expiration time
+ int nip42_auth_required_events; // Whether NIP-42 auth is required for EVENT submission
+ int nip42_auth_required_subscriptions; // Whether NIP-42 auth is required for REQ operations
+ int auth_challenge_sent; // Whether challenge has been sent (0/1)
+};
+
+// NIP-11 HTTP session data structure for managing buffer lifetime
+struct nip11_session_data {
+ char* json_buffer;
+ size_t json_length;
+ int headers_sent;
+ int body_sent;
+};
+
+// Function declarations
+int start_websocket_relay(int port_override, int strict_port);
+
+// Auth rules checking function from request_validator.c
+int check_database_auth_rules(const char *pubkey, const char *operation, const char *resource_hash);
+
+#endif // WEBSOCKETS_H
\ No newline at end of file
diff --git a/tests/1_nip_test.sh b/tests/1_nip_test.sh
index b6ca91b..9d5feb5 100755
--- a/tests/1_nip_test.sh
+++ b/tests/1_nip_test.sh
@@ -146,27 +146,115 @@ test_subscription() {
local filter="$2"
local description="$3"
local expected_count="$4"
-
+
print_step "Testing subscription: $description"
-
+
# Create REQ message
local req_message="[\"REQ\",\"$sub_id\",$filter]"
-
+
print_info "Testing filter: $filter"
-
+
# Send subscription and collect events
local response=""
if command -v websocat &> /dev/null; then
response=$(echo -e "$req_message\n[\"CLOSE\",\"$sub_id\"]" | timeout 3s websocat "$RELAY_URL" 2>/dev/null || echo "")
fi
-
-
+
+
# Count EVENT responses (lines containing ["EVENT","sub_id",...])
local event_count=0
+ local filter_mismatch_count=0
if [[ -n "$response" ]]; then
event_count=$(echo "$response" | grep -c "\"EVENT\"" 2>/dev/null || echo "0")
+ filter_mismatch_count=$(echo "$response" | grep -c "filter does not match" 2>/dev/null || echo "0")
fi
-
+
+ # Clean up the filter_mismatch_count (remove any extra spaces/newlines)
+ filter_mismatch_count=$(echo "$filter_mismatch_count" | tr -d '[:space:]' | sed 's/[^0-9]//g')
+ if [[ -z "$filter_mismatch_count" ]]; then
+ filter_mismatch_count=0
+ fi
+
+ # Debug: Show what we found
+ print_info "Found $event_count events, $filter_mismatch_count filter mismatches"
+
+ # Check for filter mismatches (protocol violation)
+ if [[ "$filter_mismatch_count" -gt 0 ]]; then
+ print_error "$description - PROTOCOL VIOLATION: Relay sent $filter_mismatch_count events that don't match filter!"
+ print_error "Filter: $filter"
+ print_error "This indicates improper server-side filtering - relay should only send matching events"
+ return 1
+ fi
+
+ # Additional check: Analyze returned events against filter criteria
+ local filter_violation_count=0
+ if [[ -n "$response" && "$event_count" -gt 0 ]]; then
+ # Parse filter to check for violations
+ if echo "$filter" | grep -q '"kinds":\['; then
+ # Kind filter - check that all returned events have matching kinds
+ local allowed_kinds=$(echo "$filter" | sed 's/.*"kinds":\[\([^]]*\)\].*/\1/' | sed 's/[^0-9,]//g')
+ echo "$response" | grep '"EVENT"' | while IFS= read -r event_line; do
+ local event_kind=$(echo "$event_line" | jq -r '.[2].kind' 2>/dev/null)
+ if [[ -n "$event_kind" && "$event_kind" =~ ^[0-9]+$ ]]; then
+ local kind_matches=0
+ IFS=',' read -ra KIND_ARRAY <<< "$allowed_kinds"
+ for kind in "${KIND_ARRAY[@]}"; do
+ if [[ "$event_kind" == "$kind" ]]; then
+ kind_matches=1
+ break
+ fi
+ done
+ if [[ "$kind_matches" == "0" ]]; then
+ ((filter_violation_count++))
+ fi
+ fi
+ done
+ elif echo "$filter" | grep -q '"ids":\['; then
+ # ID filter - check that all returned events have matching IDs
+ local allowed_ids=$(echo "$filter" | sed 's/.*"ids":\[\([^]]*\)\].*/\1/' | sed 's/"//g' | sed 's/[][]//g')
+ echo "$response" | grep '"EVENT"' | while IFS= read -r event_line; do
+ local event_id=$(echo "$event_line" | jq -r '.[2].id' 2>/dev/null)
+ if [[ -n "$event_id" ]]; then
+ local id_matches=0
+ IFS=',' read -ra ID_ARRAY <<< "$allowed_ids"
+ for id in "${ID_ARRAY[@]}"; do
+ if [[ "$event_id" == "$id" ]]; then
+ id_matches=1
+ break
+ fi
+ done
+ if [[ "$id_matches" == "0" ]]; then
+ ((filter_violation_count++))
+ fi
+ fi
+ done
+ fi
+ fi
+
+ # Report filter violations
+ if [[ "$filter_violation_count" -gt 0 ]]; then
+ print_error "$description - FILTER VIOLATION: $filter_violation_count events don't match the filter criteria!"
+ print_error "Filter: $filter"
+ print_error "Expected only events matching the filter, but received non-matching events"
+ print_error "This indicates improper server-side filtering"
+ return 1
+ fi
+
+ # Also fail on count mismatches for strict filters (like specific IDs and kinds with expected counts)
+ if [[ "$expected_count" != "any" && "$event_count" != "$expected_count" ]]; then
+ if echo "$filter" | grep -q '"ids":\['; then
+ print_error "$description - CRITICAL VIOLATION: ID filter should return exactly $expected_count event(s), got $event_count"
+ print_error "Filter: $filter"
+ print_error "ID queries must return exactly the requested event or none"
+ return 1
+ elif echo "$filter" | grep -q '"kinds":\[' && [[ "$expected_count" =~ ^[0-9]+$ ]]; then
+ print_error "$description - FILTER VIOLATION: Kind filter expected $expected_count event(s), got $event_count"
+ print_error "Filter: $filter"
+ print_error "This suggests improper filtering - events of wrong kinds are being returned"
+ return 1
+ fi
+ fi
+
if [[ "$expected_count" == "any" ]]; then
if [[ $event_count -gt 0 ]]; then
print_success "$description - Found $event_count events"
@@ -178,7 +266,7 @@ test_subscription() {
else
print_warning "$description - Expected $expected_count events, found $event_count"
fi
-
+
# Show a few sample events for verification (first 2)
if [[ $event_count -gt 0 && "$description" == "All events" ]]; then
print_info "Sample events (first 2):"
@@ -189,7 +277,7 @@ test_subscription() {
echo " - ID: ${event_id:0:16}... Kind: $event_kind Content: ${event_content:0:30}..."
done
fi
-
+
echo # Add blank line for readability
return 0
}
@@ -290,30 +378,64 @@ run_comprehensive_test() {
# Test subscription filters
print_step "Testing various subscription filters..."
-
+
+ local test_failures=0
+
# Test 1: Get all events
- test_subscription "test_all" '{}' "All events" "any"
-
+ if ! test_subscription "test_all" '{}' "All events" "any"; then
+ ((test_failures++))
+ fi
+
# Test 2: Get events by kind
- test_subscription "test_kind1" '{"kinds":[1]}' "Kind 1 events only" "2"
- test_subscription "test_kind0" '{"kinds":[0]}' "Kind 0 events only" "any"
-
+ if ! test_subscription "test_kind1" '{"kinds":[1]}' "Kind 1 events only" "any"; then
+ ((test_failures++))
+ fi
+ if ! test_subscription "test_kind0" '{"kinds":[0]}' "Kind 0 events only" "any"; then
+ ((test_failures++))
+ fi
+
# Test 3: Get events by author (pubkey)
local test_pubkey=$(echo "$regular1" | jq -r '.pubkey' 2>/dev/null)
- test_subscription "test_author" "{\"authors\":[\"$test_pubkey\"]}" "Events by specific author" "any"
-
+ if ! test_subscription "test_author" "{\"authors\":[\"$test_pubkey\"]}" "Events by specific author" "any"; then
+ ((test_failures++))
+ fi
+
# Test 4: Get recent events (time-based)
local recent_timestamp=$(($(date +%s) - 200))
- test_subscription "test_recent" "{\"since\":$recent_timestamp}" "Recent events" "any"
-
+ if ! test_subscription "test_recent" "{\"since\":$recent_timestamp}" "Recent events" "any"; then
+ ((test_failures++))
+ fi
+
# Test 5: Get events with specific tags
- test_subscription "test_tag_type" '{"#type":["regular"]}' "Events with type=regular tag" "any"
-
+ if ! test_subscription "test_tag_type" '{"#type":["regular"]}' "Events with type=regular tag" "any"; then
+ ((test_failures++))
+ fi
+
# Test 6: Multiple kinds
- test_subscription "test_multi_kinds" '{"kinds":[0,1]}' "Multiple kinds (0,1)" "any"
-
+ if ! test_subscription "test_multi_kinds" '{"kinds":[0,1]}' "Multiple kinds (0,1)" "any"; then
+ ((test_failures++))
+ fi
+
# Test 7: Limit results
- test_subscription "test_limit" '{"kinds":[1],"limit":1}' "Limited to 1 event" "1"
+ if ! test_subscription "test_limit" '{"kinds":[1],"limit":1}' "Limited to 1 event" "1"; then
+ ((test_failures++))
+ fi
+
+ # Test 8: Specific event ID query (tests for "filter does not match" bug)
+ if [[ ${#REGULAR_EVENT_IDS[@]} -gt 0 ]]; then
+ local test_event_id="${REGULAR_EVENT_IDS[0]}"
+ if ! test_subscription "test_specific_id" "{\"ids\":[\"$test_event_id\"]}" "Specific event ID query" "1"; then
+ ((test_failures++))
+ fi
+ fi
+
+ # Report subscription test results
+ if [[ $test_failures -gt 0 ]]; then
+ print_error "SUBSCRIPTION TESTS FAILED: $test_failures test(s) detected protocol violations"
+ return 1
+ else
+ print_success "All subscription tests passed"
+ fi
print_header "PHASE 4: Database Verification"
@@ -321,17 +443,28 @@ run_comprehensive_test() {
print_step "Verifying database contents..."
if command -v sqlite3 &> /dev/null; then
- print_info "Events by type in database:"
- sqlite3 db/c_nostr_relay.db "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" | while read line; do
- echo " $line"
- done
-
- print_info "Recent events in database:"
- sqlite3 db/c_nostr_relay.db "SELECT substr(id, 1, 16) || '...' as short_id, event_type, kind, substr(content, 1, 30) || '...' as short_content FROM events ORDER BY created_at DESC LIMIT 5;" | while read line; do
- echo " $line"
- done
-
- print_success "Database verification complete"
+ # Find the database file (should be in build/ directory with relay pubkey as filename)
+ local db_file=""
+ if [[ -d "../build" ]]; then
+ db_file=$(find ../build -name "*.db" -type f | head -1)
+ fi
+
+ if [[ -n "$db_file" && -f "$db_file" ]]; then
+ print_info "Events by type in database ($db_file):"
+ sqlite3 "$db_file" "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null | while read line; do
+ echo " $line"
+ done
+
+ print_info "Recent events in database:"
+ sqlite3 "$db_file" "SELECT substr(id, 1, 16) || '...' as short_id, event_type, kind, substr(content, 1, 30) || '...' as short_content FROM events ORDER BY created_at DESC LIMIT 5;" 2>/dev/null | while read line; do
+ echo " $line"
+ done
+
+ print_success "Database verification complete"
+ else
+ print_warning "Database file not found in build/ directory"
+ print_info "Expected database files: build/*.db (named after relay pubkey)"
+ fi
else
print_warning "sqlite3 not available for database verification"
fi
@@ -352,6 +485,11 @@ if run_comprehensive_test; then
exit 0
else
echo
- print_error "Some tests failed"
+ print_error "❌ TESTS FAILED: Protocol violations detected!"
+ print_error "The C-Relay has critical issues that need to be fixed:"
+ print_error " - Server-side filtering is not implemented properly"
+ print_error " - Events are sent to clients regardless of subscription filters"
+ print_error " - This violates the Nostr protocol specification"
+ echo
exit 1
fi
\ No newline at end of file
diff --git a/tests/nip42_test.log b/tests/nip42_test.log
new file mode 100644
index 0000000..bf27312
--- /dev/null
+++ b/tests/nip42_test.log
@@ -0,0 +1,88 @@
+=== NIP-42 Authentication Test Started ===
+2025-09-30 11:15:28 - Starting NIP-42 authentication tests
+[34m[1m[INFO][0m === Starting NIP-42 Authentication Tests ===
+[34m[1m[INFO][0m Checking dependencies...
+[32m[1m[SUCCESS][0m Dependencies check complete
+[34m[1m[INFO][0m Test 1: Checking NIP-42 support in relay info
+[32m[1m[SUCCESS][0m NIP-42 is advertised in supported NIPs
+2025-09-30 11:15:28 - Supported NIPs: 1,9,11,13,15,20,40,42
+[34m[1m[INFO][0m Test 2: Testing AUTH challenge generation
+[34m[1m[INFO][0m Found admin private key, configuring NIP-42 authentication...
+[33m[1m[WARNING][0m Failed to create configuration event - proceeding with manual test
+[34m[1m[INFO][0m Test 3: Testing complete NIP-42 authentication flow
+[34m[1m[INFO][0m Generated test keypair: test_pubkey
+[34m[1m[INFO][0m Attempting to publish event without authentication...
+[34m[1m[INFO][0m Publishing test event to relay...
+2025-09-30 11:15:30 - Event publish result: connecting to ws://localhost:8888... ok.
+{"kind":1,"id":"acfc4da1903ce1c065f2c472348b21837a322c79cb4b248c62de5cff9b5b6607","pubkey":"d3e8d83eabac2a28e21039136a897399f4866893dd43bfbf0bdc8391913a4013","created_at":1759245329,"tags":[],"content":"NIP-42 test event - should require auth","sig":"2051b3da705214d5b5e95fb5b4dd9f1c893666965f7c51ccd2a9ccd495b67dd76ed3ce9768f0f2a16a3f9a602368e8102758ca3cc1408280094abf7e92fcc75e"}
+publishing to ws://localhost:8888... success.
+[32m[1m[SUCCESS][0m Relay requested authentication as expected
+[34m[1m[INFO][0m Test 4: Testing WebSocket AUTH message handling
+[34m[1m[INFO][0m Testing WebSocket connection and AUTH message...
+[34m[1m[INFO][0m Sending test message via WebSocket...
+2025-09-30 11:15:30 - WebSocket response:
+[34m[1m[INFO][0m No AUTH challenge in WebSocket response
+[34m[1m[INFO][0m Test 5: Testing NIP-42 configuration options
+[34m[1m[INFO][0m Retrieving current relay configuration...
+[33m[1m[WARNING][0m Could not retrieve configuration events
+[34m[1m[INFO][0m Test 6: Testing NIP-42 performance and stability
+[34m[1m[INFO][0m Testing multiple authentication attempts...
+2025-09-30 11:15:31 - Attempt 1: .297874340s - connecting to ws://localhost:8888... ok.
+{"kind":1,"id":"0d742f093b7be0ce811068e7a6171573dd225418c9459f5c7e9580f57d88af7b","pubkey":"37d1a52ec83a837eb8c6ae46df5c892f338c65ae0c29eb4873e775082252a18a","created_at":1759245331,"tags":[],"content":"Performance test event 1","sig":"d4aec950c47fbd4c1da637b84fafbde570adf86e08795236fb6a3f7e12d2dbaa16cb38cbb68d3b9755d186b20800bdb84b0a050f8933d06b10991a9542fe9909"}
+publishing to ws://localhost:8888... success.
+2025-09-30 11:15:32 - Attempt 2: .270493759s - connecting to ws://localhost:8888... ok.
+{"kind":1,"id":"b45ae1b0458e284ed89b6de453bab489d506352680f6d37c8a5f0aed9eebc7a5","pubkey":"37d1a52ec83a837eb8c6ae46df5c892f338c65ae0c29eb4873e775082252a18a","created_at":1759245331,"tags":[],"content":"Performance test event 2","sig":"f9702aa537ec1485d151a0115c38c7f6f1bc05a63929be784e33850b46be6a961996eb922b8b337d607312c8e4583590ee35f38330300e19ab921f94926719c5"}
+publishing to ws://localhost:8888... success.
+2025-09-30 11:15:32 - Attempt 3: .239220029s - connecting to ws://localhost:8888... ok.
+{"kind":1,"id":"5f70f9cb2a30a12e7d088e62a9295ef2fbea4f40a1d8b07006db03f610c5abce","pubkey":"37d1a52ec83a837eb8c6ae46df5c892f338c65ae0c29eb4873e775082252a18a","created_at":1759245332,"tags":[],"content":"Performance test event 3","sig":"ea2e1611ce3ddea3aa73764f4542bad7d922fc0d2ed40e58dcc2a66cb6e046bfae22d6baef296eb51d965a22b2a07394fc5f8664e3a7777382ae523431c782cd"}
+publishing to ws://localhost:8888... success.
+2025-09-30 11:15:33 - Attempt 4: .221429674s - connecting to ws://localhost:8888... ok.
+{"kind":1,"id":"eafcf5f7e0bd0be35267f13ff93eef339faec6a5af13fe451fee2b7443b9de6e","pubkey":"37d1a52ec83a837eb8c6ae46df5c892f338c65ae0c29eb4873e775082252a18a","created_at":1759245332,"tags":[],"content":"Performance test event 4","sig":"976017abe67582af29d46cd54159ce0465c94caf348be35f26b6522cb48c4c9ce5ba9835e92873cf96a906605a032071360fc85beea815a8e4133a4f45d2bf0a"}
+publishing to ws://localhost:8888... success.
+2025-09-30 11:15:33 - Attempt 5: .242410067s - connecting to ws://localhost:8888... ok.
+{"kind":1,"id":"c7cf6776000a325b1180240c61ef20b849b84dee3f5d2efed4c1a9e9fbdbd7b1","pubkey":"37d1a52ec83a837eb8c6ae46df5c892f338c65ae0c29eb4873e775082252a18a","created_at":1759245333,"tags":[],"content":"Performance test event 5","sig":"18b4575bd644146451dcf86607d75f358828ce2907e8904bd08b903ff5d79ec5a69ff60168735975cc406dcee788fd22fc7bf7c97fb7ac6dff3580eda56cee2e"}
+publishing to ws://localhost:8888... success.
+[32m[1m[SUCCESS][0m Performance test completed: 5/5 successful responses
+[34m[1m[INFO][0m Test 7: Testing kind-specific NIP-42 authentication requirements
+[34m[1m[INFO][0m Generated test keypair for kind-specific tests: test_pubkey
+[34m[1m[INFO][0m Testing kind 1 event (regular note) - should work without authentication...
+2025-09-30 11:15:34 - Kind 1 event result: connecting to ws://localhost:8888... ok.
+{"kind":1,"id":"012690335e48736fd29769669d2bda15a079183c1d0f27b8400366a54b5b9ddd","pubkey":"ad362b9bbf61b140c5f677a2d091d622fef6fa186c579e6600dd8b24a85a2260","created_at":1759245334,"tags":[],"content":"Regular note - should not require auth","sig":"a3a0ce218666d2a374983a343bc24da5a727ce251c23828171021f15a3ab441a0c86f56200321467914ce4bee9a987f1de301151467ae639d7f941bac7fbe68e"}
+publishing to ws://localhost:8888... success.
+[32m[1m[SUCCESS][0m Kind 1 event accepted without authentication (correct behavior)
+[34m[1m[INFO][0m Testing kind 4 event (direct message) - should require authentication...
+2025-09-30 11:15:44 - Kind 4 event result: connecting to ws://localhost:8888... ok.
+{"kind":4,"id":"e629dd91320d48c1e3103ec16e40c707c2ee8143012c9ad8bb9d32f98610f447","pubkey":"ad362b9bbf61b140c5f677a2d091d622fef6fa186c579e6600dd8b24a85a2260","created_at":1759245334,"tags":[["p,test_pubkey"]],"content":"This is a direct message - should require auth","sig":"7677b3f2932fb4979bab3da6d241217b7ea2010411fc8bf5a51f6987f38696d5634f91a30b13e0f4861479ceabff995b3bb2eb2fc74af5f3d1175235d5448ce2"}
+publishing to ws://localhost:8888...
+[32m[1m[SUCCESS][0m Kind 4 event requested authentication (correct behavior for DMs)
+[34m[1m[INFO][0m Testing kind 14 event (chat message) - should require authentication...
+2025-09-30 11:15:55 - Kind 14 event result: connecting to ws://localhost:8888... ok.
+{"kind":14,"id":"a5398c5851dd72a8980723c91d35345bd0088b800102180dd41af7056f1cad50","pubkey":"ad362b9bbf61b140c5f677a2d091d622fef6fa186c579e6600dd8b24a85a2260","created_at":1759245344,"tags":[["p,test_pubkey"]],"content":"Chat message - should require auth","sig":"62d43f3f81755d4ef81cbfc8aca9abc11f28b0c45640f19d3dd41a09bae746fe7a4e9d8e458c416dcd2cab02deb090ce1e29e8426d9be5445d130eaa00d339f2"}
+publishing to ws://localhost:8888...
+[32m[1m[SUCCESS][0m Kind 14 event requested authentication (correct behavior for DMs)
+[34m[1m[INFO][0m Testing other event kinds - should work without authentication...
+2025-09-30 11:15:55 - Kind 0 event result: connecting to ws://localhost:8888... ok.
+{"kind":0,"id":"069ac4db07da3230681aa37ab9e6a2aa48e2c199245259681e45ffb2f1b21846","pubkey":"ad362b9bbf61b140c5f677a2d091d622fef6fa186c579e6600dd8b24a85a2260","created_at":1759245355,"tags":[],"content":"Test event kind 0 - should not require auth","sig":"3c99b97c0ea2d18bc88fc07b2e95e213b6a6af804512d62158f8fd63cc24a3937533b830f59d38ccacccf98ba2fb0ed7467b16271154d4dd37fbc075eba32e49"}
+publishing to ws://localhost:8888... success.
+[32m[1m[SUCCESS][0m Kind 0 event accepted without authentication (correct)
+2025-09-30 11:15:56 - Kind 3 event result: connecting to ws://localhost:8888... ok.
+{"kind":3,"id":"1dd1ccb13ebd0d50b2aa79dbb938b408a24f0a4dd9f872b717ed91ae6729051c","pubkey":"ad362b9bbf61b140c5f677a2d091d622fef6fa186c579e6600dd8b24a85a2260","created_at":1759245355,"tags":[],"content":"Test event kind 3 - should not require auth","sig":"c205cc76f687c3957cf8b35cd8346fd8c2e44d9ef82324b95a7eef7f57429fb6f2ab1d0263dd5d00204dd90e626d5918a8710341b0d68a5095b41455f49cf0dd"}
+publishing to ws://localhost:8888... success.
+[32m[1m[SUCCESS][0m Kind 3 event accepted without authentication (correct)
+2025-09-30 11:15:56 - Kind 7 event result: connecting to ws://localhost:8888... ok.
+{"kind":7,"id":"b6161b1da8a4d362e3c230df99c4f87b6311ef6e9f67e03a2476f8a6366352c1","pubkey":"ad362b9bbf61b140c5f677a2d091d622fef6fa186c579e6600dd8b24a85a2260","created_at":1759245356,"tags":[],"content":"Test event kind 7 - should not require auth","sig":"ab06c4b00a04d726109acd02d663e30188ff9ee854cf877e854fda90dd776a649ef3fab8ae5b530b4e6b5530490dd536a281a721e471bd3748a0dacc4eac9622"}
+publishing to ws://localhost:8888... success.
+[32m[1m[SUCCESS][0m Kind 7 event accepted without authentication (correct)
+[34m[1m[INFO][0m Kind-specific authentication test completed
+[34m[1m[INFO][0m === NIP-42 Test Results Summary ===
+[32m[1m[SUCCESS][0m Dependencies: PASS
+[32m[1m[SUCCESS][0m NIP-42 Support: PASS
+[32m[1m[SUCCESS][0m Auth Challenge: PASS
+[32m[1m[SUCCESS][0m Auth Flow: PASS
+[32m[1m[SUCCESS][0m WebSocket AUTH: PASS
+[32m[1m[SUCCESS][0m Configuration: PASS
+[32m[1m[SUCCESS][0m Performance: PASS
+[32m[1m[SUCCESS][0m Kind-Specific Auth: PASS
+[32m[1m[SUCCESS][0m All NIP-42 tests completed successfully!
+[32m[1m[SUCCESS][0m NIP-42 authentication implementation is working correctly
+[34m[1m[INFO][0m === NIP-42 Authentication Tests Complete ===
diff --git a/tests/white_black_list_test.sh b/tests/white_black_list_test.sh
deleted file mode 100755
index f909ab9..0000000
--- a/tests/white_black_list_test.sh
+++ /dev/null
@@ -1,1054 +0,0 @@
-#!/bin/bash
-
-# =======================================================================
-# C-Relay Whitelist/Blacklist Authentication Rules Test Script
-# =======================================================================
-#
-# This test validates the whitelist and blacklist functionality of the
-# C-Relay server through the WebSocket admin API.
-#
-# Test Credentials (Test Mode):
-# - Admin Private Key: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
-# - Admin Public Key: 6a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3
-# - Relay Public Key: 4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa
-#
-# =======================================================================
-
-set -e # Exit on any error
-
-# =======================================================================
-# CONFIGURATION
-# =======================================================================
-
-# Test mode credentials (from current relay startup)
-ADMIN_PRIVKEY="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
-ADMIN_PUBKEY="6a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3"
-RELAY_PUBKEY="4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"
-
-# Server configuration
-RELAY_HOST="127.0.0.1"
-RELAY_PORT="8888"
-RELAY_URL="ws://${RELAY_HOST}:${RELAY_PORT}"
-
-# Test configuration
-TIMEOUT=5
-TEMP_DIR="/tmp/c_relay_test_$$"
-
-# WebSocket connection state (simplified - no persistent connections)
-# These variables are kept for compatibility but not used
-WS_CONNECTED=0
-
-# Color codes for output
-RED='\033[0;31m'
-GREEN='\033[0;32m'
-YELLOW='\033[0;33m'
-BLUE='\033[0;34m'
-BOLD='\033[1m'
-RESET='\033[0m'
-
-# Test tracking
-TESTS_RUN=0
-TESTS_PASSED=0
-TESTS_FAILED=0
-
-# =======================================================================
-# UTILITY FUNCTIONS
-# =======================================================================
-
-log() {
- echo -e "${BLUE}[$(date '+%H:%M:%S')]${RESET} $1"
-}
-
-log_success() {
- echo -e "${GREEN}[SUCCESS]${RESET} $1"
-}
-
-log_error() {
- echo -e "${RED}[ERROR]${RESET} $1"
-}
-
-log_warning() {
- echo -e "${YELLOW}[WARNING]${RESET} $1"
-}
-
-log_info() {
- echo -e "${BLUE}[INFO]${RESET} $1"
-}
-
-increment_test() {
- TESTS_RUN=$((TESTS_RUN + 1))
-}
-
-pass_test() {
- TESTS_PASSED=$((TESTS_PASSED + 1))
- log_success "Test $TESTS_RUN: PASSED - $1"
- echo ""
- echo ""
-}
-
-fail_test() {
- TESTS_FAILED=$((TESTS_FAILED + 1))
- log_error "Test $TESTS_RUN: FAILED - $1"
- echo ""
- echo ""
-}
-
-# Generate test keypairs
-generate_test_keypair() {
- local name=$1
- local privkey_file="${TEMP_DIR}/${name}_privkey"
- local pubkey_file="${TEMP_DIR}/${name}_pubkey"
-
- # Generate private key using nak key --gen (following pattern from other tests)
- local privkey=$(nak key generate 2>/dev/null)
- if [ $? -ne 0 ] || [ -z "$privkey" ]; then
- log_error "Failed to generate private key for $name"
- return 1
- fi
-
- echo "$privkey" > "$privkey_file"
-
- # Derive public key using nak
- local pubkey=$(nak key public "$privkey" 2>/dev/null)
- if [ $? -ne 0 ] || [ -z "$pubkey" ]; then
- log_error "Failed to generate public key for $name"
- return 1
- fi
-
- echo "$pubkey" > "$pubkey_file"
-
- log_info "Generated keypair for $name: pubkey=$pubkey"
-
- # Export for use in calling functions
- eval "${name}_PRIVKEY=\"$privkey\""
- eval "${name}_PUBKEY=\"$pubkey\""
-}
-
-# NIP-44 encryption helper function using nak
-encrypt_nip44_content() {
- local content="$1"
- local sender_privkey="$2"
- local receiver_pubkey="$3"
-
- if [ -z "$content" ] || [ -z "$sender_privkey" ] || [ -z "$receiver_pubkey" ]; then
- log_error "encrypt_nip44_content: missing required parameters"
- return 1
- fi
-
- log_info "DEBUG: About to encrypt content: '$content'" >&2
- log_info "DEBUG: Sender privkey: $sender_privkey" >&2
- log_info "DEBUG: Receiver pubkey: $receiver_pubkey" >&2
-
- # Use nak to perform NIP-44 encryption with correct syntax:
- # nak encrypt --recipient-pubkey --sec [plaintext]
- local encrypted_content
- encrypted_content=$(nak encrypt --recipient-pubkey "$receiver_pubkey" --sec "$sender_privkey" "$content" 2>/dev/null)
-
- if [ $? -ne 0 ] || [ -z "$encrypted_content" ]; then
- log_error "Failed to encrypt content with NIP-44"
- log_error "Content: $content"
- log_error "Sender privkey: $sender_privkey"
- log_error "Receiver pubkey: $receiver_pubkey"
- return 1
- fi
-
- # Validate that encrypted content is valid base64 and doesn't contain problematic characters
- if ! echo "$encrypted_content" | grep -q '^[A-Za-z0-9+/]*=*$'; then
- log_error "Encrypted content contains invalid characters for JSON: $encrypted_content"
- return 1
- fi
-
- # Check if encrypted content is valid UTF-8/base64
- if ! echo "$encrypted_content" | base64 -d >/dev/null 2>&1; then
- log_warning "Encrypted content may not be valid base64: $encrypted_content"
- fi
-
- log_info "DEBUG: Encrypted content: $encrypted_content" >&2
- log_info "Successfully encrypted content with NIP-44" >&2
- echo "$encrypted_content"
- return 0
-}
-
-# NIP-44 decryption helper function using nak
-decrypt_nip44_content() {
- local encrypted_content="$1"
- local receiver_privkey="$2"
- local sender_pubkey="$3"
-
- if [ -z "$encrypted_content" ] || [ -z "$receiver_privkey" ] || [ -z "$sender_pubkey" ]; then
- log_error "decrypt_nip44_content: missing required parameters"
- return 1
- fi
-
- log_info "DEBUG: Decrypting content: $encrypted_content"
-
- # Use nak to perform NIP-44 decryption with correct syntax:
- # nak decrypt --sender-pubkey --sec [encrypted_content]
- local decrypted_content
- decrypted_content=$(nak decrypt --sender-pubkey "$sender_pubkey" --sec "$receiver_privkey" "$encrypted_content" 2>/dev/null)
-
- if [ $? -ne 0 ] || [ -z "$decrypted_content" ]; then
- log_error "Failed to decrypt content with NIP-44"
- log_error "Encrypted content: $encrypted_content"
- log_error "Receiver privkey: $receiver_privkey"
- log_error "Sender pubkey: $sender_pubkey"
- return 1
- fi
-
- log_info "DEBUG: Decrypted content: $decrypted_content"
- log_info "Successfully decrypted content with NIP-44"
- echo "$decrypted_content"
- return 0
-}
-
-# Send WebSocket message and capture response (simplified pattern from 1_nip_test.sh)
-send_websocket_message() {
- local message="$1"
- local timeout="${2:-$TIMEOUT}"
-
- # Use websocat to send message and capture response (following pattern from tests/1_nip_test.sh)
- local response=""
- if command -v websocat &> /dev/null; then
- response=$(printf '%s\n' "$message" | timeout "$timeout" websocat "$RELAY_URL" 2>&1 || echo "Connection failed")
-
- # Check if connection failed
- if [[ "$response" == *"Connection failed"* ]]; then
- log_error "Failed to connect to relay"
- return 1
- fi
-
- else
- log_error "websocat not found - required for WebSocket testing"
- log_error "Please install websocat for WebSocket communication"
- return 1
- fi
-
- echo "$response"
-}
-
-# Send admin event and capture response (simplified pattern from 1_nip_test.sh)
-send_admin_event() {
- local event_json="$1"
- local description="$2"
- local timeout_seconds="${3:-10}"
-
- log_info "Sending admin event: $description"
-
- # Create EVENT message using jq to properly handle special characters
- local event_message
- event_message=$(jq -n --argjson event "$event_json" '["EVENT", $event]')
-
- # Validate that the event message is valid UTF-8 (temporarily disabled for debugging)
- # if ! echo "$event_message" | iconv -f utf-8 -t utf-8 >/dev/null 2>&1; then
- # log_error "Event message contains invalid UTF-8 characters"
- # return 1
- # fi
-
- # Use websocat to send event and capture OK response
- local response=""
- if command -v websocat &> /dev/null; then
- log_info "Sending event using websocat..."
-
- # Debug: Show what we're sending
- log_info "DEBUG: Event message being sent: $event_message"
-
- # Write to temporary file to avoid shell interpretation issues
- local temp_file="${TEMP_DIR}/event_message_$$"
- printf '%s\n' "$event_message" > "$temp_file"
-
- # Send via websocat using file input with delay to receive response
- response=$(timeout "$timeout_seconds" sh -c "cat '$temp_file'; sleep 0.5" | websocat "$RELAY_URL" 2>&1)
- local websocat_exit_code=$?
-
- # Clean up temp file
- rm -f "$temp_file"
-
- log_info "DEBUG: Websocat exit code: $websocat_exit_code"
- log_info "DEBUG: Websocat response: $response"
-
- # Check for specific websocat errors
- if [[ "$response" == *"UTF-8 failure"* ]]; then
- log_error "UTF-8 encoding error in event data for $description"
- log_error "Event message: $event_message"
- return 1
- elif [[ "$response" == *"Connection failed"* ]] || [[ "$response" == *"Connection refused"* ]] || [[ "$response" == *"timeout"* ]]; then
- log_error "Failed to connect to relay for $description"
- return 1
- elif [[ "$response" == *"error running"* ]]; then
- log_error "Websocat error for $description: $response"
- return 1
- elif [ $websocat_exit_code -eq 0 ]; then
- log_info "Event sent successfully via websocat"
- else
- log_warning "Websocat returned exit code $websocat_exit_code"
- fi
-
- else
- log_error "websocat not found - required for WebSocket testing"
- return 1
- fi
-
- echo "$response"
-}
-
-# Send admin query and wait for encrypted response
-send_admin_query() {
- local event_json="$1"
- local description="$2"
- local timeout_seconds="${3:-15}"
-
- log_info "Sending admin query: $description"
-
- # Create EVENT message using jq to properly handle special characters
- local event_message
- event_message=$(jq -n --argjson event "$event_json" '["EVENT", $event]')
-
- # For queries, we need to also send a REQ to get the response
- local sub_id="admin_query_$(date +%s)"
- local req_message="[\"REQ\",\"$sub_id\",{\"kinds\":[23456],\"authors\":[\"$RELAY_PUBKEY\"],\"#p\":[\"$ADMIN_PUBKEY\"]}]"
- local close_message="[\"CLOSE\",\"$sub_id\"]"
-
- # Send query event and subscription in sequence
- local response=""
- if command -v websocat &> /dev/null; then
- response=$(printf '%s\n%s\n%s\n' "$event_message" "$req_message" "$close_message" | timeout "$timeout_seconds" websocat "$RELAY_URL" 2>&1 || echo "Connection failed")
-
- # Check if connection failed
- if [[ "$response" == *"Connection failed"* ]]; then
- log_error "Failed to connect to relay for $description"
- return 1
- fi
-
- # Look for EVENT responses that might contain encrypted query data
- local event_response=$(echo "$response" | grep '"EVENT"' | tail -1)
- if [ -n "$event_response" ]; then
- # Extract the event JSON from the EVENT message
- local event_json=$(echo "$event_response" | jq -r '.[2]' 2>/dev/null)
-
- if [ -n "$event_json" ] && [ "$event_json" != "null" ]; then
- # Check if this is a kind 23456 response event
- local event_kind=$(echo "$event_json" | jq -r '.kind' 2>/dev/null)
-
- if [ "$event_kind" = "23456" ]; then
- # Extract encrypted content and decrypt it
- local encrypted_content=$(echo "$event_json" | jq -r '.content' 2>/dev/null)
-
- if [ -n "$encrypted_content" ] && [ "$encrypted_content" != "null" ]; then
- # Decrypt the response using NIP-44
- local decrypted_content
- decrypted_content=$(decrypt_nip44_content "$encrypted_content" "$ADMIN_PRIVKEY" "$RELAY_PUBKEY")
-
- if [ $? -eq 0 ] && [ -n "$decrypted_content" ]; then
- log_info "Successfully decrypted query response"
- echo "$decrypted_content"
- return 0
- else
- log_warning "Failed to decrypt query response content"
- fi
- fi
- fi
- fi
- fi
-
- else
- log_error "websocat not found - required for WebSocket testing"
- return 1
- fi
-
- # Return the raw response if no encrypted content found
- echo "$response"
-}
-
-# Create and send auth rule event
-send_auth_rule_event() {
- local action="$1" # "add" or "remove"
- local rule_type="$2" # "whitelist" or "blacklist"
- local pattern_type="$3" # "pubkey" or "hash"
- local pattern_value="$4" # actual pubkey or hash value
- local description="$5" # optional description
-
- log_info "Creating auth rule event: $action $rule_type $pattern_type $pattern_value"
-
- # Create command array according to README.md API specification
- # Format: ["blacklist", "pubkey", "abc123..."] or ["whitelist", "pubkey", "def456..."]
- local command_array="[\"$rule_type\", \"$pattern_type\", \"$pattern_value\"]"
-
- # Encrypt the command content using NIP-44
- local encrypted_content
- encrypted_content=$(encrypt_nip44_content "$command_array" "$ADMIN_PRIVKEY" "$RELAY_PUBKEY")
-
- if [ $? -ne 0 ] || [ -z "$encrypted_content" ]; then
- log_error "Failed to encrypt auth rule command content"
- return 1
- fi
-
- # Create the auth rule event using nak with NIP-44 encrypted content
- # Using Kind 23456 (admin commands) with proper relay targeting and encrypted content
- local event_json
- event_json=$(nak event -k 23456 --content "$encrypted_content" \
- -t "p=$RELAY_PUBKEY" \
- --sec "$ADMIN_PRIVKEY" 2>/dev/null)
-
- if [ $? -ne 0 ] || [ -z "$event_json" ]; then
- log_error "Failed to create auth rule event with nak"
- return 1
- fi
-
- log_info "DEBUG: Created event JSON: $event_json"
-
- # Send the event using simplified WebSocket pattern
- log_info "Publishing auth rule event to relay..."
- local result
- result=$(send_admin_event "$event_json" "auth rule $action")
- local exit_code=$?
-
- log_info "Auth rule event result: $result"
-
- # Check if response indicates success
- if [ $exit_code -eq 0 ] && echo "$result" | grep -q -i '"OK".*true'; then
- log_success "Auth rule $action successful"
- return 0
- else
- log_error "Auth rule $action failed: $result (exit code: $exit_code)"
- return 1
- fi
-}
-
-# Clear all auth rules using the new system command functionality
-clear_all_auth_rules() {
- log_info "Clearing all existing auth rules..."
-
- # Create command array according to README.md API specification
- # Format: ["system_command", "clear_all_auth_rules"]
- local command_array="[\"system_command\", \"clear_all_auth_rules\"]"
- log_info "DEBUG: Command array: $command_array"
-
- # Encrypt the command content using NIP-44
- local encrypted_content
- encrypted_content=$(encrypt_nip44_content "$command_array" "$ADMIN_PRIVKEY" "$RELAY_PUBKEY")
-
- if [ $? -ne 0 ] || [ -z "$encrypted_content" ]; then
- log_error "Failed to encrypt system command content"
- return 1
- fi
-
- log_info "DEBUG: Encrypted content: $encrypted_content"
-
- # Create system command event to clear all auth rules
- # Using Kind 23456 (admin commands) with proper relay targeting and encrypted content
- local event_json
- event_json=$(nak event -k 23456 --content "$encrypted_content" \
- -t "p=$RELAY_PUBKEY" \
- --sec "$ADMIN_PRIVKEY" 2>/dev/null)
-
- if [ $? -ne 0 ] || [ -z "$event_json" ]; then
- log_error "Failed to create clear auth rules event with nak"
- return 1
- fi
-
- log_info "DEBUG: Created event JSON: $event_json"
-
- # Send the event using simplified WebSocket pattern
- log_info "Sending clear all auth rules command..."
- local result
- result=$(send_admin_event "$event_json" "clear all auth rules")
- local exit_code=$?
-
- log_info "Clear auth rules result: $result"
-
- # Check if response indicates success
- if [ $exit_code -eq 0 ] && echo "$result" | grep -q -i '"OK".*true'; then
- log_success "All auth rules cleared successfully"
- return 0
- else
- log_error "Failed to clear auth rules: $result (exit code: $exit_code)"
- return 1
- fi
-}
-
-# Test event publishing with a specific key
-test_event_publishing() {
- local test_privkey="$1"
- local test_pubkey="$2"
- local expected_result="$3" # "success" or "blocked"
- local description="$4"
-
- log_info "Testing event publishing: $description"
-
- # Create a simple test event (kind 1 - text note) using nak like NIP-42 test
- local test_content="Test message from $test_pubkey at $(date)"
- local test_event
- test_event=$(nak event -k 1 --content "$test_content" --sec "$test_privkey" 2>/dev/null)
-
- if [ $? -ne 0 ] || [ -z "$test_event" ]; then
- log_error "Failed to create test event"
- return 1
- fi
-
- # Send the event using nak directly (more reliable than websocat)
- log_info "Publishing test event to relay..."
- local result
- result=$(printf '%s\n' "$test_event" | timeout 10s nak event "$RELAY_URL" 2>&1)
- local exit_code=$?
-
- log_info "Event publishing result: $result"
-
- # Check result against expectation
- if [ "$expected_result" = "success" ]; then
- if [ $exit_code -eq 0 ] && echo "$result" | grep -q -i "success\|OK.*true\|published"; then
- log_success "Event publishing allowed as expected"
- return 0
- else
- log_error "Event publishing was blocked but should have been allowed: $result"
- return 1
- fi
- else # expected_result = "blocked"
- if [ $exit_code -ne 0 ] || echo "$result" | grep -q -i "blocked\|denied\|rejected\|auth.*required\|OK.*false"; then
- log_success "Event publishing blocked as expected"
- return 0
- else
- log_error "Event publishing was allowed but should have been blocked: $result"
- return 1
- fi
- fi
-}
-
-# =======================================================================
-# SETUP AND INITIALIZATION
-# =======================================================================
-
-setup_test_environment() {
- log "Setting up test environment..."
-
- # Create temporary directory
- mkdir -p "$TEMP_DIR"
-
- # Check if required tools are available - like NIP-42 test
- log_info "Checking dependencies..."
-
- if ! command -v nak &> /dev/null; then
- log_error "nak client not found. Please install: go install github.com/fiatjaf/nak@latest"
- exit 1
- fi
-
- if ! command -v jq &> /dev/null; then
- log_error "jq not found. Please install jq for JSON processing"
- exit 1
- fi
-
-
- if ! command -v timeout &> /dev/null; then
- log_error "timeout not found. Please install coreutils"
- exit 1
- fi
-
- if ! command -v websocat &> /dev/null; then
- log_error "websocat not found - required for WebSocket testing"
- log_error "Please install websocat for WebSocket communication"
- exit 1
- fi
-
- log_success "Dependencies check complete"
-
- # Generate test keypairs
- generate_test_keypair "TEST1"
- generate_test_keypair "TEST2"
- generate_test_keypair "TEST3"
-
- log_success "Test environment setup complete"
-}
-
-# =======================================================================
-# TEST FUNCTIONS
-# =======================================================================
-
-# Test 1: Admin Authentication
-test_admin_authentication() {
- increment_test
- log "Test $TESTS_RUN: Admin Authentication"
-
- # Create a simple configuration event to test admin authentication
- # Using Kind 23456 (admin commands) with NIP-44 encrypted content
- # Format: ["system_command", "system_status"]
- local command_array="[\"system_command\", \"system_status\"]"
-
- # Encrypt the command content using NIP-44
- local encrypted_content
- encrypted_content=$(encrypt_nip44_content "$command_array" "$ADMIN_PRIVKEY" "$RELAY_PUBKEY")
-
- if [ $? -ne 0 ] || [ -z "$encrypted_content" ]; then
- fail_test "Failed to encrypt admin authentication test content"
- return
- fi
-
- local config_event
- config_event=$(nak event -k 23456 --content "$encrypted_content" \
- -t "p=$RELAY_PUBKEY" \
- --sec "$ADMIN_PRIVKEY" 2>/dev/null)
-
- if [ $? -ne 0 ]; then
- fail_test "Failed to create admin test event"
- return
- fi
-
- # Send admin event using the proper admin event function
- local response
- response=$(send_admin_event "$config_event" "admin authentication test")
- local exit_code=$?
-
- log_info "Admin authentication result: $response"
-
- if [ $exit_code -eq 0 ] && echo "$response" | grep -q '"OK".*true'; then
- pass_test "Admin authentication successful"
- else
- fail_test "Admin authentication failed: $response (exit code: $exit_code)"
- fi
-}
-
-# Test 2: Auth Rules Storage and Query Test
-test_auth_rules_storage_query() {
- increment_test
- log "Test $TESTS_RUN: Auth Rules Storage and Query Test"
-
- # Clear all existing rules to start fresh
- clear_all_auth_rules
-
- # Add a simple blacklist rule
- log_info "Adding test blacklist rule..."
- if send_auth_rule_event "add" "blacklist" "pubkey" "$TEST1_PUBKEY" "Test storage blacklist entry"; then
- log_success "Auth rule added successfully"
-
- # Wait a moment for rule to be processed
- sleep 1
-
- # Query all auth rules using admin query
- log_info "Querying all auth rules..."
- # Create command array according to README.md API specification
- # Format: ["auth_query", "all"]
- local command_array="[\"auth_query\", \"all\"]"
-
- # Encrypt the command content using NIP-44
- local encrypted_content
- encrypted_content=$(encrypt_nip44_content "$command_array" "$ADMIN_PRIVKEY" "$RELAY_PUBKEY")
-
- if [ $? -ne 0 ] || [ -z "$encrypted_content" ]; then
- fail_test "Failed to encrypt auth query content"
- return
- fi
-
- local query_event
- query_event=$(nak event -k 23456 --content "$encrypted_content" \
- -t "p=$RELAY_PUBKEY" \
- --sec "$ADMIN_PRIVKEY" 2>/dev/null)
-
- if [ $? -ne 0 ] || [ -z "$query_event" ]; then
- fail_test "Failed to create auth query event"
- return
- fi
-
- # Send the query event using simplified WebSocket pattern
- log_info "Sending auth query to relay..."
- local decrypted_response
- decrypted_response=$(send_admin_query "$query_event" "auth rules query")
- local exit_code=$?
-
- if [ $exit_code -eq 0 ] && [ -n "$decrypted_response" ]; then
- log_info "Decrypted query response: $decrypted_response"
-
- # Check if the decrypted response contains our test rule
- if echo "$decrypted_response" | grep -q "$TEST1_PUBKEY"; then
- pass_test "Auth rule storage and query working - found test rule in decrypted query results"
- else
- fail_test "Auth rule not found in decrypted query results - rule may not have been stored"
- fi
- else
- fail_test "Failed to receive or decrypt auth query response"
- fi
- else
- fail_test "Failed to add auth rule for storage test"
- fi
-}
-
-# Test 3: Basic Whitelist Functionality
-test_basic_whitelist() {
- increment_test
- log "Test $TESTS_RUN: Basic Whitelist Functionality"
-
- # Clear all existing rules to start fresh
- clear_all_auth_rules
-
- # Add TEST1 pubkey to whitelist
- if send_auth_rule_event "add" "whitelist" "pubkey" "$TEST1_PUBKEY" "Test whitelist entry"; then
- # Test that whitelisted pubkey can publish
- if test_event_publishing "$TEST1_PRIVKEY" "$TEST1_PUBKEY" "success" "whitelisted pubkey"; then
- pass_test "Basic whitelist functionality working"
- else
- fail_test "Whitelisted pubkey could not publish events"
- fi
- else
- fail_test "Failed to add pubkey to whitelist"
- fi
-}
-
-# Test 4: Basic Blacklist Functionality
-test_basic_blacklist() {
- increment_test
- log "Test $TESTS_RUN: Basic Blacklist Functionality"
-
- # Clear all existing rules to start fresh
- clear_all_auth_rules
-
- # Add TEST2 pubkey to blacklist
- if send_auth_rule_event "add" "blacklist" "pubkey" "$TEST2_PUBKEY" "Test blacklist entry"; then
- # Test that blacklisted pubkey cannot publish
- if test_event_publishing "$TEST2_PRIVKEY" "$TEST2_PUBKEY" "blocked" "blacklisted pubkey"; then
- pass_test "Basic blacklist functionality working"
- else
- fail_test "Blacklisted pubkey was able to publish events"
- fi
- else
- fail_test "Failed to add pubkey to blacklist"
- fi
-}
-
-# Test 5: Rule Removal
-test_rule_removal() {
- increment_test
- log "Test $TESTS_RUN: Rule Removal"
-
- # Clear all existing rules to start fresh
- clear_all_auth_rules
-
- # First add TEST2 to blacklist to test removal
- if ! send_auth_rule_event "add" "blacklist" "pubkey" "$TEST2_PUBKEY" "Test blacklist for removal"; then
- fail_test "Failed to add pubkey to blacklist for removal test"
- return
- fi
-
- # Remove TEST2 from blacklist
- if send_auth_rule_event "remove" "blacklist" "pubkey" "$TEST2_PUBKEY" "Remove test blacklist entry"; then
- # Test that previously blacklisted pubkey can now publish
- if test_event_publishing "$TEST2_PRIVKEY" "$TEST2_PUBKEY" "success" "previously blacklisted pubkey after removal"; then
- pass_test "Rule removal working correctly"
- else
- fail_test "Previously blacklisted pubkey still cannot publish after removal"
- fi
- else
- fail_test "Failed to remove pubkey from blacklist"
- fi
-}
-
-# Test 6: Multiple Users Scenario
-test_multiple_users() {
- increment_test
- log "Test $TESTS_RUN: Multiple Users Scenario"
-
- # Clear all existing rules to start fresh
- clear_all_auth_rules
-
- # Add TEST1 to whitelist and TEST3 to blacklist
- local success_count=0
-
- if send_auth_rule_event "add" "whitelist" "pubkey" "$TEST1_PUBKEY" "Multi-user test whitelist"; then
- success_count=$((success_count + 1))
- fi
-
- if send_auth_rule_event "add" "blacklist" "pubkey" "$TEST3_PUBKEY" "Multi-user test blacklist"; then
- success_count=$((success_count + 1))
- fi
-
- if [ $success_count -eq 2 ]; then
- # Test whitelisted user can publish
- if test_event_publishing "$TEST1_PRIVKEY" "$TEST1_PUBKEY" "success" "whitelisted in multi-user test"; then
- # Test blacklisted user cannot publish
- if test_event_publishing "$TEST3_PRIVKEY" "$TEST3_PUBKEY" "blocked" "blacklisted in multi-user test"; then
- pass_test "Multiple users scenario working correctly"
- else
- fail_test "Blacklisted user in multi-user scenario was not blocked"
- fi
- else
- fail_test "Whitelisted user in multi-user scenario was blocked"
- fi
- else
- fail_test "Failed to set up multiple users scenario"
- fi
-}
-
-# Test 7: Priority Testing (Blacklist vs Whitelist)
-test_priority_rules() {
- increment_test
- log "Test $TESTS_RUN: Priority Rules Testing"
-
- # Clear all existing rules to start fresh
- clear_all_auth_rules
-
- # Add same pubkey to both whitelist and blacklist
- local setup_success=0
-
- if send_auth_rule_event "add" "whitelist" "pubkey" "$TEST2_PUBKEY" "Priority test whitelist"; then
- setup_success=$((setup_success + 1))
- fi
-
- if send_auth_rule_event "add" "blacklist" "pubkey" "$TEST2_PUBKEY" "Priority test blacklist"; then
- setup_success=$((setup_success + 1))
- fi
-
- if [ $setup_success -eq 2 ]; then
- # Test which rule takes priority (typically blacklist should win)
- if test_event_publishing "$TEST2_PRIVKEY" "$TEST2_PUBKEY" "blocked" "pubkey in both whitelist and blacklist"; then
- pass_test "Priority rules working correctly (blacklist takes precedence)"
- else
- # If whitelist wins, that's also valid depending on implementation
- log_warning "Whitelist took precedence over blacklist - this may be implementation-specific"
- pass_test "Priority rules working (whitelist precedence)"
- fi
- else
- fail_test "Failed to set up priority rules test"
- fi
-}
-
-# Test 8: Hash-based Blacklist
-test_hash_blacklist() {
- increment_test
- log "Test $TESTS_RUN: Hash-based Blacklist"
-
- # Clear all existing rules to start fresh
- clear_all_auth_rules
-
- # Create a test event to get its hash
- local test_content="Content to be blacklisted by hash"
- local test_event
- test_event=$(nak event -k 1 --content "$test_content" --sec "$TEST1_PRIVKEY" 2>/dev/null)
-
- if [ $? -ne 0 ] || [ -z "$test_event" ]; then
- fail_test "Failed to create test event for hash blacklist"
- return
- fi
-
- # Extract event ID (hash) from the event using jq
- local event_id
- event_id=$(echo "$test_event" | jq -r '.id' 2>/dev/null)
-
- if [ -z "$event_id" ] || [ "$event_id" = "null" ]; then
- fail_test "Failed to extract event ID for hash blacklist test"
- return
- fi
-
- log_info "Testing hash blacklist with event ID: $event_id"
-
- # Add the event ID to hash blacklist
- if send_auth_rule_event "add" "blacklist" "hash" "$event_id" "Test hash blacklist"; then
- # Try to publish the same event using nak - should be blocked
- log_info "Attempting to publish blacklisted event..."
- local result
- result=$(printf '%s\n' "$test_event" | timeout 10s nak event "$RELAY_URL" 2>&1)
- local exit_code=$?
-
- if [ $exit_code -ne 0 ] || echo "$result" | grep -q -i "blocked\|denied\|rejected\|blacklist"; then
- pass_test "Hash-based blacklist working correctly"
- else
- fail_test "Hash-based blacklist did not block the event: $result"
- fi
- else
- fail_test "Failed to add event hash to blacklist"
- fi
-}
-
-# Test 9: WebSocket Connection Behavior
-test_websocket_behavior() {
- increment_test
- log "Test $TESTS_RUN: WebSocket Connection Behavior"
-
- # Clear all existing rules to start fresh
- clear_all_auth_rules
-
- # Test that the WebSocket connection handles multiple rapid requests
- local rapid_success_count=0
-
- for i in {1..3}; do
- local test_content="Rapid test message $i"
- local test_event
- test_event=$(nak event -k 1 --content "$test_content" --sec "$TEST1_PRIVKEY" 2>/dev/null)
-
- if [ $? -eq 0 ]; then
- local message="[\"EVENT\",$test_event]"
- local response
- response=$(send_websocket_message "$message" 5)
-
- if echo "$response" | grep -q '"OK"'; then
- rapid_success_count=$((rapid_success_count + 1))
- fi
- fi
-
- # Small delay between requests
- sleep 0.1
- done
-
- if [ $rapid_success_count -ge 2 ]; then
- pass_test "WebSocket connection handles multiple requests correctly"
- else
- fail_test "WebSocket connection failed to handle multiple rapid requests ($rapid_success_count/3 succeeded)"
- fi
-}
-
-# Test 10: Rule Persistence Verification
-test_rule_persistence() {
- increment_test
- log "Test $TESTS_RUN: Rule Persistence Verification"
-
- # Clear all existing rules to start fresh
- clear_all_auth_rules
-
- # Add a rule, then verify it persists by testing enforcement
- if send_auth_rule_event "add" "blacklist" "pubkey" "$TEST3_PUBKEY" "Persistence test blacklist"; then
- # Wait a moment for rule to be processed
- sleep 1
-
- # Test enforcement multiple times to verify persistence
- local enforcement_count=0
-
- for i in {1..2}; do
- if test_event_publishing "$TEST3_PRIVKEY" "$TEST3_PUBKEY" "blocked" "persistence test attempt $i"; then
- enforcement_count=$((enforcement_count + 1))
- fi
- sleep 0.5
- done
-
- if [ $enforcement_count -eq 2 ]; then
- pass_test "Rule persistence working correctly"
- else
- fail_test "Rule persistence failed ($enforcement_count/2 enforcements succeeded)"
- fi
- else
- fail_test "Failed to add rule for persistence test"
- fi
-}
-
-# Test 11: Cleanup and Final Verification
-test_cleanup_verification() {
- increment_test
- log "Test $TESTS_RUN: Cleanup and Final Verification"
-
- # Remove all test rules
- local cleanup_success=0
-
- # Remove whitelist entries
- if send_auth_rule_event "remove" "whitelist" "pubkey" "$TEST1_PUBKEY" "Cleanup whitelist"; then
- cleanup_success=$((cleanup_success + 1))
- fi
-
- # Remove blacklist entries
- for pubkey in "$TEST2_PUBKEY" "$TEST3_PUBKEY"; do
- if send_auth_rule_event "remove" "blacklist" "pubkey" "$pubkey" "Cleanup blacklist"; then
- cleanup_success=$((cleanup_success + 1))
- fi
- done
-
- if [ $cleanup_success -ge 2 ]; then
- # Verify that previously restricted pubkeys can now publish
- if test_event_publishing "$TEST3_PRIVKEY" "$TEST3_PUBKEY" "success" "after cleanup verification"; then
- pass_test "Cleanup and verification successful"
- else
- log_warning "Cleanup completed but restrictions may still be active"
- pass_test "Cleanup completed (partial verification)"
- fi
- else
- fail_test "Cleanup failed ($cleanup_success rules removed)"
- fi
-}
-
-# =======================================================================
-# MAIN TEST EXECUTION
-# =======================================================================
-
-run_all_tests() {
- log "Starting comprehensive whitelist/blacklist functionality tests..."
-
- # Setup
- setup_test_environment
-
-
- clear_all_auth_rules
-
- test_admin_authentication
- # test_auth_rules_storage_query
- # test_basic_whitelist
- # test_basic_blacklist
- # test_rule_removal
- # test_multiple_users
- # test_priority_rules
- # test_hash_blacklist
- # test_websocket_behavior
- # test_rule_persistence
- # test_cleanup_verification
-
- # Test summary
- echo ""
- echo -e "${BOLD}=== TEST SUMMARY ===${RESET}"
- echo -e "Tests run: ${BLUE}$TESTS_RUN${RESET}"
- echo -e "Tests passed: ${GREEN}$TESTS_PASSED${RESET}"
- echo -e "Tests failed: ${RED}$TESTS_FAILED${RESET}"
- echo ""
-
- if [ $TESTS_FAILED -eq 0 ]; then
- log_success "All tests passed! Whitelist/blacklist functionality is working correctly."
- return 0
- else
- log_error "$TESTS_FAILED out of $TESTS_RUN tests failed."
- return 1
- fi
-}
-
-# =======================================================================
-# CLEANUP FUNCTIONS
-# =======================================================================
-
-cleanup() {
- log "Cleaning up test environment..."
-
- # Remove temporary directory
- if [ -n "$TEMP_DIR" ] && [ -d "$TEMP_DIR" ]; then
- rm -rf "$TEMP_DIR"
- log_info "Temporary directory removed: $TEMP_DIR"
- fi
-
- log "Test cleanup completed."
-}
-
-# Set up cleanup trap
-trap cleanup EXIT
-
-# =======================================================================
-# SCRIPT ENTRY POINT
-# =======================================================================
-
-main() {
- echo -e "${BOLD}${BLUE}C-Relay Whitelist/Blacklist Authentication Test${RESET}"
- echo -e "${BLUE}===============================================${RESET}"
- echo ""
-
- # Check if relay is running - using websocat like the working tests
- if ! printf '%s\n' '["REQ","connection_test",{}]' | timeout 5 websocat "$RELAY_URL" >/dev/null 2>&1; then
- log_error "Cannot connect to relay at $RELAY_URL"
- log_error "Please ensure the C-Relay server is running in test mode"
- exit 1
- fi
-
- log_success "Connected to relay at $RELAY_URL"
-
- # Run all tests
- if run_all_tests; then
- echo ""
- log_success "All whitelist/blacklist tests completed successfully!"
- exit 0
- else
- echo ""
- log_error "Some tests failed."
- exit 1
- fi
-}
-
-# Run main function if script is executed directly
-if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
- main "$@"
-fi
\ No newline at end of file
diff --git a/tests/white_black_test.sh b/tests/white_black_test.sh
new file mode 100755
index 0000000..ee530de
--- /dev/null
+++ b/tests/white_black_test.sh
@@ -0,0 +1,244 @@
+#!/bin/bash
+
+# C-Relay Whitelist/Blacklist Test Script
+# Tests the relay's authentication functionality using nak
+
+set -e # Exit on any error
+
+# Configuration
+RELAY_URL="ws://localhost:8888"
+ADMIN_PRIVKEY="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ADMIN_PUBKEY="6a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3"
+RELAY_PUBKEY="4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+# Logging functions
+log_info() {
+ echo -e "${BLUE}[INFO]${NC} $1"
+}
+
+log_success() {
+ echo -e "${GREEN}[SUCCESS]${NC} $1"
+}
+
+log_error() {
+ echo -e "${RED}[ERROR]${NC} $1"
+}
+
+log_warning() {
+ echo -e "${YELLOW}[WARNING]${NC} $1"
+}
+
+# Check if nak is installed
+check_nak() {
+ if ! command -v nak &> /dev/null; then
+ log_error "nak command not found. Please install nak first."
+ log_error "Visit: https://github.com/fiatjaf/nak"
+ exit 1
+ fi
+ log_success "nak is available"
+}
+
+# Generate test keypair
+generate_test_keypair() {
+ log_info "Generating test keypair..."
+
+ # Generate private key
+ TEST_PRIVKEY=$(nak key generate 2>/dev/null)
+
+ if [ -z "$TEST_PRIVKEY" ]; then
+ log_error "Failed to generate private key"
+ exit 1
+ fi
+
+ # Derive public key from private key
+ TEST_PUBKEY=$(nak key public "$TEST_PRIVKEY" 2>/dev/null)
+
+ if [ -z "$TEST_PUBKEY" ]; then
+ log_error "Failed to derive public key from private key"
+ exit 1
+ fi
+
+ log_success "Generated test keypair:"
+ log_info " Private key: $TEST_PRIVKEY"
+ log_info " Public key: $TEST_PUBKEY"
+}
+
+# Create test event
+create_test_event() {
+ local timestamp=$(date +%s)
+ local content="Test event at timestamp $timestamp"
+
+ log_info "Creating test event (kind 1) with content: '$content'"
+
+ # Create event using nak
+ EVENT_JSON=$(nak event \
+ --kind 1 \
+ --content "$content" \
+ --sec "$TEST_PRIVKEY" \
+ --tag 't=test')
+
+ # Extract event ID
+ EVENT_ID=$(echo "$EVENT_JSON" | jq -r '.id')
+
+ if [ -z "$EVENT_ID" ] || [ "$EVENT_ID" = "null" ]; then
+ log_error "Failed to create test event"
+ exit 1
+ fi
+
+ log_success "Created test event with ID: $EVENT_ID"
+}
+
+# Test 1: Post event and verify retrieval
+test_post_and_retrieve() {
+ log_info "=== TEST 1: Post event and verify retrieval ==="
+
+ # Post the event
+ log_info "Posting test event to relay..."
+ POST_RESULT=$(echo "$EVENT_JSON" | nak event "$RELAY_URL")
+
+ if echo "$POST_RESULT" | grep -q "error\|failed\|denied"; then
+ log_error "Failed to post event: $POST_RESULT"
+ return 1
+ fi
+
+ log_success "Event posted successfully"
+
+ # Wait a moment for processing
+ sleep 2
+
+ # Try to retrieve the event
+ log_info "Retrieving event from relay..."
+ RETRIEVE_RESULT=$(nak req \
+ --id "$EVENT_ID" \
+ "$RELAY_URL")
+
+ if echo "$RETRIEVE_RESULT" | grep -q "$EVENT_ID"; then
+ log_success "Event successfully retrieved from relay"
+ return 0
+ else
+ log_error "Failed to retrieve event from relay"
+ log_error "Query result: $RETRIEVE_RESULT"
+ return 1
+ fi
+}
+
+# Send admin command to add user to blacklist
+add_to_blacklist() {
+ log_info "Adding test user to blacklist..."
+
+ # Create the admin command
+ COMMAND="[\"blacklist\", \"pubkey\", \"$TEST_PUBKEY\"]"
+
+ # Encrypt the command using NIP-44
+ ENCRYPTED_COMMAND=$(nak encrypt "$COMMAND" \
+ --sec "$ADMIN_PRIVKEY" \
+ --recipient-pubkey "$RELAY_PUBKEY")
+
+ if [ -z "$ENCRYPTED_COMMAND" ]; then
+ log_error "Failed to encrypt admin command"
+ return 1
+ fi
+
+ # Create admin event
+ ADMIN_EVENT=$(nak event \
+ --kind 23456 \
+ --content "$ENCRYPTED_COMMAND" \
+ --sec "$ADMIN_PRIVKEY" \
+ --tag "p=$RELAY_PUBKEY")
+
+ # Post admin event
+ ADMIN_RESULT=$(echo "$ADMIN_EVENT" | nak event "$RELAY_URL")
+
+ if echo "$ADMIN_RESULT" | grep -q "error\|failed\|denied"; then
+ log_error "Failed to send admin command: $ADMIN_RESULT"
+ return 1
+ fi
+
+ log_success "Admin command sent successfully - user added to blacklist"
+ # Wait for the relay to process the admin command
+ sleep 3
+}
+
+# Test 2: Try to post after blacklisting
+test_blacklist_post() {
+ log_info "=== TEST 2: Attempt to post event after blacklisting ==="
+
+ # Create a new test event
+ local timestamp=$(date +%s)
+ local content="Blacklisted test event at timestamp $timestamp"
+
+ log_info "Creating new test event for blacklisted user..."
+
+ NEW_EVENT_JSON=$(nak event \
+ --kind 1 \
+ --content "$content" \
+ --sec "$TEST_PRIVKEY" \
+ --tag 't=blacklist-test')
+
+ NEW_EVENT_ID=$(echo "$NEW_EVENT_JSON" | jq -r '.id')
+
+ # Try to post the event
+ log_info "Attempting to post event with blacklisted user..."
+ POST_RESULT=$(echo "$NEW_EVENT_JSON" | nak event "$RELAY_URL" 2>&1)
+
+ # Check if posting failed (should fail for blacklisted user)
+ if echo "$POST_RESULT" | grep -q "error\|failed\|denied\|blocked"; then
+ log_success "Event posting correctly blocked for blacklisted user"
+ return 0
+ else
+ log_error "Event posting was not blocked - blacklist may not be working"
+ log_error "Post result: $POST_RESULT"
+ return 1
+ fi
+}
+
+# Main test function
+main() {
+ log_info "Starting C-Relay Whitelist/Blacklist Test"
+ log_info "=========================================="
+
+ # Check prerequisites
+ check_nak
+
+ # Generate test keypair
+ generate_test_keypair
+
+ # Create test event
+ create_test_event
+
+ # Test 1: Post and retrieve
+ if test_post_and_retrieve; then
+ log_success "TEST 1 PASSED: Event posting and retrieval works"
+ else
+ log_error "TEST 1 FAILED: Event posting/retrieval failed"
+ exit 1
+ fi
+
+ # Add user to blacklist
+ if add_to_blacklist; then
+ log_success "Blacklist command sent successfully"
+ else
+ log_error "Failed to send blacklist command"
+ exit 1
+ fi
+
+ # Test 2: Try posting after blacklist
+ if test_blacklist_post; then
+ log_success "TEST 2 PASSED: Blacklist functionality works correctly"
+ else
+ log_error "TEST 2 FAILED: Blacklist functionality not working"
+ exit 1
+ fi
+
+ log_success "All tests passed! Whitelist/blacklist functionality is working correctly."
+}
+
+# Run main function
+main "$@"
\ No newline at end of file