v0.3.5 - nip42 implemented
This commit is contained in:
704
src/main.c
704
src/main.c
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user