// Define _GNU_SOURCE to ensure all POSIX features are available #define _GNU_SOURCE // Includes #include #include "debug.h" #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 #include "embedded_web_content.h" // Embedded web content #include "api.h" // API for embedded files #include "dm_admin.h" // DM admin functions including NIP-17 // Forward declarations for logging functions // 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 declaration for status posts int generate_and_post_status_event(void); // Forward declarations for NIP-11 relay information handling int handle_nip11_http_request(struct lws* wsi, const char* accept_header); // Forward declarations for embedded file handling int handle_embedded_file_writeable(struct lws* wsi); // 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 DM stats command handling int process_dm_stats_command(cJSON* dm_event, char* error_message, size_t error_size, struct lws* wsi); // 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); int handle_count_message(const char* sub_id, cJSON* filters, struct lws *wsi, struct per_session_data *pss); // Forward declarations for rate limiting int is_client_rate_limited_for_malformed_requests(struct per_session_data *pss); void record_malformed_request(struct per_session_data *pss); // Forward declarations for filter validation int validate_filter_array(cJSON* filters, char* error_message, size_t error_size); // Forward declarations for NOTICE message support void send_notice_message(struct lws* wsi, const char* message); // Configuration functions from config.c extern int get_config_bool(const char* key, int default_value); // Forward declarations for global state extern sqlite3* g_db; extern int g_server_running; extern volatile sig_atomic_t g_shutdown_flag; extern int g_restart_requested; extern struct lws_context *ws_context; // Global subscription manager struct subscription_manager g_subscription_manager; // Message queue functions for proper libwebsockets pattern /** * Queue a message for WebSocket writing following libwebsockets' proper pattern. * This function adds messages to a per-session queue and requests writeable callback. * * @param wsi WebSocket instance * @param pss Per-session data containing message queue * @param message Message string to write * @param length Length of message string * @param type LWS_WRITE_* type (LWS_WRITE_TEXT, etc.) * @return 0 on success, -1 on error */ int queue_message(struct lws* wsi, struct per_session_data* pss, const char* message, size_t length, enum lws_write_protocol type) { if (!wsi || !pss || !message || length == 0) { DEBUG_ERROR("queue_message: invalid parameters"); return -1; } // Allocate message queue node struct message_queue_node* node = malloc(sizeof(struct message_queue_node)); if (!node) { DEBUG_ERROR("queue_message: failed to allocate queue node"); return -1; } // Allocate buffer with LWS_PRE space size_t buffer_size = LWS_PRE + length; unsigned char* buffer = malloc(buffer_size); if (!buffer) { DEBUG_ERROR("queue_message: failed to allocate message buffer"); free(node); return -1; } // Copy message to buffer with LWS_PRE offset memcpy(buffer + LWS_PRE, message, length); // Initialize node node->data = buffer; node->length = length; node->type = type; node->next = NULL; // Add to queue (thread-safe) pthread_mutex_lock(&pss->session_lock); if (!pss->message_queue_head) { // Queue was empty pss->message_queue_head = node; pss->message_queue_tail = node; } else { // Add to end of queue pss->message_queue_tail->next = node; pss->message_queue_tail = node; } pss->message_queue_count++; pthread_mutex_unlock(&pss->session_lock); // Request writeable callback (only if not already requested) if (!pss->writeable_requested) { pss->writeable_requested = 1; lws_callback_on_writable(wsi); } DEBUG_TRACE("Queued message: len=%zu, queue_count=%d", length, pss->message_queue_count); return 0; } /** * Process message queue when the socket becomes writeable. * This function is called from LWS_CALLBACK_SERVER_WRITEABLE. * * @param wsi WebSocket instance * @param pss Per-session data containing message queue * @return 0 on success, -1 on error */ int process_message_queue(struct lws* wsi, struct per_session_data* pss) { if (!wsi || !pss) { DEBUG_ERROR("process_message_queue: invalid parameters"); return -1; } // Get next message from queue (thread-safe) pthread_mutex_lock(&pss->session_lock); struct message_queue_node* node = pss->message_queue_head; if (!node) { // Queue is empty pss->writeable_requested = 0; pthread_mutex_unlock(&pss->session_lock); return 0; } // Remove from queue pss->message_queue_head = node->next; if (!pss->message_queue_head) { pss->message_queue_tail = NULL; } pss->message_queue_count--; pthread_mutex_unlock(&pss->session_lock); // Write message (libwebsockets handles partial writes internally) int write_result = lws_write(wsi, node->data + LWS_PRE, node->length, node->type); // Free node resources free(node->data); free(node); if (write_result < 0) { DEBUG_ERROR("process_message_queue: write failed, result=%d", write_result); return -1; } DEBUG_TRACE("Processed message: wrote %d bytes, remaining in queue: %d", write_result, pss->message_queue_count); // If queue not empty, request another callback pthread_mutex_lock(&pss->session_lock); if (pss->message_queue_head) { lws_callback_on_writable(wsi); } else { pss->writeable_requested = 0; } pthread_mutex_unlock(&pss->session_lock); return 0; } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // 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 HTTP requests { char *requested_uri = (char *)in; // Check if this is an OPTIONS request char method[16] = {0}; int method_len = lws_hdr_copy(wsi, method, sizeof(method) - 1, WSI_TOKEN_GET_URI); if (method_len > 0) { method[method_len] = '\0'; if (strcmp(method, "OPTIONS") == 0) { // Handle OPTIONS request 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]; if (lws_add_http_header_status(wsi, HTTP_STATUS_OK, &p, end)) return -1; if (lws_add_http_header_by_name(wsi, (unsigned char*)"access-control-allow-origin:", (unsigned char*)"*", 1, &p, end)) return -1; if (lws_add_http_header_by_name(wsi, (unsigned char*)"access-control-allow-headers:", (unsigned char*)"content-type, accept", 20, &p, end)) return -1; if (lws_add_http_header_by_name(wsi, (unsigned char*)"access-control-allow-methods:", (unsigned char*)"GET, OPTIONS", 12, &p, end)) return -1; if (lws_add_http_header_by_name(wsi, (unsigned char*)"connection:", (unsigned char*)"close", 5, &p, end)) return -1; if (lws_finalize_http_header(wsi, &p, end)) return -1; if (lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS) < 0) return -1; return 0; } } // Check if this is a GET request to the root path if (strcmp(requested_uri, "/") == 0) { // Check if this is a WebSocket upgrade request char upgrade_header[64] = {0}; int upgrade_len = lws_hdr_copy(wsi, upgrade_header, sizeof(upgrade_header) - 1, WSI_TOKEN_UPGRADE); if (upgrade_len > 0) { upgrade_header[upgrade_len] = '\0'; if (strstr(upgrade_header, "websocket") != NULL) { DEBUG_LOG("WebSocket upgrade request - allowing connection"); return 0; } } // Not a WebSocket upgrade, check for NIP-11 request 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'; // Check if this is a NIP-11 request int is_nip11_request = (strstr(accept_header, "application/nostr+json") != NULL); if (is_nip11_request) { // Handle NIP-11 request if (handle_nip11_http_request(wsi, accept_header) == 0) { return 0; // Successfully handled } } } // Root path without NIP-11 Accept header and not WebSocket - return 404 DEBUG_WARN("Rejecting root path request - not WebSocket upgrade and not NIP-11"); lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL); return -1; } // Check for embedded API files if (handle_embedded_file_request(wsi, requested_uri) == 0) { return 0; // Successfully handled } // Return 404 for other paths lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL); return -1; } case LWS_CALLBACK_HTTP_WRITEABLE: // Handle HTTP body transmission for NIP-11 or embedded files { void* user_data = lws_wsi_user(wsi); if (user_data) { int type = *(int*)user_data; if (type == 0) { // NIP-11 struct nip11_session_data* session_data = (struct nip11_session_data*)user_data; if (session_data->headers_sent && !session_data->body_sent) { // Allocate buffer for JSON body transmission (no LWS_PRE needed for body) unsigned char *json_buf = malloc(session_data->json_length); if (!json_buf) { DEBUG_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, session_data->json_buffer, session_data->json_length); // Write JSON body int write_result = lws_write(wsi, json_buf, 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) { DEBUG_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); return 0; // Close connection after successful transmission } } else if (type == 1) { // Embedded file return handle_embedded_file_writeable(wsi); } } } break; case LWS_CALLBACK_ESTABLISHED: DEBUG_TRACE("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]; memset(client_ip, 0, sizeof(client_ip)); // Check if we should trust proxy headers int trust_proxy = get_config_bool("trust_proxy_headers", 0); if (trust_proxy) { // Try to get IP from X-Forwarded-For header first char x_forwarded_for[CLIENT_IP_MAX_LENGTH]; int header_len = lws_hdr_copy(wsi, x_forwarded_for, sizeof(x_forwarded_for) - 1, WSI_TOKEN_X_FORWARDED_FOR); if (header_len > 0) { x_forwarded_for[header_len] = '\0'; // X-Forwarded-For can contain multiple IPs (client, proxy1, proxy2, ...) // We want the first (leftmost) IP which is the original client char* comma = strchr(x_forwarded_for, ','); if (comma) { *comma = '\0'; // Truncate at first comma } // Trim leading/trailing whitespace char* ip_start = x_forwarded_for; while (*ip_start == ' ' || *ip_start == '\t') ip_start++; size_t ip_len = strlen(ip_start); while (ip_len > 0 && (ip_start[ip_len-1] == ' ' || ip_start[ip_len-1] == '\t')) { ip_start[--ip_len] = '\0'; } if (ip_len > 0 && ip_len < CLIENT_IP_MAX_LENGTH) { strncpy(client_ip, ip_start, CLIENT_IP_MAX_LENGTH - 1); client_ip[CLIENT_IP_MAX_LENGTH - 1] = '\0'; DEBUG_TRACE("Using X-Forwarded-For IP: %s", client_ip); } } // If X-Forwarded-For didn't work, try X-Real-IP if (client_ip[0] == '\0') { char x_real_ip[CLIENT_IP_MAX_LENGTH]; header_len = lws_hdr_copy(wsi, x_real_ip, sizeof(x_real_ip) - 1, WSI_TOKEN_HTTP_X_REAL_IP); if (header_len > 0) { x_real_ip[header_len] = '\0'; strncpy(client_ip, x_real_ip, CLIENT_IP_MAX_LENGTH - 1); client_ip[CLIENT_IP_MAX_LENGTH - 1] = '\0'; DEBUG_TRACE("Using X-Real-IP: %s", client_ip); } } } // Fall back to direct connection IP if proxy headers not available or not trusted if (client_ip[0] == '\0') { lws_get_peer_simple(wsi, client_ip, sizeof(client_ip)); DEBUG_TRACE("Using direct connection IP: %s", 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'; // Record connection establishment time for duration tracking pss->connection_established = time(NULL); DEBUG_LOG("WebSocket connection established from %s", pss->client_ip); // 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; DEBUG_TRACE("WebSocket connection initialization complete"); break; case LWS_CALLBACK_RECEIVE: if (len > 0) { DEBUG_TRACE("LWS_CALLBACK_RECEIVE: received %zu bytes", len); // Check if client is rate limited for malformed requests if (is_client_rate_limited_for_malformed_requests(pss)) { send_notice_message(wsi, "error: too many malformed requests - temporarily blocked"); return 0; } // Check if this is a fragmented message int is_first_fragment = lws_is_first_fragment(wsi); int is_final_fragment = lws_is_final_fragment(wsi); size_t remaining_payload = lws_remaining_packet_payload(wsi); DEBUG_TRACE("Fragment info: first=%d, final=%d, remaining=%zu, reassembly_active=%d", is_first_fragment, is_final_fragment, remaining_payload, pss->reassembly_active); // Handle message reassembly for fragmented messages // Only use reassembly if message is actually fragmented (not both first and final) int is_fragmented = (is_first_fragment && !is_final_fragment) || pss->reassembly_active; if (is_fragmented) { // Start or continue reassembly if (is_first_fragment) { // First fragment - initialize reassembly buffer if (pss->reassembly_buffer) { DEBUG_WARN("Starting new reassembly but buffer already exists - cleaning up"); free(pss->reassembly_buffer); } pss->reassembly_buffer = NULL; pss->reassembly_size = 0; pss->reassembly_capacity = 0; pss->reassembly_active = 1; DEBUG_TRACE("Starting message reassembly"); } // Ensure buffer has enough capacity size_t needed_capacity = pss->reassembly_size + len + 1; // +1 for null terminator if (needed_capacity > pss->reassembly_capacity) { size_t new_capacity = pss->reassembly_capacity == 0 ? 8192 : pss->reassembly_capacity * 2; while (new_capacity < needed_capacity) { new_capacity *= 2; } char* new_buffer = realloc(pss->reassembly_buffer, new_capacity); if (!new_buffer) { DEBUG_ERROR("Failed to allocate reassembly buffer (capacity %zu)", new_capacity); // Clean up and abort reassembly free(pss->reassembly_buffer); pss->reassembly_buffer = NULL; pss->reassembly_size = 0; pss->reassembly_capacity = 0; pss->reassembly_active = 0; send_notice_message(wsi, "error: message too large - memory allocation failed"); return 0; } pss->reassembly_buffer = new_buffer; pss->reassembly_capacity = new_capacity; DEBUG_TRACE("Expanded reassembly buffer to %zu bytes", new_capacity); } // Append fragment to buffer memcpy(pss->reassembly_buffer + pss->reassembly_size, in, len); pss->reassembly_size += len; // Check if this is the final fragment if (is_final_fragment) { // Message complete - process it pss->reassembly_buffer[pss->reassembly_size] = '\0'; pss->reassembly_active = 0; DEBUG_TRACE("Message reassembly complete: total size %zu bytes", pss->reassembly_size); // Process the complete message char* complete_message = pss->reassembly_buffer; size_t message_len = pss->reassembly_size; // Reset reassembly state (but keep buffer for reuse if needed) pss->reassembly_size = 0; // Parse JSON message DEBUG_TRACE("Parsing reassembled JSON message of length %zu", message_len); cJSON* json = cJSON_Parse(complete_message); // Process the message (same logic as before) if (json && cJSON_IsArray(json)) { // 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) { // 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) { DEBUG_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); // Use proper message queue system instead of direct lws_write if (queue_message(wsi, pss, error_str, error_len, LWS_WRITE_TEXT) != 0) { DEBUG_ERROR("Failed to queue error response message"); } free(error_str); } cJSON_Delete(error_response); cJSON_Delete(json); // Note: complete_message points to reassembly_buffer, which is managed separately // and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED return 0; } // Call unified validator with JSON string size_t event_json_len = strlen(event_json_str); int validation_result = nostr_validate_unified_request(event_json_str, event_json_len); // Map validation result to old result format (0 = success, -1 = failure) int result = (validation_result == NOSTR_SUCCESS) ? 0 : -1; // 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; } } // Cleanup event JSON string free(event_json_str); // Check for NIP-70 protected events if (result == 0) { // Check if event has protected tag ["-"] int is_protected_event = 0; cJSON* tags = cJSON_GetObjectItem(event, "tags"); if (tags && cJSON_IsArray(tags)) { cJSON* tag = NULL; cJSON_ArrayForEach(tag, tags) { if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 1) { cJSON* tag_name = cJSON_GetArrayItem(tag, 0); if (tag_name && cJSON_IsString(tag_name) && strcmp(cJSON_GetStringValue(tag_name), "-") == 0) { is_protected_event = 1; break; } } } } if (is_protected_event) { // Check if protected events are enabled using config int protected_events_enabled = get_config_bool("nip70_protected_events_enabled", 0); if (!protected_events_enabled) { // Protected events not supported result = -1; strncpy(error_message, "blocked: protected events not supported", sizeof(error_message) - 1); error_message[sizeof(error_message) - 1] = '\0'; DEBUG_WARN("Protected event rejected: protected events not enabled"); } else { // Protected events enabled - check authentication cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey"); const char* event_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : NULL; if (!pss || !pss->authenticated || !event_pubkey || strcmp(pss->authenticated_pubkey, event_pubkey) != 0) { // Not authenticated or pubkey mismatch result = -1; strncpy(error_message, "auth-required: protected event requires authentication", sizeof(error_message) - 1); error_message[sizeof(error_message) - 1] = '\0'; DEBUG_WARN("Protected event rejected: authentication required"); } } } } // 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); DEBUG_TRACE("Processing event kind %d, message length: %zu", event_kind, message_len); // Log reception of Kind 23456 events if (event_kind == 23456) { DEBUG_LOG("Admin event (kind 23456) received"); } if (event_kind == 23456) { // Enhanced admin event security - check authorization first 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 DEBUG_WARN("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'; } else { // Authorization successful - process through admin API char admin_error[512] = {0}; int admin_result = process_admin_event_in_config(event, admin_error, sizeof(admin_error), wsi); // Log results for Kind 23456 events if (event_kind == 23456) { if (admin_result != 0) { char error_result_msg[512]; if (strlen(admin_error) > 0) { // Safely truncate admin_error if too long size_t max_error_len = sizeof(error_result_msg) - 50; // Leave room for prefix size_t error_len = strlen(admin_error); if (error_len > max_error_len) { error_len = max_error_len; } char truncated_error[512]; memcpy(truncated_error, admin_error, error_len); truncated_error[error_len] = '\0'; // Use a safer approach to avoid truncation warning size_t prefix_len = snprintf(error_result_msg, sizeof(error_result_msg), "ERROR: Kind %d event processing failed: ", event_kind); if (prefix_len < sizeof(error_result_msg)) { size_t remaining = sizeof(error_result_msg) - prefix_len; size_t copy_len = strlen(truncated_error); if (copy_len >= remaining) { copy_len = remaining - 1; } memcpy(error_result_msg + prefix_len, truncated_error, copy_len); error_result_msg[prefix_len + copy_len] = '\0'; } } else { snprintf(error_result_msg, sizeof(error_result_msg), "ERROR: Kind %d event processing failed", event_kind); } DEBUG_ERROR(error_result_msg); } } if (admin_result != 0) { DEBUG_ERROR("Failed to process admin event"); result = -1; size_t error_len = strlen(admin_error); size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1; memcpy(error_message, admin_error, copy_len); error_message[copy_len] = '\0'; } else { // Admin events are processed by the admin API, not broadcast to subscriptions } } } else if (event_kind == 1059) { // Check for NIP-17 gift wrap admin messages char nip17_error[512] = {0}; cJSON* response_event = process_nip17_admin_message(event, nip17_error, sizeof(nip17_error), wsi); if (!response_event) { // Check if this is an error or if the command was already handled if (strlen(nip17_error) > 0) { // There was an actual error DEBUG_ERROR("NIP-17 admin message processing failed"); result = -1; size_t error_len = strlen(nip17_error); size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1; memcpy(error_message, nip17_error, copy_len); error_message[copy_len] = '\0'; } else { // No error message means the command was already handled (plain text commands) // Store the original gift wrap event in database if (store_event(event) != 0) { DEBUG_ERROR("Failed to store gift wrap event in database"); result = -1; strncpy(error_message, "error: failed to store gift wrap event", sizeof(error_message) - 1); } } } else { // Store the original gift wrap event in database (unlike kind 23456) if (store_event(event) != 0) { DEBUG_ERROR("Failed to store gift wrap event in database"); result = -1; strncpy(error_message, "error: failed to store gift wrap event", sizeof(error_message) - 1); cJSON_Delete(response_event); } else { // Broadcast RESPONSE event to matching persistent subscriptions broadcast_event_to_subscriptions(response_event); // Clean up response event cJSON_Delete(response_event); } } } else if (event_kind == 14) { // Check for DM stats commands addressed to relay char dm_error[512] = {0}; int dm_result = process_dm_stats_command(event, dm_error, sizeof(dm_error), wsi); if (dm_result != 0) { DEBUG_ERROR("DM stats command processing failed"); result = -1; size_t error_len = strlen(dm_error); size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1; memcpy(error_message, dm_error, copy_len); error_message[copy_len] = '\0'; } else { // Store the DM event in database if (store_event(event) != 0) { DEBUG_ERROR("Failed to store DM event in database"); result = -1; strncpy(error_message, "error: failed to store DM event", sizeof(error_message) - 1); } else { // Broadcast DM event to matching persistent subscriptions broadcast_event_to_subscriptions(event); } } } else { // Check if this is an ephemeral event (kinds 20000-29999) // Per NIP-01: ephemeral events are broadcast but never stored if (event_kind >= 20000 && event_kind < 30000) { DEBUG_TRACE("Ephemeral event (kind %d) - broadcasting without storage", event_kind); // Broadcast directly to subscriptions without database storage broadcast_event_to_subscriptions(event); } else { DEBUG_TRACE("Storing regular event in database"); // Regular event - store in database and broadcast if (store_event(event) != 0) { DEBUG_ERROR("Failed to store event in database"); result = -1; strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1); } else { DEBUG_LOG("Event stored and broadcast (kind %d)", event_kind); // Broadcast event to matching persistent subscriptions broadcast_event_to_subscriptions(event); } } } } else { // Event without valid kind - try normal storage DEBUG_WARN("Event without valid kind - trying normal storage"); if (store_event(event) != 0) { DEBUG_ERROR("Failed to store event without kind in database"); result = -1; strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1); } else { 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 : "")); char *response_str = cJSON_Print(response); if (response_str) { size_t response_len = strlen(response_str); // DEBUG: Log WebSocket frame details before sending DEBUG_TRACE("WS_FRAME_SEND: type=OK len=%zu data=%.100s%s", response_len, response_str, response_len > 100 ? "..." : ""); // Queue message for proper libwebsockets pattern if (queue_message(wsi, pss, response_str, response_len, LWS_WRITE_TEXT) != 0) { DEBUG_ERROR("Failed to queue OK response message"); } free(response_str); } cJSON_Delete(response); } } } else if (strcmp(msg_type, "REQ") == 0) { DEBUG_TRACE("REQ message received, starting processing"); // Check NIP-42 authentication for REQ subscriptions if required if (pss && pss->nip42_auth_required_subscriptions && !pss->authenticated) { DEBUG_TRACE("REQ rejected: NIP-42 authentication required"); if (!pss->auth_challenge_sent) { send_nip42_auth_challenge(wsi, pss); } else { send_notice_message(wsi, "NIP-42 authentication required for subscriptions"); DEBUG_WARN("REQ rejected: NIP-42 authentication required"); } cJSON_Delete(json); // Note: complete_message points to reassembly_buffer, which is managed separately // and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED return 0; } DEBUG_TRACE("REQ message passed authentication check"); // 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); DEBUG_TRACE("Processing REQ message for subscription %s", subscription_id); // Validate subscription ID before processing if (!subscription_id) { DEBUG_TRACE("REQ rejected: NULL subscription ID"); send_notice_message(wsi, "error: invalid subscription ID"); DEBUG_WARN("REQ rejected: NULL subscription ID"); record_malformed_request(pss); cJSON_Delete(json); // Note: complete_message points to reassembly_buffer, which is managed separately // and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED return 0; } // Validate subscription ID if (!validate_subscription_id(subscription_id)) { DEBUG_TRACE("REQ rejected: invalid subscription ID format"); send_notice_message(wsi, "error: invalid subscription ID"); DEBUG_WARN("REQ rejected: invalid subscription ID"); cJSON_Delete(json); // Note: complete_message points to reassembly_buffer, which is managed separately // and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED return 0; } DEBUG_TRACE("REQ subscription ID validated: %s", subscription_id); // Create array of filter objects from position 2 onwards cJSON* filters = cJSON_CreateArray(); if (!filters) { DEBUG_TRACE("REQ failed: could not create filters array"); send_notice_message(wsi, "error: failed to process filters"); DEBUG_ERROR("REQ failed: could not create filters array"); cJSON_Delete(json); // Note: complete_message points to reassembly_buffer, which is managed separately // and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED return 0; } int json_size = cJSON_GetArraySize(json); int filter_count = 0; for (int i = 2; i < json_size; i++) { cJSON* filter = cJSON_GetArrayItem(json, i); if (filter) { cJSON_AddItemToArray(filters, cJSON_Duplicate(filter, 1)); filter_count++; } } DEBUG_TRACE("REQ created %d filters from message", filter_count); // Validate filters before processing char filter_error[512] = {0}; if (!validate_filter_array(filters, filter_error, sizeof(filter_error))) { DEBUG_TRACE("REQ rejected: filter validation failed - %s", filter_error); send_notice_message(wsi, filter_error); DEBUG_WARN("REQ rejected: invalid filters"); record_malformed_request(pss); cJSON_Delete(filters); cJSON_Delete(json); // Note: complete_message points to reassembly_buffer, which is managed separately // and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED return 0; } DEBUG_TRACE("REQ filters validated successfully"); DEBUG_TRACE("About to call handle_req_message for subscription %s", subscription_id); handle_req_message(subscription_id, filters, wsi, pss); DEBUG_TRACE("handle_req_message completed for subscription %s", subscription_id); // Clean up the filters array we created cJSON_Delete(filters); DEBUG_LOG("REQ subscription %s processed, sending EOSE", subscription_id); // Send EOSE (End of Stored Events) cJSON* eose_response = cJSON_CreateArray(); if (eose_response) { 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); // DEBUG: Log WebSocket frame details before sending DEBUG_TRACE("WS_FRAME_SEND: type=EOSE len=%zu data=%.100s%s", eose_len, eose_str, eose_len > 100 ? "..." : ""); // Queue message for proper libwebsockets pattern if (queue_message(wsi, pss, eose_str, eose_len, LWS_WRITE_TEXT) != 0) { DEBUG_ERROR("Failed to queue EOSE message"); } free(eose_str); } cJSON_Delete(eose_response); } } else { send_notice_message(wsi, "error: missing or invalid subscription ID in REQ"); DEBUG_WARN("REQ rejected: missing or invalid subscription ID"); } } else if (strcmp(msg_type, "COUNT") == 0) { // Check NIP-42 authentication for COUNT requests 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 count requests"); DEBUG_WARN("COUNT rejected: NIP-42 authentication required"); } cJSON_Delete(json); // Note: complete_message points to reassembly_buffer, which is managed separately // and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED return 0; } // Handle COUNT 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)); } } // Validate filters before processing char filter_error[512] = {0}; if (!validate_filter_array(filters, filter_error, sizeof(filter_error))) { send_notice_message(wsi, filter_error); DEBUG_WARN("COUNT rejected: invalid filters"); record_malformed_request(pss); cJSON_Delete(filters); cJSON_Delete(json); // Note: complete_message points to reassembly_buffer, which is managed separately // and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED return 0; } handle_count_message(subscription_id, filters, wsi, pss); // Clean up the filters array we created cJSON_Delete(filters); } } 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); // Validate subscription ID before processing if (!subscription_id) { send_notice_message(wsi, "error: invalid subscription ID in CLOSE"); DEBUG_WARN("CLOSE rejected: NULL subscription ID"); cJSON_Delete(json); // Note: complete_message points to reassembly_buffer, which is managed separately // and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED return 0; } // Validate subscription ID if (!validate_subscription_id(subscription_id)) { send_notice_message(wsi, "error: invalid subscription ID in CLOSE"); DEBUG_WARN("CLOSE rejected: invalid subscription ID"); cJSON_Delete(json); // Note: complete_message points to reassembly_buffer, which is managed separately // and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED return 0; } // CRITICAL FIX: Mark subscription as inactive in global manager FIRST // This prevents other threads from accessing it during removal pthread_mutex_lock(&g_subscription_manager.subscriptions_lock); subscription_t* target_sub = g_subscription_manager.active_subscriptions; while (target_sub) { if (strcmp(target_sub->id, subscription_id) == 0 && target_sub->wsi == wsi) { target_sub->active = 0; // Mark as inactive immediately break; } target_sub = target_sub->next; } pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock); // Now safe to remove from session list 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); } // Finally remove from global manager (which will free it) remove_subscription_from_manager(subscription_id, wsi); // Subscription closed } else { send_notice_message(wsi, "error: missing or invalid subscription ID in CLOSE"); DEBUG_WARN("CLOSE rejected: missing or invalid subscription ID"); } } 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"); DEBUG_WARN("Received AUTH message with invalid payload type"); } } else { send_notice_message(wsi, "AUTH message requires payload"); DEBUG_WARN("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); DEBUG_WARN(unknown_msg); send_notice_message(wsi, "Unknown message type"); } } } // Clean up the reassembled message if (json) cJSON_Delete(json); // Note: complete_message points to reassembly_buffer, which is managed separately // and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED return 0; // Fragmented message processed } else { // Not the final fragment - continue accumulating DEBUG_TRACE("Accumulated %zu bytes so far, waiting for more fragments", pss->reassembly_size); return 0; } } // Handle non-fragmented messages (original code path) char *message = malloc(len + 1); if (message) { memcpy(message, in, len); message[len] = '\0'; // Parse JSON message (this is the normal program flow) DEBUG_TRACE("Parsing JSON message of length %zu", strlen(message)); cJSON* json = cJSON_Parse(message); if (json && cJSON_IsArray(json)) { // 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 for debugging cJSON* pubkey_obj = cJSON_GetObjectItem(event_obj, "pubkey"); const char* event_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : "unknown"; // 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); // Special case: allow kind 14 DMs addressed to relay to bypass auth (admin commands) int bypass_auth = 0; if (event_kind == 14 && event_obj && cJSON_IsObject(event_obj)) { cJSON* tags = cJSON_GetObjectItem(event_obj, "tags"); if (tags && cJSON_IsArray(tags)) { const char* relay_pubkey = get_config_value("relay_pubkey"); if (relay_pubkey) { 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 (tag_name && cJSON_IsString(tag_name) && strcmp(cJSON_GetStringValue(tag_name), "p") == 0 && tag_value && cJSON_IsString(tag_value) && strcmp(cJSON_GetStringValue(tag_value), relay_pubkey) == 0) { bypass_auth = 1; break; } } } } } } // Special case: allow kind 23456 admin events from authorized admin to bypass auth if (event_kind == 23456 && event_pubkey) { const char* admin_pubkey = get_config_value("admin_pubkey"); if (admin_pubkey && strcmp(event_pubkey, admin_pubkey) == 0) { bypass_auth = 1; } else { DEBUG_INFO("DEBUG: Kind 23456 event but pubkey mismatch or no admin pubkey"); } } if (pss && auth_required && !pss->authenticated && !bypass_auth) { if (!pss->auth_challenge_sent) { 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); DEBUG_WARN("Event rejected: NIP-42 authentication required for kind"); } 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) { DEBUG_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); // Use proper message queue system instead of direct lws_write if (queue_message(wsi, pss, error_str, error_len, LWS_WRITE_TEXT) != 0) { DEBUG_ERROR("Failed to queue error response message"); } free(error_str); } cJSON_Delete(error_response); return 0; } // Call unified validator with JSON string size_t event_json_len = strlen(event_json_str); int validation_result = nostr_validate_unified_request(event_json_str, event_json_len); // Map validation result to old result format (0 = success, -1 = failure) int result = (validation_result == NOSTR_SUCCESS) ? 0 : -1; // 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; } } // Cleanup event JSON string free(event_json_str); // Check for NIP-70 protected events if (result == 0) { // Check if event has protected tag ["-"] int is_protected_event = 0; cJSON* tags = cJSON_GetObjectItem(event, "tags"); if (tags && cJSON_IsArray(tags)) { cJSON* tag = NULL; cJSON_ArrayForEach(tag, tags) { if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 1) { cJSON* tag_name = cJSON_GetArrayItem(tag, 0); if (tag_name && cJSON_IsString(tag_name) && strcmp(cJSON_GetStringValue(tag_name), "-") == 0) { is_protected_event = 1; break; } } } } if (is_protected_event) { // Check if protected events are enabled using config int protected_events_enabled = get_config_bool("nip70_protected_events_enabled", 0); if (!protected_events_enabled) { // Protected events not supported result = -1; strncpy(error_message, "blocked: protected events not supported", sizeof(error_message) - 1); error_message[sizeof(error_message) - 1] = '\0'; DEBUG_WARN("Protected event rejected: protected events not enabled"); } else { // Protected events enabled - check authentication cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey"); const char* event_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : NULL; if (!pss || !pss->authenticated || !event_pubkey || strcmp(pss->authenticated_pubkey, event_pubkey) != 0) { // Not authenticated or pubkey mismatch result = -1; strncpy(error_message, "auth-required: protected event requires authentication", sizeof(error_message) - 1); error_message[sizeof(error_message) - 1] = '\0'; DEBUG_WARN("Protected event rejected: authentication required"); } } } } // 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); DEBUG_TRACE("Processing event kind %d, message length: %zu", event_kind, strlen(message)); // Log reception of Kind 23456 events if (event_kind == 23456) { DEBUG_LOG("Admin event (kind 23456) received"); } if (event_kind == 23456) { // Enhanced admin event security - check authorization first 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 DEBUG_WARN("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'; } else { // Authorization successful - process through admin API char admin_error[512] = {0}; int admin_result = process_admin_event_in_config(event, admin_error, sizeof(admin_error), wsi); // Log results for Kind 23456 events if (event_kind == 23456) { if (admin_result != 0) { char error_result_msg[512]; if (strlen(admin_error) > 0) { // Safely truncate admin_error if too long size_t max_error_len = sizeof(error_result_msg) - 50; // Leave room for prefix size_t error_len = strlen(admin_error); if (error_len > max_error_len) { error_len = max_error_len; } char truncated_error[512]; memcpy(truncated_error, admin_error, error_len); truncated_error[error_len] = '\0'; // Use a safer approach to avoid truncation warning size_t prefix_len = snprintf(error_result_msg, sizeof(error_result_msg), "ERROR: Kind %d event processing failed: ", event_kind); if (prefix_len < sizeof(error_result_msg)) { size_t remaining = sizeof(error_result_msg) - prefix_len; size_t copy_len = strlen(truncated_error); if (copy_len >= remaining) { copy_len = remaining - 1; } memcpy(error_result_msg + prefix_len, truncated_error, copy_len); error_result_msg[prefix_len + copy_len] = '\0'; } } else { snprintf(error_result_msg, sizeof(error_result_msg), "ERROR: Kind %d event processing failed", event_kind); } DEBUG_ERROR(error_result_msg); } } if (admin_result != 0) { DEBUG_ERROR("Failed to process admin event"); result = -1; size_t error_len = strlen(admin_error); size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1; memcpy(error_message, admin_error, copy_len); error_message[copy_len] = '\0'; } else { // Admin events are processed by the admin API, not broadcast to subscriptions } } } else if (event_kind == 1059) { // Check for NIP-17 gift wrap admin messages char nip17_error[512] = {0}; cJSON* response_event = process_nip17_admin_message(event, nip17_error, sizeof(nip17_error), wsi); if (!response_event) { // Check if this is an error or if the command was already handled if (strlen(nip17_error) > 0) { // There was an actual error DEBUG_ERROR("NIP-17 admin message processing failed"); result = -1; size_t error_len = strlen(nip17_error); size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1; memcpy(error_message, nip17_error, copy_len); error_message[copy_len] = '\0'; } else { // No error message means the command was already handled (plain text commands) // Store the original gift wrap event in database if (store_event(event) != 0) { DEBUG_ERROR("Failed to store gift wrap event in database"); result = -1; strncpy(error_message, "error: failed to store gift wrap event", sizeof(error_message) - 1); } } } else { // Store the original gift wrap event in database (unlike kind 23456) if (store_event(event) != 0) { DEBUG_ERROR("Failed to store gift wrap event in database"); result = -1; strncpy(error_message, "error: failed to store gift wrap event", sizeof(error_message) - 1); cJSON_Delete(response_event); } else { // Broadcast RESPONSE event to matching persistent subscriptions broadcast_event_to_subscriptions(response_event); // Clean up response event cJSON_Delete(response_event); } } } else if (event_kind == 14) { // Check for DM stats commands addressed to relay char dm_error[512] = {0}; int dm_result = process_dm_stats_command(event, dm_error, sizeof(dm_error), wsi); if (dm_result != 0) { DEBUG_ERROR("DM stats command processing failed"); result = -1; size_t error_len = strlen(dm_error); size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1; memcpy(error_message, dm_error, copy_len); error_message[copy_len] = '\0'; } else { // Store the DM event in database if (store_event(event) != 0) { DEBUG_ERROR("Failed to store DM event in database"); result = -1; strncpy(error_message, "error: failed to store DM event", sizeof(error_message) - 1); } else { // Broadcast DM event to matching persistent subscriptions broadcast_event_to_subscriptions(event); } } } else { // Check if this is an ephemeral event (kinds 20000-29999) // Per NIP-01: ephemeral events are broadcast but never stored if (event_kind >= 20000 && event_kind < 30000) { DEBUG_TRACE("Ephemeral event (kind %d) - broadcasting without storage", event_kind); // Broadcast directly to subscriptions without database storage broadcast_event_to_subscriptions(event); } else { DEBUG_TRACE("Storing regular event in database"); // Regular event - store in database and broadcast if (store_event(event) != 0) { DEBUG_ERROR("Failed to store event in database"); result = -1; strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1); } else { DEBUG_LOG("Event stored and broadcast (kind %d)", event_kind); // Broadcast event to matching persistent subscriptions broadcast_event_to_subscriptions(event); } } } } else { // Event without valid kind - try normal storage DEBUG_WARN("Event without valid kind - trying normal storage"); if (store_event(event) != 0) { DEBUG_ERROR("Failed to store event without kind in database"); result = -1; strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1); } else { 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 : "")); char *response_str = cJSON_Print(response); if (response_str) { size_t response_len = strlen(response_str); // DEBUG: Log WebSocket frame details before sending DEBUG_TRACE("WS_FRAME_SEND: type=OK len=%zu data=%.100s%s", response_len, response_str, response_len > 100 ? "..." : ""); // Queue message for proper libwebsockets pattern if (queue_message(wsi, pss, response_str, response_len, LWS_WRITE_TEXT) != 0) { DEBUG_ERROR("Failed to queue OK response message"); } free(response_str); } cJSON_Delete(response); } } } else if (strcmp(msg_type, "REQ") == 0) { DEBUG_TRACE("REQ message received, starting processing"); // Log the full REQ message for debugging // Check NIP-42 authentication for REQ subscriptions if required if (pss && pss->nip42_auth_required_subscriptions && !pss->authenticated) { DEBUG_TRACE("REQ rejected: NIP-42 authentication required"); if (!pss->auth_challenge_sent) { send_nip42_auth_challenge(wsi, pss); } else { send_notice_message(wsi, "NIP-42 authentication required for subscriptions"); DEBUG_WARN("REQ rejected: NIP-42 authentication required"); } cJSON_Delete(json); free(message); return 0; } DEBUG_TRACE("REQ message passed authentication check"); // 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); DEBUG_TRACE("Processing REQ message for subscription %s", subscription_id); // Validate subscription ID before processing if (!subscription_id) { DEBUG_TRACE("REQ rejected: NULL subscription ID"); send_notice_message(wsi, "error: invalid subscription ID"); DEBUG_WARN("REQ rejected: NULL subscription ID"); record_malformed_request(pss); cJSON_Delete(json); free(message); return 0; } // Validate subscription ID if (!validate_subscription_id(subscription_id)) { DEBUG_TRACE("REQ rejected: invalid subscription ID format"); send_notice_message(wsi, "error: invalid subscription ID"); DEBUG_WARN("REQ rejected: invalid subscription ID"); cJSON_Delete(json); free(message); return 0; } DEBUG_TRACE("REQ subscription ID validated: %s", subscription_id); // Create array of filter objects from position 2 onwards cJSON* filters = cJSON_CreateArray(); if (!filters) { DEBUG_TRACE("REQ failed: could not create filters array"); send_notice_message(wsi, "error: failed to process filters"); DEBUG_ERROR("REQ failed: could not create filters array"); cJSON_Delete(json); free(message); return 0; } int json_size = cJSON_GetArraySize(json); int filter_count = 0; for (int i = 2; i < json_size; i++) { cJSON* filter = cJSON_GetArrayItem(json, i); if (filter) { cJSON_AddItemToArray(filters, cJSON_Duplicate(filter, 1)); filter_count++; } } DEBUG_TRACE("REQ created %d filters from message", filter_count); // Validate filters before processing char filter_error[512] = {0}; if (!validate_filter_array(filters, filter_error, sizeof(filter_error))) { DEBUG_TRACE("REQ rejected: filter validation failed - %s", filter_error); send_notice_message(wsi, filter_error); DEBUG_WARN("REQ rejected: invalid filters"); record_malformed_request(pss); cJSON_Delete(filters); cJSON_Delete(json); free(message); return 0; } DEBUG_TRACE("REQ filters validated successfully"); DEBUG_TRACE("About to call handle_req_message for subscription %s", subscription_id); handle_req_message(subscription_id, filters, wsi, pss); DEBUG_TRACE("handle_req_message completed for subscription %s", subscription_id); // Clean up the filters array we created cJSON_Delete(filters); DEBUG_LOG("REQ subscription %s processed, sending EOSE", subscription_id); // Send EOSE (End of Stored Events) cJSON* eose_response = cJSON_CreateArray(); if (eose_response) { 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); // DEBUG: Log WebSocket frame details before sending DEBUG_TRACE("WS_FRAME_SEND: type=EOSE len=%zu data=%.100s%s", eose_len, eose_str, eose_len > 100 ? "..." : ""); // Queue message for proper libwebsockets pattern if (queue_message(wsi, pss, eose_str, eose_len, LWS_WRITE_TEXT) != 0) { DEBUG_ERROR("Failed to queue EOSE message"); } free(eose_str); } cJSON_Delete(eose_response); } } else { send_notice_message(wsi, "error: missing or invalid subscription ID in REQ"); DEBUG_WARN("REQ rejected: missing or invalid subscription ID"); } } else if (strcmp(msg_type, "COUNT") == 0) { // Check NIP-42 authentication for COUNT requests 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 count requests"); DEBUG_WARN("COUNT rejected: NIP-42 authentication required"); } cJSON_Delete(json); free(message); return 0; } // Handle COUNT 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)); } } // Validate filters before processing char filter_error[512] = {0}; if (!validate_filter_array(filters, filter_error, sizeof(filter_error))) { send_notice_message(wsi, filter_error); DEBUG_WARN("COUNT rejected: invalid filters"); record_malformed_request(pss); cJSON_Delete(filters); cJSON_Delete(json); free(message); return 0; } handle_count_message(subscription_id, filters, wsi, pss); // Clean up the filters array we created cJSON_Delete(filters); } } 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); // Validate subscription ID before processing if (!subscription_id) { send_notice_message(wsi, "error: invalid subscription ID in CLOSE"); DEBUG_WARN("CLOSE rejected: NULL subscription ID"); cJSON_Delete(json); free(message); return 0; } // Validate subscription ID if (!validate_subscription_id(subscription_id)) { send_notice_message(wsi, "error: invalid subscription ID in CLOSE"); DEBUG_WARN("CLOSE rejected: invalid subscription ID"); cJSON_Delete(json); free(message); return 0; } // CRITICAL FIX: Mark subscription as inactive in global manager FIRST // This prevents other threads from accessing it during removal pthread_mutex_lock(&g_subscription_manager.subscriptions_lock); subscription_t* target_sub = g_subscription_manager.active_subscriptions; while (target_sub) { if (strcmp(target_sub->id, subscription_id) == 0 && target_sub->wsi == wsi) { target_sub->active = 0; // Mark as inactive immediately break; } target_sub = target_sub->next; } pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock); // Now safe to remove from session list 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); } // Finally remove from global manager (which will free it) remove_subscription_from_manager(subscription_id, wsi); // Subscription closed } else { send_notice_message(wsi, "error: missing or invalid subscription ID in CLOSE"); DEBUG_WARN("CLOSE rejected: missing or invalid subscription ID"); } } 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"); DEBUG_WARN("Received AUTH message with invalid payload type"); } } else { send_notice_message(wsi, "AUTH message requires payload"); DEBUG_WARN("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); DEBUG_WARN(unknown_msg); send_notice_message(wsi, "Unknown message type"); } } } if (json) cJSON_Delete(json); free(message); } } break; case LWS_CALLBACK_SERVER_WRITEABLE: // Handle message queue when socket becomes writeable if (pss) { process_message_queue(wsi, pss); } break; case LWS_CALLBACK_CLOSED: DEBUG_TRACE("WebSocket connection closed"); // Enhanced closure logging with detailed diagnostics if (pss) { // Calculate connection duration time_t now = time(NULL); long duration = (pss->connection_established > 0) ? (long)(now - pss->connection_established) : 0; // Determine closure reason const char* reason = "client_disconnect"; if (g_shutdown_flag || !g_server_running) { reason = "server_shutdown"; } // Format authentication status char auth_status[80]; if (pss->authenticated && strlen(pss->authenticated_pubkey) > 0) { // Show first 8 chars of pubkey for identification snprintf(auth_status, sizeof(auth_status), "yes(%.8s...)", pss->authenticated_pubkey); } else { snprintf(auth_status, sizeof(auth_status), "no"); } // Log comprehensive closure information DEBUG_LOG("WebSocket CLOSED: ip=%s duration=%lds subscriptions=%d authenticated=%s reason=%s", pss->client_ip, duration, pss->subscription_count, auth_status, reason); // Clean up message queue to prevent memory leaks while (pss->message_queue_head) { struct message_queue_node* node = pss->message_queue_head; pss->message_queue_head = node->next; free(node->data); free(node); } pss->message_queue_tail = NULL; pss->message_queue_count = 0; pss->writeable_requested = 0; // Clean up message reassembly buffer if (pss->reassembly_buffer) { free(pss->reassembly_buffer); pss->reassembly_buffer = NULL; } pss->reassembly_size = 0; pss->reassembly_capacity = 0; pss->reassembly_active = 0; // Clean up session subscriptions - copy IDs first to avoid use-after-free pthread_mutex_lock(&pss->session_lock); // First pass: collect subscription IDs safely typedef struct temp_sub_id { char id[SUBSCRIPTION_ID_MAX_LENGTH]; struct temp_sub_id* next; } temp_sub_id_t; temp_sub_id_t* temp_ids = NULL; temp_sub_id_t* temp_tail = NULL; int temp_count = 0; struct subscription* sub = pss->subscriptions; while (sub) { if (sub->active) { // Only process active subscriptions temp_sub_id_t* temp = malloc(sizeof(temp_sub_id_t)); if (temp) { memcpy(temp->id, sub->id, SUBSCRIPTION_ID_MAX_LENGTH); temp->id[SUBSCRIPTION_ID_MAX_LENGTH - 1] = '\0'; temp->next = NULL; if (!temp_ids) { temp_ids = temp; temp_tail = temp; } else { temp_tail->next = temp; temp_tail = temp; } temp_count++; } } sub = sub->session_next; } // Clear session list immediately pss->subscriptions = NULL; pss->subscription_count = 0; pthread_mutex_unlock(&pss->session_lock); // Second pass: remove from global manager using copied IDs temp_sub_id_t* current_temp = temp_ids; while (current_temp) { temp_sub_id_t* next_temp = current_temp->next; remove_subscription_from_manager(current_temp->id, wsi); free(current_temp); current_temp = next_temp; } pthread_mutex_destroy(&pss->session_lock); } else { DEBUG_LOG("WebSocket CLOSED: ip=unknown duration=0s subscriptions=0 authenticated=no reason=unknown"); } DEBUG_TRACE("WebSocket connection cleanup complete"); break; default: break; } return 0; } // WebSocket protocol definition static struct lws_protocols protocols[] = { { "nostr-relay-protocol", nostr_relay_callback, sizeof(struct per_session_data), 65536, // 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; // Starting libwebsockets-based Nostr relay server // Set libwebsockets log level to errors only lws_set_log_level(LLL_USER | LLL_ERR, NULL); 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)) { // Checking port availability // 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); DEBUG_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); DEBUG_WARN(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); DEBUG_ERROR(error_msg); return -1; } } // Port appears available, try creating libwebsockets context info.port = actual_port; // Attempting to bind libwebsockets 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); DEBUG_WARN(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); DEBUG_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); DEBUG_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); DEBUG_WARN(startup_msg); } else { snprintf(startup_msg, sizeof(startup_msg), "WebSocket relay started on ws://127.0.0.1:%d", actual_port); } // Static variable for status post timing (initialize to 0 for immediate first post) static time_t last_status_post_time = 0; // Main event loop with proper signal handling while (g_server_running && !g_shutdown_flag) { int result = lws_service(ws_context, 1000); if (result < 0) { DEBUG_ERROR("libwebsockets service error"); break; } // Check if it's time to post status update time_t current_time = time(NULL); int status_post_hours = get_config_int("kind_1_status_posts_hours", 0); if (status_post_hours > 0) { int seconds_interval = status_post_hours * 3600; // Convert hours to seconds if (current_time - last_status_post_time >= seconds_interval) { last_status_post_time = current_time; generate_and_post_status_event(); } } } lws_context_destroy(ws_context); ws_context = NULL; return 0; } // Process DM stats command int process_dm_stats_command(cJSON* dm_event, char* error_message, size_t error_size, struct lws* wsi) { // Suppress unused parameter warning (void)wsi; if (!dm_event || !error_message) { return -1; } // Check if DM is addressed to relay cJSON* tags = cJSON_GetObjectItem(dm_event, "tags"); if (!tags || !cJSON_IsArray(tags)) { strncpy(error_message, "DM missing or invalid tags", error_size - 1); return -1; } const char* relay_pubkey = get_config_value("relay_pubkey"); if (!relay_pubkey) { strncpy(error_message, "Could not get relay pubkey", error_size - 1); return -1; } // Look for "p" tag with relay pubkey int addressed_to_relay = 0; 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 (tag_name && cJSON_IsString(tag_name) && strcmp(cJSON_GetStringValue(tag_name), "p") == 0 && tag_value && cJSON_IsString(tag_value) && strcmp(cJSON_GetStringValue(tag_value), relay_pubkey) == 0) { addressed_to_relay = 1; break; } } } if (!addressed_to_relay) { // Not addressed to relay, allow normal processing return 0; } // Get sender pubkey cJSON* pubkey_obj = cJSON_GetObjectItem(dm_event, "pubkey"); if (!pubkey_obj || !cJSON_IsString(pubkey_obj)) { strncpy(error_message, "DM missing sender pubkey", error_size - 1); return -1; } const char* sender_pubkey = cJSON_GetStringValue(pubkey_obj); // Check if sender is admin const char* admin_pubkey = get_config_value("admin_pubkey"); if (!admin_pubkey || strlen(admin_pubkey) == 0 || strcmp(sender_pubkey, admin_pubkey) != 0) { strncpy(error_message, "Unauthorized: not admin", error_size - 1); return -1; } // Get relay private key for decryption char* relay_privkey_hex = get_relay_private_key(); if (!relay_privkey_hex) { strncpy(error_message, "Could not get relay private key", error_size - 1); return -1; } // Convert relay private key to bytes unsigned char relay_privkey[32]; if (nostr_hex_to_bytes(relay_privkey_hex, relay_privkey, sizeof(relay_privkey)) != 0) { free(relay_privkey_hex); strncpy(error_message, "Failed to convert relay private key", error_size - 1); return -1; } free(relay_privkey_hex); // Convert sender pubkey to bytes unsigned char sender_pubkey_bytes[32]; if (nostr_hex_to_bytes(sender_pubkey, sender_pubkey_bytes, sizeof(sender_pubkey_bytes)) != 0) { strncpy(error_message, "Failed to convert sender pubkey", error_size - 1); return -1; } // Get encrypted content cJSON* content_obj = cJSON_GetObjectItem(dm_event, "content"); if (!content_obj || !cJSON_IsString(content_obj)) { strncpy(error_message, "DM missing content", error_size - 1); return -1; } const char* encrypted_content = cJSON_GetStringValue(content_obj); // Decrypt content char decrypted_content[16384]; int decrypt_result = nostr_nip44_decrypt(relay_privkey, sender_pubkey_bytes, encrypted_content, decrypted_content, sizeof(decrypted_content)); if (decrypt_result != NOSTR_SUCCESS) { char decrypt_error[256]; snprintf(decrypt_error, sizeof(decrypt_error), "NIP-44 decryption failed: %d", decrypt_result); strncpy(error_message, decrypt_error, error_size - 1); return -1; } // Check if content is "stats" if (strcmp(decrypted_content, "stats") != 0) { // Not a stats command, allow normal processing return 0; } // Processing DM stats command from admin // Generate stats JSON char* stats_json = generate_stats_json(); if (!stats_json) { strncpy(error_message, "Failed to generate stats", error_size - 1); return -1; } // Encrypt stats for response char encrypted_response[4096]; int encrypt_result = nostr_nip44_encrypt(relay_privkey, sender_pubkey_bytes, stats_json, encrypted_response, sizeof(encrypted_response)); free(stats_json); if (encrypt_result != NOSTR_SUCCESS) { char encrypt_error[256]; snprintf(encrypt_error, sizeof(encrypt_error), "NIP-44 encryption failed: %d", encrypt_result); strncpy(error_message, encrypt_error, error_size - 1); return -1; } // Create DM response event cJSON* dm_response = cJSON_CreateObject(); cJSON_AddStringToObject(dm_response, "id", ""); // Will be set by event creation cJSON_AddStringToObject(dm_response, "pubkey", relay_pubkey); cJSON_AddNumberToObject(dm_response, "created_at", (double)time(NULL)); cJSON_AddNumberToObject(dm_response, "kind", 14); cJSON_AddStringToObject(dm_response, "content", encrypted_response); // Add tags: p tag for recipient (admin) cJSON* response_tags = cJSON_CreateArray(); cJSON* p_tag = cJSON_CreateArray(); cJSON_AddItemToArray(p_tag, cJSON_CreateString("p")); cJSON_AddItemToArray(p_tag, cJSON_CreateString(sender_pubkey)); cJSON_AddItemToArray(response_tags, p_tag); cJSON_AddItemToObject(dm_response, "tags", response_tags); // Add signature placeholder cJSON_AddStringToObject(dm_response, "sig", ""); // Will be set by event creation/signing // Store and broadcast the DM response int store_result = store_event(dm_response); if (store_result != 0) { cJSON_Delete(dm_response); strncpy(error_message, "Failed to store DM response", error_size - 1); return -1; } // Broadcast to subscriptions broadcast_event_to_subscriptions(dm_response); cJSON_Delete(dm_response); return 0; } // Handle NIP-45 COUNT message int handle_count_message(const char* sub_id, cJSON* filters, struct lws *wsi, struct per_session_data *pss) { (void)pss; // Suppress unused parameter warning if (!cJSON_IsArray(filters)) { DEBUG_ERROR("COUNT filters is not an array"); return 0; } // Parameter binding helpers char** bind_params = NULL; int bind_param_count = 0; int bind_param_capacity = 0; int total_count = 0; // Process each filter in the array for (int i = 0; i < cJSON_GetArraySize(filters); i++) { cJSON* filter = cJSON_GetArrayItem(filters, i); if (!filter || !cJSON_IsObject(filter)) { DEBUG_WARN("Invalid filter object in COUNT"); continue; } // Reset bind params for this filter for (int j = 0; j < bind_param_count; j++) { free(bind_params[j]); } free(bind_params); bind_params = NULL; bind_param_count = 0; bind_param_capacity = 0; // Build SQL COUNT query based on filter - exclude ephemeral events (kinds 20000-29999) from historical queries char sql[1024] = "SELECT COUNT(*) FROM events WHERE 1=1 AND (kind < 20000 OR kind >= 30000)"; char* sql_ptr = sql + strlen(sql); int remaining = sizeof(sql) - strlen(sql); // Note: Expiration filtering will be done at application level // after retrieving events to ensure compatibility with all SQLite versions // Handle kinds filter cJSON* kinds = cJSON_GetObjectItem(filter, "kinds"); if (kinds && cJSON_IsArray(kinds)) { int kind_count = cJSON_GetArraySize(kinds); if (kind_count > 0) { snprintf(sql_ptr, remaining, " AND kind IN ("); sql_ptr += strlen(sql_ptr); remaining = sizeof(sql) - strlen(sql); for (int k = 0; k < kind_count; k++) { cJSON* kind = cJSON_GetArrayItem(kinds, k); if (cJSON_IsNumber(kind)) { if (k > 0) { snprintf(sql_ptr, remaining, ","); sql_ptr++; remaining--; } snprintf(sql_ptr, remaining, "%d", (int)cJSON_GetNumberValue(kind)); 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 authors filter cJSON* authors = cJSON_GetObjectItem(filter, "authors"); if (authors && cJSON_IsArray(authors)) { int author_count = 0; // Count valid authors for (int a = 0; a < cJSON_GetArraySize(authors); a++) { cJSON* author = cJSON_GetArrayItem(authors, a); if (cJSON_IsString(author)) { author_count++; } } if (author_count > 0) { 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++) { if (a > 0) { snprintf(sql_ptr, remaining, ","); sql_ptr++; remaining--; } snprintf(sql_ptr, remaining, "?"); sql_ptr += strlen(sql_ptr); remaining = sizeof(sql) - strlen(sql); } snprintf(sql_ptr, remaining, ")"); sql_ptr += strlen(sql_ptr); remaining = sizeof(sql) - strlen(sql); // Add author values to bind params for (int a = 0; a < cJSON_GetArraySize(authors); a++) { cJSON* author = cJSON_GetArrayItem(authors, a); if (cJSON_IsString(author)) { if (bind_param_count >= bind_param_capacity) { bind_param_capacity = bind_param_capacity == 0 ? 16 : bind_param_capacity * 2; bind_params = realloc(bind_params, bind_param_capacity * sizeof(char*)); } bind_params[bind_param_count++] = strdup(cJSON_GetStringValue(author)); } } } } // Handle ids filter cJSON* ids = cJSON_GetObjectItem(filter, "ids"); if (ids && cJSON_IsArray(ids)) { int id_count = 0; // Count valid ids for (int i = 0; i < cJSON_GetArraySize(ids); i++) { cJSON* id = cJSON_GetArrayItem(ids, i); if (cJSON_IsString(id)) { id_count++; } } 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++) { if (i > 0) { snprintf(sql_ptr, remaining, ","); sql_ptr++; remaining--; } snprintf(sql_ptr, remaining, "?"); sql_ptr += strlen(sql_ptr); remaining = sizeof(sql) - strlen(sql); } snprintf(sql_ptr, remaining, ")"); sql_ptr += strlen(sql_ptr); remaining = sizeof(sql) - strlen(sql); // Add id values to bind params for (int i = 0; i < cJSON_GetArraySize(ids); i++) { cJSON* id = cJSON_GetArrayItem(ids, i); if (cJSON_IsString(id)) { if (bind_param_count >= bind_param_capacity) { bind_param_capacity = bind_param_capacity == 0 ? 16 : bind_param_capacity * 2; bind_params = realloc(bind_params, bind_param_capacity * sizeof(char*)); } bind_params[bind_param_count++] = strdup(cJSON_GetStringValue(id)); } } } } // Handle tag filters (#e, #p, #t, etc.) cJSON* filter_item = NULL; cJSON_ArrayForEach(filter_item, filter) { const char* filter_key = filter_item->string; if (filter_key && filter_key[0] == '#' && strlen(filter_key) > 1) { // This is a tag filter like "#e", "#p", etc. const char* tag_name = filter_key + 1; // Get the tag name (e, p, t, type, etc.) if (cJSON_IsArray(filter_item)) { int tag_value_count = 0; // Count valid tag values for (int i = 0; i < cJSON_GetArraySize(filter_item); i++) { cJSON* tag_value = cJSON_GetArrayItem(filter_item, i); if (cJSON_IsString(tag_value)) { tag_value_count++; } } if (tag_value_count > 0) { // Use EXISTS with parameterized query snprintf(sql_ptr, remaining, " AND EXISTS (SELECT 1 FROM json_each(json(tags)) WHERE json_extract(value, '$[0]') = ? AND json_extract(value, '$[1]') IN ("); sql_ptr += strlen(sql_ptr); remaining = sizeof(sql) - strlen(sql); for (int i = 0; i < tag_value_count; i++) { if (i > 0) { snprintf(sql_ptr, remaining, ","); sql_ptr++; remaining--; } snprintf(sql_ptr, remaining, "?"); sql_ptr += strlen(sql_ptr); remaining = sizeof(sql) - strlen(sql); } snprintf(sql_ptr, remaining, "))"); sql_ptr += strlen(sql_ptr); remaining = sizeof(sql) - strlen(sql); // Add tag name and values to bind params if (bind_param_count >= bind_param_capacity) { bind_param_capacity = bind_param_capacity == 0 ? 16 : bind_param_capacity * 2; bind_params = realloc(bind_params, bind_param_capacity * sizeof(char*)); } bind_params[bind_param_count++] = strdup(tag_name); for (int i = 0; i < cJSON_GetArraySize(filter_item); i++) { cJSON* tag_value = cJSON_GetArrayItem(filter_item, i); if (cJSON_IsString(tag_value)) { if (bind_param_count >= bind_param_capacity) { bind_param_capacity = bind_param_capacity == 0 ? 16 : bind_param_capacity * 2; bind_params = realloc(bind_params, bind_param_capacity * sizeof(char*)); } bind_params[bind_param_count++] = strdup(cJSON_GetStringValue(tag_value)); } } } } } } // Handle search filter (NIP-50) cJSON* search = cJSON_GetObjectItem(filter, "search"); if (search && cJSON_IsString(search)) { const char* search_term = cJSON_GetStringValue(search); if (search_term && strlen(search_term) > 0) { // Search in both content and tag values using LIKE // Escape single quotes in search term for SQL safety char escaped_search[256]; size_t escaped_len = 0; for (size_t i = 0; search_term[i] && escaped_len < sizeof(escaped_search) - 1; i++) { if (search_term[i] == '\'') { escaped_search[escaped_len++] = '\''; escaped_search[escaped_len++] = '\''; } else { escaped_search[escaped_len++] = search_term[i]; } } escaped_search[escaped_len] = '\0'; // Add search conditions for content and tags // Use tags LIKE to search within the JSON string representation of tags snprintf(sql_ptr, remaining, " AND (content LIKE '%%%s%%' OR tags LIKE '%%\"%s\"%%')", escaped_search, escaped_search); sql_ptr += strlen(sql_ptr); remaining = sizeof(sql) - strlen(sql); } } // Handle since filter cJSON* since = cJSON_GetObjectItem(filter, "since"); if (since && cJSON_IsNumber(since)) { snprintf(sql_ptr, remaining, " AND created_at >= %ld", (long)cJSON_GetNumberValue(since)); sql_ptr += strlen(sql_ptr); remaining = sizeof(sql) - strlen(sql); } // Handle until filter cJSON* until = cJSON_GetObjectItem(filter, "until"); if (until && cJSON_IsNumber(until)) { snprintf(sql_ptr, remaining, " AND created_at <= %ld", (long)cJSON_GetNumberValue(until)); sql_ptr += strlen(sql_ptr); remaining = sizeof(sql) - strlen(sql); } // Execute count query // Execute count query sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { char error_msg[256]; snprintf(error_msg, sizeof(error_msg), "Failed to prepare COUNT query: %s", sqlite3_errmsg(g_db)); DEBUG_ERROR(error_msg); continue; } // Bind parameters for (int i = 0; i < bind_param_count; i++) { sqlite3_bind_text(stmt, i + 1, bind_params[i], -1, SQLITE_TRANSIENT); } int filter_count = 0; if (sqlite3_step(stmt) == SQLITE_ROW) { filter_count = sqlite3_column_int(stmt, 0); } // Filter count calculated sqlite3_finalize(stmt); total_count += filter_count; } // Total count calculated // Send COUNT response - NIP-45 format: ["COUNT", , {"count": }] cJSON* count_response = cJSON_CreateArray(); cJSON_AddItemToArray(count_response, cJSON_CreateString("COUNT")); cJSON_AddItemToArray(count_response, cJSON_CreateString(sub_id)); // Create count object as per NIP-45 specification cJSON* count_obj = cJSON_CreateObject(); cJSON_AddNumberToObject(count_obj, "count", total_count); cJSON_AddItemToArray(count_response, count_obj); char *count_str = cJSON_Print(count_response); if (count_str) { size_t count_len = strlen(count_str); // DEBUG: Log WebSocket frame details before sending DEBUG_TRACE("WS_FRAME_SEND: type=COUNT len=%zu data=%.100s%s", count_len, count_str, count_len > 100 ? "..." : ""); // Queue message for proper libwebsockets pattern if (queue_message(wsi, pss, count_str, count_len, LWS_WRITE_TEXT) != 0) { DEBUG_ERROR("Failed to queue COUNT message"); } free(count_str); } cJSON_Delete(count_response); // Cleanup bind params for (int i = 0; i < bind_param_count; i++) { free(bind_params[i]); } free(bind_params); return total_count; } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // RATE LIMITING FUNCTIONS ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// /** * Check if a client is currently rate limited for malformed requests */ int is_client_rate_limited_for_malformed_requests(struct per_session_data *pss) { if (!pss) { return 0; } time_t now = time(NULL); // Check if currently blocked if (pss->malformed_request_blocked_until > now) { return 1; } // Reset block if expired if (pss->malformed_request_blocked_until > 0 && pss->malformed_request_blocked_until <= now) { pss->malformed_request_blocked_until = 0; pss->malformed_request_count = 0; pss->malformed_request_window_start = now; } // Check if within current hour window if (pss->malformed_request_window_start == 0 || (now - pss->malformed_request_window_start) >= 3600) { // 1 hour // Start new window pss->malformed_request_window_start = now; pss->malformed_request_count = 0; } // Check if exceeded limit if (pss->malformed_request_count >= MAX_MALFORMED_REQUESTS_PER_HOUR) { // Block for the specified duration pss->malformed_request_blocked_until = now + MALFORMED_REQUEST_BLOCK_DURATION; DEBUG_WARN("Client rate limited for malformed requests"); return 1; } return 0; } /** * Record a malformed request for rate limiting purposes */ void record_malformed_request(struct per_session_data *pss) { if (!pss) { return; } time_t now = time(NULL); // Initialize window if needed if (pss->malformed_request_window_start == 0) { pss->malformed_request_window_start = now; pss->malformed_request_count = 0; } // Reset window if hour has passed if ((now - pss->malformed_request_window_start) >= 3600) { pss->malformed_request_window_start = now; pss->malformed_request_count = 0; } // Increment count pss->malformed_request_count++; } /** * Validate if a string is valid hexadecimal of specified length */ int is_valid_hex_string(const char* str, size_t expected_len) { if (!str || strlen(str) != expected_len) { return 0; } for (size_t i = 0; i < expected_len; i++) { char c = str[i]; if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { return 0; } } return 1; } /** * Validate a filter array for REQ and COUNT messages */ int validate_filter_array(cJSON* filters, char* error_message, size_t error_size) { if (!filters || !cJSON_IsArray(filters)) { snprintf(error_message, error_size, "error: filters must be an array"); return 0; } int filter_count = cJSON_GetArraySize(filters); if (filter_count > MAX_FILTERS_PER_REQUEST) { snprintf(error_message, error_size, "error: too many filters (max %d)", MAX_FILTERS_PER_REQUEST); return 0; } // Validate each filter object for (int i = 0; i < filter_count; i++) { cJSON* filter = cJSON_GetArrayItem(filters, i); if (!filter || !cJSON_IsObject(filter)) { snprintf(error_message, error_size, "error: filter %d is not an object", i); return 0; } // Validate filter fields cJSON* filter_item = NULL; cJSON_ArrayForEach(filter_item, filter) { const char* key = filter_item->string; if (!key) continue; // Validate authors array if (strcmp(key, "authors") == 0) { if (!cJSON_IsArray(filter_item)) { snprintf(error_message, error_size, "error: authors must be an array"); return 0; } int author_count = cJSON_GetArraySize(filter_item); if (author_count > MAX_AUTHORS_PER_FILTER) { snprintf(error_message, error_size, "error: too many authors (max %d)", MAX_AUTHORS_PER_FILTER); return 0; } for (int j = 0; j < author_count; j++) { cJSON* author = cJSON_GetArrayItem(filter_item, j); if (!cJSON_IsString(author)) { snprintf(error_message, error_size, "error: author %d is not a string", j); return 0; } const char* author_str = cJSON_GetStringValue(author); if (!is_valid_hex_string(author_str, 64)) { snprintf(error_message, error_size, "error: invalid author hex string"); return 0; } } } // Validate ids array else if (strcmp(key, "ids") == 0) { if (!cJSON_IsArray(filter_item)) { snprintf(error_message, error_size, "error: ids must be an array"); return 0; } int id_count = cJSON_GetArraySize(filter_item); if (id_count > MAX_IDS_PER_FILTER) { snprintf(error_message, error_size, "error: too many ids (max %d)", MAX_IDS_PER_FILTER); return 0; } for (int j = 0; j < id_count; j++) { cJSON* id = cJSON_GetArrayItem(filter_item, j); if (!cJSON_IsString(id)) { snprintf(error_message, error_size, "error: id %d is not a string", j); return 0; } const char* id_str = cJSON_GetStringValue(id); if (!is_valid_hex_string(id_str, 64)) { snprintf(error_message, error_size, "error: invalid id hex string"); return 0; } } } // Validate kinds array else if (strcmp(key, "kinds") == 0) { if (!cJSON_IsArray(filter_item)) { snprintf(error_message, error_size, "error: kinds must be an array"); return 0; } int kind_count = cJSON_GetArraySize(filter_item); if (kind_count > MAX_KINDS_PER_FILTER) { snprintf(error_message, error_size, "error: too many kinds (max %d)", MAX_KINDS_PER_FILTER); return 0; } for (int j = 0; j < kind_count; j++) { cJSON* kind = cJSON_GetArrayItem(filter_item, j); if (!cJSON_IsNumber(kind)) { snprintf(error_message, error_size, "error: kind %d is not a number", j); return 0; } int kind_val = (int)cJSON_GetNumberValue(kind); if (kind_val < 0 || kind_val > MAX_KIND_VALUE) { snprintf(error_message, error_size, "error: invalid kind value %d", kind_val); return 0; } } } // Validate since/until timestamps else if (strcmp(key, "since") == 0 || strcmp(key, "until") == 0) { if (!cJSON_IsNumber(filter_item)) { snprintf(error_message, error_size, "error: %s must be a number", key); return 0; } double timestamp = cJSON_GetNumberValue(filter_item); if (timestamp < 0 || timestamp > MAX_TIMESTAMP_VALUE) { snprintf(error_message, error_size, "error: invalid %s timestamp", key); return 0; } } // Validate limit else if (strcmp(key, "limit") == 0) { if (!cJSON_IsNumber(filter_item)) { snprintf(error_message, error_size, "error: limit must be a number"); return 0; } int limit_val = (int)cJSON_GetNumberValue(filter_item); if (limit_val < 0 || limit_val > MAX_LIMIT_VALUE) { snprintf(error_message, error_size, "error: invalid limit value %d", limit_val); return 0; } } // Validate search term else if (strcmp(key, "search") == 0) { if (!cJSON_IsString(filter_item)) { snprintf(error_message, error_size, "error: search must be a string"); return 0; } const char* search_str = cJSON_GetStringValue(filter_item); size_t search_len = strlen(search_str); if (search_len > MAX_SEARCH_LENGTH) { snprintf(error_message, error_size, "error: search term too long (max %d)", MAX_SEARCH_LENGTH); return 0; } // Check for SQL injection characters if (strchr(search_str, ';') || strstr(search_str, "--") || strstr(search_str, "/*") || strstr(search_str, "*/")) { snprintf(error_message, error_size, "error: invalid characters in search term"); return 0; } } // Validate tag filters (#e, #p, #t, etc.) else if (key[0] == '#' && strlen(key) > 1) { if (!cJSON_IsArray(filter_item)) { snprintf(error_message, error_size, "error: %s must be an array", key); return 0; } int tag_count = cJSON_GetArraySize(filter_item); if (tag_count > MAX_TAG_VALUES_PER_FILTER) { snprintf(error_message, error_size, "error: too many %s values (max %d)", key, MAX_TAG_VALUES_PER_FILTER); return 0; } for (int j = 0; j < tag_count; j++) { cJSON* tag_value = cJSON_GetArrayItem(filter_item, j); if (!cJSON_IsString(tag_value)) { snprintf(error_message, error_size, "error: %s[%d] is not a string", key, j); return 0; } const char* tag_str = cJSON_GetStringValue(tag_value); size_t tag_len = strlen(tag_str); if (tag_len > MAX_TAG_VALUE_LENGTH) { snprintf(error_message, error_size, "error: %s value too long (max %d)", key, MAX_TAG_VALUE_LENGTH); return 0; } } } // Unknown filter keys are allowed but ignored } } return 1; // All filters valid }