#define _GNU_SOURCE #include #include #include #include #include #include #include #include #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 // Forward declarations for unified request validator int nostr_validate_unified_request(const char* json_string, size_t json_length); int ginxsom_request_validator_init(const char* db_path, const char* app_name); void ginxsom_request_validator_cleanup(void); // Forward declarations for NIP-42 functions from request_validator.c int nostr_nip42_generate_challenge(char *challenge_buffer, size_t buffer_size); int nostr_nip42_verify_auth_event(cJSON *event, const char *challenge_id, const char *relay_url, int time_tolerance_seconds); // Color constants for logging #define RED "\033[31m" #define GREEN "\033[32m" #define YELLOW "\033[33m" #define BLUE "\033[34m" #define BOLD "\033[1m" #define RESET "\033[0m" // Global state sqlite3* g_db = NULL; // Non-static so config.c can access it static int g_server_running = 1; static struct lws_context *ws_context = NULL; // NIP-11 relay information structure struct relay_info { char name[RELAY_NAME_MAX_LENGTH]; char description[RELAY_DESCRIPTION_MAX_LENGTH]; char banner[RELAY_URL_MAX_LENGTH]; char icon[RELAY_URL_MAX_LENGTH]; char pubkey[RELAY_PUBKEY_MAX_LENGTH]; char contact[RELAY_CONTACT_MAX_LENGTH]; char software[RELAY_URL_MAX_LENGTH]; char version[64]; char privacy_policy[RELAY_URL_MAX_LENGTH]; char terms_of_service[RELAY_URL_MAX_LENGTH]; cJSON* supported_nips; // Array of supported NIP numbers cJSON* limitation; // Server limitations object cJSON* retention; // Event retention policies array cJSON* relay_countries; // Array of country codes cJSON* language_tags; // Array of language tags cJSON* tags; // Array of content tags char posting_policy[RELAY_URL_MAX_LENGTH]; cJSON* fees; // Payment fee structure char payments_url[RELAY_URL_MAX_LENGTH]; }; // Global relay information instance moved to unified cache // static struct relay_info g_relay_info = {0}; // REMOVED - now in g_unified_cache.relay_info // NIP-13 PoW configuration structure struct pow_config { int enabled; // 0 = disabled, 1 = enabled int min_pow_difficulty; // Minimum required difficulty (0 = no requirement) int validation_flags; // Bitflags for validation options int require_nonce_tag; // 1 = require nonce tag presence int reject_lower_targets; // 1 = reject if committed < actual difficulty int strict_format; // 1 = enforce strict nonce tag format int anti_spam_mode; // 1 = full anti-spam validation }; // Global PoW configuration instance struct pow_config g_pow_config = { .enabled = 1, // Enable PoW validation by default .min_pow_difficulty = 0, // No minimum difficulty by default .validation_flags = NOSTR_POW_VALIDATE_BASIC, .require_nonce_tag = 0, // Don't require nonce tags by default .reject_lower_targets = 0, // Allow lower committed targets by default .strict_format = 0, // Relaxed format validation by default .anti_spam_mode = 0 // Basic validation by default }; // NIP-40 Expiration configuration structure struct expiration_config { int enabled; // 0 = disabled, 1 = enabled int strict_mode; // 1 = reject expired events on submission int filter_responses; // 1 = filter expired events from responses int delete_expired; // 1 = delete expired events from DB (future feature) long grace_period; // Grace period in seconds for clock skew }; // Global expiration configuration instance struct expiration_config g_expiration_config = { .enabled = 1, // Enable expiration handling by default .strict_mode = 1, // Reject expired events on submission by default .filter_responses = 1, // Filter expired events from responses by default .delete_expired = 0, // Don't delete by default (keep for audit) .grace_period = 1 // 1 second grace period for testing (was 300) }; ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // DATA STRUCTURES ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // Forward declarations typedef struct subscription_filter subscription_filter_t; typedef struct subscription subscription_t; typedef struct subscription_manager subscription_manager_t; // Subscription filter structure struct subscription_filter { // Filter criteria (all optional) cJSON* kinds; // Array of event kinds [1,2,3] cJSON* authors; // Array of author pubkeys cJSON* ids; // Array of event IDs long since; // Unix timestamp (0 = not set) long until; // Unix timestamp (0 = not set) int limit; // Result limit (0 = no limit) cJSON* tag_filters; // Object with tag filters: {"#e": ["id1"], "#p": ["pubkey1"]} // Linked list for multiple filters per subscription struct subscription_filter* next; }; // Active subscription structure struct subscription { char id[SUBSCRIPTION_ID_MAX_LENGTH]; // Subscription ID struct lws* wsi; // WebSocket connection handle subscription_filter_t* filters; // Linked list of filters (OR'd together) time_t created_at; // When subscription was created int events_sent; // Counter for sent events int active; // 1 = active, 0 = closed // Client info for logging char client_ip[CLIENT_IP_MAX_LENGTH]; // Client IP address // Linked list pointers struct subscription* next; // Next subscription globally struct subscription* session_next; // Next subscription for this session }; // Enhanced per-session data with subscription management and NIP-42 authentication struct per_session_data { int authenticated; subscription_t* subscriptions; // Head of this session's subscription list pthread_mutex_t session_lock; // Per-session thread safety char client_ip[CLIENT_IP_MAX_LENGTH]; // Client IP for logging int subscription_count; // Number of subscriptions for this session // NIP-42 Authentication State char authenticated_pubkey[65]; // Authenticated public key (64 hex + null) char active_challenge[65]; // Current challenge for this session (64 hex + null) time_t challenge_created; // When challenge was created time_t challenge_expires; // Challenge expiration time int nip42_auth_required_events; // Whether NIP-42 auth is required for EVENT submission int nip42_auth_required_subscriptions; // Whether NIP-42 auth is required for REQ operations int auth_challenge_sent; // Whether challenge has been sent (0/1) }; // Global subscription manager struct subscription_manager { subscription_t* active_subscriptions; // Head of global subscription list pthread_mutex_t subscriptions_lock; // Global thread safety int total_subscriptions; // Current count // Configuration int max_subscriptions_per_client; // Default: 20 int max_total_subscriptions; // Default: 5000 // Statistics uint64_t total_created; // Lifetime subscription count uint64_t total_events_broadcast; // Lifetime event broadcast count }; // Global subscription manager instance static subscription_manager_t g_subscription_manager = { .active_subscriptions = NULL, .subscriptions_lock = PTHREAD_MUTEX_INITIALIZER, .total_subscriptions = 0, .max_subscriptions_per_client = MAX_SUBSCRIPTIONS_PER_CLIENT, // Will be updated from config .max_total_subscriptions = MAX_TOTAL_SUBSCRIPTIONS, // Will be updated from config .total_created = 0, .total_events_broadcast = 0 }; // Forward declarations for logging functions void log_info(const char* message); void log_success(const char* message); void log_error(const char* message); void log_warning(const char* message); // Forward declaration for subscription manager configuration void update_subscription_manager_config(void); // Forward declarations for subscription database logging void log_subscription_created(const subscription_t* sub); void log_subscription_closed(const char* sub_id, const char* client_ip, const char* reason); void log_subscription_disconnected(const char* client_ip); void log_event_broadcast(const char* event_id, const char* sub_id, const char* client_ip); void update_subscription_events_sent(const char* sub_id, int events_sent); // Forward declarations for NIP-01 event handling const char* extract_d_tag_value(cJSON* tags); int check_and_handle_replaceable_event(int kind, const char* pubkey, long created_at); int check_and_handle_addressable_event(int kind, const char* pubkey, const char* d_tag_value, long created_at); int handle_event_message(cJSON* event, char* error_message, size_t error_size); // Forward declaration for unified validation int nostr_validate_unified_request(const char* json_string, size_t json_length); // Forward declaration for admin event processing (kinds 23455 and 23456) int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size, struct lws* wsi); // Forward declaration for enhanced admin event authorization int is_authorized_admin_event(cJSON* event, char* error_message, size_t error_size); // Forward declaration for NOTICE message support void send_notice_message(struct lws* wsi, const char* message); // Forward declarations for NIP-42 authentication functions 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-09 deletion request handling int handle_deletion_request(cJSON* event, char* error_message, size_t error_size); int delete_events_by_id(const char* requester_pubkey, cJSON* event_ids); int delete_events_by_address(const char* requester_pubkey, cJSON* addresses, long deletion_timestamp); int mark_event_as_deleted(const char* event_id, const char* deletion_event_id, const char* reason); // Forward declaration for database functions int store_event(cJSON* event); // Forward declarations for NIP-11 relay information handling void init_relay_info(); void cleanup_relay_info(); cJSON* generate_relay_info_json(); int handle_nip11_http_request(struct lws* wsi, const char* accept_header); // Forward declarations for NIP-13 PoW validation void init_pow_config(); int validate_event_pow(cJSON* event, char* error_message, size_t error_size); // Forward declarations for NIP-40 expiration handling void init_expiration_config(); long extract_expiration_timestamp(cJSON* tags); int is_event_expired(cJSON* event, time_t current_time); int validate_event_expiration(cJSON* event, char* error_message, size_t error_size); ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // PERSISTENT SUBSCRIPTIONS SYSTEM ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // Create a subscription filter from cJSON filter object subscription_filter_t* create_subscription_filter(cJSON* filter_json) { if (!filter_json || !cJSON_IsObject(filter_json)) { return NULL; } subscription_filter_t* filter = calloc(1, sizeof(subscription_filter_t)); if (!filter) { return NULL; } // Copy filter criteria cJSON* kinds = cJSON_GetObjectItem(filter_json, "kinds"); if (kinds && cJSON_IsArray(kinds)) { filter->kinds = cJSON_Duplicate(kinds, 1); } cJSON* authors = cJSON_GetObjectItem(filter_json, "authors"); if (authors && cJSON_IsArray(authors)) { filter->authors = cJSON_Duplicate(authors, 1); } cJSON* ids = cJSON_GetObjectItem(filter_json, "ids"); if (ids && cJSON_IsArray(ids)) { filter->ids = cJSON_Duplicate(ids, 1); } cJSON* since = cJSON_GetObjectItem(filter_json, "since"); if (since && cJSON_IsNumber(since)) { filter->since = (long)cJSON_GetNumberValue(since); } cJSON* until = cJSON_GetObjectItem(filter_json, "until"); if (until && cJSON_IsNumber(until)) { filter->until = (long)cJSON_GetNumberValue(until); } cJSON* limit = cJSON_GetObjectItem(filter_json, "limit"); if (limit && cJSON_IsNumber(limit)) { filter->limit = (int)cJSON_GetNumberValue(limit); } // Handle tag filters (e.g., {"#e": ["id1"], "#p": ["pubkey1"]}) cJSON* item = NULL; cJSON_ArrayForEach(item, filter_json) { if (item->string && strlen(item->string) >= 2 && item->string[0] == '#') { if (!filter->tag_filters) { filter->tag_filters = cJSON_CreateObject(); } if (filter->tag_filters) { cJSON_AddItemToObject(filter->tag_filters, item->string, cJSON_Duplicate(item, 1)); } } } return filter; } // Free a subscription filter void free_subscription_filter(subscription_filter_t* filter) { if (!filter) return; if (filter->kinds) cJSON_Delete(filter->kinds); if (filter->authors) cJSON_Delete(filter->authors); if (filter->ids) cJSON_Delete(filter->ids); if (filter->tag_filters) cJSON_Delete(filter->tag_filters); if (filter->next) { free_subscription_filter(filter->next); } free(filter); } // Create a new subscription subscription_t* create_subscription(const char* sub_id, struct lws* wsi, cJSON* filters_array, const char* client_ip) { if (!sub_id || !wsi || !filters_array) { return NULL; } subscription_t* sub = calloc(1, sizeof(subscription_t)); if (!sub) { return NULL; } // Copy subscription ID (truncate if too long) strncpy(sub->id, sub_id, SUBSCRIPTION_ID_MAX_LENGTH - 1); sub->id[SUBSCRIPTION_ID_MAX_LENGTH - 1] = '\0'; // Set WebSocket connection sub->wsi = wsi; // Set client IP if (client_ip) { strncpy(sub->client_ip, client_ip, CLIENT_IP_MAX_LENGTH - 1); sub->client_ip[CLIENT_IP_MAX_LENGTH - 1] = '\0'; } // Set timestamps and state sub->created_at = time(NULL); sub->events_sent = 0; sub->active = 1; // Convert filters array to linked list subscription_filter_t* filter_tail = NULL; int filter_count = 0; if (cJSON_IsArray(filters_array)) { cJSON* filter_json = NULL; cJSON_ArrayForEach(filter_json, filters_array) { if (filter_count >= MAX_FILTERS_PER_SUBSCRIPTION) { log_warning("Maximum filters per subscription exceeded, ignoring excess filters"); break; } subscription_filter_t* filter = create_subscription_filter(filter_json); if (filter) { if (!sub->filters) { sub->filters = filter; filter_tail = filter; } else { filter_tail->next = filter; filter_tail = filter; } filter_count++; } } } if (filter_count == 0) { log_error("No valid filters found for subscription"); free(sub); return NULL; } return sub; } // Free a subscription void free_subscription(subscription_t* sub) { if (!sub) return; if (sub->filters) { free_subscription_filter(sub->filters); } free(sub); } // Add subscription to global manager (thread-safe) int add_subscription_to_manager(subscription_t* sub) { if (!sub) return -1; pthread_mutex_lock(&g_subscription_manager.subscriptions_lock); // Check global limits if (g_subscription_manager.total_subscriptions >= g_subscription_manager.max_total_subscriptions) { pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock); log_error("Maximum total subscriptions reached"); return -1; } // Add to global list sub->next = g_subscription_manager.active_subscriptions; g_subscription_manager.active_subscriptions = sub; g_subscription_manager.total_subscriptions++; g_subscription_manager.total_created++; pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock); // Log subscription creation to database log_subscription_created(sub); char debug_msg[256]; snprintf(debug_msg, sizeof(debug_msg), "Added subscription '%s' (total: %d)", sub->id, g_subscription_manager.total_subscriptions); log_info(debug_msg); return 0; } // Remove subscription from global manager (thread-safe) int remove_subscription_from_manager(const char* sub_id, struct lws* wsi) { if (!sub_id) return -1; pthread_mutex_lock(&g_subscription_manager.subscriptions_lock); subscription_t** current = &g_subscription_manager.active_subscriptions; while (*current) { subscription_t* sub = *current; // Match by ID and WebSocket connection if (strcmp(sub->id, sub_id) == 0 && (!wsi || sub->wsi == wsi)) { // Remove from list *current = sub->next; g_subscription_manager.total_subscriptions--; pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock); // Log subscription closure to database log_subscription_closed(sub_id, sub->client_ip, "closed"); // Update events sent counter before freeing update_subscription_events_sent(sub_id, sub->events_sent); char debug_msg[256]; snprintf(debug_msg, sizeof(debug_msg), "Removed subscription '%s' (total: %d)", sub_id, g_subscription_manager.total_subscriptions); log_info(debug_msg); free_subscription(sub); return 0; } current = &(sub->next); } pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock); char debug_msg[256]; snprintf(debug_msg, sizeof(debug_msg), "Subscription '%s' not found for removal", sub_id); log_warning(debug_msg); return -1; } // Check if an event matches a subscription filter int event_matches_filter(cJSON* event, subscription_filter_t* filter) { if (!event || !filter) { return 0; } // Check kinds filter if (filter->kinds && cJSON_IsArray(filter->kinds)) { cJSON* event_kind = cJSON_GetObjectItem(event, "kind"); if (!event_kind || !cJSON_IsNumber(event_kind)) { return 0; } int event_kind_val = (int)cJSON_GetNumberValue(event_kind); int kind_match = 0; cJSON* kind_item = NULL; cJSON_ArrayForEach(kind_item, filter->kinds) { if (cJSON_IsNumber(kind_item) && (int)cJSON_GetNumberValue(kind_item) == event_kind_val) { kind_match = 1; break; } } if (!kind_match) { return 0; } } // Check authors filter if (filter->authors && cJSON_IsArray(filter->authors)) { cJSON* event_pubkey = cJSON_GetObjectItem(event, "pubkey"); if (!event_pubkey || !cJSON_IsString(event_pubkey)) { return 0; } const char* event_pubkey_str = cJSON_GetStringValue(event_pubkey); int author_match = 0; cJSON* author_item = NULL; cJSON_ArrayForEach(author_item, filter->authors) { if (cJSON_IsString(author_item)) { const char* author_str = cJSON_GetStringValue(author_item); // Support prefix matching (partial pubkeys) if (strncmp(event_pubkey_str, author_str, strlen(author_str)) == 0) { author_match = 1; break; } } } if (!author_match) { return 0; } } // Check IDs filter if (filter->ids && cJSON_IsArray(filter->ids)) { cJSON* event_id = cJSON_GetObjectItem(event, "id"); if (!event_id || !cJSON_IsString(event_id)) { return 0; } const char* event_id_str = cJSON_GetStringValue(event_id); int id_match = 0; cJSON* id_item = NULL; cJSON_ArrayForEach(id_item, filter->ids) { if (cJSON_IsString(id_item)) { const char* id_str = cJSON_GetStringValue(id_item); // Support prefix matching (partial IDs) if (strncmp(event_id_str, id_str, strlen(id_str)) == 0) { id_match = 1; break; } } } if (!id_match) { return 0; } } // Check since filter if (filter->since > 0) { cJSON* event_created_at = cJSON_GetObjectItem(event, "created_at"); if (!event_created_at || !cJSON_IsNumber(event_created_at)) { return 0; } long event_timestamp = (long)cJSON_GetNumberValue(event_created_at); if (event_timestamp < filter->since) { return 0; } } // Check until filter if (filter->until > 0) { cJSON* event_created_at = cJSON_GetObjectItem(event, "created_at"); if (!event_created_at || !cJSON_IsNumber(event_created_at)) { return 0; } long event_timestamp = (long)cJSON_GetNumberValue(event_created_at); if (event_timestamp > filter->until) { return 0; } } // Check tag filters (e.g., #e, #p tags) if (filter->tag_filters && cJSON_IsObject(filter->tag_filters)) { cJSON* event_tags = cJSON_GetObjectItem(event, "tags"); if (!event_tags || !cJSON_IsArray(event_tags)) { return 0; // Event has no tags but filter requires tags } // Check each tag filter cJSON* tag_filter = NULL; cJSON_ArrayForEach(tag_filter, filter->tag_filters) { if (!tag_filter->string || strlen(tag_filter->string) < 2 || tag_filter->string[0] != '#') { continue; // Invalid tag filter } const char* tag_name = tag_filter->string + 1; // Skip the '#' if (!cJSON_IsArray(tag_filter)) { continue; // Tag filter must be an array } int tag_match = 0; // Search through event tags for matching tag name and value cJSON* event_tag = NULL; cJSON_ArrayForEach(event_tag, event_tags) { if (!cJSON_IsArray(event_tag) || cJSON_GetArraySize(event_tag) < 2) { continue; // Invalid tag format } cJSON* event_tag_name = cJSON_GetArrayItem(event_tag, 0); cJSON* event_tag_value = cJSON_GetArrayItem(event_tag, 1); if (!cJSON_IsString(event_tag_name) || !cJSON_IsString(event_tag_value)) { continue; } // Check if tag name matches if (strcmp(cJSON_GetStringValue(event_tag_name), tag_name) == 0) { const char* event_tag_value_str = cJSON_GetStringValue(event_tag_value); // Check if any of the filter values match this tag value cJSON* filter_value = NULL; cJSON_ArrayForEach(filter_value, tag_filter) { if (cJSON_IsString(filter_value)) { const char* filter_value_str = cJSON_GetStringValue(filter_value); // Support prefix matching for tag values if (strncmp(event_tag_value_str, filter_value_str, strlen(filter_value_str)) == 0) { tag_match = 1; break; } } } if (tag_match) { break; } } } if (!tag_match) { return 0; // This tag filter didn't match, so the event doesn't match } } } return 1; // All filters passed } // Check if an event matches any filter in a subscription (filters are OR'd together) int event_matches_subscription(cJSON* event, subscription_t* subscription) { if (!event || !subscription || !subscription->filters) { return 0; } subscription_filter_t* filter = subscription->filters; while (filter) { if (event_matches_filter(event, filter)) { return 1; // Match found (OR logic) } filter = filter->next; } return 0; // No filters matched } // Broadcast event to all matching subscriptions (thread-safe) int broadcast_event_to_subscriptions(cJSON* event) { if (!event) { return 0; } // Check if event is expired and should not be broadcast (NIP-40) pthread_mutex_lock(&g_unified_cache.cache_lock); int expiration_enabled = g_unified_cache.expiration_config.enabled; int filter_responses = g_unified_cache.expiration_config.filter_responses; pthread_mutex_unlock(&g_unified_cache.cache_lock); if (expiration_enabled && filter_responses) { time_t current_time = time(NULL); if (is_event_expired(event, current_time)) { char debug_msg[256]; cJSON* event_id_obj = cJSON_GetObjectItem(event, "id"); const char* event_id = event_id_obj ? cJSON_GetStringValue(event_id_obj) : "unknown"; snprintf(debug_msg, sizeof(debug_msg), "Skipping broadcast of expired event: %.16s", event_id); log_info(debug_msg); return 0; // Don't broadcast expired events } } int broadcasts = 0; pthread_mutex_lock(&g_subscription_manager.subscriptions_lock); subscription_t* sub = g_subscription_manager.active_subscriptions; while (sub) { if (sub->active && event_matches_subscription(event, sub)) { // Create EVENT message for this subscription cJSON* event_msg = cJSON_CreateArray(); cJSON_AddItemToArray(event_msg, cJSON_CreateString("EVENT")); cJSON_AddItemToArray(event_msg, cJSON_CreateString(sub->id)); cJSON_AddItemToArray(event_msg, cJSON_Duplicate(event, 1)); char* msg_str = cJSON_Print(event_msg); if (msg_str) { size_t msg_len = strlen(msg_str); unsigned char* buf = malloc(LWS_PRE + msg_len); if (buf) { memcpy(buf + LWS_PRE, msg_str, msg_len); // Send to WebSocket connection int write_result = lws_write(sub->wsi, buf + LWS_PRE, msg_len, LWS_WRITE_TEXT); if (write_result >= 0) { sub->events_sent++; broadcasts++; // Log event broadcast to database (optional - can be disabled for performance) cJSON* event_id_obj = cJSON_GetObjectItem(event, "id"); if (event_id_obj && cJSON_IsString(event_id_obj)) { log_event_broadcast(cJSON_GetStringValue(event_id_obj), sub->id, sub->client_ip); } } free(buf); } free(msg_str); } cJSON_Delete(event_msg); } sub = sub->next; } // Update global statistics g_subscription_manager.total_events_broadcast += broadcasts; pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock); if (broadcasts > 0) { char debug_msg[256]; snprintf(debug_msg, sizeof(debug_msg), "Broadcasted event to %d subscriptions", broadcasts); log_info(debug_msg); } return broadcasts; } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // SUBSCRIPTION DATABASE LOGGING ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // Log subscription creation to database void log_subscription_created(const subscription_t* sub) { if (!g_db || !sub) return; // Create filter JSON for logging char* filter_json = NULL; if (sub->filters) { cJSON* filters_array = cJSON_CreateArray(); subscription_filter_t* filter = sub->filters; while (filter) { cJSON* filter_obj = cJSON_CreateObject(); if (filter->kinds) { cJSON_AddItemToObject(filter_obj, "kinds", cJSON_Duplicate(filter->kinds, 1)); } if (filter->authors) { cJSON_AddItemToObject(filter_obj, "authors", cJSON_Duplicate(filter->authors, 1)); } if (filter->ids) { cJSON_AddItemToObject(filter_obj, "ids", cJSON_Duplicate(filter->ids, 1)); } if (filter->since > 0) { cJSON_AddNumberToObject(filter_obj, "since", filter->since); } if (filter->until > 0) { cJSON_AddNumberToObject(filter_obj, "until", filter->until); } if (filter->limit > 0) { cJSON_AddNumberToObject(filter_obj, "limit", filter->limit); } if (filter->tag_filters) { cJSON* tags_obj = cJSON_Duplicate(filter->tag_filters, 1); cJSON* item = NULL; cJSON_ArrayForEach(item, tags_obj) { if (item->string) { cJSON_AddItemToObject(filter_obj, item->string, cJSON_Duplicate(item, 1)); } } cJSON_Delete(tags_obj); } cJSON_AddItemToArray(filters_array, filter_obj); filter = filter->next; } filter_json = cJSON_Print(filters_array); cJSON_Delete(filters_array); } const char* sql = "INSERT INTO subscription_events (subscription_id, client_ip, event_type, filter_json) " "VALUES (?, ?, 'created', ?)"; sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_text(stmt, 1, sub->id, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, sub->client_ip, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 3, filter_json ? filter_json : "[]", -1, SQLITE_TRANSIENT); sqlite3_step(stmt); sqlite3_finalize(stmt); } if (filter_json) free(filter_json); } // Log subscription closure to database void log_subscription_closed(const char* sub_id, const char* client_ip, const char* reason) { (void)reason; // Mark as intentionally unused if (!g_db || !sub_id) return; const char* sql = "INSERT INTO subscription_events (subscription_id, client_ip, event_type) " "VALUES (?, ?, 'closed')"; sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_text(stmt, 1, sub_id, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, client_ip ? client_ip : "unknown", -1, SQLITE_STATIC); sqlite3_step(stmt); sqlite3_finalize(stmt); } // Update the corresponding 'created' entry with end time and events sent const char* update_sql = "UPDATE subscription_events " "SET ended_at = strftime('%s', 'now') " "WHERE subscription_id = ? AND event_type = 'created' AND ended_at IS NULL"; rc = sqlite3_prepare_v2(g_db, update_sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_text(stmt, 1, sub_id, -1, SQLITE_STATIC); sqlite3_step(stmt); sqlite3_finalize(stmt); } } // Log subscription disconnection to database void log_subscription_disconnected(const char* client_ip) { if (!g_db || !client_ip) return; // Mark all active subscriptions for this client as disconnected const char* sql = "UPDATE subscription_events " "SET ended_at = strftime('%s', 'now') " "WHERE client_ip = ? AND event_type = 'created' AND ended_at IS NULL"; sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_text(stmt, 1, client_ip, -1, SQLITE_STATIC); int changes = sqlite3_changes(g_db); sqlite3_step(stmt); sqlite3_finalize(stmt); if (changes > 0) { // Log a disconnection event const char* insert_sql = "INSERT INTO subscription_events (subscription_id, client_ip, event_type) " "VALUES ('disconnect', ?, 'disconnected')"; rc = sqlite3_prepare_v2(g_db, insert_sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_text(stmt, 1, client_ip, -1, SQLITE_STATIC); sqlite3_step(stmt); sqlite3_finalize(stmt); } } } } // Log event broadcast to database (optional, can be resource intensive) void log_event_broadcast(const char* event_id, const char* sub_id, const char* client_ip) { if (!g_db || !event_id || !sub_id || !client_ip) return; const char* sql = "INSERT INTO event_broadcasts (event_id, subscription_id, client_ip) " "VALUES (?, ?, ?)"; sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, sub_id, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 3, client_ip, -1, SQLITE_STATIC); sqlite3_step(stmt); sqlite3_finalize(stmt); } } // Update events sent counter for a subscription void update_subscription_events_sent(const char* sub_id, int events_sent) { if (!g_db || !sub_id) return; const char* sql = "UPDATE subscription_events " "SET events_sent = ? " "WHERE subscription_id = ? AND event_type = 'created'"; sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_int(stmt, 1, events_sent); sqlite3_bind_text(stmt, 2, sub_id, -1, SQLITE_STATIC); sqlite3_step(stmt); sqlite3_finalize(stmt); } } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // LOGGING FUNCTIONS ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // Helper function to get current timestamp string static void get_timestamp_string(char* buffer, size_t buffer_size) { time_t now = time(NULL); struct tm* local_time = localtime(&now); strftime(buffer, buffer_size, "%Y-%m-%d %H:%M:%S", local_time); } // Logging functions void log_info(const char* message) { char timestamp[32]; get_timestamp_string(timestamp, sizeof(timestamp)); printf("[%s] [INFO] %s\n", timestamp, message); fflush(stdout); } void log_success(const char* message) { char timestamp[32]; get_timestamp_string(timestamp, sizeof(timestamp)); printf("[%s] [SUCCESS] %s\n", timestamp, message); fflush(stdout); } void log_error(const char* message) { char timestamp[32]; get_timestamp_string(timestamp, sizeof(timestamp)); printf("[%s] [ERROR] %s\n", timestamp, message); fflush(stdout); } void log_warning(const char* message) { char timestamp[32]; get_timestamp_string(timestamp, sizeof(timestamp)); printf("[%s] [WARNING] %s\n", timestamp, message); fflush(stdout); } // Update subscription manager configuration from config system void update_subscription_manager_config(void) { g_subscription_manager.max_subscriptions_per_client = get_config_int("max_subscriptions_per_client", MAX_SUBSCRIPTIONS_PER_CLIENT); g_subscription_manager.max_total_subscriptions = get_config_int("max_total_subscriptions", MAX_TOTAL_SUBSCRIPTIONS); char config_msg[256]; snprintf(config_msg, sizeof(config_msg), "Subscription limits: max_per_client=%d, max_total=%d", g_subscription_manager.max_subscriptions_per_client, g_subscription_manager.max_total_subscriptions); log_info(config_msg); } // Signal handler for graceful shutdown void signal_handler(int sig) { if (sig == SIGINT || sig == SIGTERM) { log_info("Received shutdown signal"); g_server_running = 0; } } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // NOTICE MESSAGE SUPPORT ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // Send NOTICE message to client (NIP-01) void send_notice_message(struct lws* wsi, const char* message) { if (!wsi || !message) return; cJSON* notice_msg = cJSON_CreateArray(); cJSON_AddItemToArray(notice_msg, cJSON_CreateString("NOTICE")); cJSON_AddItemToArray(notice_msg, cJSON_CreateString(message)); char* msg_str = cJSON_Print(notice_msg); if (msg_str) { size_t msg_len = strlen(msg_str); unsigned char* buf = malloc(LWS_PRE + msg_len); if (buf) { memcpy(buf + LWS_PRE, msg_str, msg_len); lws_write(wsi, buf + LWS_PRE, msg_len, LWS_WRITE_TEXT); free(buf); } free(msg_str); } cJSON_Delete(notice_msg); } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // NIP-42 AUTHENTICATION FUNCTIONS ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // Send NIP-42 authentication challenge to client void send_nip42_auth_challenge(struct lws* wsi, struct per_session_data* pss) { if (!wsi || !pss) return; // Generate challenge using existing request_validator function char challenge[65]; if (nostr_nip42_generate_challenge(challenge, sizeof(challenge)) != 0) { log_error("Failed to generate NIP-42 challenge"); send_notice_message(wsi, "Authentication temporarily unavailable"); return; } // Store challenge in session pthread_mutex_lock(&pss->session_lock); strncpy(pss->active_challenge, challenge, sizeof(pss->active_challenge) - 1); pss->active_challenge[sizeof(pss->active_challenge) - 1] = '\0'; pss->challenge_created = time(NULL); pss->challenge_expires = pss->challenge_created + 600; // 10 minutes pss->auth_challenge_sent = 1; pthread_mutex_unlock(&pss->session_lock); // Send AUTH challenge message: ["AUTH", ] cJSON* auth_msg = cJSON_CreateArray(); cJSON_AddItemToArray(auth_msg, cJSON_CreateString("AUTH")); cJSON_AddItemToArray(auth_msg, cJSON_CreateString(challenge)); char* msg_str = cJSON_Print(auth_msg); if (msg_str) { size_t msg_len = strlen(msg_str); unsigned char* buf = malloc(LWS_PRE + msg_len); if (buf) { memcpy(buf + LWS_PRE, msg_str, msg_len); lws_write(wsi, buf + LWS_PRE, msg_len, LWS_WRITE_TEXT); free(buf); } free(msg_str); } cJSON_Delete(auth_msg); char debug_msg[128]; snprintf(debug_msg, sizeof(debug_msg), "NIP-42 auth challenge sent: %.16s...", challenge); log_info(debug_msg); } // Handle NIP-42 signed authentication event from client void handle_nip42_auth_signed_event(struct lws* wsi, struct per_session_data* pss, cJSON* auth_event) { if (!wsi || !pss || !auth_event) return; // Serialize event for validation char* event_json = cJSON_Print(auth_event); if (!event_json) { send_notice_message(wsi, "Invalid authentication event format"); return; } pthread_mutex_lock(&pss->session_lock); char challenge_copy[65]; strncpy(challenge_copy, pss->active_challenge, sizeof(challenge_copy) - 1); challenge_copy[sizeof(challenge_copy) - 1] = '\0'; time_t challenge_expires = pss->challenge_expires; pthread_mutex_unlock(&pss->session_lock); // Check if challenge has expired time_t current_time = time(NULL); if (current_time > challenge_expires) { free(event_json); send_notice_message(wsi, "Authentication challenge expired, please retry"); log_warning("NIP-42 authentication failed: challenge expired"); return; } // Verify authentication using existing request_validator function // Note: nostr_nip42_verify_auth_event doesn't extract pubkey, we need to do that separately int result = nostr_nip42_verify_auth_event(auth_event, challenge_copy, "ws://localhost:8888", 600); // 10 minutes tolerance char authenticated_pubkey[65] = {0}; if (result == 0) { // Extract pubkey from the auth event cJSON* pubkey_json = cJSON_GetObjectItem(auth_event, "pubkey"); if (pubkey_json && cJSON_IsString(pubkey_json)) { const char* pubkey_str = cJSON_GetStringValue(pubkey_json); if (pubkey_str && strlen(pubkey_str) == 64) { strncpy(authenticated_pubkey, pubkey_str, sizeof(authenticated_pubkey) - 1); authenticated_pubkey[sizeof(authenticated_pubkey) - 1] = '\0'; } else { result = -1; // Invalid pubkey format } } else { result = -1; // Missing pubkey } } free(event_json); if (result == 0) { // Authentication successful pthread_mutex_lock(&pss->session_lock); pss->authenticated = 1; strncpy(pss->authenticated_pubkey, authenticated_pubkey, sizeof(pss->authenticated_pubkey) - 1); pss->authenticated_pubkey[sizeof(pss->authenticated_pubkey) - 1] = '\0'; // Clear challenge memset(pss->active_challenge, 0, sizeof(pss->active_challenge)); pss->challenge_expires = 0; pss->auth_challenge_sent = 0; pthread_mutex_unlock(&pss->session_lock); char success_msg[256]; snprintf(success_msg, sizeof(success_msg), "NIP-42 authentication successful for pubkey: %.16s...", authenticated_pubkey); log_success(success_msg); send_notice_message(wsi, "NIP-42 authentication successful"); } else { // Authentication failed char error_msg[256]; snprintf(error_msg, sizeof(error_msg), "NIP-42 authentication failed (error code: %d)", result); log_warning(error_msg); send_notice_message(wsi, "NIP-42 authentication failed - invalid signature or challenge"); } } // Handle challenge response (not typically used in NIP-42, but included for completeness) void handle_nip42_auth_challenge_response(struct lws* wsi, struct per_session_data* pss, const char* challenge) { (void)wsi; (void)pss; (void)challenge; // Mark as intentionally unused // NIP-42 doesn't typically use challenge responses from client to server // This is reserved for potential future use or protocol extensions log_warning("Received unexpected challenge response from client (not part of standard NIP-42 flow)"); send_notice_message(wsi, "Challenge responses are not supported - please send signed authentication event"); } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // NIP-09 EVENT DELETION REQUEST HANDLING ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // Handle NIP-09 deletion request event (kind 5) int handle_deletion_request(cJSON* event, char* error_message, size_t error_size) { if (!event) { snprintf(error_message, error_size, "invalid: null deletion request"); return -1; } // Extract event details cJSON* kind_obj = cJSON_GetObjectItem(event, "kind"); cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey"); cJSON* created_at_obj = cJSON_GetObjectItem(event, "created_at"); cJSON* tags_obj = cJSON_GetObjectItem(event, "tags"); cJSON* content_obj = cJSON_GetObjectItem(event, "content"); cJSON* event_id_obj = cJSON_GetObjectItem(event, "id"); if (!kind_obj || !pubkey_obj || !created_at_obj || !tags_obj || !event_id_obj) { snprintf(error_message, error_size, "invalid: incomplete deletion request"); return -1; } int kind = (int)cJSON_GetNumberValue(kind_obj); if (kind != 5) { snprintf(error_message, error_size, "invalid: not a deletion request"); return -1; } const char* requester_pubkey = cJSON_GetStringValue(pubkey_obj); // Extract deletion event ID and reason (for potential logging) const char* deletion_event_id = cJSON_GetStringValue(event_id_obj); const char* reason = content_obj ? cJSON_GetStringValue(content_obj) : ""; (void)deletion_event_id; // Mark as intentionally unused for now (void)reason; // Mark as intentionally unused for now long deletion_timestamp = (long)cJSON_GetNumberValue(created_at_obj); if (!cJSON_IsArray(tags_obj)) { snprintf(error_message, error_size, "invalid: deletion request tags must be an array"); return -1; } // Collect event IDs and addresses from tags cJSON* event_ids = cJSON_CreateArray(); cJSON* addresses = cJSON_CreateArray(); cJSON* kinds_to_delete = cJSON_CreateArray(); int deletion_targets_found = 0; cJSON* tag = NULL; cJSON_ArrayForEach(tag, tags_obj) { if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) { continue; } cJSON* tag_name = cJSON_GetArrayItem(tag, 0); cJSON* tag_value = cJSON_GetArrayItem(tag, 1); if (!cJSON_IsString(tag_name) || !cJSON_IsString(tag_value)) { continue; } const char* name = cJSON_GetStringValue(tag_name); const char* value = cJSON_GetStringValue(tag_value); if (strcmp(name, "e") == 0) { // Event ID reference cJSON_AddItemToArray(event_ids, cJSON_CreateString(value)); deletion_targets_found++; } else if (strcmp(name, "a") == 0) { // Addressable event reference (kind:pubkey:d-identifier) cJSON_AddItemToArray(addresses, cJSON_CreateString(value)); deletion_targets_found++; } else if (strcmp(name, "k") == 0) { // Kind hint - store for validation but not required int kind_hint = atoi(value); if (kind_hint > 0) { cJSON_AddItemToArray(kinds_to_delete, cJSON_CreateNumber(kind_hint)); } } } if (deletion_targets_found == 0) { cJSON_Delete(event_ids); cJSON_Delete(addresses); cJSON_Delete(kinds_to_delete); snprintf(error_message, error_size, "invalid: deletion request must contain 'e' or 'a' tags"); return -1; } int deleted_count = 0; // Process event ID deletions if (cJSON_GetArraySize(event_ids) > 0) { int result = delete_events_by_id(requester_pubkey, event_ids); if (result > 0) { deleted_count += result; } } // Process addressable event deletions if (cJSON_GetArraySize(addresses) > 0) { int result = delete_events_by_address(requester_pubkey, addresses, deletion_timestamp); if (result > 0) { deleted_count += result; } } // Clean up cJSON_Delete(event_ids); cJSON_Delete(addresses); cJSON_Delete(kinds_to_delete); // Store the deletion request itself (it should be kept according to NIP-09) if (store_event(event) != 0) { log_warning("Failed to store deletion request event"); } char debug_msg[256]; snprintf(debug_msg, sizeof(debug_msg), "Deletion request processed: %d events deleted", deleted_count); log_info(debug_msg); error_message[0] = '\0'; // Success - empty error message return 0; } // Delete events by ID (with pubkey authorization) int delete_events_by_id(const char* requester_pubkey, cJSON* event_ids) { if (!g_db || !requester_pubkey || !event_ids || !cJSON_IsArray(event_ids)) { return 0; } int deleted_count = 0; cJSON* event_id = NULL; cJSON_ArrayForEach(event_id, event_ids) { if (!cJSON_IsString(event_id)) { continue; } const char* id = cJSON_GetStringValue(event_id); // First check if event exists and if requester is authorized const char* check_sql = "SELECT pubkey FROM events WHERE id = ?"; sqlite3_stmt* check_stmt; int rc = sqlite3_prepare_v2(g_db, check_sql, -1, &check_stmt, NULL); if (rc != SQLITE_OK) { continue; } sqlite3_bind_text(check_stmt, 1, id, -1, SQLITE_STATIC); if (sqlite3_step(check_stmt) == SQLITE_ROW) { const char* event_pubkey = (char*)sqlite3_column_text(check_stmt, 0); // Only delete if the requester is the author if (event_pubkey && strcmp(event_pubkey, requester_pubkey) == 0) { sqlite3_finalize(check_stmt); // Delete the event const char* delete_sql = "DELETE FROM events WHERE id = ? AND pubkey = ?"; sqlite3_stmt* delete_stmt; rc = sqlite3_prepare_v2(g_db, delete_sql, -1, &delete_stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_text(delete_stmt, 1, id, -1, SQLITE_STATIC); sqlite3_bind_text(delete_stmt, 2, requester_pubkey, -1, SQLITE_STATIC); if (sqlite3_step(delete_stmt) == SQLITE_DONE && sqlite3_changes(g_db) > 0) { deleted_count++; char debug_msg[128]; snprintf(debug_msg, sizeof(debug_msg), "Deleted event by ID: %.16s...", id); log_info(debug_msg); } sqlite3_finalize(delete_stmt); } } else { sqlite3_finalize(check_stmt); char warning_msg[128]; snprintf(warning_msg, sizeof(warning_msg), "Unauthorized deletion attempt for event: %.16s...", id); log_warning(warning_msg); } } else { sqlite3_finalize(check_stmt); char debug_msg[128]; snprintf(debug_msg, sizeof(debug_msg), "Event not found for deletion: %.16s...", id); log_info(debug_msg); } } return deleted_count; } // Delete events by addressable reference (kind:pubkey:d-identifier) int delete_events_by_address(const char* requester_pubkey, cJSON* addresses, long deletion_timestamp) { if (!g_db || !requester_pubkey || !addresses || !cJSON_IsArray(addresses)) { return 0; } int deleted_count = 0; cJSON* address = NULL; cJSON_ArrayForEach(address, addresses) { if (!cJSON_IsString(address)) { continue; } const char* addr = cJSON_GetStringValue(address); // Parse address format: kind:pubkey:d-identifier char* addr_copy = strdup(addr); if (!addr_copy) continue; char* kind_str = strtok(addr_copy, ":"); char* pubkey_str = strtok(NULL, ":"); char* d_identifier = strtok(NULL, ":"); if (!kind_str || !pubkey_str) { free(addr_copy); continue; } int kind = atoi(kind_str); // Only delete if the requester is the author if (strcmp(pubkey_str, requester_pubkey) != 0) { free(addr_copy); char warning_msg[128]; snprintf(warning_msg, sizeof(warning_msg), "Unauthorized deletion attempt for address: %.32s...", addr); log_warning(warning_msg); continue; } // Build deletion query based on whether we have d-identifier const char* delete_sql; sqlite3_stmt* delete_stmt; if (d_identifier && strlen(d_identifier) > 0) { // Delete specific addressable event with d-tag delete_sql = "DELETE FROM events WHERE kind = ? AND pubkey = ? AND created_at <= ? " "AND json_extract(tags, '$[*]') LIKE '%[\"d\",\"' || ? || '\"]%'"; } else { // Delete all events of this kind by this author up to deletion timestamp delete_sql = "DELETE FROM events WHERE kind = ? AND pubkey = ? AND created_at <= ?"; } int rc = sqlite3_prepare_v2(g_db, delete_sql, -1, &delete_stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_int(delete_stmt, 1, kind); sqlite3_bind_text(delete_stmt, 2, requester_pubkey, -1, SQLITE_STATIC); sqlite3_bind_int64(delete_stmt, 3, deletion_timestamp); if (d_identifier && strlen(d_identifier) > 0) { sqlite3_bind_text(delete_stmt, 4, d_identifier, -1, SQLITE_STATIC); } if (sqlite3_step(delete_stmt) == SQLITE_DONE) { int changes = sqlite3_changes(g_db); if (changes > 0) { deleted_count += changes; char debug_msg[128]; snprintf(debug_msg, sizeof(debug_msg), "Deleted %d events by address: %.32s...", changes, addr); log_info(debug_msg); } } sqlite3_finalize(delete_stmt); } free(addr_copy); } return deleted_count; } // Mark event as deleted (alternative to hard deletion - not used in current implementation) int mark_event_as_deleted(const char* event_id, const char* deletion_event_id, const char* reason) { (void)event_id; (void)deletion_event_id; (void)reason; // Suppress unused warnings // This function could be used if we wanted to implement soft deletion // For now, NIP-09 implementation uses hard deletion as specified return 0; } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // NIP-11 RELAY INFORMATION DOCUMENT ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // Initialize relay information using configuration system void init_relay_info() { // Get all config values first (without holding mutex to avoid deadlock) const char* relay_name = get_config_value("relay_name"); const char* relay_description = get_config_value("relay_description"); const char* relay_software = get_config_value("relay_software"); const char* relay_version = get_config_value("relay_version"); const char* relay_contact = get_config_value("relay_contact"); const char* relay_pubkey = get_config_value("relay_pubkey"); // Get config values for limitations int max_message_length = get_config_int("max_message_length", 16384); int max_subscriptions_per_client = get_config_int("max_subscriptions_per_client", 20); int max_limit = get_config_int("max_limit", 5000); int max_event_tags = get_config_int("max_event_tags", 100); int max_content_length = get_config_int("max_content_length", 8196); int default_limit = get_config_int("default_limit", 500); int admin_enabled = get_config_bool("admin_enabled", 0); pthread_mutex_lock(&g_unified_cache.cache_lock); // Update relay information fields if (relay_name) { strncpy(g_unified_cache.relay_info.name, relay_name, sizeof(g_unified_cache.relay_info.name) - 1); } else { strncpy(g_unified_cache.relay_info.name, "C Nostr Relay", sizeof(g_unified_cache.relay_info.name) - 1); } if (relay_description) { strncpy(g_unified_cache.relay_info.description, relay_description, sizeof(g_unified_cache.relay_info.description) - 1); } else { strncpy(g_unified_cache.relay_info.description, "A high-performance Nostr relay implemented in C with SQLite storage", sizeof(g_unified_cache.relay_info.description) - 1); } if (relay_software) { strncpy(g_unified_cache.relay_info.software, relay_software, sizeof(g_unified_cache.relay_info.software) - 1); } else { strncpy(g_unified_cache.relay_info.software, "https://git.laantungir.net/laantungir/c-relay.git", sizeof(g_unified_cache.relay_info.software) - 1); } if (relay_version) { strncpy(g_unified_cache.relay_info.version, relay_version, sizeof(g_unified_cache.relay_info.version) - 1); } else { strncpy(g_unified_cache.relay_info.version, "0.2.0", sizeof(g_unified_cache.relay_info.version) - 1); } if (relay_contact) { strncpy(g_unified_cache.relay_info.contact, relay_contact, sizeof(g_unified_cache.relay_info.contact) - 1); } if (relay_pubkey) { strncpy(g_unified_cache.relay_info.pubkey, relay_pubkey, sizeof(g_unified_cache.relay_info.pubkey) - 1); } // Initialize supported NIPs array g_unified_cache.relay_info.supported_nips = cJSON_CreateArray(); if (g_unified_cache.relay_info.supported_nips) { cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(1)); // NIP-01: Basic protocol cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(9)); // NIP-09: Event deletion cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(11)); // NIP-11: Relay information cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(13)); // NIP-13: Proof of Work cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(15)); // NIP-15: EOSE cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(20)); // NIP-20: Command results cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(40)); // NIP-40: Expiration Timestamp cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(42)); // NIP-42: Authentication } // Initialize server limitations using configuration g_unified_cache.relay_info.limitation = cJSON_CreateObject(); if (g_unified_cache.relay_info.limitation) { cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_message_length", max_message_length); cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_subscriptions", max_subscriptions_per_client); cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_limit", max_limit); cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_subid_length", SUBSCRIPTION_ID_MAX_LENGTH); cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_event_tags", max_event_tags); cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_content_length", max_content_length); cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "min_pow_difficulty", g_unified_cache.pow_config.min_pow_difficulty); cJSON_AddBoolToObject(g_unified_cache.relay_info.limitation, "auth_required", admin_enabled ? cJSON_True : cJSON_False); cJSON_AddBoolToObject(g_unified_cache.relay_info.limitation, "payment_required", cJSON_False); cJSON_AddBoolToObject(g_unified_cache.relay_info.limitation, "restricted_writes", cJSON_False); cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "created_at_lower_limit", 0); cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "created_at_upper_limit", 2147483647); cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "default_limit", default_limit); } // Initialize empty retention policies (can be configured later) g_unified_cache.relay_info.retention = cJSON_CreateArray(); // Initialize language tags - set to global for now g_unified_cache.relay_info.language_tags = cJSON_CreateArray(); if (g_unified_cache.relay_info.language_tags) { cJSON_AddItemToArray(g_unified_cache.relay_info.language_tags, cJSON_CreateString("*")); } // Initialize relay countries - set to global for now g_unified_cache.relay_info.relay_countries = cJSON_CreateArray(); if (g_unified_cache.relay_info.relay_countries) { cJSON_AddItemToArray(g_unified_cache.relay_info.relay_countries, cJSON_CreateString("*")); } // Initialize content tags as empty array g_unified_cache.relay_info.tags = cJSON_CreateArray(); // Initialize fees as empty object (no payment required by default) g_unified_cache.relay_info.fees = cJSON_CreateObject(); pthread_mutex_unlock(&g_unified_cache.cache_lock); log_success("Relay information initialized with default values"); } // Clean up relay information JSON objects void cleanup_relay_info() { pthread_mutex_lock(&g_unified_cache.cache_lock); if (g_unified_cache.relay_info.supported_nips) { cJSON_Delete(g_unified_cache.relay_info.supported_nips); g_unified_cache.relay_info.supported_nips = NULL; } if (g_unified_cache.relay_info.limitation) { cJSON_Delete(g_unified_cache.relay_info.limitation); g_unified_cache.relay_info.limitation = NULL; } if (g_unified_cache.relay_info.retention) { cJSON_Delete(g_unified_cache.relay_info.retention); g_unified_cache.relay_info.retention = NULL; } if (g_unified_cache.relay_info.language_tags) { cJSON_Delete(g_unified_cache.relay_info.language_tags); g_unified_cache.relay_info.language_tags = NULL; } if (g_unified_cache.relay_info.relay_countries) { cJSON_Delete(g_unified_cache.relay_info.relay_countries); g_unified_cache.relay_info.relay_countries = NULL; } if (g_unified_cache.relay_info.tags) { cJSON_Delete(g_unified_cache.relay_info.tags); g_unified_cache.relay_info.tags = NULL; } if (g_unified_cache.relay_info.fees) { cJSON_Delete(g_unified_cache.relay_info.fees); g_unified_cache.relay_info.fees = NULL; } pthread_mutex_unlock(&g_unified_cache.cache_lock); } // Generate NIP-11 compliant JSON document cJSON* generate_relay_info_json() { cJSON* info = cJSON_CreateObject(); if (!info) { log_error("Failed to create relay info JSON object"); return NULL; } pthread_mutex_lock(&g_unified_cache.cache_lock); // Add basic relay information if (strlen(g_unified_cache.relay_info.name) > 0) { cJSON_AddStringToObject(info, "name", g_unified_cache.relay_info.name); } if (strlen(g_unified_cache.relay_info.description) > 0) { cJSON_AddStringToObject(info, "description", g_unified_cache.relay_info.description); } if (strlen(g_unified_cache.relay_info.banner) > 0) { cJSON_AddStringToObject(info, "banner", g_unified_cache.relay_info.banner); } if (strlen(g_unified_cache.relay_info.icon) > 0) { cJSON_AddStringToObject(info, "icon", g_unified_cache.relay_info.icon); } if (strlen(g_unified_cache.relay_info.pubkey) > 0) { cJSON_AddStringToObject(info, "pubkey", g_unified_cache.relay_info.pubkey); } if (strlen(g_unified_cache.relay_info.contact) > 0) { cJSON_AddStringToObject(info, "contact", g_unified_cache.relay_info.contact); } // Add supported NIPs if (g_unified_cache.relay_info.supported_nips) { cJSON_AddItemToObject(info, "supported_nips", cJSON_Duplicate(g_unified_cache.relay_info.supported_nips, 1)); } // Add software information if (strlen(g_unified_cache.relay_info.software) > 0) { cJSON_AddStringToObject(info, "software", g_unified_cache.relay_info.software); } if (strlen(g_unified_cache.relay_info.version) > 0) { cJSON_AddStringToObject(info, "version", g_unified_cache.relay_info.version); } // Add policies if (strlen(g_unified_cache.relay_info.privacy_policy) > 0) { cJSON_AddStringToObject(info, "privacy_policy", g_unified_cache.relay_info.privacy_policy); } if (strlen(g_unified_cache.relay_info.terms_of_service) > 0) { cJSON_AddStringToObject(info, "terms_of_service", g_unified_cache.relay_info.terms_of_service); } if (strlen(g_unified_cache.relay_info.posting_policy) > 0) { cJSON_AddStringToObject(info, "posting_policy", g_unified_cache.relay_info.posting_policy); } // Add server limitations if (g_unified_cache.relay_info.limitation) { cJSON_AddItemToObject(info, "limitation", cJSON_Duplicate(g_unified_cache.relay_info.limitation, 1)); } // Add retention policies if configured if (g_unified_cache.relay_info.retention && cJSON_GetArraySize(g_unified_cache.relay_info.retention) > 0) { cJSON_AddItemToObject(info, "retention", cJSON_Duplicate(g_unified_cache.relay_info.retention, 1)); } // Add geographical and language information if (g_unified_cache.relay_info.relay_countries) { cJSON_AddItemToObject(info, "relay_countries", cJSON_Duplicate(g_unified_cache.relay_info.relay_countries, 1)); } if (g_unified_cache.relay_info.language_tags) { cJSON_AddItemToObject(info, "language_tags", cJSON_Duplicate(g_unified_cache.relay_info.language_tags, 1)); } if (g_unified_cache.relay_info.tags && cJSON_GetArraySize(g_unified_cache.relay_info.tags) > 0) { cJSON_AddItemToObject(info, "tags", cJSON_Duplicate(g_unified_cache.relay_info.tags, 1)); } // Add payment information if configured if (strlen(g_unified_cache.relay_info.payments_url) > 0) { cJSON_AddStringToObject(info, "payments_url", g_unified_cache.relay_info.payments_url); } if (g_unified_cache.relay_info.fees && cJSON_GetObjectItem(g_unified_cache.relay_info.fees, "admission")) { cJSON_AddItemToObject(info, "fees", cJSON_Duplicate(g_unified_cache.relay_info.fees, 1)); } pthread_mutex_unlock(&g_unified_cache.cache_lock); return info; } // Handle NIP-11 HTTP request int handle_nip11_http_request(struct lws* wsi, const char* accept_header) { log_info("Handling NIP-11 relay information request"); // Check if client accepts application/nostr+json int accepts_nostr_json = 0; if (accept_header) { if (strstr(accept_header, "application/nostr+json") != NULL) { accepts_nostr_json = 1; } } if (!accepts_nostr_json) { log_warning("HTTP request without proper Accept header for NIP-11"); // Return 406 Not Acceptable unsigned char buf[LWS_PRE + 256]; unsigned char *p = &buf[LWS_PRE]; unsigned char *start = p; unsigned char *end = &buf[sizeof(buf) - 1]; if (lws_add_http_header_status(wsi, HTTP_STATUS_NOT_ACCEPTABLE, &p, end)) { return -1; } if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE, (unsigned char*)"text/plain", 10, &p, end)) { return -1; } if (lws_add_http_header_content_length(wsi, 0, &p, end)) { return -1; } if (lws_finalize_http_header(wsi, &p, end)) { return -1; } lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS); return -1; // Close connection } // Generate relay information JSON cJSON* info_json = generate_relay_info_json(); if (!info_json) { log_error("Failed to generate relay info JSON"); unsigned char buf[LWS_PRE + 256]; unsigned char *p = &buf[LWS_PRE]; unsigned char *start = p; unsigned char *end = &buf[sizeof(buf) - 1]; if (lws_add_http_header_status(wsi, HTTP_STATUS_INTERNAL_SERVER_ERROR, &p, end)) { return -1; } if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE, (unsigned char*)"text/plain", 10, &p, end)) { return -1; } if (lws_add_http_header_content_length(wsi, 0, &p, end)) { return -1; } if (lws_finalize_http_header(wsi, &p, end)) { return -1; } lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS); return -1; } char* json_string = cJSON_Print(info_json); cJSON_Delete(info_json); if (!json_string) { log_error("Failed to serialize relay info JSON"); unsigned char buf[LWS_PRE + 256]; unsigned char *p = &buf[LWS_PRE]; unsigned char *start = p; unsigned char *end = &buf[sizeof(buf) - 1]; if (lws_add_http_header_status(wsi, HTTP_STATUS_INTERNAL_SERVER_ERROR, &p, end)) { return -1; } if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE, (unsigned char*)"text/plain", 10, &p, end)) { return -1; } if (lws_add_http_header_content_length(wsi, 0, &p, end)) { return -1; } if (lws_finalize_http_header(wsi, &p, end)) { return -1; } lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS); return -1; } size_t json_len = strlen(json_string); // Prepare HTTP response with CORS headers unsigned char buf[LWS_PRE + 1024]; unsigned char *p = &buf[LWS_PRE]; unsigned char *start = p; unsigned char *end = &buf[sizeof(buf) - 1]; // Add status if (lws_add_http_header_status(wsi, HTTP_STATUS_OK, &p, end)) { free(json_string); return -1; } // Add content type if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE, (unsigned char*)"application/nostr+json", 22, &p, end)) { free(json_string); return -1; } // Add content length if (lws_add_http_header_content_length(wsi, json_len, &p, end)) { free(json_string); return -1; } // Add CORS headers as required by NIP-11 if (lws_add_http_header_by_name(wsi, (unsigned char*)"access-control-allow-origin:", (unsigned char*)"*", 1, &p, end)) { free(json_string); return -1; } if (lws_add_http_header_by_name(wsi, (unsigned char*)"access-control-allow-headers:", (unsigned char*)"content-type, accept", 20, &p, end)) { free(json_string); return -1; } if (lws_add_http_header_by_name(wsi, (unsigned char*)"access-control-allow-methods:", (unsigned char*)"GET, OPTIONS", 12, &p, end)) { free(json_string); return -1; } // Finalize headers if (lws_finalize_http_header(wsi, &p, end)) { free(json_string); return -1; } // Write headers if (lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS) < 0) { free(json_string); return -1; } // Write JSON body unsigned char *json_buf = malloc(LWS_PRE + json_len); if (!json_buf) { free(json_string); return -1; } memcpy(json_buf + LWS_PRE, json_string, json_len); if (lws_write(wsi, json_buf + LWS_PRE, json_len, LWS_WRITE_HTTP) < 0) { free(json_string); free(json_buf); return -1; } free(json_buf); free(json_string); log_success("NIP-11 relay information served successfully"); return 0; } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // NIP-13 PROOF OF WORK VALIDATION ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // Initialize PoW configuration using configuration system void init_pow_config() { log_info("Initializing NIP-13 Proof of Work configuration"); // Get all config values first (without holding mutex to avoid deadlock) int pow_enabled = get_config_bool("pow_enabled", 1); int pow_min_difficulty = get_config_int("pow_min_difficulty", 0); const char* pow_mode = get_config_value("pow_mode"); pthread_mutex_lock(&g_unified_cache.cache_lock); // Load PoW settings from configuration system g_unified_cache.pow_config.enabled = pow_enabled; g_unified_cache.pow_config.min_pow_difficulty = pow_min_difficulty; // Configure PoW mode if (pow_mode) { if (strcmp(pow_mode, "strict") == 0) { g_unified_cache.pow_config.validation_flags = NOSTR_POW_VALIDATE_ANTI_SPAM | NOSTR_POW_STRICT_FORMAT; g_unified_cache.pow_config.require_nonce_tag = 1; g_unified_cache.pow_config.reject_lower_targets = 1; g_unified_cache.pow_config.strict_format = 1; g_unified_cache.pow_config.anti_spam_mode = 1; log_info("PoW configured in strict anti-spam mode"); } else if (strcmp(pow_mode, "full") == 0) { g_unified_cache.pow_config.validation_flags = NOSTR_POW_VALIDATE_FULL; g_unified_cache.pow_config.require_nonce_tag = 1; log_info("PoW configured in full validation mode"); } else if (strcmp(pow_mode, "basic") == 0) { g_unified_cache.pow_config.validation_flags = NOSTR_POW_VALIDATE_BASIC; log_info("PoW configured in basic validation mode"); } else if (strcmp(pow_mode, "disabled") == 0) { g_unified_cache.pow_config.enabled = 0; log_info("PoW validation disabled via configuration"); } } else { // Default to basic mode g_unified_cache.pow_config.validation_flags = NOSTR_POW_VALIDATE_BASIC; log_info("PoW configured in basic validation mode (default)"); } // Log final configuration char config_msg[512]; snprintf(config_msg, sizeof(config_msg), "PoW Configuration: enabled=%s, min_difficulty=%d, validation_flags=0x%x, mode=%s", g_unified_cache.pow_config.enabled ? "true" : "false", g_unified_cache.pow_config.min_pow_difficulty, g_unified_cache.pow_config.validation_flags, g_unified_cache.pow_config.anti_spam_mode ? "anti-spam" : (g_unified_cache.pow_config.validation_flags & NOSTR_POW_VALIDATE_FULL) ? "full" : "basic"); log_info(config_msg); pthread_mutex_unlock(&g_unified_cache.cache_lock); } // Validate event Proof of Work according to NIP-13 int validate_event_pow(cJSON* event, char* error_message, size_t error_size) { pthread_mutex_lock(&g_unified_cache.cache_lock); int enabled = g_unified_cache.pow_config.enabled; int min_pow_difficulty = g_unified_cache.pow_config.min_pow_difficulty; int validation_flags = g_unified_cache.pow_config.validation_flags; pthread_mutex_unlock(&g_unified_cache.cache_lock); if (!enabled) { return 0; // PoW validation disabled } if (!event) { snprintf(error_message, error_size, "pow: null event"); return NOSTR_ERROR_INVALID_INPUT; } // If min_pow_difficulty is 0, only validate events that have nonce tags // This allows events without PoW when difficulty requirement is 0 if (min_pow_difficulty == 0) { cJSON* tags = cJSON_GetObjectItem(event, "tags"); int has_nonce_tag = 0; if (tags && cJSON_IsArray(tags)) { cJSON* tag = NULL; cJSON_ArrayForEach(tag, tags) { if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) { cJSON* tag_name = cJSON_GetArrayItem(tag, 0); if (cJSON_IsString(tag_name)) { const char* name = cJSON_GetStringValue(tag_name); if (name && strcmp(name, "nonce") == 0) { has_nonce_tag = 1; break; } } } } } // If no minimum difficulty required and no nonce tag, skip PoW validation if (!has_nonce_tag) { return 0; // Accept event without PoW when min_difficulty=0 } } // Perform PoW validation using nostr_core_lib nostr_pow_result_t pow_result; int validation_result = nostr_validate_pow(event, min_pow_difficulty, validation_flags, &pow_result); if (validation_result != NOSTR_SUCCESS) { // Handle specific error cases with appropriate messages switch (validation_result) { case NOSTR_ERROR_NIP13_INSUFFICIENT: snprintf(error_message, error_size, "pow: insufficient difficulty: %d < %d", pow_result.actual_difficulty, min_pow_difficulty); log_warning("Event rejected: insufficient PoW difficulty"); break; case NOSTR_ERROR_NIP13_NO_NONCE_TAG: // This should not happen with min_difficulty=0 after our check above if (min_pow_difficulty > 0) { snprintf(error_message, error_size, "pow: missing required nonce tag"); log_warning("Event rejected: missing nonce tag"); } else { return 0; // Allow when min_difficulty=0 } break; case NOSTR_ERROR_NIP13_INVALID_NONCE_TAG: snprintf(error_message, error_size, "pow: invalid nonce tag format"); log_warning("Event rejected: invalid nonce tag format"); break; case NOSTR_ERROR_NIP13_TARGET_MISMATCH: snprintf(error_message, error_size, "pow: committed target (%d) lower than minimum (%d)", pow_result.committed_target, min_pow_difficulty); log_warning("Event rejected: committed target too low (anti-spam protection)"); break; case NOSTR_ERROR_NIP13_CALCULATION: snprintf(error_message, error_size, "pow: difficulty calculation failed"); log_error("PoW difficulty calculation error"); break; case NOSTR_ERROR_EVENT_INVALID_ID: snprintf(error_message, error_size, "pow: invalid event ID format"); log_warning("Event rejected: invalid event ID for PoW calculation"); break; default: snprintf(error_message, error_size, "pow: validation failed - %s", strlen(pow_result.error_detail) > 0 ? pow_result.error_detail : "unknown error"); log_warning("Event rejected: PoW validation failed"); } return validation_result; } // Log successful PoW validation (only if minimum difficulty is required) if (min_pow_difficulty > 0 || pow_result.has_nonce_tag) { char debug_msg[256]; snprintf(debug_msg, sizeof(debug_msg), "PoW validated: difficulty=%d, target=%d, nonce=%llu%s", pow_result.actual_difficulty, pow_result.committed_target, (unsigned long long)pow_result.nonce_value, pow_result.has_nonce_tag ? "" : " (no nonce tag)"); log_info(debug_msg); } return 0; // Success } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // NIP-40 EXPIRATION TIMESTAMP HANDLING ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // Initialize expiration configuration using configuration system void init_expiration_config() { log_info("Initializing NIP-40 Expiration Timestamp configuration"); // Get all config values first (without holding mutex to avoid deadlock) int expiration_enabled = get_config_bool("expiration_enabled", 1); int expiration_strict = get_config_bool("expiration_strict", 1); int expiration_filter = get_config_bool("expiration_filter", 1); int expiration_delete = get_config_bool("expiration_delete", 0); long expiration_grace_period = get_config_int("expiration_grace_period", 1); pthread_mutex_lock(&g_unified_cache.cache_lock); // Load expiration settings from configuration system g_unified_cache.expiration_config.enabled = expiration_enabled; g_unified_cache.expiration_config.strict_mode = expiration_strict; g_unified_cache.expiration_config.filter_responses = expiration_filter; g_unified_cache.expiration_config.delete_expired = expiration_delete; g_unified_cache.expiration_config.grace_period = expiration_grace_period; // Validate grace period bounds if (g_unified_cache.expiration_config.grace_period < 0 || g_unified_cache.expiration_config.grace_period > 86400) { log_warning("Invalid grace period, using default of 300 seconds"); g_unified_cache.expiration_config.grace_period = 300; } // Log final configuration char config_msg[512]; snprintf(config_msg, sizeof(config_msg), "Expiration Configuration: enabled=%s, strict_mode=%s, filter_responses=%s, grace_period=%ld seconds", g_unified_cache.expiration_config.enabled ? "true" : "false", g_unified_cache.expiration_config.strict_mode ? "true" : "false", g_unified_cache.expiration_config.filter_responses ? "true" : "false", g_unified_cache.expiration_config.grace_period); log_info(config_msg); pthread_mutex_unlock(&g_unified_cache.cache_lock); } // Extract expiration timestamp from event tags long extract_expiration_timestamp(cJSON* tags) { if (!tags || !cJSON_IsArray(tags)) { return 0; // No expiration } cJSON* tag = NULL; cJSON_ArrayForEach(tag, tags) { if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) { cJSON* tag_name = cJSON_GetArrayItem(tag, 0); cJSON* tag_value = cJSON_GetArrayItem(tag, 1); if (cJSON_IsString(tag_name) && cJSON_IsString(tag_value)) { const char* name = cJSON_GetStringValue(tag_name); const char* value = cJSON_GetStringValue(tag_value); if (name && value && strcmp(name, "expiration") == 0) { // Validate that the string contains only digits (and optional leading whitespace) const char* p = value; // Skip leading whitespace while (*p == ' ' || *p == '\t') p++; // Check if we have at least one digit if (*p == '\0') { continue; // Empty or whitespace-only string, ignore this tag } // Validate that all remaining characters are digits const char* digit_start = p; while (*p >= '0' && *p <= '9') p++; // If we didn't consume the entire string or found no digits, it's malformed if (*p != '\0' || p == digit_start) { char debug_msg[256]; snprintf(debug_msg, sizeof(debug_msg), "Ignoring malformed expiration tag value: '%.32s'", value); log_warning(debug_msg); continue; // Ignore malformed expiration tag } long expiration_ts = atol(value); if (expiration_ts > 0) { return expiration_ts; } } } } } return 0; // No valid expiration tag found } // Check if event is currently expired int is_event_expired(cJSON* event, time_t current_time) { if (!event) { return 0; // Invalid event, not expired } cJSON* tags = cJSON_GetObjectItem(event, "tags"); long expiration_ts = extract_expiration_timestamp(tags); if (expiration_ts == 0) { return 0; // No expiration timestamp, not expired } // Check if current time exceeds expiration + grace period pthread_mutex_lock(&g_unified_cache.cache_lock); long grace_period = g_unified_cache.expiration_config.grace_period; pthread_mutex_unlock(&g_unified_cache.cache_lock); return (current_time > (expiration_ts + grace_period)); } // Validate event expiration according to NIP-40 int validate_event_expiration(cJSON* event, char* error_message, size_t error_size) { pthread_mutex_lock(&g_unified_cache.cache_lock); int enabled = g_unified_cache.expiration_config.enabled; int strict_mode = g_unified_cache.expiration_config.strict_mode; long grace_period = g_unified_cache.expiration_config.grace_period; pthread_mutex_unlock(&g_unified_cache.cache_lock); if (!enabled) { return 0; // Expiration validation disabled } if (!event) { snprintf(error_message, error_size, "expiration: null event"); return -1; } // Check if event is expired time_t current_time = time(NULL); if (is_event_expired(event, current_time)) { if (strict_mode) { cJSON* tags = cJSON_GetObjectItem(event, "tags"); long expiration_ts = extract_expiration_timestamp(tags); snprintf(error_message, error_size, "invalid: event expired (expiration=%ld, current=%ld, grace=%ld)", expiration_ts, (long)current_time, grace_period); log_warning("Event rejected: expired timestamp"); return -1; } else { // In non-strict mode, log but allow expired events char debug_msg[256]; snprintf(debug_msg, sizeof(debug_msg), "Accepting expired event (strict_mode disabled)"); log_info(debug_msg); } } return 0; // Success } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // DATABASE FUNCTIONS ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // Initialize database connection and schema int init_database(const char* database_path_override) { // Priority 1: Command line database path override const char* db_path = database_path_override; // Priority 2: Configuration system (if available) if (!db_path) { db_path = get_config_value("database_path"); } // Priority 3: Default path if (!db_path) { db_path = DEFAULT_DATABASE_PATH; } int rc = sqlite3_open(db_path, &g_db); if (rc != SQLITE_OK) { log_error("Cannot open database"); return -1; } char success_msg[256]; snprintf(success_msg, sizeof(success_msg), "Database connection established: %s", db_path); log_success(success_msg); // Check if database is already initialized by looking for the events table const char* check_sql = "SELECT name FROM sqlite_master WHERE type='table' AND name='events'"; sqlite3_stmt* check_stmt; rc = sqlite3_prepare_v2(g_db, check_sql, -1, &check_stmt, NULL); if (rc == SQLITE_OK) { int has_events_table = (sqlite3_step(check_stmt) == SQLITE_ROW); sqlite3_finalize(check_stmt); if (has_events_table) { log_info("Database schema already exists, checking version"); // Check existing schema version and migrate if needed const char* version_sql = "SELECT value FROM schema_info WHERE key = 'version'"; sqlite3_stmt* version_stmt; const char* db_version = NULL; int needs_migration = 0; if (sqlite3_prepare_v2(g_db, version_sql, -1, &version_stmt, NULL) == SQLITE_OK) { if (sqlite3_step(version_stmt) == SQLITE_ROW) { db_version = (char*)sqlite3_column_text(version_stmt, 0); char version_msg[256]; snprintf(version_msg, sizeof(version_msg), "Existing database schema version: %s", db_version ? db_version : "unknown"); log_info(version_msg); // Check if migration is needed if (!db_version || strcmp(db_version, "5") == 0) { needs_migration = 1; log_info("Database migration needed: v5 -> v6 (adding auth_rules table)"); } else if (strcmp(db_version, "6") == 0) { log_info("Database is already at current schema version v6"); } else if (strcmp(db_version, EMBEDDED_SCHEMA_VERSION) == 0) { log_info("Database is at current schema version"); } else { char warning_msg[256]; snprintf(warning_msg, sizeof(warning_msg), "Unknown database schema version: %s", db_version); log_warning(warning_msg); } } else { log_info("Database exists but no version information found, assuming migration needed"); needs_migration = 1; } sqlite3_finalize(version_stmt); } else { log_info("Cannot read schema version, assuming migration needed"); needs_migration = 1; } // Perform migration if needed if (needs_migration) { log_info("Performing database schema migration to v6"); // Check if auth_rules table already exists const char* check_auth_rules_sql = "SELECT name FROM sqlite_master WHERE type='table' AND name='auth_rules'"; sqlite3_stmt* check_stmt; int has_auth_rules = 0; if (sqlite3_prepare_v2(g_db, check_auth_rules_sql, -1, &check_stmt, NULL) == SQLITE_OK) { has_auth_rules = (sqlite3_step(check_stmt) == SQLITE_ROW); sqlite3_finalize(check_stmt); } if (!has_auth_rules) { // Add auth_rules table const char* create_auth_rules_sql = "CREATE TABLE IF NOT EXISTS auth_rules (" " id INTEGER PRIMARY KEY AUTOINCREMENT," " rule_type TEXT NOT NULL," // 'pubkey_whitelist', 'pubkey_blacklist', 'hash_blacklist' " operation TEXT NOT NULL," // 'event', 'event_kind_1', etc. " rule_target TEXT NOT NULL," // pubkey, hash, or other identifier " enabled INTEGER DEFAULT 1," // 0 = disabled, 1 = enabled " priority INTEGER DEFAULT 1000," // Lower numbers = higher priority " description TEXT," // Optional description " created_at INTEGER DEFAULT (strftime('%s', 'now'))," " UNIQUE(rule_type, operation, rule_target)" ");"; char* error_msg = NULL; int rc = sqlite3_exec(g_db, create_auth_rules_sql, NULL, NULL, &error_msg); if (rc != SQLITE_OK) { char error_log[512]; snprintf(error_log, sizeof(error_log), "Failed to create auth_rules table: %s", error_msg ? error_msg : "unknown error"); log_error(error_log); if (error_msg) sqlite3_free(error_msg); return -1; } log_success("Created auth_rules table"); } else { log_info("auth_rules table already exists, skipping creation"); } // Update schema version to v6 const char* update_version_sql = "INSERT OR REPLACE INTO schema_info (key, value, updated_at) " "VALUES ('version', '6', strftime('%s', 'now'))"; char* error_msg = NULL; int rc = sqlite3_exec(g_db, update_version_sql, NULL, NULL, &error_msg); if (rc != SQLITE_OK) { char error_log[512]; snprintf(error_log, sizeof(error_log), "Failed to update schema version: %s", error_msg ? error_msg : "unknown error"); log_error(error_log); if (error_msg) sqlite3_free(error_msg); return -1; } log_success("Database migration to v6 completed successfully"); } } else { // Initialize database schema using embedded SQL log_info("Initializing database schema from embedded SQL"); // Execute the embedded schema SQL char* error_msg = NULL; rc = sqlite3_exec(g_db, EMBEDDED_SCHEMA_SQL, NULL, NULL, &error_msg); if (rc != SQLITE_OK) { char error_log[512]; snprintf(error_log, sizeof(error_log), "Failed to initialize database schema: %s", error_msg ? error_msg : "unknown error"); log_error(error_log); if (error_msg) { sqlite3_free(error_msg); } return -1; } log_success("Database schema initialized successfully"); // Log schema version information char version_msg[256]; snprintf(version_msg, sizeof(version_msg), "Database schema version: %s", EMBEDDED_SCHEMA_VERSION); log_info(version_msg); } } else { log_error("Failed to check existing database schema"); return -1; } return 0; } // Close database connection void close_database() { if (g_db) { sqlite3_close(g_db); g_db = NULL; log_info("Database connection closed"); } } // Event type classification typedef enum { EVENT_TYPE_REGULAR, EVENT_TYPE_REPLACEABLE, EVENT_TYPE_EPHEMERAL, EVENT_TYPE_ADDRESSABLE, EVENT_TYPE_UNKNOWN } event_type_t; event_type_t classify_event_kind(int kind) { if ((kind >= 1000 && kind < 10000) || (kind >= 4 && kind < 45) || kind == 1 || kind == 2) { return EVENT_TYPE_REGULAR; } if ((kind >= 10000 && kind < 20000) || kind == 0 || kind == 3) { return EVENT_TYPE_REPLACEABLE; } if (kind >= 20000 && kind < 30000) { return EVENT_TYPE_EPHEMERAL; } if (kind >= 30000 && kind < 40000) { return EVENT_TYPE_ADDRESSABLE; } return EVENT_TYPE_UNKNOWN; } const char* event_type_to_string(event_type_t type) { switch (type) { case EVENT_TYPE_REGULAR: return "regular"; case EVENT_TYPE_REPLACEABLE: return "replaceable"; case EVENT_TYPE_EPHEMERAL: return "ephemeral"; case EVENT_TYPE_ADDRESSABLE: return "addressable"; default: return "unknown"; } } // Helper function to extract d tag value from tags array const char* extract_d_tag_value(cJSON* tags) { if (!tags || !cJSON_IsArray(tags)) { return NULL; } cJSON* tag = NULL; cJSON_ArrayForEach(tag, tags) { if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) { cJSON* tag_name = cJSON_GetArrayItem(tag, 0); cJSON* tag_value = cJSON_GetArrayItem(tag, 1); if (cJSON_IsString(tag_name) && cJSON_IsString(tag_value)) { const char* name = cJSON_GetStringValue(tag_name); if (name && strcmp(name, "d") == 0) { return cJSON_GetStringValue(tag_value); } } } } return NULL; } // Check and handle replaceable events according to NIP-01 int check_and_handle_replaceable_event(int kind, const char* pubkey, long created_at) { if (!g_db || !pubkey) return 0; const char* sql = "SELECT created_at FROM events WHERE kind = ? AND pubkey = ? ORDER BY created_at DESC LIMIT 1"; sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { return 0; // Allow storage on DB error } sqlite3_bind_int(stmt, 1, kind); sqlite3_bind_text(stmt, 2, pubkey, -1, SQLITE_STATIC); int result = 0; if (sqlite3_step(stmt) == SQLITE_ROW) { long existing_created_at = sqlite3_column_int64(stmt, 0); if (created_at <= existing_created_at) { result = -1; // Older or same timestamp, reject } else { // Delete older versions const char* delete_sql = "DELETE FROM events WHERE kind = ? AND pubkey = ? AND created_at < ?"; sqlite3_stmt* delete_stmt; if (sqlite3_prepare_v2(g_db, delete_sql, -1, &delete_stmt, NULL) == SQLITE_OK) { sqlite3_bind_int(delete_stmt, 1, kind); sqlite3_bind_text(delete_stmt, 2, pubkey, -1, SQLITE_STATIC); sqlite3_bind_int64(delete_stmt, 3, created_at); sqlite3_step(delete_stmt); sqlite3_finalize(delete_stmt); } } } sqlite3_finalize(stmt); return result; } // Check and handle addressable events according to NIP-01 int check_and_handle_addressable_event(int kind, const char* pubkey, const char* d_tag_value, long created_at) { if (!g_db || !pubkey) return 0; // If no d tag, treat as regular replaceable if (!d_tag_value) { return check_and_handle_replaceable_event(kind, pubkey, created_at); } const char* sql = "SELECT created_at FROM events WHERE kind = ? AND pubkey = ? AND json_extract(tags, '$[*][1]') = ? " "AND json_extract(tags, '$[*][0]') = 'd' ORDER BY created_at DESC LIMIT 1"; sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { return 0; // Allow storage on DB error } sqlite3_bind_int(stmt, 1, kind); sqlite3_bind_text(stmt, 2, pubkey, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 3, d_tag_value, -1, SQLITE_STATIC); int result = 0; if (sqlite3_step(stmt) == SQLITE_ROW) { long existing_created_at = sqlite3_column_int64(stmt, 0); if (created_at <= existing_created_at) { result = -1; // Older or same timestamp, reject } else { // Delete older versions with same kind, pubkey, and d tag const char* delete_sql = "DELETE FROM events WHERE kind = ? AND pubkey = ? AND created_at < ? " "AND json_extract(tags, '$[*][1]') = ? AND json_extract(tags, '$[*][0]') = 'd'"; sqlite3_stmt* delete_stmt; if (sqlite3_prepare_v2(g_db, delete_sql, -1, &delete_stmt, NULL) == SQLITE_OK) { sqlite3_bind_int(delete_stmt, 1, kind); sqlite3_bind_text(delete_stmt, 2, pubkey, -1, SQLITE_STATIC); sqlite3_bind_int64(delete_stmt, 3, created_at); sqlite3_bind_text(delete_stmt, 4, d_tag_value, -1, SQLITE_STATIC); sqlite3_step(delete_stmt); sqlite3_finalize(delete_stmt); } } } sqlite3_finalize(stmt); return result; } // Store event in database int store_event(cJSON* event) { if (!g_db || !event) { return -1; } // Extract event fields cJSON* id = cJSON_GetObjectItem(event, "id"); cJSON* pubkey = cJSON_GetObjectItem(event, "pubkey"); cJSON* created_at = cJSON_GetObjectItem(event, "created_at"); cJSON* kind = cJSON_GetObjectItem(event, "kind"); cJSON* content = cJSON_GetObjectItem(event, "content"); cJSON* sig = cJSON_GetObjectItem(event, "sig"); cJSON* tags = cJSON_GetObjectItem(event, "tags"); if (!id || !pubkey || !created_at || !kind || !content || !sig) { log_error("Invalid event - missing required fields"); return -1; } // Classify event type event_type_t type = classify_event_kind((int)cJSON_GetNumberValue(kind)); // Serialize tags to JSON (use empty array if no tags) char* tags_json = NULL; if (tags && cJSON_IsArray(tags)) { tags_json = cJSON_Print(tags); } else { tags_json = strdup("[]"); } if (!tags_json) { log_error("Failed to serialize tags to JSON"); return -1; } // Prepare SQL statement for event insertion const char* sql = "INSERT INTO events (id, pubkey, created_at, kind, event_type, content, sig, tags) " "VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { log_error("Failed to prepare event insert statement"); free(tags_json); return -1; } // Bind parameters sqlite3_bind_text(stmt, 1, cJSON_GetStringValue(id), -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, cJSON_GetStringValue(pubkey), -1, SQLITE_STATIC); sqlite3_bind_int64(stmt, 3, (sqlite3_int64)cJSON_GetNumberValue(created_at)); sqlite3_bind_int(stmt, 4, (int)cJSON_GetNumberValue(kind)); sqlite3_bind_text(stmt, 5, event_type_to_string(type), -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 6, cJSON_GetStringValue(content), -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 7, cJSON_GetStringValue(sig), -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 8, tags_json, -1, SQLITE_TRANSIENT); // Execute statement rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { if (rc == SQLITE_CONSTRAINT) { log_warning("Event already exists in database"); free(tags_json); return 0; // Not an error, just duplicate } char error_msg[256]; snprintf(error_msg, sizeof(error_msg), "Failed to insert event: %s", sqlite3_errmsg(g_db)); log_error(error_msg); free(tags_json); return -1; } free(tags_json); log_success("Event stored in database"); return 0; } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // EVENT STORAGE AND RETRIEVAL ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// cJSON* retrieve_event(const char* event_id) { if (!g_db || !event_id) { return NULL; } const char* sql = "SELECT id, pubkey, created_at, kind, content, sig, tags FROM events WHERE id = ?"; sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { return NULL; } sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_STATIC); cJSON* event = NULL; if (sqlite3_step(stmt) == SQLITE_ROW) { event = cJSON_CreateObject(); cJSON_AddStringToObject(event, "id", (char*)sqlite3_column_text(stmt, 0)); cJSON_AddStringToObject(event, "pubkey", (char*)sqlite3_column_text(stmt, 1)); cJSON_AddNumberToObject(event, "created_at", sqlite3_column_int64(stmt, 2)); cJSON_AddNumberToObject(event, "kind", sqlite3_column_int(stmt, 3)); cJSON_AddStringToObject(event, "content", (char*)sqlite3_column_text(stmt, 4)); cJSON_AddStringToObject(event, "sig", (char*)sqlite3_column_text(stmt, 5)); // Parse tags JSON const char* tags_json = (char*)sqlite3_column_text(stmt, 6); if (tags_json) { cJSON* tags = cJSON_Parse(tags_json); if (tags) { cJSON_AddItemToObject(event, "tags", tags); } else { cJSON_AddItemToObject(event, "tags", cJSON_CreateArray()); } } else { cJSON_AddItemToObject(event, "tags", cJSON_CreateArray()); } } sqlite3_finalize(stmt); return event; } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // SUBSCRIPTION HANDLERS ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, struct per_session_data *pss) { log_info("Handling REQ message for persistent subscription"); if (!cJSON_IsArray(filters)) { log_error("REQ filters is not an array"); return 0; } // Check for kind 33334 configuration event requests BEFORE creating subscription int config_events_sent = 0; int has_config_request = 0; // Check if any filter requests kind 33334 (configuration events) for (int i = 0; i < cJSON_GetArraySize(filters); i++) { cJSON* filter = cJSON_GetArrayItem(filters, i); if (filter && cJSON_IsObject(filter)) { if (req_filter_requests_config_events(filter)) { has_config_request = 1; // Generate synthetic config event for this subscription cJSON* filters_array = cJSON_CreateArray(); cJSON_AddItemToArray(filters_array, cJSON_Duplicate(filter, 1)); cJSON* event_msg = generate_synthetic_config_event_for_subscription(sub_id, filters_array); if (event_msg) { char* msg_str = cJSON_Print(event_msg); if (msg_str) { size_t msg_len = strlen(msg_str); unsigned char* buf = malloc(LWS_PRE + msg_len); if (buf) { memcpy(buf + LWS_PRE, msg_str, msg_len); lws_write(wsi, buf + LWS_PRE, msg_len, LWS_WRITE_TEXT); config_events_sent++; free(buf); } free(msg_str); } cJSON_Delete(event_msg); } cJSON_Delete(filters_array); char debug_msg[256]; snprintf(debug_msg, sizeof(debug_msg), "Generated %d synthetic config events for subscription %s", config_events_sent, sub_id); log_info(debug_msg); break; // Only generate once per subscription } } } // If only config events were requested, we can return early after sending EOSE // But still create the subscription for future config updates // Check session subscription limits if (pss && pss->subscription_count >= g_subscription_manager.max_subscriptions_per_client) { log_error("Maximum subscriptions per client exceeded"); // Send CLOSED notice cJSON* closed_msg = cJSON_CreateArray(); cJSON_AddItemToArray(closed_msg, cJSON_CreateString("CLOSED")); cJSON_AddItemToArray(closed_msg, cJSON_CreateString(sub_id)); cJSON_AddItemToArray(closed_msg, cJSON_CreateString("error: too many subscriptions")); char* closed_str = cJSON_Print(closed_msg); if (closed_str) { size_t closed_len = strlen(closed_str); unsigned char* buf = malloc(LWS_PRE + closed_len); if (buf) { memcpy(buf + LWS_PRE, closed_str, closed_len); lws_write(wsi, buf + LWS_PRE, closed_len, LWS_WRITE_TEXT); free(buf); } free(closed_str); } cJSON_Delete(closed_msg); return has_config_request ? config_events_sent : 0; } // Create persistent subscription subscription_t* subscription = create_subscription(sub_id, wsi, filters, pss ? pss->client_ip : "unknown"); if (!subscription) { log_error("Failed to create subscription"); return has_config_request ? config_events_sent : 0; } // Add to global manager if (add_subscription_to_manager(subscription) != 0) { log_error("Failed to add subscription to global manager"); free_subscription(subscription); // Send CLOSED notice cJSON* closed_msg = cJSON_CreateArray(); cJSON_AddItemToArray(closed_msg, cJSON_CreateString("CLOSED")); cJSON_AddItemToArray(closed_msg, cJSON_CreateString(sub_id)); cJSON_AddItemToArray(closed_msg, cJSON_CreateString("error: subscription limit reached")); char* closed_str = cJSON_Print(closed_msg); if (closed_str) { size_t closed_len = strlen(closed_str); unsigned char* buf = malloc(LWS_PRE + closed_len); if (buf) { memcpy(buf + LWS_PRE, closed_str, closed_len); lws_write(wsi, buf + LWS_PRE, closed_len, LWS_WRITE_TEXT); free(buf); } free(closed_str); } cJSON_Delete(closed_msg); return has_config_request ? config_events_sent : 0; } // Add to session's subscription list (if session data available) if (pss) { pthread_mutex_lock(&pss->session_lock); subscription->session_next = pss->subscriptions; pss->subscriptions = subscription; pss->subscription_count++; pthread_mutex_unlock(&pss->session_lock); } int events_sent = config_events_sent; // Start with synthetic config events // 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"); continue; } // Build SQL query based on filter char sql[1024] = "SELECT id, pubkey, created_at, kind, content, sig, tags FROM events WHERE 1=1"; 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 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); } // Add ordering and limit snprintf(sql_ptr, remaining, " ORDER BY created_at DESC"); sql_ptr += strlen(sql_ptr); remaining = sizeof(sql) - strlen(sql); // Handle limit filter cJSON* limit = cJSON_GetObjectItem(filter, "limit"); if (limit && cJSON_IsNumber(limit)) { int limit_val = (int)cJSON_GetNumberValue(limit); if (limit_val > 0 && limit_val <= 5000) { snprintf(sql_ptr, remaining, " LIMIT %d", limit_val); } } else { // Default limit to prevent excessive queries snprintf(sql_ptr, remaining, " LIMIT 500"); } // Debug: Log the SQL query being executed char debug_msg[1280]; snprintf(debug_msg, sizeof(debug_msg), "Executing SQL: %s", sql); log_info(debug_msg); // Execute query and send events 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 subscription query: %s", sqlite3_errmsg(g_db)); log_error(error_msg); continue; } int row_count = 0; while (sqlite3_step(stmt) == SQLITE_ROW) { row_count++; // Build event JSON cJSON* event = cJSON_CreateObject(); cJSON_AddStringToObject(event, "id", (char*)sqlite3_column_text(stmt, 0)); cJSON_AddStringToObject(event, "pubkey", (char*)sqlite3_column_text(stmt, 1)); cJSON_AddNumberToObject(event, "created_at", sqlite3_column_int64(stmt, 2)); cJSON_AddNumberToObject(event, "kind", sqlite3_column_int(stmt, 3)); cJSON_AddStringToObject(event, "content", (char*)sqlite3_column_text(stmt, 4)); cJSON_AddStringToObject(event, "sig", (char*)sqlite3_column_text(stmt, 5)); // Parse tags JSON const char* tags_json = (char*)sqlite3_column_text(stmt, 6); cJSON* tags = NULL; if (tags_json) { tags = cJSON_Parse(tags_json); } if (!tags) { tags = cJSON_CreateArray(); } cJSON_AddItemToObject(event, "tags", tags); // Check expiration filtering (NIP-40) at application level pthread_mutex_lock(&g_unified_cache.cache_lock); int expiration_enabled = g_unified_cache.expiration_config.enabled; int filter_responses = g_unified_cache.expiration_config.filter_responses; pthread_mutex_unlock(&g_unified_cache.cache_lock); if (expiration_enabled && filter_responses) { time_t current_time = time(NULL); if (is_event_expired(event, current_time)) { // Skip this expired event cJSON* event_id_obj = cJSON_GetObjectItem(event, "id"); const char* event_id = event_id_obj ? cJSON_GetStringValue(event_id_obj) : "unknown"; char debug_msg[256]; snprintf(debug_msg, sizeof(debug_msg), "Filtering expired event from subscription: %.16s", event_id); log_info(debug_msg); cJSON_Delete(event); continue; } } // Send EVENT message cJSON* event_msg = cJSON_CreateArray(); cJSON_AddItemToArray(event_msg, cJSON_CreateString("EVENT")); cJSON_AddItemToArray(event_msg, cJSON_CreateString(sub_id)); cJSON_AddItemToArray(event_msg, event); char* msg_str = cJSON_Print(event_msg); if (msg_str) { size_t msg_len = strlen(msg_str); unsigned char* buf = malloc(LWS_PRE + msg_len); if (buf) { memcpy(buf + LWS_PRE, msg_str, msg_len); lws_write(wsi, buf + LWS_PRE, msg_len, LWS_WRITE_TEXT); free(buf); } free(msg_str); } cJSON_Delete(event_msg); events_sent++; } char row_debug[128]; snprintf(row_debug, sizeof(row_debug), "Query returned %d rows", row_count); log_info(row_debug); sqlite3_finalize(stmt); } char events_debug[128]; snprintf(events_debug, sizeof(events_debug), "Total events sent: %d", events_sent); log_info(events_debug); return events_sent; } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // ADMIN EVENT AUTHORIZATION ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // Enhanced admin event authorization function int is_authorized_admin_event(cJSON* event, char* error_buffer, size_t error_buffer_size) { if (!event || !error_buffer) { if (error_buffer && error_buffer_size > 0) { snprintf(error_buffer, error_buffer_size, "Invalid parameters for admin authorization"); } return -1; } // Step 1: Verify event kind is admin type cJSON *kind_json = cJSON_GetObjectItem(event, "kind"); if (!kind_json || !cJSON_IsNumber(kind_json)) { snprintf(error_buffer, error_buffer_size, "Missing or invalid event kind"); return -1; } int event_kind = kind_json->valueint; if (event_kind != 23455 && event_kind != 23456) { snprintf(error_buffer, error_buffer_size, "Event kind %d is not an admin event type", event_kind); return -1; } // Step 2: Check if event targets this relay (look for 'p' tag with our relay pubkey) cJSON *tags = cJSON_GetObjectItem(event, "tags"); if (!tags || !cJSON_IsArray(tags)) { // No tags array - treat as regular event for different relay log_info("Admin event has no tags array - treating as event for different relay"); snprintf(error_buffer, error_buffer_size, "Admin event not targeting this relay (no tags)"); return -1; } int targets_this_relay = 0; cJSON *tag; cJSON_ArrayForEach(tag, tags) { if (cJSON_IsArray(tag)) { cJSON *tag_name = cJSON_GetArrayItem(tag, 0); cJSON *tag_value = cJSON_GetArrayItem(tag, 1); if (tag_name && cJSON_IsString(tag_name) && tag_value && cJSON_IsString(tag_value) && strcmp(tag_name->valuestring, "p") == 0) { // Compare with our relay pubkey const char* relay_pubkey = get_config_value("relay_pubkey"); if (relay_pubkey && strcmp(tag_value->valuestring, relay_pubkey) == 0) { targets_this_relay = 1; break; } } } } if (!targets_this_relay) { // Admin event for different relay - not an error, just not for us log_info("Admin event targets different relay - treating as regular event"); snprintf(error_buffer, error_buffer_size, "Admin event not targeting this relay"); return -1; } // Step 3: Verify admin signature authorization cJSON *pubkey_json = cJSON_GetObjectItem(event, "pubkey"); if (!pubkey_json || !cJSON_IsString(pubkey_json)) { log_warning("Unauthorized admin event attempt: missing or invalid pubkey"); snprintf(error_buffer, error_buffer_size, "Unauthorized admin event attempt: missing pubkey"); return -1; } // Get admin pubkey from configuration const char* admin_pubkey = get_config_value("admin_pubkey"); if (!admin_pubkey || strlen(admin_pubkey) == 0) { log_warning("Unauthorized admin event attempt: no admin pubkey configured"); snprintf(error_buffer, error_buffer_size, "Unauthorized admin event attempt: no admin configured"); return -1; } // Compare pubkeys if (strcmp(pubkey_json->valuestring, admin_pubkey) != 0) { log_warning("Unauthorized admin event attempt: pubkey mismatch"); char warning_msg[256]; snprintf(warning_msg, sizeof(warning_msg), "Unauthorized admin event attempt from pubkey: %.32s...", pubkey_json->valuestring); log_warning(warning_msg); snprintf(error_buffer, error_buffer_size, "Unauthorized admin event attempt: invalid admin pubkey"); return -1; } // Step 4: Verify event signature if (nostr_verify_event_signature(event) != 0) { log_warning("Unauthorized admin event attempt: invalid signature"); snprintf(error_buffer, error_buffer_size, "Unauthorized admin event attempt: signature verification failed"); return -1; } // All checks passed - authorized admin event log_info("Admin event authorization successful"); 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 NIP-11 relay information requests (HTTP GET to root path) { char *requested_uri = (char *)in; log_info("HTTP request received"); // Check if this is a GET request to the root path if (strcmp(requested_uri, "/") == 0) { // Get Accept header char accept_header[256] = {0}; int header_len = lws_hdr_copy(wsi, accept_header, sizeof(accept_header) - 1, WSI_TOKEN_HTTP_ACCEPT); if (header_len > 0) { accept_header[header_len] = '\0'; // Handle NIP-11 request if (handle_nip11_http_request(wsi, accept_header) == 0) { return 0; // Successfully handled } } else { log_warning("HTTP request without Accept header"); } // Return 404 for other requests lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL); return -1; } // Return 404 for non-root paths lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL); return -1; } case LWS_CALLBACK_HTTP_WRITEABLE: // HTTP response continuation if needed break; case LWS_CALLBACK_ESTABLISHED: log_info("WebSocket connection established"); memset(pss, 0, sizeof(*pss)); pthread_mutex_init(&pss->session_lock, NULL); // Get real client IP address char client_ip[CLIENT_IP_MAX_LENGTH]; lws_get_peer_simple(wsi, client_ip, sizeof(client_ip)); // Ensure client_ip is null-terminated and copy safely client_ip[CLIENT_IP_MAX_LENGTH - 1] = '\0'; size_t ip_len = strlen(client_ip); size_t copy_len = (ip_len < CLIENT_IP_MAX_LENGTH - 1) ? ip_len : CLIENT_IP_MAX_LENGTH - 1; memcpy(pss->client_ip, client_ip, copy_len); pss->client_ip[copy_len] = '\0'; // Initialize NIP-42 authentication state pss->authenticated = 0; pss->nip42_auth_required_events = get_config_bool("nip42_auth_required_events", 0); pss->nip42_auth_required_subscriptions = get_config_bool("nip42_auth_required_subscriptions", 0); pss->auth_challenge_sent = 0; memset(pss->authenticated_pubkey, 0, sizeof(pss->authenticated_pubkey)); memset(pss->active_challenge, 0, sizeof(pss->active_challenge)); pss->challenge_created = 0; pss->challenge_expires = 0; break; case LWS_CALLBACK_RECEIVE: if (len > 0) { char *message = malloc(len + 1); if (message) { memcpy(message, in, len); message[len] = '\0'; // Parse JSON message (this is the normal program flow) cJSON* json = cJSON_Parse(message); if (json && cJSON_IsArray(json)) { // Log the complete parsed JSON message once char* complete_message = cJSON_Print(json); if (complete_message) { char debug_msg[2048]; snprintf(debug_msg, sizeof(debug_msg), "Received complete WebSocket message: %s", complete_message); log_info(debug_msg); free(complete_message); } // Get message type cJSON* type = cJSON_GetArrayItem(json, 0); if (type && cJSON_IsString(type)) { const char* msg_type = cJSON_GetStringValue(type); if (strcmp(msg_type, "EVENT") == 0) { // Extract event for kind-specific NIP-42 authentication check cJSON* event_obj = cJSON_GetArrayItem(json, 1); if (event_obj && cJSON_IsObject(event_obj)) { // Extract event kind for kind-specific NIP-42 authentication check cJSON* kind_obj = cJSON_GetObjectItem(event_obj, "kind"); int event_kind = kind_obj && cJSON_IsNumber(kind_obj) ? (int)cJSON_GetNumberValue(kind_obj) : -1; // Extract pubkey and event ID for debugging cJSON* pubkey_obj = cJSON_GetObjectItem(event_obj, "pubkey"); cJSON* id_obj = cJSON_GetObjectItem(event_obj, "id"); const char* event_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : "unknown"; const char* event_id = id_obj ? cJSON_GetStringValue(id_obj) : "unknown"; char debug_event_msg[512]; snprintf(debug_event_msg, sizeof(debug_event_msg), "DEBUG EVENT: Processing kind %d event from pubkey %.16s... ID %.16s...", event_kind, event_pubkey, event_id); log_info(debug_event_msg); // Check if NIP-42 authentication is required for this event kind or globally int auth_required = is_nip42_auth_globally_required() || is_nip42_auth_required_for_kind(event_kind); char debug_auth_msg[256]; snprintf(debug_auth_msg, sizeof(debug_auth_msg), "DEBUG AUTH: auth_required=%d, pss->authenticated=%d, event_kind=%d", auth_required, pss ? pss->authenticated : -1, event_kind); log_info(debug_auth_msg); if (pss && auth_required && !pss->authenticated) { if (!pss->auth_challenge_sent) { log_info("DEBUG AUTH: Sending NIP-42 authentication challenge"); send_nip42_auth_challenge(wsi, pss); } else { char auth_msg[256]; if (event_kind == 4 || event_kind == 14) { snprintf(auth_msg, sizeof(auth_msg), "NIP-42 authentication required for direct message events (kind %d)", event_kind); } else { snprintf(auth_msg, sizeof(auth_msg), "NIP-42 authentication required for event kind %d", event_kind); } send_notice_message(wsi, auth_msg); log_warning("Event rejected: NIP-42 authentication required for kind"); char debug_msg[128]; snprintf(debug_msg, sizeof(debug_msg), "Auth required for kind %d", event_kind); log_info(debug_msg); } cJSON_Delete(json); free(message); return 0; } } // Handle EVENT message cJSON* event = cJSON_GetArrayItem(json, 1); if (event && cJSON_IsObject(event)) { // Extract event JSON string for unified validator char *event_json_str = cJSON_Print(event); if (!event_json_str) { log_error("Failed to serialize event JSON for validation"); cJSON* error_response = cJSON_CreateArray(); cJSON_AddItemToArray(error_response, cJSON_CreateString("OK")); cJSON_AddItemToArray(error_response, cJSON_CreateString("unknown")); cJSON_AddItemToArray(error_response, cJSON_CreateBool(0)); cJSON_AddItemToArray(error_response, cJSON_CreateString("error: failed to process event")); char *error_str = cJSON_Print(error_response); if (error_str) { size_t error_len = strlen(error_str); unsigned char *buf = malloc(LWS_PRE + error_len); if (buf) { memcpy(buf + LWS_PRE, error_str, error_len); lws_write(wsi, buf + LWS_PRE, error_len, LWS_WRITE_TEXT); free(buf); } free(error_str); } cJSON_Delete(error_response); return 0; } log_info("DEBUG VALIDATION: Starting unified validator"); // Call unified validator with JSON string size_t event_json_len = strlen(event_json_str); int validation_result = nostr_validate_unified_request(event_json_str, event_json_len); // Map validation result to old result format (0 = success, -1 = failure) int result = (validation_result == NOSTR_SUCCESS) ? 0 : -1; char debug_validation_msg[256]; snprintf(debug_validation_msg, sizeof(debug_validation_msg), "DEBUG VALIDATION: validation_result=%d, result=%d", validation_result, result); log_info(debug_validation_msg); // Generate error message based on validation result char error_message[512] = {0}; if (result != 0) { switch (validation_result) { case NOSTR_ERROR_INVALID_INPUT: strncpy(error_message, "invalid: malformed event structure", sizeof(error_message) - 1); break; case NOSTR_ERROR_EVENT_INVALID_SIGNATURE: strncpy(error_message, "invalid: signature verification failed", sizeof(error_message) - 1); break; case NOSTR_ERROR_EVENT_INVALID_ID: strncpy(error_message, "invalid: event id verification failed", sizeof(error_message) - 1); break; case NOSTR_ERROR_EVENT_INVALID_PUBKEY: strncpy(error_message, "invalid: invalid pubkey format", sizeof(error_message) - 1); break; case -103: // NOSTR_ERROR_EVENT_EXPIRED strncpy(error_message, "rejected: event expired", sizeof(error_message) - 1); break; case -102: // NOSTR_ERROR_NIP42_DISABLED strncpy(error_message, "auth-required: NIP-42 authentication required", sizeof(error_message) - 1); break; case -101: // NOSTR_ERROR_AUTH_REQUIRED strncpy(error_message, "blocked: pubkey not authorized", sizeof(error_message) - 1); break; default: strncpy(error_message, "error: validation failed", sizeof(error_message) - 1); break; } char debug_error_msg[256]; snprintf(debug_error_msg, sizeof(debug_error_msg), "DEBUG VALIDATION ERROR: %s", error_message); log_warning(debug_error_msg); } else { log_info("DEBUG VALIDATION: Event validated successfully using unified validator"); } // Cleanup event JSON string free(event_json_str); // Check for admin events (kinds 23455 and 23456) and intercept them if (result == 0) { cJSON* kind_obj = cJSON_GetObjectItem(event, "kind"); if (kind_obj && cJSON_IsNumber(kind_obj)) { int event_kind = (int)cJSON_GetNumberValue(kind_obj); log_info("DEBUG ADMIN: Checking if admin event processing is needed"); // Log reception of Kind 23455 and 23456 events if (event_kind == 23455 || event_kind == 23456) { char* event_json_debug = cJSON_Print(event); char debug_received_msg[1024]; snprintf(debug_received_msg, sizeof(debug_received_msg), "RECEIVED Kind %d event: %s", event_kind, event_json_debug ? event_json_debug : "Failed to serialize"); log_info(debug_received_msg); if (event_json_debug) { free(event_json_debug); } } if (event_kind == 23455 || event_kind == 23456) { // Enhanced admin event security - check authorization first log_info("DEBUG ADMIN: Admin event detected, checking authorization"); char auth_error[512] = {0}; int auth_result = is_authorized_admin_event(event, auth_error, sizeof(auth_error)); if (auth_result != 0) { // Authorization failed - log and reject log_warning("DEBUG ADMIN: Admin event authorization failed"); result = -1; size_t error_len = strlen(auth_error); size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1; memcpy(error_message, auth_error, copy_len); error_message[copy_len] = '\0'; char debug_auth_error_msg[600]; snprintf(debug_auth_error_msg, sizeof(debug_auth_error_msg), "DEBUG ADMIN AUTH ERROR: %.400s", auth_error); log_warning(debug_auth_error_msg); } else { // Authorization successful - process through admin API log_info("DEBUG ADMIN: Admin event authorized, processing through admin API"); char admin_error[512] = {0}; int admin_result = process_admin_event_in_config(event, admin_error, sizeof(admin_error), wsi); char debug_admin_msg[256]; snprintf(debug_admin_msg, sizeof(debug_admin_msg), "DEBUG ADMIN: process_admin_event_in_config returned %d", admin_result); log_info(debug_admin_msg); // Log results for Kind 23455 and 23456 events if (event_kind == 23455 || event_kind == 23456) { if (admin_result == 0) { char success_result_msg[256]; snprintf(success_result_msg, sizeof(success_result_msg), "SUCCESS: Kind %d event processed successfully", event_kind); log_success(success_result_msg); } else { char error_result_msg[512]; snprintf(error_result_msg, sizeof(error_result_msg), "ERROR: Kind %d event processing failed: %s", event_kind, admin_error); log_error(error_result_msg); } } if (admin_result != 0) { log_error("DEBUG ADMIN: Failed to process admin event through admin API"); result = -1; size_t error_len = strlen(admin_error); size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1; memcpy(error_message, admin_error, copy_len); error_message[copy_len] = '\0'; char debug_admin_error_msg[600]; snprintf(debug_admin_error_msg, sizeof(debug_admin_error_msg), "DEBUG ADMIN ERROR: %.400s", admin_error); log_error(debug_admin_error_msg); } else { log_success("DEBUG ADMIN: Admin event processed successfully through admin API"); // Admin events are processed by the admin API, not broadcast to subscriptions } } } else { // Regular event - store in database and broadcast log_info("DEBUG STORAGE: Regular event - storing in database"); if (store_event(event) != 0) { log_error("DEBUG STORAGE: Failed to store event in database"); result = -1; strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1); } else { log_info("DEBUG STORAGE: Event stored successfully in database"); // Broadcast event to matching persistent subscriptions int broadcast_count = broadcast_event_to_subscriptions(event); char debug_broadcast_msg[128]; snprintf(debug_broadcast_msg, sizeof(debug_broadcast_msg), "DEBUG BROADCAST: Event broadcast to %d subscriptions", broadcast_count); log_info(debug_broadcast_msg); } } } else { // Event without valid kind - try normal storage log_warning("DEBUG STORAGE: Event without valid kind - trying normal storage"); if (store_event(event) != 0) { log_error("DEBUG STORAGE: Failed to store event without kind in database"); result = -1; strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1); } else { log_info("DEBUG STORAGE: Event without kind stored successfully in database"); broadcast_event_to_subscriptions(event); } } } // Send OK response cJSON* event_id = cJSON_GetObjectItem(event, "id"); if (event_id && cJSON_IsString(event_id)) { cJSON* response = cJSON_CreateArray(); cJSON_AddItemToArray(response, cJSON_CreateString("OK")); cJSON_AddItemToArray(response, cJSON_CreateString(cJSON_GetStringValue(event_id))); cJSON_AddItemToArray(response, cJSON_CreateBool(result == 0)); cJSON_AddItemToArray(response, cJSON_CreateString(strlen(error_message) > 0 ? error_message : "")); // TODO: REPLACE - Remove wasteful cJSON_Print conversion char *response_str = cJSON_Print(response); if (response_str) { char debug_response_msg[512]; snprintf(debug_response_msg, sizeof(debug_response_msg), "DEBUG RESPONSE: Sending OK response: %s", response_str); log_info(debug_response_msg); size_t response_len = strlen(response_str); unsigned char *buf = malloc(LWS_PRE + response_len); if (buf) { memcpy(buf + LWS_PRE, response_str, response_len); int write_result = lws_write(wsi, buf + LWS_PRE, response_len, LWS_WRITE_TEXT); char debug_write_msg[128]; snprintf(debug_write_msg, sizeof(debug_write_msg), "DEBUG RESPONSE: lws_write returned %d", write_result); log_info(debug_write_msg); free(buf); } free(response_str); } cJSON_Delete(response); } } } else if (strcmp(msg_type, "REQ") == 0) { // Check NIP-42 authentication for REQ subscriptions if required if (pss && pss->nip42_auth_required_subscriptions && !pss->authenticated) { if (!pss->auth_challenge_sent) { send_nip42_auth_challenge(wsi, pss); } else { send_notice_message(wsi, "NIP-42 authentication required for subscriptions"); log_warning("REQ rejected: NIP-42 authentication required"); } cJSON_Delete(json); free(message); return 0; } // Handle REQ message cJSON* sub_id = cJSON_GetArrayItem(json, 1); if (sub_id && cJSON_IsString(sub_id)) { const char* subscription_id = cJSON_GetStringValue(sub_id); // Create array of filter objects from position 2 onwards cJSON* filters = cJSON_CreateArray(); int json_size = cJSON_GetArraySize(json); for (int i = 2; i < json_size; i++) { cJSON* filter = cJSON_GetArrayItem(json, i); if (filter) { cJSON_AddItemToArray(filters, cJSON_Duplicate(filter, 1)); } } handle_req_message(subscription_id, filters, wsi, pss); // Clean up the filters array we created cJSON_Delete(filters); // Send EOSE (End of Stored Events) cJSON* eose_response = cJSON_CreateArray(); cJSON_AddItemToArray(eose_response, cJSON_CreateString("EOSE")); cJSON_AddItemToArray(eose_response, cJSON_CreateString(subscription_id)); char *eose_str = cJSON_Print(eose_response); if (eose_str) { size_t eose_len = strlen(eose_str); unsigned char *buf = malloc(LWS_PRE + eose_len); if (buf) { memcpy(buf + LWS_PRE, eose_str, eose_len); lws_write(wsi, buf + LWS_PRE, eose_len, LWS_WRITE_TEXT); free(buf); } free(eose_str); } cJSON_Delete(eose_response); } } else if (strcmp(msg_type, "CLOSE") == 0) { // Handle CLOSE message cJSON* sub_id = cJSON_GetArrayItem(json, 1); if (sub_id && cJSON_IsString(sub_id)) { const char* subscription_id = cJSON_GetStringValue(sub_id); // Remove from global manager remove_subscription_from_manager(subscription_id, wsi); // Remove from session list if present if (pss) { pthread_mutex_lock(&pss->session_lock); subscription_t** current = &pss->subscriptions; while (*current) { if (strcmp((*current)->id, subscription_id) == 0) { subscription_t* to_remove = *current; *current = to_remove->session_next; pss->subscription_count--; break; } current = &((*current)->session_next); } pthread_mutex_unlock(&pss->session_lock); } char debug_msg[256]; snprintf(debug_msg, sizeof(debug_msg), "Closed subscription: %s", subscription_id); log_info(debug_msg); } } else if (strcmp(msg_type, "AUTH") == 0) { // Handle NIP-42 AUTH message if (cJSON_GetArraySize(json) >= 2) { cJSON* auth_payload = cJSON_GetArrayItem(json, 1); if (cJSON_IsString(auth_payload)) { // AUTH challenge response: ["AUTH", ] (unusual) handle_nip42_auth_challenge_response(wsi, pss, cJSON_GetStringValue(auth_payload)); } else if (cJSON_IsObject(auth_payload)) { // AUTH signed event: ["AUTH", ] (standard NIP-42) handle_nip42_auth_signed_event(wsi, pss, auth_payload); } else { send_notice_message(wsi, "Invalid AUTH message format"); log_warning("Received AUTH message with invalid payload type"); } } else { send_notice_message(wsi, "AUTH message requires payload"); log_warning("Received AUTH message without payload"); } } else { // Unknown message type char unknown_msg[128]; snprintf(unknown_msg, sizeof(unknown_msg), "Unknown message type: %.32s", msg_type); log_warning(unknown_msg); send_notice_message(wsi, "Unknown message type"); } } } if (json) cJSON_Delete(json); free(message); } } break; case LWS_CALLBACK_CLOSED: log_info("WebSocket connection closed"); // Clean up session subscriptions if (pss) { pthread_mutex_lock(&pss->session_lock); subscription_t* sub = pss->subscriptions; while (sub) { subscription_t* next = sub->session_next; remove_subscription_from_manager(sub->id, wsi); sub = next; } pss->subscriptions = NULL; pss->subscription_count = 0; pthread_mutex_unlock(&pss->session_lock); pthread_mutex_destroy(&pss->session_lock); } break; default: break; } return 0; } // WebSocket protocol definition static struct lws_protocols protocols[] = { { "nostr-relay-protocol", nostr_relay_callback, sizeof(struct per_session_data), 4096, // rx buffer size 0, NULL, 0 }, { NULL, NULL, 0, 0, 0, NULL, 0 } // terminator }; // Check if a port is available for binding int check_port_available(int port) { int sockfd; struct sockaddr_in addr; int result; // Create a socket sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { return 0; // Cannot create socket, assume port unavailable } // Set up the address structure memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY; addr.sin_port = htons(port); // Try to bind to the port result = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)); // Close the socket close(sockfd); // Return 1 if bind succeeded (port available), 0 if failed (port in use) return (result == 0) ? 1 : 0; } // Start libwebsockets-based WebSocket Nostr relay server int start_websocket_relay(int port_override, int strict_port) { struct lws_context_creation_info info; log_info("Starting libwebsockets-based Nostr relay server..."); memset(&info, 0, sizeof(info)); // Use port override if provided, otherwise use configuration int configured_port = (port_override > 0) ? port_override : get_config_int("relay_port", DEFAULT_PORT); int actual_port = configured_port; int port_attempts = 0; const int max_port_attempts = 10; // Increased from 5 to 10 // Minimal libwebsockets configuration info.protocols = protocols; info.gid = -1; info.uid = -1; info.options = LWS_SERVER_OPTION_VALIDATE_UTF8; // Remove interface restrictions - let system choose // info.vhost_name = NULL; // info.iface = NULL; // Increase max connections for relay usage info.max_http_header_pool = 16; info.timeout_secs = 10; // Max payload size for Nostr events info.max_http_header_data = 4096; // Find an available port with pre-checking (or fail immediately in strict mode) while (port_attempts < (strict_port ? 1 : max_port_attempts)) { char attempt_msg[256]; snprintf(attempt_msg, sizeof(attempt_msg), "Checking port availability: %d", actual_port); log_info(attempt_msg); // Pre-check if port is available if (!check_port_available(actual_port)) { port_attempts++; if (strict_port) { char error_msg[256]; snprintf(error_msg, sizeof(error_msg), "Strict port mode: port %d is not available", actual_port); log_error(error_msg); return -1; } else if (port_attempts < max_port_attempts) { char retry_msg[256]; snprintf(retry_msg, sizeof(retry_msg), "Port %d is in use, trying port %d (attempt %d/%d)", actual_port, actual_port + 1, port_attempts + 1, max_port_attempts); log_warning(retry_msg); actual_port++; continue; } else { char error_msg[512]; snprintf(error_msg, sizeof(error_msg), "Failed to find available port after %d attempts (tried ports %d-%d)", max_port_attempts, configured_port, actual_port); log_error(error_msg); return -1; } } // Port appears available, try creating libwebsockets context info.port = actual_port; char binding_msg[256]; snprintf(binding_msg, sizeof(binding_msg), "Attempting to bind libwebsockets to port %d", actual_port); log_info(binding_msg); ws_context = lws_create_context(&info); if (ws_context) { // Success! Port binding worked break; } // libwebsockets failed even though port check passed // This could be due to timing or different socket options int errno_saved = errno; char lws_error_msg[256]; snprintf(lws_error_msg, sizeof(lws_error_msg), "libwebsockets failed to bind to port %d (errno: %d)", actual_port, errno_saved); log_warning(lws_error_msg); port_attempts++; if (strict_port) { char error_msg[256]; snprintf(error_msg, sizeof(error_msg), "Strict port mode: failed to bind to port %d", actual_port); log_error(error_msg); break; } else if (port_attempts < max_port_attempts) { actual_port++; continue; } // If we get here, we've exhausted attempts break; } if (!ws_context) { char error_msg[512]; snprintf(error_msg, sizeof(error_msg), "Failed to create libwebsockets context after %d attempts. Last attempted port: %d", port_attempts, actual_port); log_error(error_msg); perror("libwebsockets creation error"); return -1; } char startup_msg[256]; if (actual_port != configured_port) { snprintf(startup_msg, sizeof(startup_msg), "WebSocket relay started on ws://127.0.0.1:%d (configured port %d was unavailable)", actual_port, configured_port); log_warning(startup_msg); } else { snprintf(startup_msg, sizeof(startup_msg), "WebSocket relay started on ws://127.0.0.1:%d", actual_port); } log_success(startup_msg); // Main event loop with proper signal handling while (g_server_running) { int result = lws_service(ws_context, 1000); if (result < 0) { log_error("libwebsockets service error"); break; } } log_info("Shutting down WebSocket server..."); lws_context_destroy(ws_context); ws_context = NULL; log_success("WebSocket relay shut down cleanly"); return 0; } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // MAIN PROGRAM ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // Print usage information void print_usage(const char* program_name) { printf("Usage: %s [OPTIONS]\n", program_name); printf("\n"); printf("C Nostr Relay Server - Event-Based Configuration\n"); printf("\n"); printf("Options:\n"); printf(" -h, --help Show this help message\n"); printf(" -v, --version Show version information\n"); printf(" -p, --port PORT Override relay port (first-time startup only)\n"); printf(" -a, --admin-privkey HEX Override admin private key (64-char hex)\n"); printf(" -r, --relay-privkey HEX Override relay private key (64-char hex)\n"); printf(" --strict-port Fail if exact port is unavailable (no port increment)\n"); printf("\n"); printf("Configuration:\n"); printf(" This relay uses event-based configuration stored in the database.\n"); printf(" On first startup, keys are automatically generated and printed once.\n"); printf(" Command line options like --port only apply during first-time setup.\n"); printf(" After initial setup, all configuration is managed via database events.\n"); printf(" Database file: .db (created automatically)\n"); printf("\n"); printf("Port Binding:\n"); printf(" Default: Try up to 10 consecutive ports if requested port is busy\n"); printf(" --strict-port: Fail immediately if exact requested port is unavailable\n"); printf("\n"); printf("Examples:\n"); printf(" %s # Start relay (auto-configure on first run)\n", program_name); printf(" %s -p 8080 # First-time setup with port 8080\n", program_name); printf(" %s --port 9000 # First-time setup with port 9000\n", program_name); printf(" %s --strict-port # Fail if default port 8888 is unavailable\n", program_name); printf(" %s -p 8080 --strict-port # Fail if port 8080 is unavailable\n", program_name); printf(" %s --help # Show this help\n", program_name); printf(" %s --version # Show version info\n", program_name); printf("\n"); } // Print version information void print_version() { printf("C Nostr Relay Server v1.0.0\n"); printf("Event-based configuration system\n"); printf("Built with nostr_core_lib integration\n"); printf("\n"); } int main(int argc, char* argv[]) { // Initialize CLI options structure cli_options_t cli_options = { .port_override = -1, // -1 = not set .admin_privkey_override = {0}, // Empty string = not set .relay_privkey_override = {0}, // Empty string = not set .strict_port = 0 // 0 = allow port increment (default) }; // Parse command line arguments for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { print_usage(argv[0]); return 0; } else if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--version") == 0) { print_version(); return 0; } else if (strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "--port") == 0) { // Port override option if (i + 1 >= argc) { log_error("Port option requires a value. Use --help for usage information."); print_usage(argv[0]); return 1; } // Parse port number char* endptr; long port = strtol(argv[i + 1], &endptr, 10); if (endptr == argv[i + 1] || *endptr != '\0' || port < 1 || port > 65535) { log_error("Invalid port number. Port must be between 1 and 65535."); print_usage(argv[0]); return 1; } cli_options.port_override = (int)port; i++; // Skip the port argument char port_msg[128]; snprintf(port_msg, sizeof(port_msg), "Port override specified: %d", cli_options.port_override); log_info(port_msg); } else if (strcmp(argv[i], "-a") == 0 || strcmp(argv[i], "--admin-privkey") == 0) { // Admin private key override option if (i + 1 >= argc) { log_error("Admin privkey option requires a value. Use --help for usage information."); print_usage(argv[0]); return 1; } // Validate private key format (must be 64 hex characters) if (strlen(argv[i + 1]) != 64) { log_error("Invalid admin private key length. Must be exactly 64 hex characters."); print_usage(argv[0]); return 1; } // Validate hex format for (int j = 0; j < 64; j++) { char c = argv[i + 1][j]; if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { log_error("Invalid admin private key format. Must contain only hex characters (0-9, a-f, A-F)."); print_usage(argv[0]); return 1; } } strncpy(cli_options.admin_privkey_override, argv[i + 1], sizeof(cli_options.admin_privkey_override) - 1); cli_options.admin_privkey_override[sizeof(cli_options.admin_privkey_override) - 1] = '\0'; i++; // Skip the key argument log_info("Admin private key override specified"); } else if (strcmp(argv[i], "-r") == 0 || strcmp(argv[i], "--relay-privkey") == 0) { // Relay private key override option if (i + 1 >= argc) { log_error("Relay privkey option requires a value. Use --help for usage information."); print_usage(argv[0]); return 1; } // Validate private key format (must be 64 hex characters) if (strlen(argv[i + 1]) != 64) { log_error("Invalid relay private key length. Must be exactly 64 hex characters."); print_usage(argv[0]); return 1; } // Validate hex format for (int j = 0; j < 64; j++) { char c = argv[i + 1][j]; if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { log_error("Invalid relay private key format. Must contain only hex characters (0-9, a-f, A-F)."); print_usage(argv[0]); return 1; } } strncpy(cli_options.relay_privkey_override, argv[i + 1], sizeof(cli_options.relay_privkey_override) - 1); cli_options.relay_privkey_override[sizeof(cli_options.relay_privkey_override) - 1] = '\0'; i++; // Skip the key argument log_info("Relay private key override specified"); } else if (strcmp(argv[i], "--strict-port") == 0) { // Strict port mode option cli_options.strict_port = 1; log_info("Strict port mode enabled - will fail if exact port is unavailable"); } else { log_error("Unknown argument. Use --help for usage information."); print_usage(argv[0]); return 1; } } // Set up signal handlers signal(SIGINT, signal_handler); signal(SIGTERM, signal_handler); printf(BLUE BOLD "=== C Nostr Relay Server ===" RESET "\n"); // Initialize nostr library FIRST (required for key generation and event creation) if (nostr_init() != 0) { log_error("Failed to initialize nostr library"); return 1; } // Check if this is first-time startup or existing relay if (is_first_time_startup()) { log_info("First-time startup detected"); // Initialize event-based configuration system if (init_configuration_system(NULL, NULL) != 0) { log_error("Failed to initialize event-based configuration system"); nostr_cleanup(); return 1; } // Run first-time startup sequence (generates keys, sets up database path, but doesn't store private key yet) if (first_time_startup_sequence(&cli_options) != 0) { log_error("Failed to complete first-time startup sequence"); cleanup_configuration_system(); nostr_cleanup(); return 1; } // Initialize database with the generated relay pubkey if (init_database(g_database_path) != 0) { log_error("Failed to initialize database after first-time setup"); cleanup_configuration_system(); nostr_cleanup(); return 1; } // Now that database is available, store the relay private key securely const char* relay_privkey = get_temp_relay_private_key(); if (relay_privkey) { if (store_relay_private_key(relay_privkey) != 0) { log_error("Failed to store relay private key securely after database initialization"); cleanup_configuration_system(); nostr_cleanup(); return 1; } log_success("Relay private key stored securely in database"); } else { log_error("Relay private key not available from first-time startup"); cleanup_configuration_system(); nostr_cleanup(); return 1; } // Systematically add pubkeys to config table if (add_pubkeys_to_config_table() != 0) { log_warning("Failed to add pubkeys to config table systematically"); } else { log_success("Pubkeys added to config table systematically"); } // Retry storing the configuration event now that database is initialized if (retry_store_initial_config_event() != 0) { log_warning("Failed to store initial configuration event after database init"); } // Now store the pubkeys in config table since database is available const char* admin_pubkey = get_admin_pubkey_cached(); const char* relay_pubkey_from_cache = get_relay_pubkey_cached(); if (admin_pubkey && strlen(admin_pubkey) == 64) { set_config_value_in_table("admin_pubkey", admin_pubkey, "string", "Administrator public key", "authentication", 0); log_success("Admin pubkey stored in config table for first-time startup"); } if (relay_pubkey_from_cache && strlen(relay_pubkey_from_cache) == 64) { set_config_value_in_table("relay_pubkey", relay_pubkey_from_cache, "string", "Relay public key", "relay", 0); log_success("Relay pubkey stored in config table for first-time startup"); } } else { log_info("Existing relay detected"); // Find existing database file char** existing_files = find_existing_db_files(); if (!existing_files || !existing_files[0]) { log_error("No existing relay database found"); nostr_cleanup(); return 1; } // Extract relay pubkey from filename char* relay_pubkey = extract_pubkey_from_filename(existing_files[0]); if (!relay_pubkey) { log_error("Failed to extract relay pubkey from database filename"); // Free the files array for (int i = 0; existing_files[i]; i++) { free(existing_files[i]); } free(existing_files); nostr_cleanup(); return 1; } // Initialize event-based configuration system if (init_configuration_system(NULL, NULL) != 0) { log_error("Failed to initialize event-based configuration system"); free(relay_pubkey); for (int i = 0; existing_files[i]; i++) { free(existing_files[i]); } free(existing_files); nostr_cleanup(); return 1; } // Setup existing relay (sets database path and loads config) if (startup_existing_relay(relay_pubkey) != 0) { log_error("Failed to setup existing relay"); cleanup_configuration_system(); free(relay_pubkey); for (int i = 0; existing_files[i]; i++) { free(existing_files[i]); } free(existing_files); nostr_cleanup(); return 1; } // Initialize database with existing database path if (init_database(g_database_path) != 0) { log_error("Failed to initialize existing database"); cleanup_configuration_system(); free(relay_pubkey); for (int i = 0; existing_files[i]; i++) { free(existing_files[i]); } free(existing_files); nostr_cleanup(); return 1; } // Load configuration from database cJSON* config_event = load_config_event_from_database(relay_pubkey); if (config_event) { if (apply_configuration_from_event(config_event) != 0) { log_warning("Failed to apply configuration from database"); } else { log_success("Configuration loaded from database"); // Extract admin pubkey from the config event and store in config table for unified cache access cJSON* pubkey_obj = cJSON_GetObjectItem(config_event, "pubkey"); const char* admin_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : NULL; // Store both admin and relay pubkeys in config table for unified cache if (admin_pubkey && strlen(admin_pubkey) == 64) { set_config_value_in_table("admin_pubkey", admin_pubkey, "string", "Administrator public key", "authentication", 0); log_info("Admin pubkey stored in config table for existing relay"); } if (relay_pubkey && strlen(relay_pubkey) == 64) { set_config_value_in_table("relay_pubkey", relay_pubkey, "string", "Relay public key", "relay", 0); log_info("Relay pubkey stored in config table for existing relay"); } } cJSON_Delete(config_event); } else { log_warning("No configuration event found in existing database"); } // Free memory free(relay_pubkey); for (int i = 0; existing_files[i]; i++) { free(existing_files[i]); } free(existing_files); } // Verify database is now available if (!g_db) { log_error("Database not available after initialization"); cleanup_configuration_system(); nostr_cleanup(); return 1; } // Configuration system is now fully initialized with event-based approach // All configuration is loaded from database events // Initialize unified request validator system if (ginxsom_request_validator_init(g_database_path, "c-relay") != 0) { log_error("Failed to initialize unified request validator"); cleanup_configuration_system(); nostr_cleanup(); close_database(); return 1; } log_success("Unified request validator initialized"); // Initialize NIP-11 relay information init_relay_info(); // Initialize NIP-13 PoW configuration init_pow_config(); // Initialize NIP-40 expiration configuration init_expiration_config(); // Update subscription manager configuration update_subscription_manager_config(); log_info("Starting relay server..."); // Start WebSocket Nostr relay server (port from configuration) int result = start_websocket_relay(-1, cli_options.strict_port); // Let config system determine port, pass strict_port flag // Cleanup cleanup_relay_info(); ginxsom_request_validator_cleanup(); cleanup_configuration_system(); nostr_cleanup(); close_database(); if (result == 0) { log_success("Server shutdown complete"); } else { log_error("Server shutdown with errors"); } return result; }