v0.3.5 - nip42 implemented

This commit is contained in:
Your Name
2025-09-13 08:49:09 -04:00
parent 1690b58c67
commit f3d6afead1
24 changed files with 3705 additions and 532 deletions

View File

@@ -22,6 +22,16 @@
#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"
@@ -73,7 +83,7 @@ struct pow_config {
};
// Global PoW configuration instance
static struct pow_config g_pow_config = {
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,
@@ -93,12 +103,12 @@ struct expiration_config {
};
// Global expiration configuration instance
static struct expiration_config g_expiration_config = {
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 = 300 // 5 minutes grace period for clock skew
.grace_period = 1 // 1 second grace period for testing (was 300)
};
@@ -145,13 +155,22 @@ struct subscription {
struct subscription* session_next; // Next subscription for this session
};
// Enhanced per-session data with subscription management
// 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
@@ -202,12 +221,20 @@ int check_and_handle_replaceable_event(int kind, const char* pubkey, long create
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 configuration event handling (kind 33334)
int handle_configuration_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);
@@ -926,24 +953,39 @@ void update_subscription_events_sent(const char* sub_id, int events_sent) {
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
// 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) {
printf(BLUE "[INFO]" RESET " %s\n", message);
char timestamp[32];
get_timestamp_string(timestamp, sizeof(timestamp));
printf("[%s] " BLUE "[INFO]" RESET " %s\n", timestamp, message);
fflush(stdout);
}
void log_success(const char* message) {
printf(GREEN "[SUCCESS]" RESET " %s\n", message);
char timestamp[32];
get_timestamp_string(timestamp, sizeof(timestamp));
printf("[%s] " GREEN "[SUCCESS]" RESET " %s\n", timestamp, message);
fflush(stdout);
}
void log_error(const char* message) {
printf(RED "[ERROR]" RESET " %s\n", message);
char timestamp[32];
get_timestamp_string(timestamp, sizeof(timestamp));
printf("[%s] " RED "[ERROR]" RESET " %s\n", timestamp, message);
fflush(stdout);
}
void log_warning(const char* message) {
printf(YELLOW "[WARNING]" RESET " %s\n", message);
char timestamp[32];
get_timestamp_string(timestamp, sizeof(timestamp));
printf("[%s] " YELLOW "[WARNING]" RESET " %s\n", timestamp, message);
fflush(stdout);
}
@@ -997,6 +1039,146 @@ void send_notice_message(struct lws* wsi, const char* message) {
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
@@ -1345,6 +1527,7 @@ void init_relay_info() {
cJSON_AddItemToArray(g_relay_info.supported_nips, cJSON_CreateNumber(15)); // NIP-15: EOSE
cJSON_AddItemToArray(g_relay_info.supported_nips, cJSON_CreateNumber(20)); // NIP-20: Command results
cJSON_AddItemToArray(g_relay_info.supported_nips, cJSON_CreateNumber(40)); // NIP-40: Expiration Timestamp
cJSON_AddItemToArray(g_relay_info.supported_nips, cJSON_CreateNumber(42)); // NIP-42: Authentication
}
// Initialize server limitations using configuration
@@ -1840,7 +2023,7 @@ void init_expiration_config() {
g_expiration_config.strict_mode = get_config_bool("expiration_strict", 1);
g_expiration_config.filter_responses = get_config_bool("expiration_filter", 1);
g_expiration_config.delete_expired = get_config_bool("expiration_delete", 0);
g_expiration_config.grace_period = get_config_int("expiration_grace_period", 300);
g_expiration_config.grace_period = get_config_int("expiration_grace_period", 1);
// Validate grace period bounds
if (g_expiration_config.grace_period < 0 || g_expiration_config.grace_period > 86400) {
@@ -1876,6 +2059,30 @@ long extract_expiration_timestamp(cJSON* tags) {
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;
@@ -1980,22 +2187,106 @@ int init_database(const char* database_path_override) {
sqlite3_finalize(check_stmt);
if (has_events_table) {
log_info("Database schema already exists, skipping initialization");
log_info("Database schema already exists, checking version");
// Log existing schema version if available
// 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) {
const char* db_version = (char*)sqlite3_column_text(version_stmt, 0);
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");
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
@@ -2557,6 +2848,11 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
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;
}
@@ -2598,133 +2894,7 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
return events_sent;
}
// Handle EVENT message (publish)
int handle_event_message(cJSON* event, char* error_message, size_t error_size) {
log_info("Handling EVENT message with full NIP-01 validation");
if (!event) {
snprintf(error_message, error_size, "invalid: null event");
return NOSTR_ERROR_INVALID_INPUT;
}
// Step 1: Validate event structure
int structure_result = nostr_validate_event_structure(event);
if (structure_result != NOSTR_SUCCESS) {
switch (structure_result) {
case NOSTR_ERROR_EVENT_INVALID_STRUCTURE:
snprintf(error_message, error_size, "invalid: malformed event structure");
break;
case NOSTR_ERROR_EVENT_INVALID_ID:
snprintf(error_message, error_size, "invalid: invalid event id format");
break;
case NOSTR_ERROR_EVENT_INVALID_PUBKEY:
snprintf(error_message, error_size, "invalid: invalid pubkey format");
break;
case NOSTR_ERROR_EVENT_INVALID_CREATED_AT:
snprintf(error_message, error_size, "invalid: invalid created_at timestamp");
break;
case NOSTR_ERROR_EVENT_INVALID_KIND:
snprintf(error_message, error_size, "invalid: invalid event kind");
break;
case NOSTR_ERROR_EVENT_INVALID_TAGS:
snprintf(error_message, error_size, "invalid: invalid tags format");
break;
case NOSTR_ERROR_EVENT_INVALID_CONTENT:
snprintf(error_message, error_size, "invalid: invalid content");
break;
default:
snprintf(error_message, error_size, "invalid: event structure validation failed");
}
return structure_result;
}
// Step 2: Verify event signature
int signature_result = nostr_verify_event_signature(event);
if (signature_result != NOSTR_SUCCESS) {
if (signature_result == NOSTR_ERROR_EVENT_INVALID_SIGNATURE) {
snprintf(error_message, error_size, "invalid: event signature verification failed");
} else if (signature_result == NOSTR_ERROR_EVENT_INVALID_ID) {
snprintf(error_message, error_size, "invalid: event id does not match computed hash");
} else {
snprintf(error_message, error_size, "invalid: cryptographic validation failed");
}
return signature_result;
}
// Step 3: Validate Proof of Work (NIP-13) if enabled
int pow_result = validate_event_pow(event, error_message, error_size);
if (pow_result != 0) {
return pow_result; // PoW validation failed, error message already set
}
// Step 4: Validate expiration timestamp (NIP-40) if enabled
int expiration_result = validate_event_expiration(event, error_message, error_size);
if (expiration_result != 0) {
return expiration_result; // Expiration validation failed, error message already set
}
// Step 5: Complete event validation (combines structure + signature + additional checks)
int validation_result = nostr_validate_event(event);
if (validation_result != NOSTR_SUCCESS) {
snprintf(error_message, error_size, "invalid: complete event validation failed");
return validation_result;
}
// Step 6: Check for special event types and handle accordingly
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
cJSON* created_at_obj = cJSON_GetObjectItem(event, "created_at");
if (kind_obj && pubkey_obj && created_at_obj) {
int kind = (int)cJSON_GetNumberValue(kind_obj);
const char* pubkey = cJSON_GetStringValue(pubkey_obj);
long created_at = (long)cJSON_GetNumberValue(created_at_obj);
// NIP-09: Handle deletion requests (kind 5)
if (kind == 5) {
return handle_deletion_request(event, error_message, error_size);
}
// Kind 33334: Handle configuration events
if (kind == 33334) {
return handle_configuration_event(event, error_message, error_size);
}
// Handle replaceable events (NIP-01)
event_type_t event_type = classify_event_kind(kind);
if (event_type == EVENT_TYPE_REPLACEABLE) {
// For replaceable events, check if we have a newer version
if (check_and_handle_replaceable_event(kind, pubkey, created_at) < 0) {
snprintf(error_message, error_size, "duplicate: older replaceable event ignored");
return -2; // Special code for duplicate/older event
}
} else if (event_type == EVENT_TYPE_ADDRESSABLE) {
// For addressable events, check d tag
cJSON* tags = cJSON_GetObjectItem(event, "tags");
if (tags && cJSON_IsArray(tags)) {
const char* d_tag_value = extract_d_tag_value(tags);
if (check_and_handle_addressable_event(kind, pubkey, d_tag_value, created_at) < 0) {
snprintf(error_message, error_size, "duplicate: older addressable event ignored");
return -2;
}
}
} else if (event_type == EVENT_TYPE_EPHEMERAL) {
// Ephemeral events should not be stored
error_message[0] = '\0'; // Success but no storage - empty error message
return 0; // Accept but don't store
}
}
// Step 7: Store event in database
if (store_event(event) == 0) {
error_message[0] = '\0'; // Success - empty error message
log_success("Event validated and stored successfully");
return 0;
}
snprintf(error_message, error_size, "error: failed to store event in database");
return -1;
}
@@ -2781,9 +2951,22 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
log_info("WebSocket connection established");
memset(pss, 0, sizeof(*pss));
pthread_mutex_init(&pss->session_lock, NULL);
// TODO: Get real client IP address
strncpy(pss->client_ip, "127.0.0.1", CLIENT_IP_MAX_LENGTH - 1);
// Get real client IP address
char client_ip[CLIENT_IP_MAX_LENGTH];
lws_get_peer_simple(wsi, client_ip, sizeof(client_ip));
strncpy(pss->client_ip, client_ip, CLIENT_IP_MAX_LENGTH - 1);
pss->client_ip[CLIENT_IP_MAX_LENGTH - 1] = '\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:
@@ -2804,15 +2987,123 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
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;
// 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);
if (pss && auth_required && !pss->authenticated) {
if (!pss->auth_challenge_sent) {
send_nip42_auth_challenge(wsi, pss);
} else {
char auth_msg[256];
if (event_kind == 4 || event_kind == 14) {
snprintf(auth_msg, sizeof(auth_msg),
"NIP-42 authentication required for direct message events (kind %d)", event_kind);
} else {
snprintf(auth_msg, sizeof(auth_msg),
"NIP-42 authentication required for event kind %d", event_kind);
}
send_notice_message(wsi, auth_msg);
log_warning("Event rejected: NIP-42 authentication required for kind");
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)) {
char error_message[512] = {0};
int result = handle_event_message(event, error_message, sizeof(error_message));
// 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;
}
// Broadcast event to matching persistent subscriptions
// Call unified validator with JSON string
size_t event_json_len = strlen(event_json_str);
int validation_result = nostr_validate_unified_request(event_json_str, event_json_len);
// Map validation result to old result format (0 = success, -1 = failure)
int result = (validation_result == NOSTR_SUCCESS) ? 0 : -1;
// Generate error message based on validation result
char error_message[512] = {0};
if (result != 0) {
switch (validation_result) {
case NOSTR_ERROR_INVALID_INPUT:
strncpy(error_message, "invalid: malformed event structure", sizeof(error_message) - 1);
break;
case NOSTR_ERROR_EVENT_INVALID_SIGNATURE:
strncpy(error_message, "invalid: signature verification failed", sizeof(error_message) - 1);
break;
case NOSTR_ERROR_EVENT_INVALID_ID:
strncpy(error_message, "invalid: event id verification failed", sizeof(error_message) - 1);
break;
case NOSTR_ERROR_EVENT_INVALID_PUBKEY:
strncpy(error_message, "invalid: invalid pubkey format", sizeof(error_message) - 1);
break;
case -103: // NOSTR_ERROR_EVENT_EXPIRED
strncpy(error_message, "rejected: event expired", sizeof(error_message) - 1);
break;
case -102: // NOSTR_ERROR_NIP42_DISABLED
strncpy(error_message, "auth-required: NIP-42 authentication required", sizeof(error_message) - 1);
break;
case -101: // NOSTR_ERROR_AUTH_REQUIRED
strncpy(error_message, "blocked: pubkey not authorized", sizeof(error_message) - 1);
break;
default:
strncpy(error_message, "error: validation failed", sizeof(error_message) - 1);
break;
}
} else {
log_info("Event validated successfully using unified validator");
}
// Cleanup event JSON string
free(event_json_str);
// Store event in database and broadcast to subscriptions
if (result == 0) {
broadcast_event_to_subscriptions(event);
// Store the event in the database first
if (store_event(event) != 0) {
log_error("Failed to store event in database");
result = -1;
strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1);
} else {
log_info("Event stored successfully in database");
// Broadcast event to matching persistent subscriptions
broadcast_event_to_subscriptions(event);
}
}
// Send OK response
@@ -2824,6 +3115,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
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) {
size_t response_len = strlen(response_str);
@@ -2839,6 +3131,19 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
}
}
} 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);
@@ -2909,6 +3214,31 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
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");
}
}
}
@@ -3114,9 +3444,6 @@ int start_websocket_relay(int port_override) {
return 0;
}
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
// MAIN PROGRAM
@@ -3133,6 +3460,8 @@ void print_usage(const char* program_name) {
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("\n");
printf("Configuration:\n");
printf(" This relay uses event-based configuration stored in the database.\n");
@@ -3161,7 +3490,9 @@ void print_version() {
int main(int argc, char* argv[]) {
// Initialize CLI options structure
cli_options_t cli_options = {
.port_override = -1 // -1 = not set
.port_override = -1, // -1 = not set
.admin_privkey_override = {0}, // Empty string = not set
.relay_privkey_override = {0} // Empty string = not set
};
// Parse command line arguments
@@ -3196,6 +3527,66 @@ int main(int argc, char* argv[]) {
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 {
log_error("Unknown argument. Use --help for usage information.");
print_usage(argv[0]);
@@ -3208,7 +3599,7 @@ int main(int argc, char* argv[]) {
signal(SIGTERM, signal_handler);
printf(BLUE BOLD "=== C Nostr Relay Server ===" RESET "\n");
printf("Event-based configuration system\n\n");
// Initialize nostr library FIRST (required for key generation and event creation)
if (nostr_init() != 0) {
@@ -3358,6 +3749,16 @@ int main(int argc, char* argv[]) {
// 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();
@@ -3377,6 +3778,7 @@ int main(int argc, char* argv[]) {
// Cleanup
cleanup_relay_info();
ginxsom_request_validator_cleanup();
cleanup_configuration_system();
nostr_cleanup();
close_database();