2943 lines
160 KiB
C
2943 lines
160 KiB
C
// Define _GNU_SOURCE to ensure all POSIX features are available
|
|
#define _GNU_SOURCE
|
|
|
|
// Includes
|
|
#include <stdio.h>
|
|
#include "debug.h"
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <signal.h>
|
|
#include <time.h>
|
|
#include <pthread.h>
|
|
#include <sqlite3.h>
|
|
|
|
// Include libwebsockets after pthread.h to ensure pthread_rwlock_t is defined
|
|
#include <libwebsockets.h>
|
|
#include <errno.h>
|
|
#include <sys/socket.h>
|
|
#include <netinet/in.h>
|
|
#include <arpa/inet.h>
|
|
|
|
// 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", <challenge>] (unusual)
|
|
handle_nip42_auth_challenge_response(wsi, pss, cJSON_GetStringValue(auth_payload));
|
|
} else if (cJSON_IsObject(auth_payload)) {
|
|
// AUTH signed event: ["AUTH", <event>] (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", <challenge>] (unusual)
|
|
handle_nip42_auth_challenge_response(wsi, pss, cJSON_GetStringValue(auth_payload));
|
|
} else if (cJSON_IsObject(auth_payload)) {
|
|
// AUTH signed event: ["AUTH", <event>] (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", <subscription_id>, {"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
|
|
}
|