v0.0.11 - All auth tests working

This commit is contained in:
Your Name
2025-09-10 11:08:33 -04:00
parent 30473100b8
commit 6a4e15670d
10 changed files with 556 additions and 104 deletions

View File

@@ -97,6 +97,12 @@ typedef struct {
int error_code; // NOSTR_SUCCESS or specific error code
char reason[256]; // Human-readable reason for denial/acceptance
char pubkey[65]; // Extracted pubkey from validated event (if available)
// NEW: File data for upload operations (Option 1 implementation)
unsigned char *file_data; // File content buffer (malloc'd by validator)
size_t file_size; // Size of file data in bytes
char expected_hash[65]; // Expected SHA-256 hash from Blossom event
int owns_file_data; // 1 if validator owns memory, 0 if not
} nostr_request_result_t;
// Challenge structure for NIP-42
@@ -126,8 +132,12 @@ int nostr_generate_nip42_challenge(char* challenge_out, size_t challenge_size, c
const char* nostr_request_validator_get_last_violation_type(void);
void nostr_request_validator_clear_violation(void);
// File data cleanup function
void nostr_request_result_free_file_data(nostr_request_result_t* result);
// Upload handling
void handle_upload_request(void);
void handle_upload_request_with_validation(nostr_request_result_t* validation_result);
// Blob metadata structure
typedef struct {

View File

@@ -1159,6 +1159,267 @@ process_file_upload:
printf("\n}\n");
}
// Handle PUT /upload requests with pre-validated file data from validator
void handle_upload_request_with_validation(nostr_request_result_t* validation_result) {
// Log the incoming request
log_request("PUT", "/upload", "pending", 0);
// Get HTTP headers
const char *content_type = getenv("CONTENT_TYPE");
const char *content_length_str = getenv("CONTENT_LENGTH");
// Validate required headers
if (!content_type) {
send_error_response(
400, "missing_header", "Content-Type header required",
"The Content-Type header must be specified for file uploads");
log_request("PUT", "/upload", "none", 400);
return;
}
if (!content_length_str) {
send_error_response(
400, "missing_header", "Content-Length header required",
"The Content-Length header must be specified for file uploads");
log_request("PUT", "/upload", "none", 400);
return;
}
long content_length = atol(content_length_str);
if (content_length <= 0 ||
content_length > 100 * 1024 * 1024) { // 100MB limit
send_error_response(413, "payload_too_large",
"File size must be between 1 byte and 100MB",
"Maximum allowed file size is 100MB");
log_request("PUT", "/upload", "none", 413);
return;
}
// Get Authorization header for authentication
const char *auth_header = getenv("HTTP_AUTHORIZATION");
// Extract uploader pubkey from validation result
const char *uploader_pubkey = NULL;
if (validation_result && strlen(validation_result->pubkey) == 64) {
uploader_pubkey = validation_result->pubkey;
log_request("PUT", "/upload", "authenticated", 0);
} else if (auth_header) {
log_request("PUT", "/upload", "auth_provided", 0);
} else {
log_request("PUT", "/upload", "anonymous", 0);
}
// Use file data from validator if available, otherwise read from stdin
unsigned char *file_data = NULL;
size_t file_size = 0;
int should_free_file_data = 0;
if (validation_result && validation_result->file_data && validation_result->file_size > 0) {
// Use file data provided by validator
file_data = validation_result->file_data;
file_size = validation_result->file_size;
should_free_file_data = 0; // Validator owns the memory
fprintf(stderr, "UPLOAD: Using file data from validator (%zu bytes)\n", file_size);
} else {
// Fallback: read from stdin (for non-Blossom uploads or when validation didn't provide file data)
file_data = malloc(content_length);
if (!file_data) {
printf("Status: 500 Internal Server Error\r\n");
printf("Content-Type: text/plain\r\n\r\n");
printf("Memory allocation failed\n");
return;
}
size_t bytes_read = fread(file_data, 1, content_length, stdin);
if (bytes_read != (size_t)content_length) {
free(file_data);
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: text/plain\r\n\r\n");
printf("Failed to read complete file data\n");
return;
}
file_size = bytes_read;
should_free_file_data = 1; // We own the memory
fprintf(stderr, "UPLOAD: Read file data from stdin (%zu bytes)\n", file_size);
}
// Calculate SHA-256 hash using nostr_core function
unsigned char hash[32];
if (nostr_sha256(file_data, file_size, hash) != NOSTR_SUCCESS) {
if (should_free_file_data) free(file_data);
printf("Status: 500 Internal Server Error\r\n");
printf("Content-Type: text/plain\r\n\r\n");
printf("Hash calculation failed\n");
return;
}
// Convert hash to hex string
char sha256_hex[65];
nostr_bytes_to_hex(hash, 32, sha256_hex);
fflush(stderr);
// Check if authentication rules are enabled using nostr_core_lib system
int auth_required = nostr_auth_rules_enabled();
fprintf(stderr, "AUTH: auth_rules_enabled = %d, auth_header present: %s\r\n",
auth_required, auth_header ? "YES" : "NO");
// If authentication is required but no auth header provided, fail immediately
if (auth_required && !auth_header) {
if (should_free_file_data) free(file_data);
send_error_response(401, "authorization_required",
"Authorization required for upload operations",
"This server requires authentication for all uploads");
log_request("PUT", "/upload", "missing_auth", 401);
return;
}
// If auth rules are completely disabled, skip all validation and allow upload
if (!auth_required) {
fprintf(stderr, "AUTH: Authentication rules disabled - skipping all "
"validation and allowing upload\n");
// Skip validation and proceed to file processing
goto process_file_upload;
}
// Authentication was handled by centralized validation system
// uploader_pubkey should be set from validation result
process_file_upload:
// Get dimensions from in-memory buffer before saving file
int width = 0, height = 0;
nip94_get_dimensions(file_data, file_size, content_type, &width,
&height);
// Determine file extension from Content-Type using centralized mapping
const char *extension = mime_to_extension(content_type);
// Save file to blobs directory with SHA-256 + extension
char filepath[MAX_PATH_LEN];
snprintf(filepath, sizeof(filepath), "blobs/%s%s", sha256_hex, extension);
FILE *outfile = fopen(filepath, "wb");
if (!outfile) {
if (should_free_file_data) free(file_data);
printf("Status: 500 Internal Server Error\r\n");
printf("Content-Type: text/plain\r\n\r\n");
printf("Failed to create file\n");
return;
}
size_t bytes_written = fwrite(file_data, 1, file_size, outfile);
fclose(outfile);
// Set file permissions to 644 (owner read/write, group/others read) -
// standard for web files
if (chmod(filepath, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) != 0) {
fprintf(stderr, "WARNING: Failed to set file permissions for %s\r\n",
filepath);
// Continue anyway - this is not a fatal error
}
// Don't free file_data here if validator owns it - main() will handle cleanup
if (should_free_file_data) {
free(file_data);
}
if (bytes_written != file_size) {
// Clean up partial file
unlink(filepath);
printf("Status: 500 Internal Server Error\r\n");
printf("Content-Type: text/plain\r\n\r\n");
printf("Failed to write complete file\n");
return;
}
// Extract filename from Content-Disposition header if present
const char *filename = NULL;
const char *content_disposition = getenv("HTTP_CONTENT_DISPOSITION");
if (content_disposition) {
// Look for filename= in Content-Disposition header
const char *filename_start = strstr(content_disposition, "filename=");
if (filename_start) {
filename_start += 9; // Skip "filename="
// Handle quoted filenames
if (*filename_start == '"') {
filename_start++; // Skip opening quote
// Find closing quote
const char *filename_end = strchr(filename_start, '"');
if (filename_end) {
// Extract filename between quotes
static char filename_buffer[256];
size_t filename_len = filename_end - filename_start;
if (filename_len < sizeof(filename_buffer)) {
strncpy(filename_buffer, filename_start, filename_len);
filename_buffer[filename_len] = '\0';
filename = filename_buffer;
}
}
} else {
// Unquoted filename - extract until space or end
const char *filename_end = filename_start;
while (*filename_end && *filename_end != ' ' && *filename_end != ';') {
filename_end++;
}
static char filename_buffer[256];
size_t filename_len = filename_end - filename_start;
if (filename_len < sizeof(filename_buffer)) {
strncpy(filename_buffer, filename_start, filename_len);
filename_buffer[filename_len] = '\0';
filename = filename_buffer;
}
}
}
}
// Store blob metadata in database
time_t uploaded_time = time(NULL);
if (!insert_blob_metadata(sha256_hex, file_size, content_type,
uploaded_time, uploader_pubkey, filename)) {
// Database insertion failed - clean up the physical file to maintain
// consistency
unlink(filepath);
printf("Status: 500 Internal Server Error\r\n");
printf("Content-Type: text/plain\r\n\r\n");
printf("Failed to store blob metadata\n");
return;
}
// Get origin from config
char origin[256];
nip94_get_origin(origin, sizeof(origin));
// Build canonical blob URL
char blob_url[512];
nip94_build_blob_url(origin, sha256_hex, content_type, blob_url,
sizeof(blob_url));
// Return success response with blob descriptor
printf("Status: 200 OK\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\n");
printf(" \"sha256\": \"%s\",\n", sha256_hex);
printf(" \"size\": %zu,\n", file_size);
printf(" \"type\": \"%s\",\n", content_type);
printf(" \"uploaded\": %ld,\n", uploaded_time);
printf(" \"url\": \"%s\"", blob_url);
// Add NIP-94 metadata if enabled
if (nip94_is_enabled()) {
printf(",\n");
nip94_emit_field(blob_url, content_type, sha256_hex, file_size, width,
height);
}
printf("\n}\n");
}
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
// NIP-42 Authentication Support
@@ -1271,6 +1532,8 @@ int main(void) {
fprintf(stderr,
"STARTUP: Request validator system initialized successfully\r\n");
fflush(stderr);
/////////////////////////////////////////////////////////////////////
// THIS IS WHERE THE REQUESTS ENTER THE FastCGI
/////////////////////////////////////////////////////////////////////
@@ -1328,7 +1591,7 @@ int main(void) {
.mime_type = getenv("CONTENT_TYPE"),
.file_size = getenv("CONTENT_LENGTH") ? atol(getenv("CONTENT_LENGTH")) : 0,
.request_url = "ginxsom",
.challenge_id = NULL, // Validator will extract from NIP-42 event content
.challenge_id = NULL, // Validator will extract from NIP-42 event tags
.nip42_enabled = 1, // Let validator check actual config
.client_ip = getenv("REMOTE_ADDR"),
.app_context = NULL
@@ -1406,8 +1669,9 @@ int main(void) {
} else if (strcmp(request_method, "PUT") == 0 &&
strcmp(request_uri, "/upload") == 0) {
// Handle PUT /upload requests with pre-validated auth
// TODO: Pass validated result to existing handler
handle_upload_request();
handle_upload_request_with_validation(&result);
// Clean up file data allocated by validator
nostr_request_result_free_file_data(&result);

View File

@@ -214,6 +214,10 @@ int nostr_validate_unified_request(const nostr_unified_request_t *request,
result->valid = 1; // Default allow
result->error_code = NOSTR_SUCCESS;
strcpy(result->reason, "No validation required");
result->file_data = NULL;
result->file_size = 0;
result->owns_file_data = 0;
memset(result->expected_hash, 0, sizeof(result->expected_hash));
// Reload config if needed
if (!g_auth_cache.cache_valid || time(NULL) > g_auth_cache.cache_expires) {
@@ -419,6 +423,9 @@ int nostr_validate_unified_request(const nostr_unified_request_t *request,
sprintf(kind_msg, "VALIDATOR_DEBUG: STEP 9 PASSED - Event kind: %d\n",
event_kind);
// Variable to store expected hash from Blossom events for Phase 4 validation
char expected_hash_from_event[65] = {0};
/////////////////////////////////////////////////////////////////////
// NIP42
@@ -499,10 +506,10 @@ int nostr_validate_unified_request(const nostr_unified_request_t *request,
if (!request->request_url || !challenge_for_validation) {
validator_debug_log(
"VALIDATOR_DEBUG: STEP 10 FAILED - NIP-42 requires request_url and "
"challenge (from event content or parameter)\n");
"challenge (from event tags)\n");
result->valid = 0;
result->error_code = NOSTR_ERROR_NIP42_NOT_CONFIGURED;
strcpy(result->reason, "NIP-42 authentication requires request_url and challenge");
strcpy(result->reason, "NIP-42 authentication requires request_url and challenge in event tags");
cJSON_Delete(event);
return NOSTR_SUCCESS;
}
@@ -543,7 +550,7 @@ int nostr_validate_unified_request(const nostr_unified_request_t *request,
strcpy(result->reason, "Event signature verification failed. Check private key and event serialization.");
break;
case NOSTR_ERROR_EVENT_INVALID_CONTENT:
strcpy(result->reason, "Event content is invalid. Challenge must be in event content field.");
strcpy(result->reason, "Event content is invalid. Challenge must be in event tags according to NIP-42.");
break;
case NOSTR_ERROR_EVENT_INVALID_TAGS:
strcpy(result->reason, "Required tags missing. Auth event must include 'relay' and 'expiration' tags.");
@@ -574,59 +581,80 @@ int nostr_validate_unified_request(const nostr_unified_request_t *request,
// expiration Early exit: Expired or mismatched events rejected
validator_debug_log("VALIDATOR_DEBUG: STEP 10 - Processing Blossom "
"authentication (kind 24242)\n");
// Blossom protocol authentication (kind 24242)
if (request->operation && request->resource_hash) {
char blossom_valid_msg[512];
sprintf(blossom_valid_msg,
"VALIDATOR_DEBUG: Validating Blossom event for operation='%s', "
"hash='%s'\n",
request->operation ? request->operation : "NULL",
request->resource_hash ? request->resource_hash : "NULL");
validator_debug_log(blossom_valid_msg);
// Blossom protocol authentication (kind 24242) - ALWAYS validate kind 24242 events
char blossom_valid_msg[512];
sprintf(blossom_valid_msg,
"VALIDATOR_DEBUG: Validating Blossom event for operation='%s', "
"hash='%s'\n",
request->operation ? request->operation : "NULL",
request->resource_hash ? request->resource_hash : "NULL");
validator_debug_log(blossom_valid_msg);
int blossom_result = validate_blossom_event(event, request->resource_hash,
request->operation);
if (blossom_result != NOSTR_SUCCESS) {
char blossom_fail_msg[256];
sprintf(blossom_fail_msg,
"VALIDATOR_DEBUG: STEP 10 FAILED - Blossom validation failed "
"(error=%d)\n",
blossom_result);
validator_debug_log(blossom_fail_msg);
result->valid = 0;
result->error_code = blossom_result;
// Map specific Blossom error codes to detailed error messages
switch (blossom_result) {
case NOSTR_ERROR_EVENT_EXPIRED:
strcpy(result->reason, "Authorization event has expired. Create a new signed event with future expiration.");
break;
case NOSTR_ERROR_EVENT_INVALID_CONTENT:
strcpy(result->reason, "Event missing required tags. Blossom events need 't' (method) and 'x' (hash) tags.");
break;
case NOSTR_ERROR_EVENT_INVALID_TAGS:
strcpy(result->reason, "Invalid or missing Blossom tags. Check 't' tag matches operation and 'x' tag matches file hash.");
break;
case NOSTR_ERROR_EVENT_INVALID_SIGNATURE:
strcpy(result->reason, "Event signature verification failed. Check private key and event serialization.");
break;
case NOSTR_ERROR_EVENT_INVALID_KIND:
strcpy(result->reason, "Invalid event kind. Blossom authorization events must use kind 24242.");
break;
default:
snprintf(result->reason, sizeof(result->reason),
"Blossom event does not authorize this operation (error code: %d). Check tags and expiration.",
blossom_result);
break;
}
cJSON_Delete(event);
return NOSTR_SUCCESS;
int blossom_result = validate_blossom_event(event, request->resource_hash,
request->operation);
if (blossom_result != NOSTR_SUCCESS) {
char blossom_fail_msg[256];
sprintf(blossom_fail_msg,
"VALIDATOR_DEBUG: STEP 10 FAILED - Blossom validation failed "
"(error=%d)\n",
blossom_result);
validator_debug_log(blossom_fail_msg);
result->valid = 0;
result->error_code = blossom_result;
// Map specific Blossom error codes to detailed error messages
switch (blossom_result) {
case NOSTR_ERROR_EVENT_EXPIRED:
strcpy(result->reason, "Authorization event has expired. Create a new signed event with future expiration.");
break;
case NOSTR_ERROR_EVENT_INVALID_CONTENT:
strcpy(result->reason, "Event missing required tags. Blossom events need 't' (method) and 'x' tags.");
break;
case NOSTR_ERROR_EVENT_INVALID_TAGS:
strcpy(result->reason, "Invalid or missing Blossom tags. Check 't' tag matches operation and 'x' tag matches file hash.");
break;
case NOSTR_ERROR_EVENT_INVALID_SIGNATURE:
strcpy(result->reason, "Event signature verification failed. Check private key and event serialization.");
break;
case NOSTR_ERROR_EVENT_INVALID_KIND:
strcpy(result->reason, "Invalid event kind. Blossom authorization events must use kind 24242.");
break;
default:
snprintf(result->reason, sizeof(result->reason),
"Blossom event does not authorize this operation (error code: %d). Check tags and expiration.",
blossom_result);
break;
}
} else {
validator_debug_log("VALIDATOR_DEBUG: Skipping Blossom validation (no "
"operation/hash specified)\n");
cJSON_Delete(event);
return NOSTR_SUCCESS;
}
// Extract expected hash from event 'x' tag for Phase 4 use
cJSON *tags = cJSON_GetObjectItem(event, "tags");
if (tags && cJSON_IsArray(tags)) {
cJSON *tag = NULL;
cJSON_ArrayForEach(tag, tags) {
if (!cJSON_IsArray(tag)) continue;
cJSON *tag_name = cJSON_GetArrayItem(tag, 0);
if (!tag_name || !cJSON_IsString(tag_name)) continue;
const char *tag_name_str = cJSON_GetStringValue(tag_name);
if (strcmp(tag_name_str, "x") == 0) {
cJSON *hash_value = cJSON_GetArrayItem(tag, 1);
if (hash_value && cJSON_IsString(hash_value)) {
const char *event_hash = cJSON_GetStringValue(hash_value);
if (event_hash && strlen(event_hash) == 64) {
strncpy(expected_hash_from_event, event_hash, 64);
expected_hash_from_event[64] = '\0';
break;
}
}
}
}
}
validator_debug_log(
"VALIDATOR_DEBUG: STEP 10 PASSED - Blossom authentication succeeded\n");
strcpy(result->reason, "Blossom authentication passed");
@@ -725,6 +753,101 @@ int nostr_validate_unified_request(const nostr_unified_request_t *request,
// 16. Cache Storage - Store decision for future requests (5-minute TTL)
/////////////////////////////////////////////////////////////////////
// PHASE 4: FILE CONTENT VALIDATION (Upload Operations Only ~100ms)
/////////////////////////////////////////////////////////////////////
// Only run expensive file processing AFTER all auth checks pass
// This ensures blacklisted users never trigger file I/O
if (event_kind == 24242 && request->operation &&
strcmp(request->operation, "upload") == 0 &&
strlen(expected_hash_from_event) == 64) {
validator_debug_log("VALIDATOR_DEBUG: PHASE 4 - Starting file content validation for Blossom upload\n");
char phase4_msg[256];
sprintf(phase4_msg, "VALIDATOR_DEBUG: PHASE 4 - Expected hash: %.16s...\n", expected_hash_from_event);
validator_debug_log(phase4_msg);
// Get content length from request
long content_length = request->file_size;
if (content_length <= 0 || content_length > 100 * 1024 * 1024) { // 100MB limit
validator_debug_log("VALIDATOR_DEBUG: PHASE 4 FAILED - Invalid content length\n");
result->valid = 0;
result->error_code = NOSTR_ERROR_AUTH_REQUIRED;
strcpy(result->reason, "Invalid file size for upload validation");
return NOSTR_SUCCESS;
}
// Allocate buffer for file data
unsigned char *file_data = malloc(content_length);
if (!file_data) {
validator_debug_log("VALIDATOR_DEBUG: PHASE 4 FAILED - Memory allocation failed\n");
result->valid = 0;
result->error_code = NOSTR_ERROR_AUTH_REQUIRED;
strcpy(result->reason, "Memory allocation failed for file validation");
return NOSTR_SUCCESS;
}
// Read file data from stdin
size_t bytes_read = fread(file_data, 1, content_length, stdin);
if (bytes_read != (size_t)content_length) {
free(file_data);
validator_debug_log("VALIDATOR_DEBUG: PHASE 4 FAILED - Failed to read complete file data\n");
result->valid = 0;
result->error_code = NOSTR_ERROR_AUTH_REQUIRED;
strcpy(result->reason, "Failed to read complete file data for validation");
return NOSTR_SUCCESS;
}
char read_msg[256];
sprintf(read_msg, "VALIDATOR_DEBUG: PHASE 4 - Read %zu bytes from stdin\n", bytes_read);
validator_debug_log(read_msg);
// Calculate SHA-256 hash of file content
unsigned char hash_bytes[32];
if (nostr_sha256(file_data, bytes_read, hash_bytes) != NOSTR_SUCCESS) {
free(file_data);
validator_debug_log("VALIDATOR_DEBUG: PHASE 4 FAILED - Hash calculation failed\n");
result->valid = 0;
result->error_code = NOSTR_ERROR_AUTH_REQUIRED;
strcpy(result->reason, "Failed to calculate file hash");
return NOSTR_SUCCESS;
}
// Convert hash to hex string
char calculated_hash[65];
nostr_bytes_to_hex(hash_bytes, 32, calculated_hash);
char hash_msg[256];
sprintf(hash_msg, "VALIDATOR_DEBUG: PHASE 4 - Calculated hash: %.16s...\n", calculated_hash);
validator_debug_log(hash_msg);
// Compare hashes
if (strcmp(calculated_hash, expected_hash_from_event) != 0) {
free(file_data);
validator_debug_log("VALIDATOR_DEBUG: PHASE 4 FAILED - Hash mismatch detected\n");
char mismatch_msg[512];
sprintf(mismatch_msg, "VALIDATOR_DEBUG: PHASE 4 - Expected: %.16s..., Got: %.16s...\n",
expected_hash_from_event, calculated_hash);
validator_debug_log(mismatch_msg);
result->valid = 0;
result->error_code = NOSTR_ERROR_AUTH_REQUIRED;
strcpy(result->reason, "File hash mismatch. Uploaded file does not match hash in authorization event.");
return NOSTR_SUCCESS;
}
// Hash matches - store file data in result for main handler to use
result->file_data = file_data;
result->file_size = bytes_read;
result->owns_file_data = 1;
strncpy(result->expected_hash, expected_hash_from_event, 64);
result->expected_hash[64] = '\0';
validator_debug_log("VALIDATOR_DEBUG: PHASE 4 PASSED - File hash validation successful, file data stored in result\n");
}
// All validations passed
result->valid = 1;
result->error_code = NOSTR_SUCCESS;
@@ -782,6 +905,18 @@ void ginxsom_request_validator_cleanup(void) {
nostr_request_validator_clear_violation();
}
/**
* Free file data allocated by validator
*/
void nostr_request_result_free_file_data(nostr_request_result_t *result) {
if (result && result->file_data && result->owns_file_data) {
free(result->file_data);
result->file_data = NULL;
result->file_size = 0;
result->owns_file_data = 0;
}
}
//=============================================================================
// HELPER FUNCTIONS
//=============================================================================
@@ -1014,61 +1149,70 @@ static int validate_blossom_event(cJSON *event, const char *expected_hash,
return NOSTR_ERROR_EVENT_INVALID_CONTENT;
}
// Look for required tags if method and hash are specified
if (method || expected_hash) {
cJSON *tags = cJSON_GetObjectItem(event, "tags");
if (!tags || !cJSON_IsArray(tags)) {
return NOSTR_ERROR_EVENT_INVALID_CONTENT;
}
// ALL Blossom events (kind 24242) must have proper tag structure
cJSON *tags = cJSON_GetObjectItem(event, "tags");
if (!tags || !cJSON_IsArray(tags)) {
return NOSTR_ERROR_EVENT_INVALID_CONTENT;
}
int found_method = (method == NULL);
int found_hash = (expected_hash == NULL);
time_t expiration = 0;
// Track what we find in the event tags
int has_t_tag = 0;
int has_x_tag = 0;
int method_matches = (method == NULL); // If no expected method, consider it matched
int hash_matches = (expected_hash == NULL); // If no expected hash, consider it matched
time_t expiration = 0;
cJSON *tag = NULL;
cJSON_ArrayForEach(tag, tags) {
if (!cJSON_IsArray(tag))
continue;
cJSON *tag = NULL;
cJSON_ArrayForEach(tag, tags) {
if (!cJSON_IsArray(tag))
continue;
cJSON *tag_name = cJSON_GetArrayItem(tag, 0);
if (!tag_name || !cJSON_IsString(tag_name))
continue;
cJSON *tag_name = cJSON_GetArrayItem(tag, 0);
if (!tag_name || !cJSON_IsString(tag_name))
continue;
const char *tag_name_str = cJSON_GetStringValue(tag_name);
const char *tag_name_str = cJSON_GetStringValue(tag_name);
if (strcmp(tag_name_str, "t") == 0 && method) {
cJSON *method_value = cJSON_GetArrayItem(tag, 1);
if (method_value && cJSON_IsString(method_value)) {
const char *event_method = cJSON_GetStringValue(method_value);
if (strcmp(event_method, method) == 0) {
found_method = 1;
}
}
} else if (strcmp(tag_name_str, "x") == 0 && expected_hash) {
cJSON *hash_value = cJSON_GetArrayItem(tag, 1);
if (hash_value && cJSON_IsString(hash_value)) {
const char *event_hash = cJSON_GetStringValue(hash_value);
if (strcmp(event_hash, expected_hash) == 0) {
found_hash = 1;
}
}
} else if (strcmp(tag_name_str, "expiration") == 0) {
cJSON *exp_value = cJSON_GetArrayItem(tag, 1);
if (exp_value && cJSON_IsString(exp_value)) {
expiration = (time_t)atol(cJSON_GetStringValue(exp_value));
if (strcmp(tag_name_str, "t") == 0) {
has_t_tag = 1;
cJSON *method_value = cJSON_GetArrayItem(tag, 1);
if (method_value && cJSON_IsString(method_value)) {
const char *event_method = cJSON_GetStringValue(method_value);
if (method && strcmp(event_method, method) == 0) {
method_matches = 1;
}
}
} else if (strcmp(tag_name_str, "x") == 0) {
has_x_tag = 1;
cJSON *hash_value = cJSON_GetArrayItem(tag, 1);
if (hash_value && cJSON_IsString(hash_value)) {
const char *event_hash = cJSON_GetStringValue(hash_value);
if (expected_hash && strcmp(event_hash, expected_hash) == 0) {
hash_matches = 1;
}
}
} else if (strcmp(tag_name_str, "expiration") == 0) {
cJSON *exp_value = cJSON_GetArrayItem(tag, 1);
if (exp_value && cJSON_IsString(exp_value)) {
expiration = (time_t)atol(cJSON_GetStringValue(exp_value));
}
}
}
if (!found_method || !found_hash) {
return NOSTR_ERROR_EVENT_INVALID_CONTENT;
}
// Blossom events MUST have both 't' and 'x' tags
if (!has_t_tag || !has_x_tag) {
return NOSTR_ERROR_EVENT_INVALID_CONTENT;
}
// Check expiration
time_t now = time(NULL);
if (expiration > 0 && now > expiration) {
return NOSTR_ERROR_EVENT_EXPIRED;
}
// If we have expected values, they must match
if (!method_matches || !hash_matches) {
return NOSTR_ERROR_EVENT_INVALID_CONTENT;
}
// Check expiration
time_t now = time(NULL);
if (expiration > 0 && now > expiration) {
return NOSTR_ERROR_EVENT_EXPIRED;
}
return NOSTR_SUCCESS;