/* * Ginxsom Blossom Server - FastCGI Application * Handles HEAD requests and other dynamic operations */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include "ginxsom.h" // Detailed debugging macros (matching test_auth_debug.c) #define LOG_STEP(step, msg, ...) fprintf(stderr, "STEP %s: " msg "\n", step, ##__VA_ARGS__) #define LOG_SUCCESS(msg, ...) fprintf(stderr, "SUCCESS: " msg "\n", ##__VA_ARGS__) #define LOG_ERROR(msg, ...) fprintf(stderr, "ERROR: " msg "\n", ##__VA_ARGS__) #define LOG_INFO(msg, ...) fprintf(stderr, "ℹINFO: " msg "\n", ##__VA_ARGS__) #define LOG_DIVIDER() fprintf(stderr, "═══════════════════════════════════════════════════════════════════\n") #define MAX_SHA256_LEN 65 #define MAX_PATH_LEN 512 #define MAX_MIME_LEN 128 // Database path #define DB_PATH "db/ginxsom.db" // Function declarations void send_error_response(int status_code, const char* error_type, const char* message, const char* details); void log_request(const char* method, const char* uri, const char* auth_status, int status_code); // Blob metadata structure typedef struct { char sha256[MAX_SHA256_LEN]; long size; char type[MAX_MIME_LEN]; long uploaded_at; char filename[256]; int found; } blob_metadata_t; // Insert blob metadata into database int insert_blob_metadata(const char* sha256, long size, const char* type, long uploaded_at, const char* uploader_pubkey, const char* filename) { sqlite3* db; sqlite3_stmt* stmt; int rc; rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL); if (rc) { fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db)); return 0; } const char* sql = "INSERT INTO blobs (sha256, size, type, uploaded_at, uploader_pubkey, filename) VALUES (?, ?, ?, ?, ?, ?)"; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { fprintf(stderr, "SQL error: %s\n", sqlite3_errmsg(db)); sqlite3_close(db); return 0; } // Bind parameters sqlite3_bind_text(stmt, 1, sha256, -1, SQLITE_STATIC); sqlite3_bind_int64(stmt, 2, size); sqlite3_bind_text(stmt, 3, type, -1, SQLITE_STATIC); sqlite3_bind_int64(stmt, 4, uploaded_at); if (uploader_pubkey) { sqlite3_bind_text(stmt, 5, uploader_pubkey, -1, SQLITE_STATIC); } else { sqlite3_bind_null(stmt, 5); } if (filename) { sqlite3_bind_text(stmt, 6, filename, -1, SQLITE_STATIC); } else { sqlite3_bind_null(stmt, 6); } rc = sqlite3_step(stmt); int success = 0; if (rc == SQLITE_DONE) { success = 1; } else if (rc == SQLITE_CONSTRAINT) { // This is actually OK - blob already exists with same hash success = 1; } else { success = 0; } sqlite3_finalize(stmt); sqlite3_close(db); return success; } // Get blob metadata from database int get_blob_metadata(const char* sha256, blob_metadata_t* metadata) { sqlite3* db; sqlite3_stmt* stmt; int rc; fprintf(stderr, "DEBUG: get_blob_metadata() called with sha256='%s'\r\n", sha256); fprintf(stderr, "DEBUG: Opening database at path: %s\r\n", DB_PATH); rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); if (rc) { fprintf(stderr, "DEBUG: Database open FAILED: %s\r\n", sqlite3_errmsg(db)); fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db)); return 0; } fprintf(stderr, "DEBUG: Database opened successfully\r\n"); const char* sql = "SELECT sha256, size, type, uploaded_at, filename FROM blobs WHERE sha256 = ?"; fprintf(stderr, "DEBUG: Preparing SQL: %s\r\n", sql); rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { fprintf(stderr, "DEBUG: SQL prepare FAILED: %s\r\n", sqlite3_errmsg(db)); fprintf(stderr, "SQL error: %s\n", sqlite3_errmsg(db)); sqlite3_close(db); return 0; } fprintf(stderr, "DEBUG: SQL prepared successfully\r\n"); fprintf(stderr, "DEBUG: Binding parameter sha256='%s'\r\n", sha256); sqlite3_bind_text(stmt, 1, sha256, -1, SQLITE_STATIC); fprintf(stderr, "DEBUG: Executing SQL query...\r\n"); rc = sqlite3_step(stmt); fprintf(stderr, "DEBUG: sqlite3_step() returned: %d (SQLITE_ROW=%d, SQLITE_DONE=%d)\r\n", rc, SQLITE_ROW, SQLITE_DONE); if (rc == SQLITE_ROW) { fprintf(stderr, "DEBUG: Row found! Extracting metadata...\r\n"); strncpy(metadata->sha256, (char*)sqlite3_column_text(stmt, 0), MAX_SHA256_LEN-1); metadata->size = sqlite3_column_int64(stmt, 1); strncpy(metadata->type, (char*)sqlite3_column_text(stmt, 2), MAX_MIME_LEN-1); metadata->uploaded_at = sqlite3_column_int64(stmt, 3); const char* filename = (char*)sqlite3_column_text(stmt, 4); if (filename) { strncpy(metadata->filename, filename, 255); } else { metadata->filename[0] = '\0'; } metadata->found = 1; fprintf(stderr, "DEBUG: Metadata extracted - size=%ld, type='%s'\r\n", metadata->size, metadata->type); } else { fprintf(stderr, "DEBUG: No row found for sha256='%s'\r\n", sha256); metadata->found = 0; } sqlite3_finalize(stmt); sqlite3_close(db); fprintf(stderr, "DEBUG: Database closed, returning %d\r\n", metadata->found); return metadata->found; } // Check if physical file exists (with extension based on MIME type) int file_exists_with_type(const char* sha256, const char* mime_type) { char filepath[MAX_PATH_LEN]; const char* extension = ""; // Determine file extension based on MIME type if (strstr(mime_type, "image/jpeg")) { extension = ".jpg"; } else if (strstr(mime_type, "image/webp")) { extension = ".webp"; } else if (strstr(mime_type, "image/png")) { extension = ".png"; } else if (strstr(mime_type, "image/gif")) { extension = ".gif"; } else if (strstr(mime_type, "video/mp4")) { extension = ".mp4"; } else if (strstr(mime_type, "video/webm")) { extension = ".webm"; } else if (strstr(mime_type, "audio/mpeg")) { extension = ".mp3"; } else if (strstr(mime_type, "audio/ogg")) { extension = ".ogg"; } else if (strstr(mime_type, "text/plain")) { extension = ".txt"; } snprintf(filepath, sizeof(filepath), "blobs/%s%s", sha256, extension); fprintf(stderr, "DEBUG: file_exists_with_type() checking path: '%s' (MIME: %s)\r\n", filepath, mime_type); struct stat st; int result = stat(filepath, &st); fprintf(stderr, "DEBUG: stat() returned: %d (0=success, -1=fail)\r\n", result); if (result == 0) { fprintf(stderr, "DEBUG: File exists! Size: %ld bytes\r\n", st.st_size); return 1; } else { fprintf(stderr, "DEBUG: File does not exist or stat failed\r\n"); return 0; } } // Handle HEAD request for blob void handle_head_request(const char* sha256) { blob_metadata_t metadata = {0}; fprintf(stderr, "DEBUG: handle_head_request called with sha256=%s\r\n", sha256); // Validate SHA-256 format (64 hex characters) if (strlen(sha256) != 64) { fprintf(stderr, "DEBUG: SHA-256 length validation failed: %zu\r\n", strlen(sha256)); printf("Status: 400 Bad Request\r\n"); printf("Content-Type: text/plain\r\n\r\n"); printf("Invalid SHA-256 hash format\n"); return; } fprintf(stderr, "DEBUG: SHA-256 length validation passed\r\n"); // Check if blob exists in database - this is the single source of truth if (!get_blob_metadata(sha256, &metadata)) { fprintf(stderr, "DEBUG: Database lookup failed for sha256=%s\r\n", sha256); printf("Status: 404 Not Found\r\n"); printf("Content-Type: text/plain\r\n\r\n"); printf("Blob not found\n"); return; } fprintf(stderr, "DEBUG: Database lookup succeeded - blob exists\r\n"); // Return successful HEAD response with metadata from database printf("Status: 200 OK\r\n"); printf("Content-Type: %s\r\n", metadata.type); printf("Content-Length: %ld\r\n", metadata.size); printf("Cache-Control: public, max-age=31536000, immutable\r\n"); printf("ETag: \"%s\"\r\n", metadata.sha256); // Add timing header for debugging printf("X-Ginxsom-Server: FastCGI\r\n"); printf("X-Ginxsom-Timestamp: %ld\r\n", time(NULL)); if (strlen(metadata.filename) > 0) { printf("X-Original-Filename: %s\r\n", metadata.filename); } printf("\r\n"); // HEAD request - no body content } // Extract SHA-256 from request URI (Blossom compliant - ignores any extension) const char* extract_sha256_from_uri(const char* uri) { static char sha256_buffer[MAX_SHA256_LEN]; if (!uri || uri[0] != '/') { return NULL; } const char* start = uri + 1; // Skip leading '/' // Extract exactly 64 hex characters, ignoring anything after (extensions, etc.) int len = 0; for (int i = 0; i < 64 && start[i] != '\0'; i++) { char c = start[i]; if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { // If we hit a non-hex character before 64 chars, it's invalid if (len < 64) { return NULL; } break; } sha256_buffer[i] = c; len = i + 1; } // Must be exactly 64 hex characters if (len != 64) { return NULL; } sha256_buffer[64] = '\0'; return sha256_buffer; } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // BUD 02 - Upload & Authentication ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // Parse Authorization header and extract JSON event int parse_authorization_header(const char* auth_header, char* event_json, size_t json_size) { if (!auth_header || !event_json) { fprintf(stderr, "DEBUG: parse_authorization_header - invalid parameters: auth_header=%p, event_json=%p\n", (void*)auth_header, (void*)event_json); return NOSTR_ERROR_INVALID_INPUT; } fprintf(stderr, "DEBUG: parse_authorization_header called with header: %.50s...\n", auth_header); // Check for "Nostr " prefix (case-insensitive) const char* prefix = "nostr "; size_t prefix_len = strlen(prefix); if (strncasecmp(auth_header, prefix, prefix_len) != 0) { fprintf(stderr, "DEBUG: Authorization header missing 'Nostr ' prefix (found: %.10s)\n", auth_header); return NOSTR_ERROR_INVALID_INPUT; } // Extract base64 encoded event after "Nostr " const char* base64_event = auth_header + prefix_len; fprintf(stderr, "DEBUG: Extracted base64 event (length=%zu): %.100s...\n", strlen(base64_event), base64_event); // Decode base64 to JSON using nostr_core_lib base64 decode unsigned char decoded_buffer[4096]; size_t decoded_len = base64_decode(base64_event, decoded_buffer); fprintf(stderr, "DEBUG: Base64 decode result - decoded_len=%zu\n", decoded_len); if (decoded_len == 0) { fprintf(stderr, "DEBUG: Failed to decode base64 event - base64_decode returned 0\n"); return NOSTR_ERROR_INVALID_INPUT; } if (decoded_len >= json_size) { fprintf(stderr, "DEBUG: Decoded JSON too large for buffer (decoded_len=%zu, json_size=%zu)\n", decoded_len, json_size); return NOSTR_ERROR_INVALID_INPUT; } // Copy decoded JSON to output buffer memcpy(event_json, decoded_buffer, decoded_len); event_json[decoded_len] = '\0'; fprintf(stderr, "DEBUG: Successfully decoded JSON (length=%zu): %s\n", decoded_len, event_json); return NOSTR_SUCCESS; } // Validate Blossom-specific event requirements (kind 24242) int validate_blossom_event(cJSON* event, const char* expected_hash, const char* method) { if (!event) { return NOSTR_ERROR_INVALID_INPUT; } fprintf(stderr, "DEBUG: Validating Blossom event\r\n"); // Check event kind (must be 24242 for Blossom uploads) cJSON* kind_json = cJSON_GetObjectItem(event, "kind"); if (!kind_json || !cJSON_IsNumber(kind_json)) { fprintf(stderr, "DEBUG: Event missing or invalid 'kind' field\r\n"); return NOSTR_ERROR_EVENT_INVALID_CONTENT; } int kind = cJSON_GetNumberValue(kind_json); if (kind != 24242) { fprintf(stderr, "DEBUG: Event kind %d is not 24242 (Blossom upload)\r\n", kind); return NOSTR_ERROR_EVENT_INVALID_CONTENT; } // Check that created_at exists (basic validation) cJSON* created_at_json = cJSON_GetObjectItem(event, "created_at"); if (!created_at_json || !cJSON_IsNumber(created_at_json)) { fprintf(stderr, "DEBUG: Event missing or invalid 'created_at' field\r\n"); return NOSTR_ERROR_EVENT_INVALID_CONTENT; } // Look for expiration in tags cJSON* tags = cJSON_GetObjectItem(event, "tags"); if (!tags || !cJSON_IsArray(tags)) { fprintf(stderr, "DEBUG: Event missing or invalid 'tags' field\r\n"); return NOSTR_ERROR_EVENT_INVALID_CONTENT; } time_t expiration = 0; int found_method = 0; int found_hash = 0; // Parse tags for 't' (method), 'x' (hash), and 'expiration' 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, "t") == 0) { // Method tag 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; fprintf(stderr, "DEBUG: Found matching method tag: %s\r\n", event_method); } } } else if (strcmp(tag_name_str, "x") == 0) { // Hash tag 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) { found_hash = 1; fprintf(stderr, "DEBUG: Found matching hash tag: %s\r\n", event_hash); } } } else if (strcmp(tag_name_str, "expiration") == 0) { // Expiration tag cJSON* exp_value = cJSON_GetArrayItem(tag, 1); if (exp_value && cJSON_IsString(exp_value)) { expiration = (time_t)atol(cJSON_GetStringValue(exp_value)); fprintf(stderr, "DEBUG: Found expiration tag: %ld\r\n", expiration); } } } // Check if method matches (required) if (!found_method) { fprintf(stderr, "DEBUG: Event missing or invalid method tag\r\n"); return NOSTR_ERROR_EVENT_INVALID_CONTENT; } // Check if hash matches (if provided) if (expected_hash && !found_hash) { fprintf(stderr, "DEBUG: Event hash doesn't match expected hash\r\n"); return NOSTR_ERROR_EVENT_INVALID_CONTENT; } // Check expiration time_t now = time(NULL); if (expiration > 0 && now > expiration) { fprintf(stderr, "DEBUG: Event expired (now: %ld, exp: %ld)\r\n", now, expiration); return NOSTR_ERROR_EVENT_INVALID_CONTENT; } fprintf(stderr, "DEBUG: Blossom event validation passed\r\n"); return NOSTR_SUCCESS; } // Forward declarations for detailed validation functions int detailed_structure_validation(cJSON* event); int detailed_signature_validation(cJSON* event); void analyze_event_fields(cJSON* event); void hex_dump(const char* label, const unsigned char* data, size_t len); /** * Detailed structure validation with step-by-step logging */ int detailed_structure_validation(cJSON* event) { LOG_DIVIDER(); LOG_STEP("STRUCT-1", "Starting detailed structure validation"); if (!event || !cJSON_IsObject(event)) { LOG_ERROR("Event is null or not a JSON object"); return NOSTR_ERROR_EVENT_INVALID_STRUCTURE; } LOG_SUCCESS("Event is valid JSON object"); // Check each required field existence LOG_STEP("STRUCT-2", "Checking required field existence"); const char* required_fields[] = {"id", "pubkey", "created_at", "kind", "tags", "content", "sig"}; for (int i = 0; i < 7; i++) { cJSON* field = cJSON_GetObjectItem(event, required_fields[i]); if (!field) { LOG_ERROR("Missing required field: %s", required_fields[i]); return NOSTR_ERROR_EVENT_INVALID_STRUCTURE; } LOG_SUCCESS("Field '%s' exists", required_fields[i]); } // Get all fields for detailed validation cJSON* id_item = cJSON_GetObjectItem(event, "id"); cJSON* pubkey_item = cJSON_GetObjectItem(event, "pubkey"); cJSON* created_at_item = cJSON_GetObjectItem(event, "created_at"); cJSON* kind_item = cJSON_GetObjectItem(event, "kind"); cJSON* tags_item = cJSON_GetObjectItem(event, "tags"); cJSON* content_item = cJSON_GetObjectItem(event, "content"); cJSON* sig_item = cJSON_GetObjectItem(event, "sig"); // Validate field types LOG_STEP("STRUCT-3", "Validating field types"); if (!cJSON_IsString(id_item)) { LOG_ERROR("Field 'id' is not a string (type: %d)", id_item->type); return NOSTR_ERROR_EVENT_INVALID_ID; } LOG_SUCCESS("Field 'id' is string"); if (!cJSON_IsString(pubkey_item)) { LOG_ERROR("Field 'pubkey' is not a string (type: %d)", pubkey_item->type); return NOSTR_ERROR_EVENT_INVALID_PUBKEY; } LOG_SUCCESS("Field 'pubkey' is string"); if (!cJSON_IsNumber(created_at_item)) { LOG_ERROR("Field 'created_at' is not a number (type: %d)", created_at_item->type); return NOSTR_ERROR_EVENT_INVALID_CREATED_AT; } LOG_SUCCESS("Field 'created_at' is number"); if (!cJSON_IsNumber(kind_item)) { LOG_ERROR("Field 'kind' is not a number (type: %d)", kind_item->type); return NOSTR_ERROR_EVENT_INVALID_KIND; } LOG_SUCCESS("Field 'kind' is number"); if (!cJSON_IsArray(tags_item)) { LOG_ERROR("Field 'tags' is not an array (type: %d)", tags_item->type); return NOSTR_ERROR_EVENT_INVALID_TAGS; } LOG_SUCCESS("Field 'tags' is array"); if (!cJSON_IsString(content_item)) { LOG_ERROR("Field 'content' is not a string (type: %d)", content_item->type); return NOSTR_ERROR_EVENT_INVALID_CONTENT; } LOG_SUCCESS("Field 'content' is string"); if (!cJSON_IsString(sig_item)) { LOG_ERROR("Field 'sig' is not a string (type: %d)", sig_item->type); return NOSTR_ERROR_EVENT_INVALID_SIGNATURE; } LOG_SUCCESS("Field 'sig' is string"); // Validate hex string lengths LOG_STEP("STRUCT-4", "Validating hex string lengths"); const char* id_str = cJSON_GetStringValue(id_item); const char* pubkey_str = cJSON_GetStringValue(pubkey_item); const char* sig_str = cJSON_GetStringValue(sig_item); LOG_INFO("ID string: '%s' (length: %zu)", id_str, id_str ? strlen(id_str) : 0); if (!id_str || strlen(id_str) != 64) { LOG_ERROR("ID string invalid length (expected 64, got %zu)", id_str ? strlen(id_str) : 0); return NOSTR_ERROR_EVENT_INVALID_ID; } LOG_SUCCESS("ID string length is correct (64 chars)"); LOG_INFO("Pubkey string: '%s' (length: %zu)", pubkey_str, pubkey_str ? strlen(pubkey_str) : 0); if (!pubkey_str || strlen(pubkey_str) != 64) { LOG_ERROR("Pubkey string invalid length (expected 64, got %zu)", pubkey_str ? strlen(pubkey_str) : 0); return NOSTR_ERROR_EVENT_INVALID_PUBKEY; } LOG_SUCCESS("Pubkey string length is correct (64 chars)"); LOG_INFO("Signature string: '%s' (length: %zu)", sig_str, sig_str ? strlen(sig_str) : 0); if (!sig_str || strlen(sig_str) != 128) { LOG_ERROR("Signature string invalid length (expected 128, got %zu)", sig_str ? strlen(sig_str) : 0); return NOSTR_ERROR_EVENT_INVALID_SIGNATURE; } LOG_SUCCESS("Signature string length is correct (128 chars)"); // Validate hex characters LOG_STEP("STRUCT-5", "Validating hex characters"); LOG_INFO("Checking ID hex characters..."); for (int i = 0; i < 64; i++) { char c = id_str[i]; if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) { LOG_ERROR("Invalid hex character in ID at position %d: '%c' (0x%02x)", i, c, (unsigned char)c); return NOSTR_ERROR_EVENT_INVALID_ID; } } LOG_SUCCESS("ID hex characters are valid (lowercase)"); LOG_INFO("Checking pubkey hex characters..."); for (int i = 0; i < 64; i++) { char c = pubkey_str[i]; if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) { LOG_ERROR("Invalid hex character in pubkey at position %d: '%c' (0x%02x)", i, c, (unsigned char)c); return NOSTR_ERROR_EVENT_INVALID_PUBKEY; } } LOG_SUCCESS("Pubkey hex characters are valid (lowercase)"); LOG_INFO("Checking signature hex characters..."); for (int i = 0; i < 128; i++) { char c = sig_str[i]; if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) { LOG_ERROR("Invalid hex character in signature at position %d: '%c' (0x%02x)", i, c, (unsigned char)c); return NOSTR_ERROR_EVENT_INVALID_SIGNATURE; } } LOG_SUCCESS("Signature hex characters are valid (lowercase)"); // Validate timestamp LOG_STEP("STRUCT-6", "Validating timestamp"); double created_at = cJSON_GetNumberValue(created_at_item); LOG_INFO("Created_at timestamp: %.0f", created_at); if (created_at < 0) { LOG_ERROR("Invalid timestamp (negative): %.0f", created_at); return NOSTR_ERROR_EVENT_INVALID_CREATED_AT; } // Convert to human readable time time_t timestamp = (time_t)created_at; char time_str[100]; struct tm* tm_info = gmtime(×tamp); strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S UTC", tm_info); LOG_SUCCESS("Timestamp is valid: %s", time_str); // Validate kind LOG_STEP("STRUCT-7", "Validating kind"); double kind = cJSON_GetNumberValue(kind_item); LOG_INFO("Event kind: %.0f", kind); if (kind < 0 || kind > 65535 || kind != (int)kind) { LOG_ERROR("Invalid kind value: %.0f (must be integer 0-65535)", kind); return NOSTR_ERROR_EVENT_INVALID_KIND; } LOG_SUCCESS("Kind is valid: %d", (int)kind); // Validate tags array structure LOG_STEP("STRUCT-8", "Validating tags array structure"); int tag_count = cJSON_GetArraySize(tags_item); LOG_INFO("Tags array has %d elements", tag_count); cJSON* tag_item; int tag_index = 0; cJSON_ArrayForEach(tag_item, tags_item) { if (!cJSON_IsArray(tag_item)) { LOG_ERROR("Tag at index %d is not an array (type: %d)", tag_index, tag_item->type); return NOSTR_ERROR_EVENT_INVALID_TAGS; } int tag_element_count = cJSON_GetArraySize(tag_item); LOG_INFO("Tag[%d] has %d elements", tag_index, tag_element_count); cJSON* tag_element; int element_index = 0; cJSON_ArrayForEach(tag_element, tag_item) { if (!cJSON_IsString(tag_element)) { LOG_ERROR("Tag[%d][%d] is not a string (type: %d)", tag_index, element_index, tag_element->type); return NOSTR_ERROR_EVENT_INVALID_TAGS; } const char* tag_value = cJSON_GetStringValue(tag_element); LOG_INFO("Tag[%d][%d]: '%s'", tag_index, element_index, tag_value); element_index++; } tag_index++; } LOG_SUCCESS("Tags array structure is valid"); // Validate content LOG_STEP("STRUCT-9", "Validating content"); const char* content_str = cJSON_GetStringValue(content_item); LOG_INFO("Content: '%s' (length: %zu)", content_str, content_str ? strlen(content_str) : 0); LOG_SUCCESS("Content is valid string"); LOG_SUCCESS("Structure validation completed successfully"); return NOSTR_SUCCESS; } /** * Detailed signature validation with step-by-step logging */ int detailed_signature_validation(cJSON* event) { LOG_DIVIDER(); LOG_STEP("CRYPTO-1", "Starting detailed signature validation"); if (!event) { LOG_ERROR("Event is null"); return NOSTR_ERROR_INVALID_INPUT; } // Get event fields cJSON* id_item = cJSON_GetObjectItem(event, "id"); cJSON* pubkey_item = cJSON_GetObjectItem(event, "pubkey"); cJSON* created_at_item = cJSON_GetObjectItem(event, "created_at"); cJSON* kind_item = cJSON_GetObjectItem(event, "kind"); cJSON* tags_item = cJSON_GetObjectItem(event, "tags"); cJSON* content_item = cJSON_GetObjectItem(event, "content"); cJSON* sig_item = cJSON_GetObjectItem(event, "sig"); if (!id_item || !pubkey_item || !created_at_item || !kind_item || !tags_item || !content_item || !sig_item) { LOG_ERROR("Missing required fields for signature validation"); return NOSTR_ERROR_EVENT_INVALID_STRUCTURE; } // Create serialization array LOG_STEP("CRYPTO-2", "Creating serialization array"); cJSON* serialize_array = cJSON_CreateArray(); if (!serialize_array) { LOG_ERROR("Failed to create serialization array"); return NOSTR_ERROR_MEMORY_FAILED; } cJSON_AddItemToArray(serialize_array, cJSON_CreateNumber(0)); cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(pubkey_item, 1)); cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(created_at_item, 1)); cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(kind_item, 1)); cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(tags_item, 1)); cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(content_item, 1)); LOG_SUCCESS("Serialization array created"); // Convert to JSON string LOG_STEP("CRYPTO-3", "Converting to JSON string"); char* serialize_string = cJSON_PrintUnformatted(serialize_array); cJSON_Delete(serialize_array); if (!serialize_string) { LOG_ERROR("Failed to serialize array to JSON string"); return NOSTR_ERROR_MEMORY_FAILED; } LOG_SUCCESS("JSON serialization string created"); LOG_INFO("Serialization string (length %zu): %s", strlen(serialize_string), serialize_string); // Hash the serialized event LOG_STEP("CRYPTO-4", "Computing SHA256 hash"); unsigned char event_hash[32]; if (nostr_sha256((const unsigned char*)serialize_string, strlen(serialize_string), event_hash) != 0) { LOG_ERROR("SHA256 hashing failed"); free(serialize_string); return NOSTR_ERROR_CRYPTO_FAILED; } LOG_SUCCESS("SHA256 hash computed"); hex_dump("Event hash", event_hash, 32); // Convert hash to hex for event ID verification LOG_STEP("CRYPTO-5", "Verifying event ID"); char calculated_id[65]; nostr_bytes_to_hex(event_hash, 32, calculated_id); const char* provided_id = cJSON_GetStringValue(id_item); LOG_INFO("Calculated ID: %s", calculated_id); LOG_INFO("Provided ID: %s", provided_id); if (!provided_id || strcmp(calculated_id, provided_id) != 0) { LOG_ERROR("Event ID mismatch!"); LOG_ERROR(" Expected: %s", calculated_id); LOG_ERROR(" Got: %s", provided_id ? provided_id : "NULL"); free(serialize_string); return NOSTR_ERROR_EVENT_INVALID_ID; } LOG_SUCCESS("Event ID verification passed"); // Prepare signature verification LOG_STEP("CRYPTO-6", "Preparing signature verification"); const char* pubkey_str = cJSON_GetStringValue(pubkey_item); const char* sig_str = cJSON_GetStringValue(sig_item); if (!pubkey_str || !sig_str) { LOG_ERROR("Missing pubkey or signature strings"); free(serialize_string); return NOSTR_ERROR_EVENT_INVALID_STRUCTURE; } // Convert hex strings to bytes LOG_STEP("CRYPTO-7", "Converting hex strings to bytes"); unsigned char pubkey_bytes[32]; unsigned char sig_bytes[64]; if (nostr_hex_to_bytes(pubkey_str, pubkey_bytes, 32) != 0) { LOG_ERROR("Failed to convert pubkey hex to bytes"); free(serialize_string); return NOSTR_ERROR_CRYPTO_FAILED; } LOG_SUCCESS("Pubkey hex converted to bytes"); hex_dump("Pubkey bytes", pubkey_bytes, 32); if (nostr_hex_to_bytes(sig_str, sig_bytes, 64) != 0) { LOG_ERROR("Failed to convert signature hex to bytes"); free(serialize_string); return NOSTR_ERROR_CRYPTO_FAILED; } LOG_SUCCESS("Signature hex converted to bytes"); hex_dump("Signature bytes", sig_bytes, 64); // Verify signature using nostr_core_lib function (avoiding direct secp256k1 calls) LOG_STEP("CRYPTO-8", "Verifying signature using nostr_verify_event_signature()"); // Create a temporary event structure for verification cJSON* temp_event = cJSON_CreateObject(); if (!temp_event) { LOG_ERROR("Failed to create temporary event for verification"); free(serialize_string); return NOSTR_ERROR_MEMORY_FAILED; } // Copy all required fields to temp event cJSON_AddItemToObject(temp_event, "id", cJSON_Duplicate(id_item, 1)); cJSON_AddItemToObject(temp_event, "pubkey", cJSON_Duplicate(pubkey_item, 1)); cJSON_AddItemToObject(temp_event, "created_at", cJSON_Duplicate(created_at_item, 1)); cJSON_AddItemToObject(temp_event, "kind", cJSON_Duplicate(kind_item, 1)); cJSON_AddItemToObject(temp_event, "tags", cJSON_Duplicate(tags_item, 1)); cJSON_AddItemToObject(temp_event, "content", cJSON_Duplicate(content_item, 1)); cJSON_AddItemToObject(temp_event, "sig", cJSON_Duplicate(sig_item, 1)); LOG_INFO("Calling nostr_verify_event_signature() for detailed crypto validation"); int crypto_verify_result = nostr_verify_event_signature(temp_event); LOG_INFO("nostr_verify_event_signature returned: %d (%s)", crypto_verify_result, nostr_strerror(crypto_verify_result)); cJSON_Delete(temp_event); if (crypto_verify_result != NOSTR_SUCCESS) { LOG_ERROR("Signature verification FAILED!"); LOG_ERROR("nostr_verify_event_signature returned error: %d (%s)", crypto_verify_result, nostr_strerror(crypto_verify_result)); free(serialize_string); return crypto_verify_result; } LOG_SUCCESS("Signature verification PASSED using nostr_core_lib!"); free(serialize_string); return NOSTR_SUCCESS; } /** * Analyze event fields in detail */ void analyze_event_fields(cJSON* event) { LOG_DIVIDER(); LOG_STEP("ANALYZE-1", "Analyzing event field details"); cJSON* field; cJSON_ArrayForEach(field, event) { if (field->string) { LOG_INFO("Field '%s':", field->string); if (cJSON_IsString(field)) { const char* value = cJSON_GetStringValue(field); LOG_INFO(" Type: String"); LOG_INFO(" Value: '%s'", value); LOG_INFO(" Length: %zu", value ? strlen(value) : 0); } else if (cJSON_IsNumber(field)) { double value = cJSON_GetNumberValue(field); LOG_INFO(" Type: Number"); LOG_INFO(" Value: %.0f", value); } else if (cJSON_IsArray(field)) { int size = cJSON_GetArraySize(field); LOG_INFO(" Type: Array"); LOG_INFO(" Size: %d", size); } else { LOG_INFO(" Type: Other (%d)", field->type); } } } } /** * Print hex dump of binary data */ void hex_dump(const char* label, const unsigned char* data, size_t len) { LOG_INFO("%s (%zu bytes):", label, len); for (size_t i = 0; i < len; i += 16) { fprintf(stderr, " %04zx: ", i); for (size_t j = 0; j < 16; j++) { if (i + j < len) { fprintf(stderr, "%02x ", data[i + j]); } else { fprintf(stderr, " "); } } fprintf(stderr, " |"); for (size_t j = 0; j < 16 && i + j < len; j++) { unsigned char c = data[i + j]; fprintf(stderr, "%c", (c >= 32 && c <= 126) ? c : '.'); } fprintf(stderr, "|\n"); } } // Main authentication function with comprehensive step-by-step logging int authenticate_request(const char* auth_header, const char* method, const char* file_hash) { LOG_DIVIDER(); LOG_STEP("SERVER-1", "Starting server-style authentication (mirroring test_auth_debug.c)"); if (!auth_header) { LOG_ERROR("No authorization header provided"); return NOSTR_ERROR_INVALID_INPUT; } LOG_INFO("Server-style auth called with method: %s, hash: %s", method ? method : "null", file_hash ? file_hash : "null"); // Parse authorization header (same as server) char event_json[4096]; LOG_STEP("SERVER-2", "Calling parse_authorization_header"); int parse_result = parse_authorization_header(auth_header, event_json, sizeof(event_json)); if (parse_result != NOSTR_SUCCESS) { LOG_ERROR("Authorization header parsing failed: %d (%s)", parse_result, nostr_strerror(parse_result)); return parse_result; } LOG_SUCCESS("parse_authorization_header succeeded"); // Parse JSON event (same as server) LOG_STEP("SERVER-3", "Calling cJSON_Parse on JSON string"); LOG_INFO("JSON to parse: %s", event_json); cJSON* event = cJSON_Parse(event_json); if (!event) { LOG_ERROR("Failed to parse JSON event with cJSON_Parse"); return NOSTR_ERROR_EVENT_INVALID_CONTENT; } LOG_SUCCESS("cJSON_Parse succeeded, event parsed"); // Print complete parsed JSON like server does char* parsed_json_str = cJSON_Print(event); LOG_INFO("Parsed JSON: %s", parsed_json_str ? parsed_json_str : "NULL"); if (parsed_json_str) free(parsed_json_str); // Debug: Print event fields before validation (same as server) cJSON* id_json = cJSON_GetObjectItem(event, "id"); cJSON* pubkey_json = cJSON_GetObjectItem(event, "pubkey"); cJSON* sig_json = cJSON_GetObjectItem(event, "sig"); cJSON* kind_json = cJSON_GetObjectItem(event, "kind"); cJSON* created_at_json = cJSON_GetObjectItem(event, "created_at"); LOG_STEP("SERVER-4", "Event fields before validation"); LOG_INFO(" id: %s", id_json && cJSON_IsString(id_json) ? cJSON_GetStringValue(id_json) : "MISSING/INVALID"); LOG_INFO(" pubkey: %s", pubkey_json && cJSON_IsString(pubkey_json) ? cJSON_GetStringValue(pubkey_json) : "MISSING/INVALID"); LOG_INFO(" sig: %s", sig_json && cJSON_IsString(sig_json) ? cJSON_GetStringValue(sig_json) : "MISSING/INVALID"); LOG_INFO(" kind: %d", kind_json && cJSON_IsNumber(kind_json) ? (int)cJSON_GetNumberValue(kind_json) : -999); LOG_INFO(" created_at: %ld", created_at_json && cJSON_IsNumber(created_at_json) ? (long)cJSON_GetNumberValue(created_at_json) : -999); // Detailed pubkey analysis (same as server) if (pubkey_json && cJSON_IsString(pubkey_json)) { const char* pubkey_str = cJSON_GetStringValue(pubkey_json); LOG_STEP("SERVER-5", "Detailed pubkey analysis"); LOG_INFO(" Pubkey: %s", pubkey_str ? pubkey_str : "NULL"); LOG_INFO(" Length: %zu", pubkey_str ? strlen(pubkey_str) : 0); if (pubkey_str && strlen(pubkey_str) == 64) { LOG_INFO(" Character analysis (first 10): "); for (int i = 0; i < 10; i++) { char c = pubkey_str[i]; fprintf(stderr, "%c(0x%02x) ", c, (unsigned char)c); } fprintf(stderr, "\n"); } } // Pre-validation pubkey analysis (same as server) LOG_STEP("SERVER-6", "Pre-validation pubkey analysis"); if (pubkey_json && cJSON_IsString(pubkey_json)) { const char* pubkey_str = cJSON_GetStringValue(pubkey_json); LOG_INFO(" Pubkey: %s", pubkey_str ? pubkey_str : "NULL"); LOG_INFO(" Length: %zu", pubkey_str ? strlen(pubkey_str) : 0); if (pubkey_str && strlen(pubkey_str) == 64) { LOG_INFO(" Character analysis (first 10): "); for (int i = 0; i < 10; i++) { char c = pubkey_str[i]; fprintf(stderr, "%c(%d) ", c, (int)c); } fprintf(stderr, "\n"); LOG_INFO(" Character validation test: "); int valid_chars = 1; for (int i = 0; i < 64; i++) { char c = pubkey_str[i]; if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) { fprintf(stderr, "INVALID at pos %d: %c(%d) ", i, c, (int)c); valid_chars = 0; } } if (valid_chars) { fprintf(stderr, "ALL VALID (lowercase hex)\n"); } else { fprintf(stderr, "\n"); } } } // Detailed validation analysis (same as server) LOG_STEP("SERVER-7", "Starting detailed validation analysis"); // Test structure validation first (same as server) LOG_INFO("Testing structure validation..."); int structure_result = nostr_validate_event_structure(event); LOG_INFO("nostr_validate_event_structure returned: %d (%s)", structure_result, nostr_strerror(structure_result)); // EMERGENCY DEBUG: Write structure validation result to file FILE* debug_file = fopen("debug_validation.log", "a"); if (debug_file) { fprintf(debug_file, "=== STRUCTURE VALIDATION DEBUG ===\n"); fprintf(debug_file, "nostr_validate_event_structure result: %d (%s)\n", structure_result, nostr_strerror(structure_result)); if (structure_result != NOSTR_SUCCESS) { fprintf(debug_file, "STRUCTURE VALIDATION FAILED!\n"); // Log the event JSON for debugging char* event_str = cJSON_Print(event); if (event_str) { fprintf(debug_file, "Event JSON: %s\n", event_str); free(event_str); } } fprintf(debug_file, "=== END STRUCTURE DEBUG ===\n\n"); fclose(debug_file); } if (structure_result != NOSTR_SUCCESS) { LOG_ERROR("STRUCTURE validation failed!"); cJSON_Delete(event); return structure_result; } LOG_SUCCESS("Structure validation PASSED"); // Test crypto validation separately (same as server) LOG_INFO("Testing cryptographic verification..."); int crypto_result = nostr_verify_event_signature(event); LOG_INFO("nostr_verify_event_signature returned: %d (%s)", crypto_result, nostr_strerror(crypto_result)); // EMERGENCY DEBUG: Write crypto validation result to file FILE* debug_file2 = fopen("debug_validation.log", "a"); if (debug_file2) { fprintf(debug_file2, "=== CRYPTO VALIDATION DEBUG ===\n"); fprintf(debug_file2, "nostr_verify_event_signature result: %d (%s)\n", crypto_result, nostr_strerror(crypto_result)); if (crypto_result != NOSTR_SUCCESS) { fprintf(debug_file2, "CRYPTO VALIDATION FAILED!\n"); if (pubkey_json && cJSON_IsString(pubkey_json)) { const char* pubkey_str = cJSON_GetStringValue(pubkey_json); fprintf(debug_file2, "Failed pubkey: %s (length: %zu)\n", pubkey_str ? pubkey_str : "NULL", pubkey_str ? strlen(pubkey_str) : 0); } } fprintf(debug_file2, "=== END CRYPTO DEBUG ===\n\n"); fclose(debug_file2); } if (crypto_result != NOSTR_SUCCESS) { LOG_ERROR("CRYPTO verification failed!"); if (pubkey_json && cJSON_IsString(pubkey_json)) { const char* pubkey_str = cJSON_GetStringValue(pubkey_json); LOG_ERROR("Failed pubkey: %s (length: %zu)", pubkey_str ? pubkey_str : "NULL", pubkey_str ? strlen(pubkey_str) : 0); } cJSON_Delete(event); return crypto_result; } LOG_SUCCESS("Crypto verification PASSED"); // Finally test complete validation (same as server) LOG_INFO("Testing complete validation..."); int validation_result = nostr_validate_event(event); LOG_INFO("nostr_validate_event returned: %d (%s)", validation_result, nostr_strerror(validation_result)); // EMERGENCY DEBUG: Write complete validation result to file FILE* debug_file3 = fopen("debug_validation.log", "a"); if (debug_file3) { fprintf(debug_file3, "=== COMPLETE VALIDATION DEBUG ===\n"); fprintf(debug_file3, "nostr_validate_event result: %d (%s)\n", validation_result, nostr_strerror(validation_result)); if (validation_result != NOSTR_SUCCESS) { fprintf(debug_file3, "COMPLETE VALIDATION FAILED!\n"); if (pubkey_json && cJSON_IsString(pubkey_json)) { const char* pubkey_str = cJSON_GetStringValue(pubkey_json); fprintf(debug_file3, "Pubkey length: %zu, value: %s\n", pubkey_str ? strlen(pubkey_str) : 0, pubkey_str ? pubkey_str : "NULL"); } } fprintf(debug_file3, "=== END COMPLETE DEBUG ===\n\n"); fclose(debug_file3); } if (validation_result != NOSTR_SUCCESS) { LOG_ERROR("COMPLETE validation failed: %d (%s)", validation_result, nostr_strerror(validation_result)); // Additional debug: Check specific validation issues (same as server) if (pubkey_json && cJSON_IsString(pubkey_json)) { const char* pubkey_str = cJSON_GetStringValue(pubkey_json); LOG_ERROR("Pubkey length: %zu, value: %s", pubkey_str ? strlen(pubkey_str) : 0, pubkey_str ? pubkey_str : "NULL"); } cJSON_Delete(event); return validation_result; } LOG_SUCCESS("Complete validation PASSED"); // Run our detailed validations for additional debugging LOG_STEP("SERVER-8", "Running detailed structure validation"); int detailed_struct_result = detailed_structure_validation(event); if (detailed_struct_result != NOSTR_SUCCESS) { LOG_ERROR("Detailed structure validation failed: %d (%s)", detailed_struct_result, nostr_strerror(detailed_struct_result)); cJSON_Delete(event); return detailed_struct_result; } LOG_SUCCESS("Detailed structure validation PASSED"); LOG_STEP("SERVER-9", "Running detailed signature validation"); int detailed_crypto_result = detailed_signature_validation(event); if (detailed_crypto_result != NOSTR_SUCCESS) { LOG_ERROR("Detailed signature validation failed: %d (%s)", detailed_crypto_result, nostr_strerror(detailed_crypto_result)); cJSON_Delete(event); return detailed_crypto_result; } LOG_SUCCESS("Detailed signature validation PASSED"); // Analyze event fields analyze_event_fields(event); // Validate Blossom-specific requirements LOG_STEP("SERVER-10", "Validating Blossom-specific requirements"); int blossom_result = validate_blossom_event(event, file_hash, method); if (blossom_result != NOSTR_SUCCESS) { LOG_ERROR("Blossom event validation failed: %d (%s)", blossom_result, nostr_strerror(blossom_result)); cJSON_Delete(event); return blossom_result; } LOG_SUCCESS("Blossom event validation PASSED"); cJSON_Delete(event); LOG_SUCCESS("Server-style authentication successful, returning NOSTR_SUCCESS"); return NOSTR_SUCCESS; } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // AUTHENTICATION RULES SYSTEM (4.1.2) ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // Authentication rule result structure typedef struct { int allowed; // 0 = denied, 1 = allowed char reason[256]; // Human-readable reason int rule_id; // Rule ID that made the decision (0 if no rule) int priority; // Priority of the rule that matched } auth_rule_result_t; // Check if authentication rules system is enabled int auth_rules_enabled(void) { sqlite3* db; sqlite3_stmt* stmt; int rc, enabled = 0; rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); if (rc) { fprintf(stderr, "DEBUG: Database open failed in auth_rules_enabled: %s\r\n", sqlite3_errmsg(db)); return 0; // Disable rules if can't check database } const char* sql = "SELECT value FROM server_config WHERE key = 'auth_rules_enabled'"; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) { const char* value = (const char*)sqlite3_column_text(stmt, 0); enabled = (value && strcmp(value, "true") == 0) ? 1 : 0; } sqlite3_finalize(stmt); } sqlite3_close(db); return enabled; } // Check pubkey whitelist rule int check_pubkey_whitelist(const char* pubkey, const char* operation, auth_rule_result_t* result) { if (!pubkey || !operation || !result) { return 0; } sqlite3* db; sqlite3_stmt* stmt; int rc; rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); if (rc) { return 0; } const char* sql = "SELECT id, priority, description FROM auth_rules " "WHERE rule_type = 'pubkey_whitelist' AND rule_target = ? " "AND (operation = ? OR operation = '*') AND enabled = 1 " "AND (expires_at IS NULL OR expires_at > strftime('%s', 'now')) " "ORDER BY priority ASC, created_at ASC LIMIT 1"; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { sqlite3_close(db); return 0; } sqlite3_bind_text(stmt, 1, pubkey, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, operation, -1, SQLITE_STATIC); rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) { result->allowed = 1; result->rule_id = sqlite3_column_int(stmt, 0); result->priority = sqlite3_column_int(stmt, 1); const char* description = (const char*)sqlite3_column_text(stmt, 2); snprintf(result->reason, sizeof(result->reason), "Allowed by whitelist rule: %s", description ? description : "pubkey whitelisted"); sqlite3_finalize(stmt); sqlite3_close(db); return 1; } sqlite3_finalize(stmt); sqlite3_close(db); return 0; } // Check pubkey blacklist rule int check_pubkey_blacklist(const char* pubkey, const char* operation, auth_rule_result_t* result) { if (!pubkey || !operation || !result) { return 0; } sqlite3* db; sqlite3_stmt* stmt; int rc; rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); if (rc) { return 0; } const char* sql = "SELECT id, priority, description FROM auth_rules " "WHERE rule_type = 'pubkey_blacklist' AND rule_target = ? " "AND (operation = ? OR operation = '*') AND enabled = 1 " "AND (expires_at IS NULL OR expires_at > strftime('%s', 'now')) " "ORDER BY priority ASC, created_at ASC LIMIT 1"; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { sqlite3_close(db); return 0; } sqlite3_bind_text(stmt, 1, pubkey, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, operation, -1, SQLITE_STATIC); rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) { result->allowed = 0; result->rule_id = sqlite3_column_int(stmt, 0); result->priority = sqlite3_column_int(stmt, 1); const char* description = (const char*)sqlite3_column_text(stmt, 2); snprintf(result->reason, sizeof(result->reason), "Denied by blacklist rule: %s", description ? description : "pubkey blacklisted"); sqlite3_finalize(stmt); sqlite3_close(db); return 1; } sqlite3_finalize(stmt); sqlite3_close(db); return 0; } // Check hash blacklist rule int check_hash_blacklist(const char* hash, const char* operation, auth_rule_result_t* result) { if (!hash || !operation || !result) { return 0; } sqlite3* db; sqlite3_stmt* stmt; int rc; rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); if (rc) { return 0; } const char* sql = "SELECT id, priority, description FROM auth_rules " "WHERE rule_type = 'hash_blacklist' AND rule_target = ? " "AND (operation = ? OR operation = '*') AND enabled = 1 " "AND (expires_at IS NULL OR expires_at > strftime('%s', 'now')) " "ORDER BY priority ASC, created_at ASC LIMIT 1"; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { sqlite3_close(db); return 0; } sqlite3_bind_text(stmt, 1, hash, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, operation, -1, SQLITE_STATIC); rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) { result->allowed = 0; result->rule_id = sqlite3_column_int(stmt, 0); result->priority = sqlite3_column_int(stmt, 1); const char* description = (const char*)sqlite3_column_text(stmt, 2); snprintf(result->reason, sizeof(result->reason), "Denied by hash blacklist rule: %s", description ? description : "hash blacklisted"); sqlite3_finalize(stmt); sqlite3_close(db); return 1; } sqlite3_finalize(stmt); sqlite3_close(db); return 0; } // Check MIME type whitelist rule int check_mime_type_whitelist(const char* mime_type, const char* operation, auth_rule_result_t* result) { if (!mime_type || !operation || !result) { return 0; } sqlite3* db; sqlite3_stmt* stmt; int rc; rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); if (rc) { return 0; } // Check for exact match or wildcard patterns (e.g., "image/*") const char* sql = "SELECT id, priority, description FROM auth_rules " "WHERE rule_type = 'mime_type_whitelist' " "AND (rule_target = ? OR (rule_target LIKE '%/*' AND ? LIKE REPLACE(rule_target, '*', '%'))) " "AND (operation = ? OR operation = '*') AND enabled = 1 " "AND (expires_at IS NULL OR expires_at > strftime('%s', 'now')) " "ORDER BY priority ASC, created_at ASC LIMIT 1"; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { sqlite3_close(db); return 0; } sqlite3_bind_text(stmt, 1, mime_type, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, mime_type, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 3, operation, -1, SQLITE_STATIC); rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) { result->allowed = 1; result->rule_id = sqlite3_column_int(stmt, 0); result->priority = sqlite3_column_int(stmt, 1); const char* description = (const char*)sqlite3_column_text(stmt, 2); snprintf(result->reason, sizeof(result->reason), "Allowed by MIME type whitelist: %s", description ? description : "MIME type whitelisted"); sqlite3_finalize(stmt); sqlite3_close(db); return 1; } sqlite3_finalize(stmt); sqlite3_close(db); return 0; } // Check MIME type blacklist rule int check_mime_type_blacklist(const char* mime_type, const char* operation, auth_rule_result_t* result) { if (!mime_type || !operation || !result) { return 0; } sqlite3* db; sqlite3_stmt* stmt; int rc; rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); if (rc) { return 0; } // Check for exact match or wildcard patterns (e.g., "application/*") const char* sql = "SELECT id, priority, description FROM auth_rules " "WHERE rule_type = 'mime_type_blacklist' " "AND (rule_target = ? OR (rule_target LIKE '%/*' AND ? LIKE REPLACE(rule_target, '*', '%'))) " "AND (operation = ? OR operation = '*') AND enabled = 1 " "AND (expires_at IS NULL OR expires_at > strftime('%s', 'now')) " "ORDER BY priority ASC, created_at ASC LIMIT 1"; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { sqlite3_close(db); return 0; } sqlite3_bind_text(stmt, 1, mime_type, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, mime_type, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 3, operation, -1, SQLITE_STATIC); rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) { result->allowed = 0; result->rule_id = sqlite3_column_int(stmt, 0); result->priority = sqlite3_column_int(stmt, 1); const char* description = (const char*)sqlite3_column_text(stmt, 2); snprintf(result->reason, sizeof(result->reason), "Denied by MIME type blacklist: %s", description ? description : "MIME type blacklisted"); sqlite3_finalize(stmt); sqlite3_close(db); return 1; } sqlite3_finalize(stmt); sqlite3_close(db); return 0; } // Check file size limit rule int check_size_limit(long file_size, const char* pubkey, const char* operation, auth_rule_result_t* result) { if (!result || file_size < 0) { return 0; } sqlite3* db; sqlite3_stmt* stmt; int rc; rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); if (rc) { return 0; } // Check for pubkey-specific or global size limits const char* sql = "SELECT id, priority, rule_value, description FROM auth_rules " "WHERE rule_type = 'size_limit' " "AND (rule_target = ? OR rule_target = '*') " "AND (operation = ? OR operation = '*') AND enabled = 1 " "AND (expires_at IS NULL OR expires_at > strftime('%s', 'now')) " "ORDER BY CASE WHEN rule_target = ? THEN 0 ELSE 1 END, priority ASC, created_at ASC LIMIT 1"; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { sqlite3_close(db); return 0; } sqlite3_bind_text(stmt, 1, pubkey ? pubkey : "*", -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, operation, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 3, pubkey ? pubkey : "*", -1, SQLITE_STATIC); rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) { const char* size_limit_str = (const char*)sqlite3_column_text(stmt, 2); long size_limit = size_limit_str ? atol(size_limit_str) : 0; if (size_limit > 0 && file_size > size_limit) { result->allowed = 0; result->rule_id = sqlite3_column_int(stmt, 0); result->priority = sqlite3_column_int(stmt, 1); const char* description = (const char*)sqlite3_column_text(stmt, 3); snprintf(result->reason, sizeof(result->reason), "File size %ld exceeds limit %ld: %s", file_size, size_limit, description ? description : "size limit exceeded"); sqlite3_finalize(stmt); sqlite3_close(db); return 1; } } sqlite3_finalize(stmt); sqlite3_close(db); return 0; } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // RULE EVALUATION ENGINE (4.1.3) ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // Cache key generation for authentication decisions void generate_auth_cache_key(const char* pubkey, const char* operation, const char* hash, const char* mime_type, long file_size, char* cache_key, size_t key_size) { char temp_buffer[1024]; snprintf(temp_buffer, sizeof(temp_buffer), "%s|%s|%s|%s|%ld", pubkey ? pubkey : "", operation ? operation : "", hash ? hash : "", mime_type ? mime_type : "", file_size); // Generate SHA-256 hash of the key components for consistent cache keys unsigned char hash_bytes[32]; if (nostr_sha256((unsigned char*)temp_buffer, strlen(temp_buffer), hash_bytes) == NOSTR_SUCCESS) { nostr_bytes_to_hex(hash_bytes, 32, cache_key); cache_key[64] = '\0'; // Ensure null termination } else { // Fallback if hashing fails strncpy(cache_key, temp_buffer, key_size - 1); cache_key[key_size - 1] = '\0'; } } // Check authentication cache for previous decisions int check_auth_cache(const char* cache_key, auth_rule_result_t* result) { if (!cache_key || !result) { return 0; } sqlite3* db; sqlite3_stmt* stmt; int rc; rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); if (rc) { return 0; } const char* sql = "SELECT allowed, rule_id, rule_reason FROM auth_cache " "WHERE cache_key = ? AND expires_at > strftime('%s', 'now')"; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { sqlite3_close(db); return 0; } sqlite3_bind_text(stmt, 1, cache_key, -1, SQLITE_STATIC); rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) { result->allowed = sqlite3_column_int(stmt, 0); result->rule_id = sqlite3_column_int(stmt, 1); result->priority = 0; // Not stored in cache const char* reason = (const char*)sqlite3_column_text(stmt, 2); if (reason) { strncpy(result->reason, reason, sizeof(result->reason) - 1); result->reason[sizeof(result->reason) - 1] = '\0'; } sqlite3_finalize(stmt); sqlite3_close(db); fprintf(stderr, "DEBUG: Cache hit for key: %.16s... (allowed=%d)\r\n", cache_key, result->allowed); return 1; } sqlite3_finalize(stmt); sqlite3_close(db); return 0; } // Store authentication decision in cache void store_auth_cache(const char* cache_key, const auth_rule_result_t* result) { if (!cache_key || !result) { return; } sqlite3* db; sqlite3_stmt* stmt; int rc; rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL); if (rc) { fprintf(stderr, "DEBUG: Failed to open database for caching: %s\r\n", sqlite3_errmsg(db)); return; } // Get cache TTL from server config (default 5 minutes) int cache_ttl = 300; const char* ttl_sql = "SELECT value FROM server_config WHERE key = 'auth_cache_ttl'"; rc = sqlite3_prepare_v2(db, ttl_sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) { const char* ttl_value = (const char*)sqlite3_column_text(stmt, 0); if (ttl_value) { cache_ttl = atoi(ttl_value); } } sqlite3_finalize(stmt); } // Insert or replace cache entry const char* insert_sql = "INSERT OR REPLACE INTO auth_cache " "(cache_key, allowed, rule_id, rule_reason, expires_at) " "VALUES (?, ?, ?, ?, strftime('%s', 'now') + ?)"; rc = sqlite3_prepare_v2(db, insert_sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_text(stmt, 1, cache_key, -1, SQLITE_STATIC); sqlite3_bind_int(stmt, 2, result->allowed); sqlite3_bind_int(stmt, 3, result->rule_id); sqlite3_bind_text(stmt, 4, result->reason, -1, SQLITE_STATIC); sqlite3_bind_int(stmt, 5, cache_ttl); rc = sqlite3_step(stmt); if (rc == SQLITE_DONE) { fprintf(stderr, "DEBUG: Cached auth decision for key: %.16s... (TTL=%d)\r\n", cache_key, cache_ttl); } else { fprintf(stderr, "DEBUG: Failed to cache auth decision: %s\r\n", sqlite3_errmsg(db)); } sqlite3_finalize(stmt); } sqlite3_close(db); } // Main rule evaluation function int evaluate_auth_rules(const char* pubkey, const char* operation, const char* hash, const char* mime_type, long file_size, auth_rule_result_t* result) { if (!result) { return 0; } // Initialize result structure memset(result, 0, sizeof(auth_rule_result_t)); result->allowed = 1; // Default allow if no rules apply strcpy(result->reason, "No rules matched - default allow"); fprintf(stderr, "DEBUG: evaluate_auth_rules called - pubkey=%s, op=%s, hash=%s, mime=%s, size=%ld\r\n", pubkey ? pubkey : "NULL", operation ? operation : "NULL", hash ? hash : "NULL", mime_type ? mime_type : "NULL", file_size); // Check if authentication rules system is enabled if (!auth_rules_enabled()) { fprintf(stderr, "DEBUG: Authentication rules system is disabled\r\n"); strcpy(result->reason, "Authentication rules system disabled - default allow"); return 1; } // Generate cache key for this request char cache_key[128]; generate_auth_cache_key(pubkey, operation, hash, mime_type, file_size, cache_key, sizeof(cache_key)); // Check cache first for performance if (check_auth_cache(cache_key, result)) { fprintf(stderr, "DEBUG: Using cached authentication decision\r\n"); return 1; } fprintf(stderr, "DEBUG: No cache hit - evaluating rules in priority order\r\n"); // Evaluate rules in priority order (lower priority number = higher precedence) auth_rule_result_t rule_result; int highest_priority = 9999; int rule_matched = 0; // 1. Check pubkey blacklist first (highest security priority) if (pubkey && check_pubkey_blacklist(pubkey, operation, &rule_result)) { if (rule_result.priority < highest_priority) { *result = rule_result; highest_priority = rule_result.priority; rule_matched = 1; fprintf(stderr, "DEBUG: Pubkey blacklist rule matched (priority %d)\r\n", rule_result.priority); } } // 2. Check hash blacklist if (hash && check_hash_blacklist(hash, operation, &rule_result)) { if (rule_result.priority < highest_priority) { *result = rule_result; highest_priority = rule_result.priority; rule_matched = 1; fprintf(stderr, "DEBUG: Hash blacklist rule matched (priority %d)\r\n", rule_result.priority); } } // 3. Check MIME type blacklist if (mime_type && check_mime_type_blacklist(mime_type, operation, &rule_result)) { if (rule_result.priority < highest_priority) { *result = rule_result; highest_priority = rule_result.priority; rule_matched = 1; fprintf(stderr, "DEBUG: MIME type blacklist rule matched (priority %d)\r\n", rule_result.priority); } } // 4. Check file size limits if (file_size > 0 && check_size_limit(file_size, pubkey, operation, &rule_result)) { if (rule_result.priority < highest_priority) { *result = rule_result; highest_priority = rule_result.priority; rule_matched = 1; fprintf(stderr, "DEBUG: Size limit rule matched (priority %d)\r\n", rule_result.priority); } } // 5. Check pubkey whitelist (only matters if not already denied) if (pubkey && result->allowed && check_pubkey_whitelist(pubkey, operation, &rule_result)) { if (rule_result.priority < highest_priority) { *result = rule_result; highest_priority = rule_result.priority; rule_matched = 1; fprintf(stderr, "DEBUG: Pubkey whitelist rule matched (priority %d)\r\n", rule_result.priority); } } // 6. Check MIME type whitelist (only if not already denied) if (mime_type && result->allowed && check_mime_type_whitelist(mime_type, operation, &rule_result)) { if (rule_result.priority < highest_priority) { *result = rule_result; highest_priority = rule_result.priority; rule_matched = 1; fprintf(stderr, "DEBUG: MIME type whitelist rule matched (priority %d)\r\n", rule_result.priority); } } // Special case: If we have whitelist rules but no whitelist matched, deny by default if (result->allowed && pubkey) { sqlite3* db; sqlite3_stmt* stmt; int rc; rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); if (rc == SQLITE_OK) { // Check if any pubkey whitelist rules exist for this operation const char* sql = "SELECT COUNT(*) FROM auth_rules " "WHERE rule_type = 'pubkey_whitelist' AND enabled = 1 " "AND (operation = ? OR operation = '*') " "AND (expires_at IS NULL OR expires_at > strftime('%s', 'now'))"; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_text(stmt, 1, operation, -1, SQLITE_STATIC); rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) { int whitelist_count = sqlite3_column_int(stmt, 0); if (whitelist_count > 0) { // Whitelist exists but didn't match - deny result->allowed = 0; result->rule_id = 0; result->priority = 0; snprintf(result->reason, sizeof(result->reason), "Denied - pubkey not in whitelist (found %d whitelist rules)", whitelist_count); rule_matched = 1; fprintf(stderr, "DEBUG: Denied due to whitelist policy - pubkey not whitelisted\r\n"); } } sqlite3_finalize(stmt); } sqlite3_close(db); } } // Cache the decision for future requests store_auth_cache(cache_key, result); fprintf(stderr, "DEBUG: Rule evaluation complete - allowed=%d, rule_id=%d, reason=%s\r\n", result->allowed, result->rule_id, result->reason); return rule_matched; } // Enhanced authentication function that integrates rule evaluation int authenticate_request_with_rules(const char* auth_header, const char* method, const char* file_hash, const char* mime_type, long file_size) { fprintf(stderr, "DEBUG: authenticate_request_with_rules called - method: %s, file_hash: %s, mime_type: %s, file_size: %ld\r\n", method ? method : "NULL", file_hash ? file_hash : "NULL", mime_type ? mime_type : "NULL", file_size); // Step 1: Basic nostr authentication (if header provided) const char* pubkey = NULL; static char pubkey_buffer[256]; if (auth_header) { fprintf(stderr, "DEBUG: Authorization header provided, starting basic nostr authentication\r\n"); // Parse and validate nostr event first int auth_result = authenticate_request(auth_header, method, file_hash); if (auth_result != NOSTR_SUCCESS) { fprintf(stderr, "DEBUG: Basic nostr authentication failed: %d (%s)\r\n", auth_result, nostr_strerror(auth_result)); return auth_result; } fprintf(stderr, "DEBUG: Basic nostr authentication PASSED\r\n"); // Extract pubkey from validated event char event_json[4096]; int parse_result = parse_authorization_header(auth_header, event_json, sizeof(event_json)); if (parse_result == NOSTR_SUCCESS) { cJSON* event = cJSON_Parse(event_json); if (event) { cJSON* pubkey_json = cJSON_GetObjectItem(event, "pubkey"); if (pubkey_json && cJSON_IsString(pubkey_json)) { const char* temp_pubkey = cJSON_GetStringValue(pubkey_json); if (temp_pubkey) { strncpy(pubkey_buffer, temp_pubkey, sizeof(pubkey_buffer)-1); pubkey_buffer[sizeof(pubkey_buffer)-1] = '\0'; pubkey = pubkey_buffer; } } cJSON_Delete(event); } } fprintf(stderr, "DEBUG: Extracted pubkey from auth: %s\r\n", pubkey ? pubkey : "NULL"); } else { fprintf(stderr, "DEBUG: No authorization header - evaluating rules for anonymous request\r\n"); } // Step 2: Evaluate authentication rules auth_rule_result_t rule_result; int rule_evaluated = evaluate_auth_rules(pubkey, method, file_hash, mime_type, file_size, &rule_result); if (rule_evaluated && !rule_result.allowed) { fprintf(stderr, "DEBUG: Request denied by authentication rules: %s\r\n", rule_result.reason); return NOSTR_ERROR_INVALID_INPUT; // Authentication denied by rules } fprintf(stderr, "DEBUG: Request allowed - nostr auth + rules passed\r\n"); return NOSTR_SUCCESS; } // Enhanced error response helper functions void send_error_response(int status_code, const char* error_type, const char* message, const char* details) { const char* status_text; switch (status_code) { case 400: status_text = "Bad Request"; break; case 401: status_text = "Unauthorized"; break; case 409: status_text = "Conflict"; break; case 413: status_text = "Payload Too Large"; break; case 500: status_text = "Internal Server Error"; break; default: status_text = "Error"; break; } printf("Status: %d %s\r\n", status_code, status_text); printf("Content-Type: application/json\r\n\r\n"); printf("{\n"); printf(" \"error\": \"%s\",\n", error_type); printf(" \"message\": \"%s\"", message); if (details) { printf(",\n \"details\": \"%s\"", details); } printf("\n}\n"); } void log_request(const char* method, const char* uri, const char* auth_status, int status_code) { time_t now = time(NULL); struct tm* tm_info = localtime(&now); char timestamp[64]; strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", tm_info); // For now, log to stdout - later can be configured to log files fprintf(stderr, "LOG: [%s] %s %s - Auth: %s - Status: %d\r\n", timestamp, method ? method : "NULL", uri ? uri : "NULL", auth_status ? auth_status : "none", status_code); } // Handle GET /list/ requests void handle_list_request(const char* pubkey) { fprintf(stderr, "DEBUG: handle_list_request called with pubkey=%s\r\n", pubkey ? pubkey : "NULL"); // Log the incoming request log_request("GET", "/list", "pending", 0); // Validate pubkey format (64 hex characters) if (!pubkey || strlen(pubkey) != 64) { send_error_response(400, "invalid_pubkey", "Invalid pubkey format", "Pubkey must be 64 hex characters"); log_request("GET", "/list", "none", 400); return; } // Validate hex characters for (int i = 0; i < 64; i++) { char c = pubkey[i]; if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { send_error_response(400, "invalid_pubkey", "Invalid pubkey format", "Pubkey must contain only hex characters"); log_request("GET", "/list", "none", 400); return; } } // Get query parameters for since/until filtering const char* query_string = getenv("QUERY_STRING"); long since_timestamp = 0; long until_timestamp = 0; if (query_string) { fprintf(stderr, "DEBUG: Query string: %s\r\n", query_string); // Parse since parameter const char* since_param = strstr(query_string, "since="); if (since_param) { since_timestamp = atol(since_param + 6); fprintf(stderr, "DEBUG: Since timestamp: %ld\r\n", since_timestamp); } // Parse until parameter const char* until_param = strstr(query_string, "until="); if (until_param) { until_timestamp = atol(until_param + 6); fprintf(stderr, "DEBUG: Until timestamp: %ld\r\n", until_timestamp); } } // Check for optional authorization const char* auth_header = getenv("HTTP_AUTHORIZATION"); const char* auth_status = "none"; if (auth_header) { fprintf(stderr, "DEBUG: Authorization header provided for list request\r\n"); int auth_result = authenticate_request_with_rules(auth_header, "list", NULL, NULL, 0); if (auth_result != NOSTR_SUCCESS) { send_error_response(401, "authentication_failed", "Invalid or expired authentication", "The provided Nostr event is invalid, expired, or does not authorize this operation"); log_request("GET", "/list", "failed", 401); return; } auth_status = "authenticated"; } // Query database for blobs uploaded by this pubkey sqlite3* db; sqlite3_stmt* stmt; int rc; rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); if (rc) { fprintf(stderr, "DEBUG: Database open failed: %s\r\n", sqlite3_errmsg(db)); send_error_response(500, "database_error", "Failed to access database", "Internal server error"); log_request("GET", "/list", auth_status, 500); return; } // Build SQL query with optional timestamp filtering char sql[1024]; if (since_timestamp > 0 && until_timestamp > 0) { snprintf(sql, sizeof(sql), "SELECT sha256, size, type, uploaded_at, filename FROM blobs WHERE uploader_pubkey = ? AND uploaded_at >= ? AND uploaded_at <= ? ORDER BY uploaded_at DESC"); } else if (since_timestamp > 0) { snprintf(sql, sizeof(sql), "SELECT sha256, size, type, uploaded_at, filename FROM blobs WHERE uploader_pubkey = ? AND uploaded_at >= ? ORDER BY uploaded_at DESC"); } else if (until_timestamp > 0) { snprintf(sql, sizeof(sql), "SELECT sha256, size, type, uploaded_at, filename FROM blobs WHERE uploader_pubkey = ? AND uploaded_at <= ? ORDER BY uploaded_at DESC"); } else { snprintf(sql, sizeof(sql), "SELECT sha256, size, type, uploaded_at, filename FROM blobs WHERE uploader_pubkey = ? ORDER BY uploaded_at DESC"); } fprintf(stderr, "DEBUG: SQL query: %s\r\n", sql); rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { fprintf(stderr, "DEBUG: SQL prepare failed: %s\r\n", sqlite3_errmsg(db)); sqlite3_close(db); send_error_response(500, "database_error", "Failed to prepare query", "Internal server error"); log_request("GET", "/list", auth_status, 500); return; } // Bind parameters sqlite3_bind_text(stmt, 1, pubkey, -1, SQLITE_STATIC); int param_index = 2; if (since_timestamp > 0) { sqlite3_bind_int64(stmt, param_index++, since_timestamp); } if (until_timestamp > 0) { sqlite3_bind_int64(stmt, param_index++, until_timestamp); } // Start JSON response printf("Status: 200 OK\r\n"); printf("Content-Type: application/json\r\n\r\n"); printf("[\n"); int first_item = 1; while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { if (!first_item) { printf(",\n"); } first_item = 0; const char* sha256 = (const char*)sqlite3_column_text(stmt, 0); long size = sqlite3_column_int64(stmt, 1); const char* type = (const char*)sqlite3_column_text(stmt, 2); long uploaded_at = sqlite3_column_int64(stmt, 3); const char* filename = (const char*)sqlite3_column_text(stmt, 4); // Determine file extension from MIME type const char* extension = ""; if (strstr(type, "image/jpeg")) { extension = ".jpg"; } else if (strstr(type, "image/webp")) { extension = ".webp"; } else if (strstr(type, "image/png")) { extension = ".png"; } else if (strstr(type, "image/gif")) { extension = ".gif"; } else if (strstr(type, "video/mp4")) { extension = ".mp4"; } else if (strstr(type, "video/webm")) { extension = ".webm"; } else if (strstr(type, "audio/mpeg")) { extension = ".mp3"; } else if (strstr(type, "audio/ogg")) { extension = ".ogg"; } else if (strstr(type, "text/plain")) { extension = ".txt"; } else { extension = ".bin"; } // Output blob descriptor JSON printf(" {\n"); printf(" \"url\": \"http://localhost:9001/%s%s\",\n", sha256, extension); printf(" \"sha256\": \"%s\",\n", sha256); printf(" \"size\": %ld,\n", size); printf(" \"type\": \"%s\",\n", type); printf(" \"uploaded\": %ld", uploaded_at); // Add optional filename if available if (filename && strlen(filename) > 0) { printf(",\n \"filename\": \"%s\"", filename); } printf("\n }"); } printf("\n]\n"); sqlite3_finalize(stmt); sqlite3_close(db); fprintf(stderr, "DEBUG: List request completed successfully\r\n"); log_request("GET", "/list", auth_status, 200); } // Handle DELETE / requests void handle_delete_request(const char* sha256) { fprintf(stderr, "DEBUG: handle_delete_request called with sha256=%s\r\n", sha256 ? sha256 : "NULL"); // Log the incoming request log_request("DELETE", "/delete", "pending", 0); // Validate SHA-256 format (64 hex characters) if (!sha256 || strlen(sha256) != 64) { send_error_response(400, "invalid_hash", "Invalid SHA-256 hash format", "Hash must be 64 hex characters"); log_request("DELETE", "/delete", "none", 400); return; } // Validate hex characters for (int i = 0; i < 64; i++) { char c = sha256[i]; if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { send_error_response(400, "invalid_hash", "Invalid SHA-256 hash format", "Hash must contain only hex characters"); log_request("DELETE", "/delete", "none", 400); return; } } // Require authorization for delete operations const char* auth_header = getenv("HTTP_AUTHORIZATION"); if (!auth_header) { send_error_response(401, "authorization_required", "Authorization required for delete operations", "Delete operations require a valid Nostr authorization event"); log_request("DELETE", "/delete", "missing_auth", 401); return; } // Authenticate the request with enhanced rules system int auth_result = authenticate_request_with_rules(auth_header, "delete", sha256, NULL, 0); if (auth_result != NOSTR_SUCCESS) { send_error_response(401, "authentication_failed", "Invalid or expired authentication", "The provided Nostr event is invalid, expired, or does not authorize this operation"); log_request("DELETE", "/delete", "failed", 401); return; } // Extract pubkey from authorization for ownership check char event_json[4096]; int parse_result = parse_authorization_header(auth_header, event_json, sizeof(event_json)); if (parse_result != NOSTR_SUCCESS) { send_error_response(401, "authentication_failed", "Failed to parse authorization", "The provided authorization could not be parsed"); log_request("DELETE", "/delete", "parse_failed", 401); return; } cJSON* event = cJSON_Parse(event_json); if (!event) { send_error_response(401, "authentication_failed", "Invalid JSON in authorization", "The provided authorization contains invalid JSON"); log_request("DELETE", "/delete", "invalid_json", 401); return; } cJSON* pubkey_json = cJSON_GetObjectItem(event, "pubkey"); if (!pubkey_json || !cJSON_IsString(pubkey_json)) { cJSON_Delete(event); send_error_response(401, "authentication_failed", "Missing pubkey in authorization", "The provided authorization does not contain a valid pubkey"); log_request("DELETE", "/delete", "missing_pubkey", 401); return; } const char* auth_pubkey = cJSON_GetStringValue(pubkey_json); fprintf(stderr, "DEBUG-DELETE: Extracted auth_pubkey from DELETE request: '%s' (length: %zu)\r\n", auth_pubkey ? auth_pubkey : "NULL", auth_pubkey ? strlen(auth_pubkey) : 0); cJSON_Delete(event); // Check if blob exists in database sqlite3* db; sqlite3_stmt* stmt; int rc; rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL); if (rc) { fprintf(stderr, "DEBUG: Database open failed: %s\r\n", sqlite3_errmsg(db)); send_error_response(500, "database_error", "Failed to access database", "Internal server error"); log_request("DELETE", "/delete", "authenticated", 500); return; } // Query blob metadata and check ownership const char* sql = "SELECT uploader_pubkey, type FROM blobs WHERE sha256 = ?"; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { fprintf(stderr, "DEBUG: SQL prepare failed: %s\r\n", sqlite3_errmsg(db)); sqlite3_close(db); send_error_response(500, "database_error", "Failed to prepare query", "Internal server error"); log_request("DELETE", "/delete", "authenticated", 500); return; } sqlite3_bind_text(stmt, 1, sha256, -1, SQLITE_STATIC); rc = sqlite3_step(stmt); if (rc != SQLITE_ROW) { sqlite3_finalize(stmt); sqlite3_close(db); send_error_response(404, "blob_not_found", "Blob not found", "The specified blob does not exist"); log_request("DELETE", "/delete", "authenticated", 404); return; } // Get blob metadata const char* uploader_pubkey = (const char*)sqlite3_column_text(stmt, 0); const char* blob_type = (const char*)sqlite3_column_text(stmt, 1); fprintf(stderr, "DEBUG-DELETE: Database query results:\r\n"); fprintf(stderr, "DEBUG-DELETE: Raw uploader_pubkey from DB: '%s'\r\n", uploader_pubkey ? uploader_pubkey : "NULL"); fprintf(stderr, "DEBUG-DELETE: Raw blob_type from DB: '%s'\r\n", blob_type ? blob_type : "NULL"); // Create copies of the strings since they may be invalidated after finalize char uploader_pubkey_copy[256] = {0}; char blob_type_copy[128] = {0}; if (uploader_pubkey) { strncpy(uploader_pubkey_copy, uploader_pubkey, sizeof(uploader_pubkey_copy) - 1); } if (blob_type) { strncpy(blob_type_copy, blob_type, sizeof(blob_type_copy) - 1); } sqlite3_finalize(stmt); fprintf(stderr, "DEBUG-DELETE: After copying strings:\r\n"); fprintf(stderr, "DEBUG-DELETE: uploader_pubkey_copy: '%s' (length: %zu)\r\n", uploader_pubkey_copy[0] ? uploader_pubkey_copy : "EMPTY", strlen(uploader_pubkey_copy)); fprintf(stderr, "DEBUG-DELETE: blob_type_copy: '%s'\r\n", blob_type_copy[0] ? blob_type_copy : "EMPTY"); // Check ownership - only the uploader can delete fprintf(stderr, "DEBUG-DELETE: Ownership verification:\r\n"); fprintf(stderr, "DEBUG-DELETE: Comparing uploader_pubkey_copy='%s'\r\n", uploader_pubkey_copy); fprintf(stderr, "DEBUG-DELETE: Against auth_pubkey='%s'\r\n", auth_pubkey ? auth_pubkey : "NULL"); fprintf(stderr, "DEBUG-DELETE: uploader_pubkey_copy[0]=%d\r\n", (int)uploader_pubkey_copy[0]); fprintf(stderr, "DEBUG-DELETE: strcmp result would be: %d\r\n", uploader_pubkey_copy[0] ? strcmp(uploader_pubkey_copy, auth_pubkey) : -999); if (!uploader_pubkey_copy[0] || strcmp(uploader_pubkey_copy, auth_pubkey) != 0) { fprintf(stderr, "DEBUG-DELETE: OWNERSHIP CHECK FAILED!\r\n"); fprintf(stderr, "DEBUG-DELETE: Reason: %s\r\n", !uploader_pubkey_copy[0] ? "uploader_pubkey_copy is empty" : "pubkeys don't match"); sqlite3_close(db); send_error_response(403, "access_denied", "Access denied", "You can only delete blobs that you uploaded"); log_request("DELETE", "/delete", "ownership_denied", 403); return; } else { fprintf(stderr, "DEBUG-DELETE: OWNERSHIP CHECK PASSED!\r\n"); } fprintf(stderr, "DEBUG: Ownership check passed, proceeding with deletion\r\n"); // Delete from database first const char* delete_sql = "DELETE FROM blobs WHERE sha256 = ?"; rc = sqlite3_prepare_v2(db, delete_sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { fprintf(stderr, "DEBUG: Delete SQL prepare failed: %s\r\n", sqlite3_errmsg(db)); sqlite3_close(db); send_error_response(500, "database_error", "Failed to prepare delete", "Internal server error"); log_request("DELETE", "/delete", "authenticated", 500); return; } sqlite3_bind_text(stmt, 1, sha256, -1, SQLITE_STATIC); rc = sqlite3_step(stmt); sqlite3_finalize(stmt); sqlite3_close(db); if (rc != SQLITE_DONE) { fprintf(stderr, "DEBUG: Database delete failed: %d\r\n", rc); send_error_response(500, "database_error", "Failed to delete blob metadata", "Internal server error"); log_request("DELETE", "/delete", "authenticated", 500); return; } fprintf(stderr, "DEBUG: Blob metadata deleted from database\r\n"); // Determine file extension from MIME type and delete physical file const char* extension = ""; if (strstr(blob_type_copy, "image/jpeg")) { extension = ".jpg"; } else if (strstr(blob_type_copy, "image/webp")) { extension = ".webp"; } else if (strstr(blob_type_copy, "image/png")) { extension = ".png"; } else if (strstr(blob_type_copy, "image/gif")) { extension = ".gif"; } else if (strstr(blob_type_copy, "video/mp4")) { extension = ".mp4"; } else if (strstr(blob_type_copy, "video/webm")) { extension = ".webm"; } else if (strstr(blob_type_copy, "audio/mpeg")) { extension = ".mp3"; } else if (strstr(blob_type_copy, "audio/ogg")) { extension = ".ogg"; } else if (strstr(blob_type_copy, "text/plain")) { extension = ".txt"; } else { extension = ".bin"; } char filepath[MAX_PATH_LEN]; snprintf(filepath, sizeof(filepath), "blobs/%s%s", sha256, extension); fprintf(stderr, "DEBUG: Attempting to delete file: %s\r\n", filepath); if (unlink(filepath) != 0) { fprintf(stderr, "DEBUG: Failed to delete physical file: %s\r\n", filepath); // File deletion failed, but database is already updated // Log warning but don't fail the request fprintf(stderr, "WARNING: Physical file deletion failed, but metadata was removed\r\n"); } else { fprintf(stderr, "DEBUG: Physical file deleted successfully\r\n"); } // Return success response printf("Status: 200 OK\r\n"); printf("Content-Type: application/json\r\n\r\n"); printf("{\n"); printf(" \"message\": \"Blob deleted successfully\",\n"); printf(" \"sha256\": \"%s\"\n", sha256); printf("}\n"); fprintf(stderr, "DEBUG: Delete operation completed successfully\r\n"); log_request("DELETE", "/delete", "authenticated", 200); } // Handle PUT /upload requests void handle_upload_request(void) { // 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"); fprintf(stderr, "DEBUG: content_type=%s\r\n", content_type ? content_type : "NULL"); fprintf(stderr, "DEBUG: content_length=%s\r\n", content_length_str ? content_length_str : "NULL"); // 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"); fprintf(stderr, "DEBUG: Raw Authorization header: %s\r\n", auth_header ? auth_header : "NULL"); // Store uploader pubkey for metadata (will be extracted during auth if provided) const char* uploader_pubkey = NULL; if (auth_header) { log_request("PUT", "/upload", "auth_provided", 0); } else { log_request("PUT", "/upload", "anonymous", 0); } // Read file data from stdin unsigned char* 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) { fprintf(stderr, "DEBUG: Expected %ld bytes, read %zu bytes\r\n", content_length, bytes_read); 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; } // Calculate SHA-256 hash using nostr_core function unsigned char hash[32]; // EMERGENCY DEBUG: Write to direct file FILE* debug_file = fopen("debug_hash_data.log", "a"); if (debug_file) { fprintf(debug_file, "=== HASH DEBUG SESSION ===\n"); fprintf(debug_file, "Content length: %ld\n", content_length); fprintf(debug_file, "File data to hash: "); for (int i = 0; i < content_length; i++) { fprintf(debug_file, "%02x", (unsigned char)file_data[i]); } fprintf(debug_file, "\n"); fprintf(debug_file, "File data as string: %.*s\n", (int)content_length, file_data); fclose(debug_file); } if (nostr_sha256(file_data, content_length, hash) != NOSTR_SUCCESS) { 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); fprintf(stderr, "DEBUG-LAAN: Calculated SHA-256: %s\r\n", sha256_hex); fflush(stderr); // EMERGENCY DEBUG: Write calculated hash to direct file FILE* debug_file2 = fopen("debug_hash_data.log", "a"); if (debug_file2) { fprintf(debug_file2, "Calculated SHA-256: %s\n", sha256_hex); fprintf(debug_file2, "=== END DEBUG SESSION ===\n\n"); fclose(debug_file2); } // TEMPORARY FIX: Bypass rules system and use simple authentication fprintf(stderr, "AUTH: About to perform authentication - auth_header present: %s\r\n", auth_header ? "YES" : "NO"); int auth_result = NOSTR_SUCCESS; if (auth_header) { fprintf(stderr, "AUTH: Calling authenticate_request with hash: %s\r\n", sha256_hex); auth_result = authenticate_request(auth_header, "upload", sha256_hex); fprintf(stderr, "AUTH: authenticate_request returned: %d\r\n", auth_result); if (auth_result != NOSTR_SUCCESS) { free(file_data); // Provide specific error messages based on the authentication failure type const char* error_type = "authentication_failed"; const char* message = "Authentication failed"; const char* details = "The request failed nostr authentication"; switch (auth_result) { case NOSTR_ERROR_EVENT_INVALID_CONTENT: error_type = "event_expired"; message = "Authentication event expired"; details = "The provided nostr event has expired and is no longer valid"; break; case NOSTR_ERROR_EVENT_INVALID_SIGNATURE: error_type = "invalid_signature"; message = "Invalid cryptographic signature"; details = "The event signature verification failed"; break; case NOSTR_ERROR_EVENT_INVALID_PUBKEY: error_type = "invalid_pubkey"; message = "Invalid public key"; details = "The event contains an invalid or malformed public key"; break; case NOSTR_ERROR_EVENT_INVALID_ID: error_type = "invalid_event_id"; message = "Invalid event ID"; details = "The event ID does not match the calculated hash"; break; case NOSTR_ERROR_INVALID_INPUT: error_type = "invalid_format"; message = "Invalid authorization format"; details = "The authorization header format is invalid or malformed"; break; default: error_type = "authentication_failed"; message = "Authentication failed"; // Use C-style string formatting for error details static char error_details_buffer[256]; snprintf(error_details_buffer, sizeof(error_details_buffer), "The request failed nostr authentication (error code: %d - %s)", auth_result, nostr_strerror(auth_result)); details = error_details_buffer; break; } send_error_response(401, error_type, message, details); log_request("PUT", "/upload", "auth_failed", 401); return; } } // Extract uploader pubkey from authorization if provided if (auth_header) { char event_json[4096]; int parse_result = parse_authorization_header(auth_header, event_json, sizeof(event_json)); if (parse_result == NOSTR_SUCCESS) { cJSON* event = cJSON_Parse(event_json); if (event) { cJSON* pubkey_json = cJSON_GetObjectItem(event, "pubkey"); if (pubkey_json && cJSON_IsString(pubkey_json)) { static char pubkey_buffer[256]; const char* temp_pubkey = cJSON_GetStringValue(pubkey_json); if (temp_pubkey) { strncpy(pubkey_buffer, temp_pubkey, sizeof(pubkey_buffer)-1); pubkey_buffer[sizeof(pubkey_buffer)-1] = '\0'; uploader_pubkey = pubkey_buffer; } } cJSON_Delete(event); } } } fprintf(stderr, "DEBUG: Authentication passed, uploader_pubkey: %s\r\n", uploader_pubkey ? uploader_pubkey : "anonymous"); // Determine file extension from Content-Type const char* extension = ""; if (strstr(content_type, "image/jpeg")) { extension = ".jpg"; } else if (strstr(content_type, "image/webp")) { extension = ".webp"; } else if (strstr(content_type, "image/png")) { extension = ".png"; } else if (strstr(content_type, "image/gif")) { extension = ".gif"; } else if (strstr(content_type, "video/mp4")) { extension = ".mp4"; } else if (strstr(content_type, "video/webm")) { extension = ".webm"; } else if (strstr(content_type, "audio/mpeg")) { extension = ".mp3"; } else if (strstr(content_type, "audio/ogg")) { extension = ".ogg"; } else if (strstr(content_type, "text/plain")) { extension = ".txt"; } else { // Default to binary extension for unknown types extension = ".bin"; } // Save file to blobs directory with SHA-256 + extension char filepath[MAX_PATH_LEN]; snprintf(filepath, sizeof(filepath), "blobs/%s%s", sha256_hex, extension); fprintf(stderr, "DEBUG: Saving file to: %s\r\n", filepath); FILE* outfile = fopen(filepath, "wb"); if (!outfile) { 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, content_length, 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 } else { fprintf(stderr, "DEBUG: File permissions set to 644 for %s\r\n", filepath); } free(file_data); if (bytes_written != (size_t)content_length) { // 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; } fprintf(stderr, "DEBUG: Successfully saved %zu bytes to %s\r\n", bytes_written, filepath); // Extract filename from Content-Disposition header if present const char* filename = NULL; const char* content_disposition = getenv("HTTP_CONTENT_DISPOSITION"); fprintf(stderr, "DEBUG: Content-Disposition header: %s\r\n", content_disposition ? content_disposition : "NULL"); if (content_disposition) { fprintf(stderr, "DEBUG: Looking for filename= in Content-Disposition header\r\n"); // Look for filename= in Content-Disposition header const char* filename_start = strstr(content_disposition, "filename="); if (filename_start) { fprintf(stderr, "DEBUG: Found filename= at position %ld\r\n", filename_start - content_disposition); filename_start += 9; // Skip "filename=" fprintf(stderr, "DEBUG: Filename value starts with: %.20s\r\n", filename_start); // Handle quoted filenames if (*filename_start == '"') { fprintf(stderr, "DEBUG: Processing quoted filename\r\n"); 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; fprintf(stderr, "DEBUG: Quoted filename length: %zu\r\n", filename_len); if (filename_len < sizeof(filename_buffer)) { strncpy(filename_buffer, filename_start, filename_len); filename_buffer[filename_len] = '\0'; filename = filename_buffer; fprintf(stderr, "DEBUG: Extracted quoted filename: '%s'\r\n", filename); } else { fprintf(stderr, "DEBUG: Quoted filename too long, skipping\r\n"); } } else { fprintf(stderr, "DEBUG: No closing quote found for filename\r\n"); } } else { fprintf(stderr, "DEBUG: Processing unquoted filename\r\n"); // 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; fprintf(stderr, "DEBUG: Unquoted filename length: %zu\r\n", filename_len); if (filename_len < sizeof(filename_buffer)) { strncpy(filename_buffer, filename_start, filename_len); filename_buffer[filename_len] = '\0'; filename = filename_buffer; fprintf(stderr, "DEBUG: Extracted unquoted filename: '%s'\r\n", filename); } else { fprintf(stderr, "DEBUG: Unquoted filename too long, skipping\r\n"); } } } else { fprintf(stderr, "DEBUG: No filename= found in Content-Disposition header\r\n"); } } else { fprintf(stderr, "DEBUG: No Content-Disposition header provided\r\n"); } fprintf(stderr, "DEBUG: Final filename after extraction: %s\r\n", filename ? filename : "NULL"); // Store blob metadata in database time_t uploaded_time = time(NULL); if (!insert_blob_metadata(sha256_hex, content_length, content_type, uploaded_time, uploader_pubkey, filename)) { // Database insertion failed - clean up the physical file to maintain consistency fprintf(stderr, "DEBUG: Database insertion failed, removing physical file\r\n"); 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; } fprintf(stderr, "DEBUG: Blob metadata successfully stored in database\r\n"); // 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\": %ld,\n", content_length); printf(" \"type\": \"%s\",\n", content_type); printf(" \"uploaded\": %ld,\n", uploaded_time); printf(" \"url\": \"http://localhost:9001/%s%s\"\n", sha256_hex, extension); printf("}\n"); fprintf(stderr, "DEBUG: Upload completed successfully with database storage\r\n"); } int main(void) { fprintf(stderr, "STARTUP: FastCGI application starting up\r\n"); fflush(stderr); // CRITICAL: Initialize nostr crypto system for cryptographic operations fprintf(stderr, "STARTUP: Initializing nostr crypto system...\r\n"); if (nostr_crypto_init() != 0) { fprintf(stderr, "FATAL ERROR: Failed to initialize nostr crypto system\r\n"); return 1; } fprintf(stderr, "STARTUP: nostr crypto system initialized successfully\r\n"); fflush(stderr); while (FCGI_Accept() >= 0) { // DEBUG: Log every request received const char* request_method = getenv("REQUEST_METHOD"); const char* request_uri = getenv("REQUEST_URI"); if (!request_method || !request_uri) { printf("Status: 400 Bad Request\r\n"); printf("Content-Type: text/plain\r\n\r\n"); printf("Invalid request\n"); continue; } // Handle HEAD requests for blob metadata if (strcmp(request_method, "HEAD") == 0) { const char* sha256 = extract_sha256_from_uri(request_uri); fprintf(stderr, "DEBUG: Extracted SHA256=%s\r\n", sha256 ? sha256 : "NULL"); if (sha256) { handle_head_request(sha256); log_request("HEAD", request_uri, "none", 200); // Assuming success - could be enhanced to track actual status } else { printf("Status: 400 Bad Request\r\n"); printf("Content-Type: text/plain\r\n\r\n"); printf("Invalid SHA-256 hash in URI\n"); log_request("HEAD", request_uri, "none", 400); } } else if (strcmp(request_method, "PUT") == 0 && strcmp(request_uri, "/upload") == 0) { // Handle PUT /upload requests with authentication handle_upload_request(); } else if (strcmp(request_method, "GET") == 0 && strncmp(request_uri, "/list/", 6) == 0) { // Handle GET /list/ requests const char* pubkey = request_uri + 6; // Skip "/list/" // Extract pubkey from URI (remove query string if present) static char pubkey_buffer[65]; const char* query_start = strchr(pubkey, '?'); size_t pubkey_len; if (query_start) { pubkey_len = query_start - pubkey; } else { pubkey_len = strlen(pubkey); } if (pubkey_len == 64) { // Valid pubkey length strncpy(pubkey_buffer, pubkey, 64); pubkey_buffer[64] = '\0'; handle_list_request(pubkey_buffer); } else { send_error_response(400, "invalid_pubkey", "Invalid pubkey format", "Pubkey must be 64 hex characters"); log_request("GET", request_uri, "none", 400); } } else if (strcmp(request_method, "DELETE") == 0) { // Handle DELETE / requests const char* sha256 = extract_sha256_from_uri(request_uri); fprintf(stderr, "DEBUG: DELETE request - extracted SHA256=%s\r\n", sha256 ? sha256 : "NULL"); if (sha256) { handle_delete_request(sha256); } else { send_error_response(400, "invalid_hash", "Invalid SHA-256 hash in URI", "URI must contain a valid 64-character hex hash"); log_request("DELETE", request_uri, "none", 400); } } else { // Other methods not implemented yet printf("Status: 501 Not Implemented\r\n"); printf("Content-Type: text/plain\r\n\r\n"); printf("Method %s not implemented\n", request_method); log_request(request_method, request_uri, "none", 501); } } return 0; }