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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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;

View File

@@ -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

View File

@@ -1 +1 @@
1c4c3b202bbe84869d7e688fd4abccf9f46a57073df1c0e3b515d4810d9b6525
f5dde2a17bd4bbca999d25dcb68ba89df84dd7c8685b35c4834addce26e9fbe6

View File

@@ -1 +1 @@
NIP-42 test content
NIP-42 authentication test content