Files
c-relay/src/main.c
2025-09-27 15:50:42 -04:00

4226 lines
184 KiB
C

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <time.h>
#include <pthread.h>
#include <sqlite3.h>
#include <libwebsockets.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// Include nostr_core_lib for Nostr functionality
#include "../nostr_core_lib/cjson/cJSON.h"
#include "../nostr_core_lib/nostr_core/nostr_core.h"
#include "../nostr_core_lib/nostr_core/nip013.h" // NIP-13: Proof of Work
#include "config.h" // Configuration management system
#include "sql_schema.h" // Embedded database schema
// 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", <challenge>]
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", <challenge>] (unusual)
handle_nip42_auth_challenge_response(wsi, pss, cJSON_GetStringValue(auth_payload));
} else if (cJSON_IsObject(auth_payload)) {
// AUTH signed event: ["AUTH", <event>] (standard NIP-42)
handle_nip42_auth_signed_event(wsi, pss, auth_payload);
} else {
send_notice_message(wsi, "Invalid AUTH message format");
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: <relay_pubkey>.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;
}