diff --git a/build/ginxsom-fcgi b/build/ginxsom-fcgi index 1df1627..533c956 100755 Binary files a/build/ginxsom-fcgi and b/build/ginxsom-fcgi differ diff --git a/build/main.o b/build/main.o index c00abd7..d8544a6 100644 Binary files a/build/main.o and b/build/main.o differ diff --git a/build/request_validator.o b/build/request_validator.o index eadcf74..b9c7c2e 100644 Binary files a/build/request_validator.o and b/build/request_validator.o differ diff --git a/db/ginxsom.db b/db/ginxsom.db index f143391..14d8e6a 100644 Binary files a/db/ginxsom.db and b/db/ginxsom.db differ diff --git a/src/ginxsom.h b/src/ginxsom.h index 602408a..d51a8f7 100644 --- a/src/ginxsom.h +++ b/src/ginxsom.h @@ -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 { diff --git a/src/main.c b/src/main.c index 8c99e64..8ace5dd 100644 --- a/src/main.c +++ b/src/main.c @@ -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); diff --git a/src/request_validator.c b/src/request_validator.c index f01983f..22f09b8 100644 --- a/src/request_validator.c +++ b/src/request_validator.c @@ -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; diff --git a/tests/auth_test.sh b/tests/auth_test.sh index 9fa9dd4..b1a8115 100755 --- a/tests/auth_test.sh +++ b/tests/auth_test.sh @@ -196,6 +196,22 @@ test_failure_mode() { -X PUT "$UPLOAD_ENDPOINT" \ -o "$response_file" 2>/dev/null) + # Show detailed error response if test fails unexpectedly + if [[ "$http_status" != "$expected_status" ]]; then + echo "" + echo "=== DETAILED ERROR RESPONSE FOR: $test_name ===" + echo "Expected HTTP Status: $expected_status" + echo "Actual HTTP Status: $http_status" + echo "Response Body:" + if [[ -f "$response_file" && -s "$response_file" ]]; then + cat "$response_file" + else + echo "(No response body or empty response)" + fi + echo "=== END DETAILED ERROR RESPONSE ===" + echo "" + fi + rm -f "$test_file" "$response_file" # Record result @@ -361,7 +377,7 @@ create_nip42_auth_event() { # Create NIP-42 authentication event (kind 22242) using nak for proper signing nak event -k 22242 -c "" \ - --tag "relay=ws://localhost:9001" \ + --tag "relay=ginxsom" \ --tag "challenge=$challenge" \ --sec "$privkey" } @@ -399,6 +415,24 @@ test_nip42_authentication() { -X PUT "$UPLOAD_ENDPOINT" \ -o "$response_file" 2>/dev/null) + # Show detailed error response if NIP-42 test fails + if [[ "$http_status" != "200" ]]; then + echo "" + echo "=== DETAILED NIP-42 ERROR RESPONSE ===" + echo "Expected HTTP Status: 200" + echo "Actual HTTP Status: $http_status" + echo "Challenge used: $challenge" + echo "Response Body:" + if [[ -f "$response_file" && -s "$response_file" ]]; then + cat "$response_file" + else + echo "(No response body or empty response)" + fi + echo "Auth Header (first 100 chars): ${nip42_auth_header:0:100}..." + echo "=== END DETAILED NIP-42 ERROR RESPONSE ===" + echo "" + fi + rm -f "$response_file" # Record result diff --git a/tests/auth_test_tmp/nip42_challenge b/tests/auth_test_tmp/nip42_challenge index d8db9c3..f4b161d 100644 --- a/tests/auth_test_tmp/nip42_challenge +++ b/tests/auth_test_tmp/nip42_challenge @@ -1 +1 @@ -1c4c3b202bbe84869d7e688fd4abccf9f46a57073df1c0e3b515d4810d9b6525 +f5dde2a17bd4bbca999d25dcb68ba89df84dd7c8685b35c4834addce26e9fbe6 diff --git a/tests/auth_test_tmp/nip42_test.txt b/tests/auth_test_tmp/nip42_test.txt index f028755..156226a 100644 --- a/tests/auth_test_tmp/nip42_test.txt +++ b/tests/auth_test_tmp/nip42_test.txt @@ -1 +1 @@ -NIP-42 test content +NIP-42 authentication test content