#define _GNU_SOURCE #include #include "debug.h" #include #include #include // Include nostr_core_lib for cJSON #include "../nostr_core_lib/cjson/cJSON.h" // Configuration management system #include "config.h" // NIP-40 Expiration configuration structure struct expiration_config { int enabled; // 0 = disabled, 1 = enabled int strict_mode; // 1 = reject expired events on submission int filter_responses; // 1 = filter expired events from responses int delete_expired; // 1 = delete expired events from DB (future feature) long grace_period; // Grace period in seconds for clock skew }; // Global expiration configuration instance struct expiration_config g_expiration_config = { .enabled = 1, // Enable expiration handling by default .strict_mode = 1, // Reject expired events on submission by default .filter_responses = 1, // Filter expired events from responses by default .delete_expired = 0, // Don't delete by default (keep for audit) .grace_period = 1 // 1 second grace period for testing (was 300) }; // Initialize expiration configuration using configuration system void init_expiration_config() { // Get all config values first (without holding mutex to avoid deadlock) int expiration_enabled = get_config_bool("expiration_enabled", 1); int expiration_strict = get_config_bool("expiration_strict", 1); int expiration_filter = get_config_bool("expiration_filter", 1); int expiration_delete = get_config_bool("expiration_delete", 0); long expiration_grace_period = get_config_int("expiration_grace_period", 1); // Load expiration settings from configuration system g_expiration_config.enabled = expiration_enabled; g_expiration_config.strict_mode = expiration_strict; g_expiration_config.filter_responses = expiration_filter; g_expiration_config.delete_expired = expiration_delete; g_expiration_config.grace_period = expiration_grace_period; // Validate grace period bounds if (g_expiration_config.grace_period < 0 || g_expiration_config.grace_period > 86400) { DEBUG_WARN("Invalid grace period, using default of 300 seconds"); g_expiration_config.grace_period = 300; } } // Extract expiration timestamp from event tags long extract_expiration_timestamp(cJSON* tags) { if (!tags || !cJSON_IsArray(tags)) { return 0; // No expiration } cJSON* tag = NULL; cJSON_ArrayForEach(tag, tags) { if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) { cJSON* tag_name = cJSON_GetArrayItem(tag, 0); cJSON* tag_value = cJSON_GetArrayItem(tag, 1); if (cJSON_IsString(tag_name) && cJSON_IsString(tag_value)) { const char* name = cJSON_GetStringValue(tag_name); const char* value = cJSON_GetStringValue(tag_value); if (name && value && strcmp(name, "expiration") == 0) { // Validate that the string contains only digits (and optional leading whitespace) const char* p = value; // Skip leading whitespace while (*p == ' ' || *p == '\t') p++; // Check if we have at least one digit if (*p == '\0') { continue; // Empty or whitespace-only string, ignore this tag } // Validate that all remaining characters are digits const char* digit_start = p; while (*p >= '0' && *p <= '9') p++; // If we didn't consume the entire string or found no digits, it's malformed if (*p != '\0' || p == digit_start) { char debug_msg[256]; snprintf(debug_msg, sizeof(debug_msg), "Ignoring malformed expiration tag value: '%.32s'", value); DEBUG_WARN(debug_msg); continue; // Ignore malformed expiration tag } long expiration_ts = atol(value); if (expiration_ts > 0) { return expiration_ts; } } } } } return 0; // No valid expiration tag found } // Check if event is currently expired int is_event_expired(cJSON* event, time_t current_time) { if (!event) { return 0; // Invalid event, not expired } cJSON* tags = cJSON_GetObjectItem(event, "tags"); long expiration_ts = extract_expiration_timestamp(tags); if (expiration_ts == 0) { return 0; // No expiration timestamp, not expired } // Check if current time exceeds expiration + grace period 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); DEBUG_WARN("Event rejected: expired timestamp"); return -1; } else { // In non-strict mode, allow expired events } } return 0; // Success }