1602 lines
65 KiB
C
1602 lines
65 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
|
|
#include "websockets.h" // WebSocket protocol implementation
|
|
#include "subscriptions.h" // Subscription management system
|
|
|
|
// 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
|
|
int g_server_running = 1; // Non-static so websockets.c can access it
|
|
struct lws_context *ws_context = NULL; // Non-static so websockets.c can access it
|
|
|
|
// 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-40 Expiration configuration (now in nip040.c)
|
|
extern struct expiration_config g_expiration_config;
|
|
|
|
// Global subscription manager instance (defined in websockets.c)
|
|
extern subscription_manager_t g_subscription_manager;
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////////////
|
|
/////////////////////////////////////////////////////////////////////////////////////////
|
|
// DATA STRUCTURES
|
|
/////////////////////////////////////////////////////////////////////////////////////////
|
|
/////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 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 (kind 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 declaration for WebSocket relay server
|
|
int start_websocket_relay(int port_override, int strict_port);
|
|
|
|
|
|
// Forward declarations for NIP-13 PoW handling (now in nip013.c)
|
|
void init_pow_config();
|
|
int validate_event_pow(cJSON* event, char* error_message, size_t error_size);
|
|
|
|
// Forward declarations for NIP-40 expiration handling (now in nip040.c)
|
|
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);
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////////////
|
|
/////////////////////////////////////////////////////////////////////////////////////////
|
|
// 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);
|
|
}
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////////////
|
|
/////////////////////////////////////////////////////////////////////////////////////////
|
|
// 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 - exclude ephemeral events (kinds 20000-29999) from historical queries
|
|
char sql[1024] = "SELECT id, pubkey, created_at, kind, content, sig, tags FROM events WHERE 1=1 AND kind < 20000";
|
|
char* sql_ptr = sql + strlen(sql);
|
|
int remaining = sizeof(sql) - strlen(sql);
|
|
|
|
// Note: Expiration filtering will be done at application level
|
|
// after retrieving events to ensure compatibility with all SQLite versions
|
|
|
|
// Handle kinds filter
|
|
cJSON* kinds = cJSON_GetObjectItem(filter, "kinds");
|
|
if (kinds && cJSON_IsArray(kinds)) {
|
|
int kind_count = cJSON_GetArraySize(kinds);
|
|
if (kind_count > 0) {
|
|
snprintf(sql_ptr, remaining, " AND kind IN (");
|
|
sql_ptr += strlen(sql_ptr);
|
|
remaining = sizeof(sql) - strlen(sql);
|
|
|
|
for (int k = 0; k < kind_count; k++) {
|
|
cJSON* kind = cJSON_GetArrayItem(kinds, k);
|
|
if (cJSON_IsNumber(kind)) {
|
|
if (k > 0) {
|
|
snprintf(sql_ptr, remaining, ",");
|
|
sql_ptr++;
|
|
remaining--;
|
|
}
|
|
snprintf(sql_ptr, remaining, "%d", (int)cJSON_GetNumberValue(kind));
|
|
sql_ptr += strlen(sql_ptr);
|
|
remaining = sizeof(sql) - strlen(sql);
|
|
}
|
|
}
|
|
snprintf(sql_ptr, remaining, ")");
|
|
sql_ptr += strlen(sql_ptr);
|
|
remaining = sizeof(sql) - strlen(sql);
|
|
}
|
|
}
|
|
|
|
// Handle authors filter
|
|
cJSON* authors = cJSON_GetObjectItem(filter, "authors");
|
|
if (authors && cJSON_IsArray(authors)) {
|
|
int author_count = cJSON_GetArraySize(authors);
|
|
if (author_count > 0) {
|
|
snprintf(sql_ptr, remaining, " AND pubkey IN (");
|
|
sql_ptr += strlen(sql_ptr);
|
|
remaining = sizeof(sql) - strlen(sql);
|
|
|
|
for (int a = 0; a < author_count; a++) {
|
|
cJSON* author = cJSON_GetArrayItem(authors, a);
|
|
if (cJSON_IsString(author)) {
|
|
if (a > 0) {
|
|
snprintf(sql_ptr, remaining, ",");
|
|
sql_ptr++;
|
|
remaining--;
|
|
}
|
|
snprintf(sql_ptr, remaining, "'%s'", cJSON_GetStringValue(author));
|
|
sql_ptr += strlen(sql_ptr);
|
|
remaining = sizeof(sql) - strlen(sql);
|
|
}
|
|
}
|
|
snprintf(sql_ptr, remaining, ")");
|
|
sql_ptr += strlen(sql_ptr);
|
|
remaining = sizeof(sql) - strlen(sql);
|
|
}
|
|
}
|
|
|
|
// Handle ids filter
|
|
cJSON* ids = cJSON_GetObjectItem(filter, "ids");
|
|
if (ids && cJSON_IsArray(ids)) {
|
|
int id_count = cJSON_GetArraySize(ids);
|
|
if (id_count > 0) {
|
|
snprintf(sql_ptr, remaining, " AND id IN (");
|
|
sql_ptr += strlen(sql_ptr);
|
|
remaining = sizeof(sql) - strlen(sql);
|
|
|
|
for (int i = 0; i < id_count; i++) {
|
|
cJSON* id = cJSON_GetArrayItem(ids, i);
|
|
if (cJSON_IsString(id)) {
|
|
if (i > 0) {
|
|
snprintf(sql_ptr, remaining, ",");
|
|
sql_ptr++;
|
|
remaining--;
|
|
}
|
|
snprintf(sql_ptr, remaining, "'%s'", cJSON_GetStringValue(id));
|
|
sql_ptr += strlen(sql_ptr);
|
|
remaining = sizeof(sql) - strlen(sql);
|
|
}
|
|
}
|
|
snprintf(sql_ptr, remaining, ")");
|
|
sql_ptr += strlen(sql_ptr);
|
|
remaining = sizeof(sql) - strlen(sql);
|
|
}
|
|
}
|
|
|
|
// Handle 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 != 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;
|
|
}
|
|
|
|
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////////////
|
|
/////////////////////////////////////////////////////////////////////////////////////////
|
|
// 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(" --strict-port Fail if exact port is unavailable (no port increment)\n");
|
|
printf(" -a, --admin-pubkey HEX Override admin public key (64-char hex)\n");
|
|
printf(" -r, --relay-privkey HEX Override relay private key (64-char hex)\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_pubkey_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-pubkey") == 0) {
|
|
// Admin public key override option
|
|
if (i + 1 >= argc) {
|
|
log_error("Admin pubkey option requires a value. Use --help for usage information.");
|
|
print_usage(argv[0]);
|
|
return 1;
|
|
}
|
|
|
|
// Validate public key format (must be 64 hex characters)
|
|
if (strlen(argv[i + 1]) != 64) {
|
|
log_error("Invalid admin public 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 public key format. Must contain only hex characters (0-9, a-f, A-F).");
|
|
print_usage(argv[0]);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
strncpy(cli_options.admin_pubkey_override, argv[i + 1], sizeof(cli_options.admin_pubkey_override) - 1);
|
|
cli_options.admin_pubkey_override[sizeof(cli_options.admin_pubkey_override) - 1] = '\0';
|
|
i++; // Skip the key argument
|
|
|
|
log_info("Admin public 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;
|
|
}
|
|
|
|
// Handle configuration setup after database is initialized
|
|
if (cli_options.admin_pubkey_override && strlen(cli_options.admin_pubkey_override) == 64) {
|
|
// Admin pubkey provided - populate config table directly
|
|
log_info("Populating config table for admin pubkey override after database initialization");
|
|
|
|
// Populate default config values in table
|
|
if (populate_default_config_values() != 0) {
|
|
log_error("Failed to populate default config values");
|
|
cleanup_configuration_system();
|
|
nostr_cleanup();
|
|
close_database();
|
|
return 1;
|
|
}
|
|
|
|
// Add pubkeys to config table
|
|
if (add_pubkeys_to_config_table() != 0) {
|
|
log_error("Failed to add pubkeys to config table");
|
|
cleanup_configuration_system();
|
|
nostr_cleanup();
|
|
close_database();
|
|
return 1;
|
|
}
|
|
|
|
log_success("Configuration populated directly in config table after database initialization");
|
|
} else {
|
|
// Admin private key available - retry storing initial config event
|
|
if (retry_store_initial_config_event() != 0) {
|
|
log_warning("Failed to store initial config event - will retry later");
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
} |