v0.2.1 - Nip-40 implemented
This commit is contained in:
205
src/main.c
205
src/main.c
@@ -97,6 +97,24 @@ static struct pow_config g_pow_config = {
|
||||
.anti_spam_mode = 0 // Basic validation by default
|
||||
};
|
||||
|
||||
// NIP-40 Expiration configuration structure
|
||||
struct expiration_config {
|
||||
int enabled; // 0 = disabled, 1 = enabled
|
||||
int strict_mode; // 1 = reject expired events on submission
|
||||
int filter_responses; // 1 = filter expired events from responses
|
||||
int delete_expired; // 1 = delete expired events from DB (future feature)
|
||||
long grace_period; // Grace period in seconds for clock skew
|
||||
};
|
||||
|
||||
// Global expiration configuration instance
|
||||
static 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
|
||||
};
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -217,6 +235,12 @@ int handle_nip11_http_request(struct lws* wsi, const char* accept_header);
|
||||
void init_pow_config();
|
||||
int validate_event_pow(cJSON* event, char* error_message, size_t error_size);
|
||||
|
||||
// Forward declarations for NIP-40 expiration handling
|
||||
void init_expiration_config();
|
||||
long extract_expiration_timestamp(cJSON* tags);
|
||||
int is_event_expired(cJSON* event, time_t current_time);
|
||||
int validate_event_expiration(cJSON* event, char* error_message, size_t error_size);
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -651,6 +675,19 @@ int broadcast_event_to_subscriptions(cJSON* event) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check if event is expired and should not be broadcast (NIP-40)
|
||||
if (g_expiration_config.enabled && g_expiration_config.filter_responses) {
|
||||
time_t current_time = time(NULL);
|
||||
if (is_event_expired(event, current_time)) {
|
||||
char debug_msg[256];
|
||||
cJSON* event_id_obj = cJSON_GetObjectItem(event, "id");
|
||||
const char* event_id = event_id_obj ? cJSON_GetStringValue(event_id_obj) : "unknown";
|
||||
snprintf(debug_msg, sizeof(debug_msg), "Skipping broadcast of expired event: %.16s", event_id);
|
||||
log_info(debug_msg);
|
||||
return 0; // Don't broadcast expired events
|
||||
}
|
||||
}
|
||||
|
||||
int broadcasts = 0;
|
||||
|
||||
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
||||
@@ -1268,6 +1305,7 @@ void init_relay_info() {
|
||||
cJSON_AddItemToArray(g_relay_info.supported_nips, cJSON_CreateNumber(13)); // NIP-13: Proof of Work
|
||||
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
|
||||
}
|
||||
|
||||
// Initialize server limitations
|
||||
@@ -1757,6 +1795,145 @@ int validate_event_pow(cJSON* event, char* error_message, size_t error_size) {
|
||||
return 0; // Success
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// NIP-40 EXPIRATION TIMESTAMP HANDLING
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Initialize expiration configuration with environment variables and defaults
|
||||
void init_expiration_config() {
|
||||
log_info("Initializing NIP-40 Expiration Timestamp configuration");
|
||||
|
||||
// Check environment variables for configuration
|
||||
const char* exp_enabled_env = getenv("RELAY_EXPIRATION_ENABLED");
|
||||
if (exp_enabled_env) {
|
||||
g_expiration_config.enabled = (strcmp(exp_enabled_env, "1") == 0 ||
|
||||
strcmp(exp_enabled_env, "true") == 0 ||
|
||||
strcmp(exp_enabled_env, "yes") == 0);
|
||||
}
|
||||
|
||||
const char* exp_strict_env = getenv("RELAY_EXPIRATION_STRICT");
|
||||
if (exp_strict_env) {
|
||||
g_expiration_config.strict_mode = (strcmp(exp_strict_env, "1") == 0 ||
|
||||
strcmp(exp_strict_env, "true") == 0 ||
|
||||
strcmp(exp_strict_env, "yes") == 0);
|
||||
}
|
||||
|
||||
const char* exp_filter_env = getenv("RELAY_EXPIRATION_FILTER");
|
||||
if (exp_filter_env) {
|
||||
g_expiration_config.filter_responses = (strcmp(exp_filter_env, "1") == 0 ||
|
||||
strcmp(exp_filter_env, "true") == 0 ||
|
||||
strcmp(exp_filter_env, "yes") == 0);
|
||||
}
|
||||
|
||||
const char* exp_delete_env = getenv("RELAY_EXPIRATION_DELETE");
|
||||
if (exp_delete_env) {
|
||||
g_expiration_config.delete_expired = (strcmp(exp_delete_env, "1") == 0 ||
|
||||
strcmp(exp_delete_env, "true") == 0 ||
|
||||
strcmp(exp_delete_env, "yes") == 0);
|
||||
}
|
||||
|
||||
const char* exp_grace_env = getenv("RELAY_EXPIRATION_GRACE_PERIOD");
|
||||
if (exp_grace_env) {
|
||||
long grace_period = atol(exp_grace_env);
|
||||
if (grace_period >= 0 && grace_period <= 86400) { // Max 24 hours
|
||||
g_expiration_config.grace_period = grace_period;
|
||||
}
|
||||
}
|
||||
|
||||
// Log final configuration
|
||||
char config_msg[512];
|
||||
snprintf(config_msg, sizeof(config_msg),
|
||||
"Expiration Configuration: enabled=%s, strict_mode=%s, filter_responses=%s, grace_period=%ld seconds",
|
||||
g_expiration_config.enabled ? "true" : "false",
|
||||
g_expiration_config.strict_mode ? "true" : "false",
|
||||
g_expiration_config.filter_responses ? "true" : "false",
|
||||
g_expiration_config.grace_period);
|
||||
log_info(config_msg);
|
||||
}
|
||||
|
||||
// Extract expiration timestamp from event tags
|
||||
long extract_expiration_timestamp(cJSON* tags) {
|
||||
if (!tags || !cJSON_IsArray(tags)) {
|
||||
return 0; // No expiration
|
||||
}
|
||||
|
||||
cJSON* tag = NULL;
|
||||
cJSON_ArrayForEach(tag, tags) {
|
||||
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
|
||||
cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
|
||||
cJSON* tag_value = cJSON_GetArrayItem(tag, 1);
|
||||
|
||||
if (cJSON_IsString(tag_name) && cJSON_IsString(tag_value)) {
|
||||
const char* name = cJSON_GetStringValue(tag_name);
|
||||
const char* value = cJSON_GetStringValue(tag_value);
|
||||
|
||||
if (name && value && strcmp(name, "expiration") == 0) {
|
||||
long expiration_ts = atol(value);
|
||||
if (expiration_ts > 0) {
|
||||
return expiration_ts;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0; // No valid expiration tag found
|
||||
}
|
||||
|
||||
// Check if event is currently expired
|
||||
int is_event_expired(cJSON* event, time_t current_time) {
|
||||
if (!event) {
|
||||
return 0; // Invalid event, not expired
|
||||
}
|
||||
|
||||
cJSON* tags = cJSON_GetObjectItem(event, "tags");
|
||||
long expiration_ts = extract_expiration_timestamp(tags);
|
||||
|
||||
if (expiration_ts == 0) {
|
||||
return 0; // No expiration timestamp, not expired
|
||||
}
|
||||
|
||||
// Check if current time exceeds expiration + grace period
|
||||
return (current_time > (expiration_ts + g_expiration_config.grace_period));
|
||||
}
|
||||
|
||||
// Validate event expiration according to NIP-40
|
||||
int validate_event_expiration(cJSON* event, char* error_message, size_t error_size) {
|
||||
if (!g_expiration_config.enabled) {
|
||||
return 0; // Expiration validation disabled
|
||||
}
|
||||
|
||||
if (!event) {
|
||||
snprintf(error_message, error_size, "expiration: null event");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Check if event is expired
|
||||
time_t current_time = time(NULL);
|
||||
if (is_event_expired(event, current_time)) {
|
||||
if (g_expiration_config.strict_mode) {
|
||||
cJSON* tags = cJSON_GetObjectItem(event, "tags");
|
||||
long expiration_ts = extract_expiration_timestamp(tags);
|
||||
|
||||
snprintf(error_message, error_size,
|
||||
"invalid: event expired (expiration=%ld, current=%ld, grace=%ld)",
|
||||
expiration_ts, (long)current_time, g_expiration_config.grace_period);
|
||||
log_warning("Event rejected: expired timestamp");
|
||||
return -1;
|
||||
} else {
|
||||
// In non-strict mode, log but allow expired events
|
||||
char debug_msg[256];
|
||||
snprintf(debug_msg, sizeof(debug_msg),
|
||||
"Accepting expired event (strict_mode disabled)");
|
||||
log_info(debug_msg);
|
||||
}
|
||||
}
|
||||
|
||||
return 0; // Success
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// DATABASE FUNCTIONS
|
||||
@@ -2165,6 +2342,9 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
|
||||
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)) {
|
||||
@@ -2293,6 +2473,16 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
|
||||
}
|
||||
cJSON_AddItemToObject(event, "tags", tags);
|
||||
|
||||
// Check expiration filtering (NIP-40) at application level
|
||||
if (g_expiration_config.enabled && g_expiration_config.filter_responses) {
|
||||
time_t current_time = time(NULL);
|
||||
if (is_event_expired(event, current_time)) {
|
||||
// Skip this expired event
|
||||
cJSON_Delete(event);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Send EVENT message
|
||||
cJSON* event_msg = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(event_msg, cJSON_CreateString("EVENT"));
|
||||
@@ -2388,14 +2578,20 @@ int handle_event_message(cJSON* event, char* error_message, size_t error_size) {
|
||||
return pow_result; // PoW validation failed, error message already set
|
||||
}
|
||||
|
||||
// Step 4: Complete event validation (combines structure + signature + additional checks)
|
||||
// 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 5: Check for special event types and handle accordingly
|
||||
// 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");
|
||||
@@ -2435,7 +2631,7 @@ int handle_event_message(cJSON* event, char* error_message, size_t error_size) {
|
||||
}
|
||||
}
|
||||
|
||||
// Step 6: Store event in database
|
||||
// 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");
|
||||
@@ -2805,6 +3001,9 @@ int main(int argc, char* argv[]) {
|
||||
// Initialize NIP-13 PoW configuration
|
||||
init_pow_config();
|
||||
|
||||
// Initialize NIP-40 expiration configuration
|
||||
init_expiration_config();
|
||||
|
||||
log_info("Starting relay server...");
|
||||
|
||||
// Start WebSocket Nostr relay server
|
||||
|
||||
Reference in New Issue
Block a user