// Define _GNU_SOURCE to ensure all POSIX features are available #define _GNU_SOURCE // Includes #include #include #include #include #include #include #include #include // Include libwebsockets after pthread.h to ensure pthread_rwlock_t is defined #include #include #include #include #include // Include nostr_core_lib for Nostr functionality #include "../nostr_core_lib/cjson/cJSON.h" #include "../nostr_core_lib/nostr_core/nostr_core.h" #include "../nostr_core_lib/nostr_core/nip013.h" // NIP-13: Proof of Work #include "config.h" // Configuration management system #include "sql_schema.h" // Embedded database schema #include "websockets.h" // WebSocket structures and constants #include "subscriptions.h" // Subscription structures and functions #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 void log_info(const char* message); void log_success(const char* message); void log_error(const char* message); void log_warning(const char* message); // Forward declarations for configuration functions const char* get_config_value(const char* key); int get_config_int(const char* key, int default_value); int get_config_bool(const char* key, int default_value); // Forward declarations for NIP-42 authentication functions int is_nip42_auth_globally_required(void); int is_nip42_auth_required_for_kind(int kind); void send_nip42_auth_challenge(struct lws* wsi, struct per_session_data* pss); void handle_nip42_auth_signed_event(struct lws* wsi, struct per_session_data* pss, cJSON* auth_event); void handle_nip42_auth_challenge_response(struct lws* wsi, struct per_session_data* pss, const char* challenge); // Forward declarations for NIP-11 relay information handling int handle_nip11_http_request(struct lws* wsi, const char* accept_header); // Forward declarations for 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 NOTICE message support void send_notice_message(struct lws* wsi, const char* message); // Forward declarations for unified cache access extern unified_config_cache_t g_unified_cache; // Forward declarations for global state extern sqlite3* g_db; extern int g_server_running; extern 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; ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // 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) { // Get Accept header char accept_header[256] = {0}; int header_len = lws_hdr_copy(wsi, accept_header, sizeof(accept_header) - 1, WSI_TOKEN_HTTP_ACCEPT); if (header_len > 0) { accept_header[header_len] = '\0'; // 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 - return 404 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) { log_error("Failed to allocate buffer for NIP-11 body transmission"); // Clean up session data free(session_data->json_buffer); free(session_data); lws_set_wsi_user(wsi, NULL); return -1; } // Copy JSON data to buffer memcpy(json_buf, 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) { log_error("Failed to write NIP-11 JSON body"); // Clean up session data free(session_data->json_buffer); free(session_data); lws_set_wsi_user(wsi, NULL); return -1; } // Mark body as sent and clean up session data session_data->body_sent = 1; free(session_data->json_buffer); free(session_data); lws_set_wsi_user(wsi, NULL); return 0; // Close connection after successful transmission } } else if (type == 1) { // Embedded file return handle_embedded_file_writeable(wsi); } } } break; case LWS_CALLBACK_ESTABLISHED: memset(pss, 0, sizeof(*pss)); pthread_mutex_init(&pss->session_lock, NULL); // Get real client IP address char client_ip[CLIENT_IP_MAX_LENGTH]; lws_get_peer_simple(wsi, client_ip, sizeof(client_ip)); // Ensure client_ip is null-terminated and copy safely client_ip[CLIENT_IP_MAX_LENGTH - 1] = '\0'; size_t ip_len = strlen(client_ip); size_t copy_len = (ip_len < CLIENT_IP_MAX_LENGTH - 1) ? ip_len : CLIENT_IP_MAX_LENGTH - 1; memcpy(pss->client_ip, client_ip, copy_len); pss->client_ip[copy_len] = '\0'; // Initialize NIP-42 authentication state pss->authenticated = 0; pss->nip42_auth_required_events = get_config_bool("nip42_auth_required_events", 0); pss->nip42_auth_required_subscriptions = get_config_bool("nip42_auth_required_subscriptions", 0); pss->auth_challenge_sent = 0; memset(pss->authenticated_pubkey, 0, sizeof(pss->authenticated_pubkey)); memset(pss->active_challenge, 0, sizeof(pss->active_challenge)); pss->challenge_created = 0; pss->challenge_expires = 0; break; case LWS_CALLBACK_RECEIVE: if (len > 0) { char *message = malloc(len + 1); if (message) { memcpy(message, in, len); message[len] = '\0'; // Parse JSON message (this is the normal program flow) cJSON* json = cJSON_Parse(message); if (json && cJSON_IsArray(json)) { // Get message type cJSON* type = cJSON_GetArrayItem(json, 0); if (type && cJSON_IsString(type)) { const char* msg_type = cJSON_GetStringValue(type); if (strcmp(msg_type, "EVENT") == 0) { // Extract event for kind-specific NIP-42 authentication check cJSON* event_obj = cJSON_GetArrayItem(json, 1); if (event_obj && cJSON_IsObject(event_obj)) { // Extract event kind for kind-specific NIP-42 authentication check cJSON* kind_obj = cJSON_GetObjectItem(event_obj, "kind"); int event_kind = kind_obj && cJSON_IsNumber(kind_obj) ? (int)cJSON_GetNumberValue(kind_obj) : -1; // Extract pubkey and event ID for debugging cJSON* pubkey_obj = cJSON_GetObjectItem(event_obj, "pubkey"); cJSON* id_obj = cJSON_GetObjectItem(event_obj, "id"); const char* event_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : "unknown"; const char* event_id = id_obj ? cJSON_GetStringValue(id_obj) : "unknown"; // 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_relay_pubkey_cached(); 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; } } } } } } 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); log_warning("Event rejected: NIP-42 authentication required for kind"); } cJSON_Delete(json); free(message); return 0; } // Check blacklist/whitelist rules regardless of NIP-42 auth settings // Blacklist should always be enforced if (event_pubkey) { // Forward declaration for auth rules checking function extern int check_database_auth_rules(const char *pubkey, const char *operation, const char *resource_hash); int auth_rules_result = check_database_auth_rules(event_pubkey, "event", NULL); if (auth_rules_result != 0) { // 0 = NOSTR_SUCCESS, non-zero = blocked char auth_rules_msg[256]; if (auth_rules_result == -101) { // NOSTR_ERROR_AUTH_REQUIRED snprintf(auth_rules_msg, sizeof(auth_rules_msg), "blocked: pubkey not authorized (blacklist/whitelist violation)"); } else { snprintf(auth_rules_msg, sizeof(auth_rules_msg), "blocked: authorization check failed (error %d)", auth_rules_result); } send_notice_message(wsi, auth_rules_msg); log_warning("Event rejected: blacklist/whitelist violation"); // Send OK response with false status cJSON* response = cJSON_CreateArray(); cJSON_AddItemToArray(response, cJSON_CreateString("OK")); cJSON_AddItemToArray(response, cJSON_CreateString(event_id)); cJSON_AddItemToArray(response, cJSON_CreateBool(0)); // false = rejected cJSON_AddItemToArray(response, cJSON_CreateString(auth_rules_msg)); char *response_str = cJSON_Print(response); if (response_str) { size_t response_len = strlen(response_str); unsigned char *buf = malloc(LWS_PRE + response_len); if (buf) { memcpy(buf + LWS_PRE, response_str, response_len); lws_write(wsi, buf + LWS_PRE, response_len, LWS_WRITE_TEXT); free(buf); } free(response_str); } cJSON_Delete(response); cJSON_Delete(json); free(message); return 0; } } } // Handle EVENT message cJSON* event = cJSON_GetArrayItem(json, 1); if (event && cJSON_IsObject(event)) { // Extract event JSON string for unified validator char *event_json_str = cJSON_Print(event); if (!event_json_str) { log_error("Failed to serialize event JSON for validation"); cJSON* error_response = cJSON_CreateArray(); cJSON_AddItemToArray(error_response, cJSON_CreateString("OK")); cJSON_AddItemToArray(error_response, cJSON_CreateString("unknown")); cJSON_AddItemToArray(error_response, cJSON_CreateBool(0)); cJSON_AddItemToArray(error_response, cJSON_CreateString("error: failed to process event")); char *error_str = cJSON_Print(error_response); if (error_str) { size_t error_len = strlen(error_str); unsigned char *buf = malloc(LWS_PRE + error_len); if (buf) { memcpy(buf + LWS_PRE, error_str, error_len); lws_write(wsi, buf + LWS_PRE, error_len, LWS_WRITE_TEXT); free(buf); } free(error_str); } cJSON_Delete(error_response); return 0; } // 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 unified cache int protected_events_enabled = g_unified_cache.nip70_protected_events_enabled; 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'; log_warning("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'; log_warning("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); // Log reception of Kind 23456 events if (event_kind == 23456) { } 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 log_warning("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]; snprintf(error_result_msg, sizeof(error_result_msg), "ERROR: Kind %d event processing failed: %s", event_kind, admin_error); log_error(error_result_msg); } } if (admin_result != 0) { log_error("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 log_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) { log_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) { log_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) { log_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) { log_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 { // Regular event - store in database and broadcast if (store_event(event) != 0) { log_error("Failed to store event in database"); result = -1; strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1); } else { // Broadcast event to matching persistent subscriptions broadcast_event_to_subscriptions(event); } } } else { // Event without valid kind - try normal storage log_warning("Event without valid kind - trying normal storage"); if (store_event(event) != 0) { log_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); unsigned char *buf = malloc(LWS_PRE + response_len); if (buf) { memcpy(buf + LWS_PRE, response_str, response_len); lws_write(wsi, buf + LWS_PRE, response_len, LWS_WRITE_TEXT); free(buf); } free(response_str); } cJSON_Delete(response); } } } else if (strcmp(msg_type, "REQ") == 0) { // Check NIP-42 authentication for REQ subscriptions if required if (pss && pss->nip42_auth_required_subscriptions && !pss->authenticated) { if (!pss->auth_challenge_sent) { send_nip42_auth_challenge(wsi, pss); } else { send_notice_message(wsi, "NIP-42 authentication required for subscriptions"); log_warning("REQ rejected: NIP-42 authentication required"); } cJSON_Delete(json); free(message); return 0; } // Handle REQ message cJSON* sub_id = cJSON_GetArrayItem(json, 1); if (sub_id && cJSON_IsString(sub_id)) { const char* subscription_id = cJSON_GetStringValue(sub_id); // Create array of filter objects from position 2 onwards cJSON* filters = cJSON_CreateArray(); int json_size = cJSON_GetArraySize(json); for (int i = 2; i < json_size; i++) { cJSON* filter = cJSON_GetArrayItem(json, i); if (filter) { cJSON_AddItemToArray(filters, cJSON_Duplicate(filter, 1)); } } handle_req_message(subscription_id, filters, wsi, pss); // Clean up the filters array we created cJSON_Delete(filters); // Send EOSE (End of Stored Events) cJSON* eose_response = cJSON_CreateArray(); cJSON_AddItemToArray(eose_response, cJSON_CreateString("EOSE")); cJSON_AddItemToArray(eose_response, cJSON_CreateString(subscription_id)); char *eose_str = cJSON_Print(eose_response); if (eose_str) { size_t eose_len = strlen(eose_str); unsigned char *buf = malloc(LWS_PRE + eose_len); if (buf) { memcpy(buf + LWS_PRE, eose_str, eose_len); lws_write(wsi, buf + LWS_PRE, eose_len, LWS_WRITE_TEXT); free(buf); } free(eose_str); } cJSON_Delete(eose_response); } } else if (strcmp(msg_type, "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"); log_warning("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)); } } 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); // Remove from global manager remove_subscription_from_manager(subscription_id, wsi); // Remove from session list if present if (pss) { pthread_mutex_lock(&pss->session_lock); struct subscription** current = &pss->subscriptions; while (*current) { if (strcmp((*current)->id, subscription_id) == 0) { struct subscription* to_remove = *current; *current = to_remove->session_next; pss->subscription_count--; break; } current = &((*current)->session_next); } pthread_mutex_unlock(&pss->session_lock); } // Subscription closed } } else if (strcmp(msg_type, "AUTH") == 0) { // Handle NIP-42 AUTH message if (cJSON_GetArraySize(json) >= 2) { cJSON* auth_payload = cJSON_GetArrayItem(json, 1); if (cJSON_IsString(auth_payload)) { // AUTH challenge response: ["AUTH", ] (unusual) handle_nip42_auth_challenge_response(wsi, pss, cJSON_GetStringValue(auth_payload)); } else if (cJSON_IsObject(auth_payload)) { // AUTH signed event: ["AUTH", ] (standard NIP-42) handle_nip42_auth_signed_event(wsi, pss, auth_payload); } else { send_notice_message(wsi, "Invalid AUTH message format"); log_warning("Received AUTH message with invalid payload type"); } } else { send_notice_message(wsi, "AUTH message requires payload"); log_warning("Received AUTH message without payload"); } } else { // Unknown message type char unknown_msg[128]; snprintf(unknown_msg, sizeof(unknown_msg), "Unknown message type: %.32s", msg_type); log_warning(unknown_msg); send_notice_message(wsi, "Unknown message type"); } } } if (json) cJSON_Delete(json); free(message); } } break; case LWS_CALLBACK_CLOSED: // Clean up session subscriptions if (pss) { pthread_mutex_lock(&pss->session_lock); struct subscription* sub = pss->subscriptions; while (sub) { struct subscription* next = sub->session_next; remove_subscription_from_manager(sub->id, wsi); sub = next; } pss->subscriptions = NULL; pss->subscription_count = 0; pthread_mutex_unlock(&pss->session_lock); pthread_mutex_destroy(&pss->session_lock); } break; default: break; } return 0; } // WebSocket protocol definition static struct lws_protocols protocols[] = { { "nostr-relay-protocol", nostr_relay_callback, sizeof(struct per_session_data), 4096, // rx buffer size 0, NULL, 0 }, { NULL, NULL, 0, 0, 0, NULL, 0 } // terminator }; // Check if a port is available for binding int check_port_available(int port) { int sockfd; struct sockaddr_in addr; int result; int reuse = 1; // Create a socket sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { return 0; // Cannot create socket, assume port unavailable } // Set SO_REUSEADDR to allow binding to ports in TIME_WAIT state // This matches libwebsockets behavior and prevents false unavailability if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) { close(sockfd); return 0; // Failed to set socket option } // Set up the address structure memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY; addr.sin_port = htons(port); // Try to bind to the port result = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)); // Close the socket close(sockfd); // Return 1 if bind succeeded (port available), 0 if failed (port in use) return (result == 0) ? 1 : 0; } // Start libwebsockets-based WebSocket Nostr relay server int start_websocket_relay(int port_override, int strict_port) { struct lws_context_creation_info info; // Starting libwebsockets-based Nostr relay server memset(&info, 0, sizeof(info)); // Use port override if provided, otherwise use configuration int configured_port = (port_override > 0) ? port_override : get_config_int("relay_port", DEFAULT_PORT); int actual_port = configured_port; int port_attempts = 0; const int max_port_attempts = 10; // Increased from 5 to 10 // Minimal libwebsockets configuration info.protocols = protocols; info.gid = -1; info.uid = -1; info.options = LWS_SERVER_OPTION_VALIDATE_UTF8; // Remove interface restrictions - let system choose // info.vhost_name = NULL; // info.iface = NULL; // Increase max connections for relay usage info.max_http_header_pool = 16; info.timeout_secs = 10; // Max payload size for Nostr events info.max_http_header_data = 4096; // Find an available port with pre-checking (or fail immediately in strict mode) while (port_attempts < (strict_port ? 1 : max_port_attempts)) { // 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); log_error(error_msg); return -1; } else if (port_attempts < max_port_attempts) { char retry_msg[256]; snprintf(retry_msg, sizeof(retry_msg), "Port %d is in use, trying port %d (attempt %d/%d)", actual_port, actual_port + 1, port_attempts + 1, max_port_attempts); log_warning(retry_msg); actual_port++; continue; } else { char error_msg[512]; snprintf(error_msg, sizeof(error_msg), "Failed to find available port after %d attempts (tried ports %d-%d)", max_port_attempts, configured_port, actual_port); log_error(error_msg); return -1; } } // Port appears available, try creating libwebsockets context info.port = actual_port; // 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); log_warning(lws_error_msg); port_attempts++; if (strict_port) { char error_msg[256]; snprintf(error_msg, sizeof(error_msg), "Strict port mode: failed to bind to port %d", actual_port); log_error(error_msg); break; } else if (port_attempts < max_port_attempts) { actual_port++; continue; } // If we get here, we've exhausted attempts break; } if (!ws_context) { char error_msg[512]; snprintf(error_msg, sizeof(error_msg), "Failed to create libwebsockets context after %d attempts. Last attempted port: %d", port_attempts, actual_port); log_error(error_msg); perror("libwebsockets creation error"); return -1; } char startup_msg[256]; if (actual_port != configured_port) { snprintf(startup_msg, sizeof(startup_msg), "WebSocket relay started on ws://127.0.0.1:%d (configured port %d was unavailable)", actual_port, configured_port); log_warning(startup_msg); } else { snprintf(startup_msg, sizeof(startup_msg), "WebSocket relay started on ws://127.0.0.1:%d", actual_port); } // Main event loop with proper signal handling while (g_server_running && !g_shutdown_flag) { int result = lws_service(ws_context, 1000); if (result < 0) { log_error("libwebsockets service error"); break; } } 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_relay_pubkey_cached(); 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_admin_pubkey_cached(); 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[4096]; 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)) { log_error("COUNT filters is not an array"); return 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)) { log_warning("Invalid filter object in COUNT"); continue; } // 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 = cJSON_GetArraySize(authors); 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++) { cJSON* author = cJSON_GetArrayItem(authors, a); if (cJSON_IsString(author)) { if (a > 0) { snprintf(sql_ptr, remaining, ","); sql_ptr++; remaining--; } snprintf(sql_ptr, remaining, "'%s'", cJSON_GetStringValue(author)); 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 ids filter cJSON* ids = cJSON_GetObjectItem(filter, "ids"); if (ids && cJSON_IsArray(ids)) { int id_count = cJSON_GetArraySize(ids); if (id_count > 0) { snprintf(sql_ptr, remaining, " AND id IN ("); sql_ptr += strlen(sql_ptr); remaining = sizeof(sql) - strlen(sql); for (int i = 0; i < id_count; i++) { cJSON* id = cJSON_GetArrayItem(ids, i); if (cJSON_IsString(id)) { if (i > 0) { snprintf(sql_ptr, remaining, ","); sql_ptr++; remaining--; } snprintf(sql_ptr, remaining, "'%s'", cJSON_GetStringValue(id)); sql_ptr += strlen(sql_ptr); remaining = sizeof(sql) - strlen(sql); } } snprintf(sql_ptr, remaining, ")"); sql_ptr += strlen(sql_ptr); remaining = sizeof(sql) - strlen(sql); } } // Handle 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 = cJSON_GetArraySize(filter_item); if (tag_value_count > 0) { // Use EXISTS with JSON extraction to check for matching tags snprintf(sql_ptr, remaining, " AND EXISTS (SELECT 1 FROM json_each(json(tags)) WHERE json_extract(value, '$[0]') = '%s' AND json_extract(value, '$[1]') IN (", tag_name); sql_ptr += strlen(sql_ptr); remaining = sizeof(sql) - strlen(sql); for (int i = 0; i < tag_value_count; i++) { cJSON* tag_value = cJSON_GetArrayItem(filter_item, i); if (cJSON_IsString(tag_value)) { if (i > 0) { snprintf(sql_ptr, remaining, ","); sql_ptr++; remaining--; } snprintf(sql_ptr, remaining, "'%s'", cJSON_GetStringValue(tag_value)); 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 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)); log_error(error_msg); continue; } int filter_count = 0; if (sqlite3_step(stmt) == SQLITE_ROW) { filter_count = sqlite3_column_int(stmt, 0); } // Filter count calculated sqlite3_finalize(stmt); total_count += filter_count; } // Total count calculated // Send COUNT response - NIP-45 format: ["COUNT", , {"count": }] cJSON* count_response = cJSON_CreateArray(); cJSON_AddItemToArray(count_response, cJSON_CreateString("COUNT")); cJSON_AddItemToArray(count_response, cJSON_CreateString(sub_id)); // Create count object as per NIP-45 specification cJSON* count_obj = cJSON_CreateObject(); cJSON_AddNumberToObject(count_obj, "count", total_count); cJSON_AddItemToArray(count_response, count_obj); char *count_str = cJSON_Print(count_response); if (count_str) { size_t count_len = strlen(count_str); unsigned char *buf = malloc(LWS_PRE + count_len); if (buf) { memcpy(buf + LWS_PRE, count_str, count_len); lws_write(wsi, buf + LWS_PRE, count_len, LWS_WRITE_TEXT); free(buf); } free(count_str); } cJSON_Delete(count_response); return total_count; }