v0.0.11 - All auth tests working
This commit is contained in:
Binary file not shown.
BIN
build/main.o
BIN
build/main.o
Binary file not shown.
Binary file not shown.
BIN
db/ginxsom.db
BIN
db/ginxsom.db
Binary file not shown.
@@ -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 {
|
||||
|
||||
270
src/main.c
270
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);
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1 +1 @@
|
||||
1c4c3b202bbe84869d7e688fd4abccf9f46a57073df1c0e3b515d4810d9b6525
|
||||
f5dde2a17bd4bbca999d25dcb68ba89df84dd7c8685b35c4834addce26e9fbe6
|
||||
|
||||
@@ -1 +1 @@
|
||||
NIP-42 test content
|
||||
NIP-42 authentication test content
|
||||
|
||||
Reference in New Issue
Block a user