/* * 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 #include "ginxsom.h" #include "admin_api.h" #include "../nostr_core_lib/nostr_core/request_validator.h" // Debug macros removed #define MAX_SHA256_LEN 65 #define MAX_PATH_LEN 512 #define MAX_MIME_LEN 128 // Database path #define DB_PATH "db/ginxsom.db" // BUD-06 X-Reason header constants #define XREASON_MISSING_SHA256 "Missing required X-SHA-256 header" #define XREASON_INVALID_SHA256 "X-SHA-256 must be 64 hex characters" #define XREASON_MISSING_LENGTH "Missing required X-Content-Length header" #define XREASON_INVALID_LENGTH "X-Content-Length must be a positive integer" #define XREASON_FILE_TOO_LARGE "File size exceeds maximum allowed (100MB)" #define XREASON_ZERO_LENGTH "File size cannot be zero" #define XREASON_BLOB_EXISTS "Blob with this hash already exists" #define XREASON_UNSUPPORTED_TYPE "Content type not supported by server policy" #define XREASON_AUTH_REQUIRED "Authorization required for upload" #define XREASON_AUTH_INVALID "Invalid or expired authorization" // Forward declarations for config system int initialize_server_config(void); int apply_config_from_event(cJSON* event); int get_config_file_path(char* path, size_t path_size); int load_server_config(const char* config_path); int run_interactive_setup(const char* config_path); // Configuration system implementation #include #include #include #include // Server configuration structure typedef struct { char admin_pubkey[256]; char admin_enabled[8]; int config_loaded; } server_config_t; // Global configuration instance static server_config_t g_server_config = {0}; // Global server private key (stored in memory only for security) static char server_private_key[128] = {0}; // Function to get XDG config directory const char* get_config_dir(char* buffer, size_t buffer_size) { const char* xdg_config = getenv("XDG_CONFIG_HOME"); if (xdg_config) { snprintf(buffer, buffer_size, "%s/ginxsom", xdg_config); return buffer; } const char* home = getenv("HOME"); if (home) { snprintf(buffer, buffer_size, "%s/.config/ginxsom", home); return buffer; } // Fallback return ".config/ginxsom"; } // Load server configuration from database or create defaults int initialize_server_config(void) { sqlite3* db = NULL; sqlite3_stmt* stmt = NULL; int rc; memset(&g_server_config, 0, sizeof(g_server_config)); // Open database rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); if (rc != SQLITE_OK) { fprintf(stderr, "CONFIG: Could not open database for config: %s\n", sqlite3_errmsg(db)); // Config database doesn't exist - leave config uninitialized g_server_config.config_loaded = 0; return 0; } // Load admin_pubkey const char* sql = "SELECT value FROM server_config WHERE key = ?"; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_text(stmt, 1, "admin_pubkey", -1, SQLITE_STATIC); rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) { const char* value = (const char*)sqlite3_column_text(stmt, 0); if (value) { strncpy(g_server_config.admin_pubkey, value, sizeof(g_server_config.admin_pubkey) - 1); } } sqlite3_finalize(stmt); } // Load admin_enabled rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_text(stmt, 1, "admin_enabled", -1, SQLITE_STATIC); rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) { const char* value = (const char*)sqlite3_column_text(stmt, 0); if (value && strcmp(value, "true") == 0) { strcpy(g_server_config.admin_enabled, "true"); } else { strcpy(g_server_config.admin_enabled, "false"); } } sqlite3_finalize(stmt); } sqlite3_close(db); g_server_config.config_loaded = 1; fprintf(stderr, "CONFIG: Server configuration loaded\n"); return 1; } // File-based configuration system // Config file path resolution int get_config_file_path(char* path, size_t path_size) { const char* home = getenv("HOME"); const char* xdg_config = getenv("XDG_CONFIG_HOME"); if (xdg_config) { snprintf(path, path_size, "%s/ginxsom/ginxsom_config_event.json", xdg_config); } else if (home) { snprintf(path, path_size, "%s/.config/ginxsom/ginxsom_config_event.json", home); } else { return 0; } return 1; } // Load and validate config event int load_server_config(const char* config_path) { FILE* file = fopen(config_path, "r"); if (!file) { return 0; // Config file doesn't exist } // Read entire file fseek(file, 0, SEEK_END); long file_size = ftell(file); fseek(file, 0, SEEK_SET); char* json_data = malloc(file_size + 1); if (!json_data) { fclose(file); return 0; } fread(json_data, 1, file_size, file); json_data[file_size] = '\0'; fclose(file); // Parse and validate JSON event cJSON* event = cJSON_Parse(json_data); free(json_data); if (!event) { fprintf(stderr, "Invalid JSON in config file\n"); return 0; } // Validate event structure and signature if (nostr_validate_event(event) != NOSTR_SUCCESS) { fprintf(stderr, "Invalid or corrupted config event\n"); cJSON_Delete(event); return 0; } // Extract configuration and apply to server int result = apply_config_from_event(event); cJSON_Delete(event); return result; } // Extract config from validated event and apply to server int apply_config_from_event(cJSON* event) { sqlite3* db; sqlite3_stmt* stmt; int rc; // Open database for config storage rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL); if (rc) { fprintf(stderr, "Failed to open database for config\n"); return 0; } // Extract admin pubkey from event cJSON* pubkey_json = cJSON_GetObjectItem(event, "pubkey"); if (!pubkey_json || !cJSON_IsString(pubkey_json)) { sqlite3_close(db); return 0; } const char* admin_pubkey = cJSON_GetStringValue(pubkey_json); // Store admin pubkey in database const char* insert_sql = "INSERT OR REPLACE INTO server_config (key, value, description) VALUES (?, ?, ?)"; rc = sqlite3_prepare_v2(db, insert_sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_text(stmt, 1, "admin_pubkey", -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, admin_pubkey, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 3, "Admin public key from config event", -1, SQLITE_STATIC); sqlite3_step(stmt); sqlite3_finalize(stmt); } // Extract server private key and store securely (in memory only) 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); cJSON* tag_value = cJSON_GetArrayItem(tag, 1); if (!tag_name || !cJSON_IsString(tag_name) || !tag_value || !cJSON_IsString(tag_value)) continue; const char* key = cJSON_GetStringValue(tag_name); const char* value = cJSON_GetStringValue(tag_value); if (strcmp(key, "server_privkey") == 0) { // Store server private key in global variable (memory only) strncpy(server_private_key, value, sizeof(server_private_key) - 1); server_private_key[sizeof(server_private_key) - 1] = '\0'; } else { // Store other config values in database rc = sqlite3_prepare_v2(db, insert_sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, value, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 3, "From config event", -1, SQLITE_STATIC); sqlite3_step(stmt); sqlite3_finalize(stmt); } } } } sqlite3_close(db); return 1; } // Interactive setup runner int run_interactive_setup(const char* config_path) { printf("\n=== Ginxsom First-Time Setup Required ===\n"); printf("No configuration found at: %s\n\n", config_path); printf("Options:\n"); printf("1. Run interactive setup wizard\n"); printf("2. Exit and create config manually\n"); printf("Choice (1/2): "); char choice[10]; if (!fgets(choice, sizeof(choice), stdin)) { return 1; } if (choice[0] == '1') { // Run setup script char script_path[512]; snprintf(script_path, sizeof(script_path), "./scripts/setup.sh \"%s\"", config_path); return system(script_path); } else { printf("\nManual setup instructions:\n"); printf("1. Run: ./scripts/generate_config.sh\n"); printf("2. Place signed config at: %s\n", config_path); printf("3. Restart ginxsom\n"); return 1; } } // 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); // BUD-06 function declarations void send_upload_error_response(int status_code, const char* error_type, const char* message, const char* x_reason); void send_upload_success_response(const char* sha256, const char* content_type, long content_length); int validate_sha256_format(const char* sha256); int validate_content_length(const char* content_length_str, long* parsed_length); int check_blob_exists(const char* sha256); int validate_upload_headers(const char** sha256, const char** content_type, long* content_length, char* error_reason, size_t reason_size); void handle_head_upload_request(void); // BUD-08 NIP-94 function declarations int nip94_is_enabled(void); int nip94_get_origin(char* out, size_t out_size); const char* mime_to_extension(const char* mime_type); void nip94_build_blob_url(const char* origin, const char* sha256, const char* mime_type, char* out, size_t out_size); int parse_png_dimensions(const unsigned char* data, size_t size, int* width, int* height); int parse_jpeg_dimensions(const unsigned char* data, size_t size, int* width, int* height); int parse_webp_dimensions(const unsigned char* data, size_t size, int* width, int* height); int nip94_get_dimensions(const unsigned char* data, size_t size, const char* mime_type, int* width, int* height); void nip94_emit_field(const char* url, const char* mime, const char* sha256, long size, int width, int height); // BUD-09 Blob Report function declarations int validate_report_event_structure(cJSON* event); int extract_blob_hashes_from_report(cJSON* event, char blob_hashes[][65], int max_hashes); int validate_report_types(cJSON* event); int store_blob_report(const char* event_json, const char* reporter_pubkey); void handle_report_request(void); // 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; rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); if (rc) { fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db)); return 0; } const char* sql = "SELECT sha256, size, type, uploaded_at, filename FROM blobs WHERE sha256 = ?"; 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; } sqlite3_bind_text(stmt, 1, sha256, -1, SQLITE_STATIC); rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) { 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; } else { metadata->found = 0; } sqlite3_finalize(stmt); sqlite3_close(db); 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 = mime_to_extension(mime_type); snprintf(filepath, sizeof(filepath), "blobs/%s%s", sha256, extension); struct stat st; int result = stat(filepath, &st); if (result == 0) { return 1; } else { return 0; } } // Handle HEAD request for blob void handle_head_request(const char* sha256) { blob_metadata_t metadata = {0}; // Validate SHA-256 format (64 hex characters) if (strlen(sha256) != 64) { printf("Status: 400 Bad Request\r\n"); printf("Content-Type: text/plain\r\n\r\n"); printf("Invalid SHA-256 hash format\n"); return; } // Check if blob exists in database - this is the single source of truth if (!get_blob_metadata(sha256, &metadata)) { printf("Status: 404 Not Found\r\n"); printf("Content-Type: text/plain\r\n\r\n"); printf("Blob not found\n"); return; } // 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) { return NOSTR_ERROR_INVALID_INPUT; } // Check for "Nostr " prefix (case-insensitive) const char* prefix = "nostr "; size_t prefix_len = strlen(prefix); if (strncasecmp(auth_header, prefix, prefix_len) != 0) { return NOSTR_ERROR_INVALID_INPUT; } // Extract base64 encoded event after "Nostr " const char* base64_event = auth_header + prefix_len; // 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); if (decoded_len == 0) { return NOSTR_ERROR_INVALID_INPUT; } if (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'; 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; } // Check event kind (must be 24242 for Blossom uploads) cJSON* kind_json = cJSON_GetObjectItem(event, "kind"); if (!kind_json || !cJSON_IsNumber(kind_json)) { return NOSTR_ERROR_EVENT_INVALID_CONTENT; } int kind = cJSON_GetNumberValue(kind_json); if (kind != 24242) { 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)) { return NOSTR_ERROR_EVENT_INVALID_CONTENT; } // Look for expiration in tags cJSON* tags = cJSON_GetObjectItem(event, "tags"); if (!tags || !cJSON_IsArray(tags)) { 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; } } } 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; } } } 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)); } } } // Check if method matches (required) if (!found_method) { return NOSTR_ERROR_EVENT_INVALID_CONTENT; } // Check if hash matches (if provided) if (expected_hash && !found_hash) { return NOSTR_ERROR_EVENT_INVALID_CONTENT; } // Check expiration time_t now = time(NULL); if (expiration > 0 && now > expiration) { return NOSTR_ERROR_EVENT_INVALID_CONTENT; } 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 function removed (debug version) // Debug functions removed (detailed_signature_validation, analyze_event_fields, hex_dump) // Main authentication function - simplified version without debug logging int authenticate_request(const char* auth_header, const char* method, const char* file_hash) { if (!auth_header) { return NOSTR_ERROR_INVALID_INPUT; } // Parse authorization header char event_json[4096]; int parse_result = parse_authorization_header(auth_header, event_json, sizeof(event_json)); if (parse_result != NOSTR_SUCCESS) { return parse_result; } // Parse JSON event cJSON* event = cJSON_Parse(event_json); if (!event) { return NOSTR_ERROR_EVENT_INVALID_CONTENT; } // Test structure validation first int structure_result = nostr_validate_event_structure(event); if (structure_result != NOSTR_SUCCESS) { cJSON_Delete(event); return structure_result; } // Test crypto validation int crypto_result = nostr_verify_event_signature(event); if (crypto_result != NOSTR_SUCCESS) { cJSON_Delete(event); return crypto_result; } // Test complete validation int validation_result = nostr_validate_event(event); if (validation_result != NOSTR_SUCCESS) { cJSON_Delete(event); return validation_result; } // Validate Blossom-specific requirements int blossom_result = validate_blossom_event(event, file_hash, method); if (blossom_result != NOSTR_SUCCESS) { cJSON_Delete(event); return blossom_result; } cJSON_Delete(event); return NOSTR_SUCCESS; } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // AUTHENTICATION RULES SYSTEM (4.1.2) ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // Old authentication system has been replaced with nostr_core_lib unified request validation // All authentication rules and cache functionality now handled by nostr_validate_request() ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // BUD 04 - Mirroring ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // HTTP download response structure typedef struct { unsigned char* data; size_t size; char content_type[128]; long http_code; size_t capacity; } mirror_download_t; // CURL write callback for collecting response data static size_t write_callback(void* contents, size_t size, size_t nmemb, mirror_download_t* response) { size_t realsize = size * nmemb; if (!response) return 0; // Check if we need to expand buffer if (response->size + realsize >= response->capacity) { size_t new_capacity = response->capacity == 0 ? 8192 : response->capacity * 2; while (new_capacity < response->size + realsize + 1) { new_capacity *= 2; } unsigned char* new_data = realloc(response->data, new_capacity); if (!new_data) { return 0; // Out of memory } response->data = new_data; response->capacity = new_capacity; } memcpy(response->data + response->size, contents, realsize); response->size += realsize; response->data[response->size] = '\0'; // Null terminate for safety return realsize; } // CURL header callback for collecting Content-Type static size_t header_callback(char* buffer, size_t size, size_t nitems, mirror_download_t* response) { size_t realsize = size * nitems; if (!response) return realsize; // Look for Content-Type header (case-insensitive) if (realsize > 14 && strncasecmp(buffer, "Content-Type:", 13) == 0) { // Skip "Content-Type:" and whitespace char* value = buffer + 13; while (*value == ' ' || *value == '\t') value++; // Find end of value (before \r\n) char* end = value; while (*end && *end != '\r' && *end != '\n') end++; // Copy content type, limiting to buffer size size_t copy_len = end - value; if (copy_len >= sizeof(response->content_type)) { copy_len = sizeof(response->content_type) - 1; } strncpy(response->content_type, value, copy_len); response->content_type[copy_len] = '\0'; } return realsize; } // Validate URL for security (prevent SSRF attacks) int validate_mirror_url(const char* url) { if (!url || strlen(url) == 0) { return 0; // Invalid URL } // Must start with https:// (security requirement) if (strncmp(url, "https://", 8) != 0) { return 0; // Only HTTPS allowed } // URL length check if (strlen(url) > 2048) { return 0; // URL too long } // Check for prohibited hosts/IPs (basic SSRF protection) const char* host_start = url + 8; // Skip "https://" // Block localhost and private IPs if (strncasecmp(host_start, "localhost", 9) == 0 || strncasecmp(host_start, "127.", 4) == 0 || strncasecmp(host_start, "192.168.", 8) == 0 || strncasecmp(host_start, "10.", 3) == 0 || strncmp(host_start, "172.16.", 7) == 0 || strncmp(host_start, "172.17.", 7) == 0 || strncmp(host_start, "172.18.", 7) == 0 || strncmp(host_start, "172.19.", 7) == 0 || strncmp(host_start, "172.2", 5) == 0 || strncmp(host_start, "172.30.", 7) == 0 || strncmp(host_start, "172.31.", 7) == 0) { return 0; // Private network blocked } return 1; // URL appears valid } // Detect/validate Content-Type const char* determine_blob_content_type(const char* url, const char* header_content_type, const unsigned char* data, size_t size) { // Priority 1: Use Content-Type header if present and valid if (header_content_type && strlen(header_content_type) > 0) { // Extract main MIME type (before semicolon) static char clean_type[128]; const char* semicolon = strchr(header_content_type, ';'); size_t len = semicolon ? (size_t)(semicolon - header_content_type) : strlen(header_content_type); if (len < sizeof(clean_type)) { strncpy(clean_type, header_content_type, len); clean_type[len] = '\0'; // Remove trailing whitespace while (len > 0 && (clean_type[len-1] == ' ' || clean_type[len-1] == '\t')) { clean_type[--len] = '\0'; } return clean_type; } } // Priority 2: Detect from URL extension if (url) { const char* dot = strrchr(url, '.'); if (dot && dot[1]) { const char* ext = dot + 1; // Remove query parameters const char* question = strchr(ext, '?'); size_t ext_len = question ? (size_t)(question - ext) : strlen(ext); if (ext_len > 0) { if (strncasecmp(ext, "png", ext_len) == 0) return "image/png"; if (strncasecmp(ext, "jpg", ext_len) == 0) return "image/jpeg"; if (strncasecmp(ext, "jpeg", ext_len) == 0) return "image/jpeg"; if (strncasecmp(ext, "gif", ext_len) == 0) return "image/gif"; if (strncasecmp(ext, "webp", ext_len) == 0) return "image/webp"; if (strncasecmp(ext, "pdf", ext_len) == 0) return "application/pdf"; if (strncasecmp(ext, "mp4", ext_len) == 0) return "video/mp4"; if (strncasecmp(ext, "mp3", ext_len) == 0) return "audio/mpeg"; if (strncasecmp(ext, "txt", ext_len) == 0) return "text/plain"; } } } // Priority 3: Basic content detection from data if (data && size >= 8) { // PNG signature if (memcmp(data, "\x89PNG\r\n\x1a\n", 8) == 0) { return "image/png"; } // JPEG signature if (size >= 3 && memcmp(data, "\xff\xd8\xff", 3) == 0) { return "image/jpeg"; } // GIF signature if (memcmp(data, "GIF87a", 6) == 0 || memcmp(data, "GIF89a", 6) == 0) { return "image/gif"; } // PDF signature if (memcmp(data, "%PDF-", 5) == 0) { return "application/pdf"; } } // Default fallback return "application/octet-stream"; } // Download blob from remote URL mirror_download_t* download_blob_from_url(const char* url, size_t max_size) { if (!url || !validate_mirror_url(url)) { return NULL; } CURL* curl = curl_easy_init(); if (!curl) { return NULL; } mirror_download_t* download = calloc(1, sizeof(mirror_download_t)); if (!download) { curl_easy_cleanup(curl); return NULL; } // Initialize download structure download->data = malloc(8192); if (!download->data) { free(download); curl_easy_cleanup(curl); return NULL; } download->capacity = 8192; download->size = 0; download->content_type[0] = '\0'; // Configure CURL curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, download); curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_callback); curl_easy_setopt(curl, CURLOPT_HEADERDATA, download); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5L); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L); curl_easy_setopt(curl, CURLOPT_USERAGENT, "Ginxsom-Blossom/1.0"); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); // Set maximum file size curl_easy_setopt(curl, CURLOPT_MAXFILESIZE, (long)max_size); // Perform the request CURLcode res = curl_easy_perform(curl); // Get HTTP response code curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &download->http_code); curl_easy_cleanup(curl); // Check for errors if (res != CURLE_OK || download->http_code != 200) { free(download->data); free(download); return NULL; } return download; } // Free download result memory void free_mirror_download(mirror_download_t* download) { if (download) { if (download->data) { free(download->data); } free(download); } } // Parse JSON request body to extract URL int parse_mirror_request_body(const char* json_body, char* url_buffer, size_t url_buffer_size) { if (!json_body || !url_buffer || url_buffer_size == 0) { return 0; } cJSON* json = cJSON_Parse(json_body); if (!json) { return 0; // Invalid JSON } cJSON* url_item = cJSON_GetObjectItem(json, "url"); if (!url_item || !cJSON_IsString(url_item)) { cJSON_Delete(json); return 0; // Missing or invalid URL field } const char* url = cJSON_GetStringValue(url_item); if (!url || strlen(url) >= url_buffer_size) { cJSON_Delete(json); return 0; // URL too long or null } strcpy(url_buffer, url); cJSON_Delete(json); return 1; // Success } // Handle PUT /mirror requests (BUD-04) void handle_mirror_request(void) { // Log the incoming request log_request("PUT", "/mirror", "pending", 0); // Get HTTP headers const char* content_type = getenv("CONTENT_TYPE"); const char* content_length_str = getenv("CONTENT_LENGTH"); // Validate Content-Type if (!content_type || strstr(content_type, "application/json") == NULL) { send_error_response(400, "invalid_content_type", "Content-Type must be application/json", "The mirror endpoint requires JSON request body"); log_request("PUT", "/mirror", "none", 400); return; } // Validate Content-Length if (!content_length_str) { send_error_response(400, "missing_header", "Content-Length header required", "The Content-Length header must be specified"); log_request("PUT", "/mirror", "none", 400); return; } long content_length = atol(content_length_str); if (content_length <= 0 || content_length > 4096) { // 4KB max for JSON send_error_response(400, "invalid_content_length", "Invalid content length", "JSON request body must be between 1 byte and 4KB"); log_request("PUT", "/mirror", "none", 400); return; } // Read JSON request body char* json_body = malloc(content_length + 1); if (!json_body) { send_error_response(500, "memory_error", "Failed to allocate memory", "Internal server error"); log_request("PUT", "/mirror", "none", 500); return; } size_t bytes_read = fread(json_body, 1, content_length, stdin); if (bytes_read != (size_t)content_length) { free(json_body); send_error_response(400, "incomplete_body", "Failed to read complete request body", "The request body was incomplete"); log_request("PUT", "/mirror", "none", 400); return; } json_body[content_length] = '\0'; // Parse JSON to extract URL char url[2048]; if (!parse_mirror_request_body(json_body, url, sizeof(url))) { free(json_body); send_error_response(400, "invalid_json", "Invalid JSON or missing URL field", "Request body must be valid JSON with 'url' field"); log_request("PUT", "/mirror", "none", 400); return; } free(json_body); // Validate URL if (!validate_mirror_url(url)) { send_error_response(400, "invalid_url", "Invalid or prohibited URL", "URL must be HTTPS and not point to private networks"); log_request("PUT", "/mirror", "none", 400); return; } // Check for authorization const char* auth_header = getenv("HTTP_AUTHORIZATION"); const char* expected_hash = NULL; const char* uploader_pubkey = NULL; static char pubkey_buffer[256]; static char hash_buffer[65]; if (auth_header) { // Parse and validate authorization int auth_result = authenticate_request(auth_header, "upload", NULL); if (auth_result != NOSTR_SUCCESS) { send_error_response(401, "authentication_failed", "Invalid authentication", "The provided authorization is invalid"); log_request("PUT", "/mirror", "auth_failed", 401); return; } // Extract hash and pubkey from authorization 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) { // Extract pubkey 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'; uploader_pubkey = pubkey_buffer; } } // Extract expected hash from 'x' tag 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; if (strcmp(cJSON_GetStringValue(tag_name), "x") == 0) { cJSON* hash_value = cJSON_GetArrayItem(tag, 1); if (hash_value && cJSON_IsString(hash_value)) { const char* temp_hash = cJSON_GetStringValue(hash_value); if (temp_hash && strlen(temp_hash) == 64) { strncpy(hash_buffer, temp_hash, sizeof(hash_buffer)-1); hash_buffer[sizeof(hash_buffer)-1] = '\0'; expected_hash = hash_buffer; break; } } } } } cJSON_Delete(event); } } if (!expected_hash) { send_error_response(400, "missing_hash", "Authorization missing expected hash", "Upload authorization must contain 'x' tag with expected hash"); log_request("PUT", "/mirror", "auth_invalid", 400); return; } } // Download the blob mirror_download_t* download = download_blob_from_url(url, 100 * 1024 * 1024); // 100MB limit if (!download) { send_error_response(400, "download_failed", "Failed to download blob from URL", "Could not fetch the specified URL or file too large"); log_request("PUT", "/mirror", uploader_pubkey ? "authenticated" : "anonymous", 400); return; } // Calculate hash of downloaded content unsigned char hash[32]; if (nostr_sha256(download->data, download->size, hash) != NOSTR_SUCCESS) { free_mirror_download(download); send_error_response(500, "hash_error", "Failed to calculate hash", "Internal server error during hash calculation"); log_request("PUT", "/mirror", uploader_pubkey ? "authenticated" : "anonymous", 500); return; } // Convert hash to hex string char sha256_hex[65]; nostr_bytes_to_hex(hash, 32, sha256_hex); // If authorization provided, verify hash matches if (expected_hash && strcmp(sha256_hex, expected_hash) != 0) { free_mirror_download(download); send_error_response(400, "hash_mismatch", "Downloaded content hash does not match authorization", "The file hash does not match the expected hash in the authorization event"); log_request("PUT", "/mirror", "auth_mismatch", 400); return; } // Determine content type const char* content_type_final = determine_blob_content_type(url, download->content_type, download->data, download->size); // Determine file extension from Content-Type using centralized mapping const char* extension = mime_to_extension(content_type_final); // Save file to blobs directory char filepath[MAX_PATH_LEN]; snprintf(filepath, sizeof(filepath), "blobs/%s%s", sha256_hex, extension); FILE* outfile = fopen(filepath, "wb"); if (!outfile) { free_mirror_download(download); send_error_response(500, "file_error", "Failed to create file", "Internal server error during file creation"); log_request("PUT", "/mirror", uploader_pubkey ? "authenticated" : "anonymous", 500); return; } size_t bytes_written = fwrite(download->data, 1, download->size, outfile); fclose(outfile); if (bytes_written != download->size) { unlink(filepath); // Clean up partial file free_mirror_download(download); send_error_response(500, "write_error", "Failed to write complete file", "Internal server error during file write"); log_request("PUT", "/mirror", uploader_pubkey ? "authenticated" : "anonymous", 500); return; } // Set file permissions chmod(filepath, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); // Store metadata in database time_t uploaded_time = time(NULL); if (!insert_blob_metadata(sha256_hex, download->size, content_type_final, uploaded_time, uploader_pubkey, NULL)) { unlink(filepath); // Clean up file free_mirror_download(download); send_error_response(500, "database_error", "Failed to store blob metadata", "Internal server error during database operation"); log_request("PUT", "/mirror", uploader_pubkey ? "authenticated" : "anonymous", 500); 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_final, blob_url, sizeof(blob_url)); // Get dimensions for NIP-94 metadata int width = 0, height = 0; nip94_get_dimensions(download->data, download->size, content_type_final, &width, &height); // 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", download->size); printf(" \"type\": \"%s\",\n", content_type_final); 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_final, sha256_hex, download->size, width, height); } printf("\n}\n"); free_mirror_download(download); log_request("PUT", "/mirror", uploader_pubkey ? "authenticated" : "anonymous", 200); } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // BUD 06 - Upload Requirements (Pre-flight Validation) ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // Enhanced error response with X-Reason header for BUD-06 void send_upload_error_response(int status_code, const char* error_type, const char* message, const char* x_reason) { 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 411: status_text = "Length Required"; break; case 413: status_text = "Content Too Large"; break; case 415: status_text = "Unsupported Media Type"; 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"); if (x_reason) { printf("X-Reason: %s\r\n", x_reason); } printf("\r\n"); printf("{\n"); printf(" \"error\": \"%s\",\n", error_type); printf(" \"message\": \"%s\"", message); if (x_reason) { printf(",\n \"x_reason\": \"%s\"", x_reason); } printf("\n}\n"); } // Success response for validated upload requirements void send_upload_success_response(const char* sha256, const char* content_type, long content_length) { printf("Status: 200 OK\r\n"); printf("Content-Type: application/json\r\n"); printf("X-Upload-Status: Ready\r\n"); printf("\r\n"); printf("{\n"); printf(" \"message\": \"Upload requirements validated\",\n"); printf(" \"sha256\": \"%s\",\n", sha256); printf(" \"content_type\": \"%s\",\n", content_type); printf(" \"content_length\": %ld\n", content_length); printf("}\n"); } // Validate SHA-256 format (64 hex characters) int validate_sha256_format(const char* sha256) { if (!sha256 || strlen(sha256) != 64) { return 0; // Invalid format } // Check that all characters are hex for (int i = 0; i < 64; i++) { char c = sha256[i]; if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { return 0; // Invalid hex character } } return 1; // Valid format } // Parse and validate X-Content-Length header int validate_content_length(const char* content_length_str, long* parsed_length) { if (!content_length_str || !parsed_length) { return 0; // Invalid input } char* endptr; long length = strtol(content_length_str, &endptr, 10); // Always set parsed_length so caller can check the actual value *parsed_length = length; // Check if conversion was successful and no trailing characters if (*endptr != '\0') { return 0; // Invalid number format } // Check for valid size range if (length <= 0) { return 0; // Zero or negative size not allowed } if (length > 100 * 1024 * 1024) { // 100MB limit return -1; // File too large } return 1; // Valid length } // Check if blob already exists in database int check_blob_exists(const char* sha256) { if (!sha256) { return 0; } blob_metadata_t metadata = {0}; return get_blob_metadata(sha256, &metadata); } // Validate upload headers and extract values int validate_upload_headers(const char** sha256, const char** content_type, long* content_length, char* error_reason, size_t reason_size) { // Get X-SHA-256 header const char* sha256_header = getenv("HTTP_X_SHA_256"); if (!sha256_header) { strncpy(error_reason, XREASON_MISSING_SHA256, reason_size - 1); error_reason[reason_size - 1] = '\0'; return 400; // Bad Request } // Validate SHA-256 format if (!validate_sha256_format(sha256_header)) { strncpy(error_reason, XREASON_INVALID_SHA256, reason_size - 1); error_reason[reason_size - 1] = '\0'; return 400; // Bad Request } // Get X-Content-Length header const char* length_header = getenv("HTTP_X_CONTENT_LENGTH"); if (!length_header) { strncpy(error_reason, XREASON_MISSING_LENGTH, reason_size - 1); error_reason[reason_size - 1] = '\0'; return 411; // Length Required } // Validate content length long parsed_length; int length_result = validate_content_length(length_header, &parsed_length); if (length_result == 0) { if (parsed_length == 0) { strncpy(error_reason, XREASON_ZERO_LENGTH, reason_size - 1); } else { strncpy(error_reason, XREASON_INVALID_LENGTH, reason_size - 1); } error_reason[reason_size - 1] = '\0'; return 400; // Bad Request } else if (length_result == -1) { strncpy(error_reason, XREASON_FILE_TOO_LARGE, reason_size - 1); error_reason[reason_size - 1] = '\0'; return 413; // Content Too Large } // Get X-Content-Type header (optional) const char* type_header = getenv("HTTP_X_CONTENT_TYPE"); // Set output values *sha256 = sha256_header; *content_type = type_header ? type_header : "application/octet-stream"; *content_length = parsed_length; return 200; // Success } // Main BUD-06 handler function void handle_head_upload_request(void) { // Log the incoming request log_request("HEAD", "/upload", "pending", 0); // Validate upload headers const char* sha256 = NULL; const char* content_type = NULL; long content_length = 0; char error_reason[256]; int validation_result = validate_upload_headers(&sha256, &content_type, &content_length, error_reason, sizeof(error_reason)); if (validation_result != 200) { // Header validation failed const char* error_type; switch (validation_result) { case 400: error_type = "invalid_headers"; break; case 411: error_type = "length_required"; break; case 413: error_type = "payload_too_large"; break; default: error_type = "validation_error"; break; } send_upload_error_response(validation_result, error_type, error_reason, error_reason); log_request("HEAD", "/upload", "none", validation_result); return; } // Check if blob already exists (duplicate detection) if (check_blob_exists(sha256)) { send_upload_error_response(409, "blob_exists", "Blob with this hash already exists", XREASON_BLOB_EXISTS); log_request("HEAD", "/upload", "none", 409); return; } // Check for optional authorization const char* auth_header = getenv("HTTP_AUTHORIZATION"); const char* auth_status = "none"; if (auth_header) { // Validate authorization if provided nostr_request_t request = { .operation = "upload", .auth_header = auth_header, .event = NULL, .resource_hash = sha256, .mime_type = content_type, .file_size = content_length, .client_ip = getenv("REMOTE_ADDR"), .app_context = NULL }; nostr_request_result_t result; int auth_result = nostr_validate_request(&request, &result); if (auth_result != NOSTR_SUCCESS || !result.valid) { const char* error_type = "authentication_failed"; const char* message = "Invalid or expired authentication"; const char* details = result.reason[0] ? result.reason : "Authentication validation failed"; // Provide more specific error messages based on the reason if (strstr(result.reason, "whitelist")) { error_type = "pubkey_not_whitelisted"; message = "Public key not authorized"; details = result.reason; } else if (strstr(result.reason, "blacklist")) { error_type = "access_denied"; message = "Access denied by policy"; details = result.reason; } else if (strstr(result.reason, "size")) { error_type = "file_too_large"; message = "File size exceeds policy limits"; details = result.reason; } send_upload_error_response(401, error_type, message, details); log_request("HEAD", "/upload", "auth_failed", 401); return; } auth_status = "authenticated"; } // All validations passed - return success send_upload_success_response(sha256, content_type, content_length); log_request("HEAD", "/upload", auth_status, 200); } // 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); } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // BUD 08 - Nip94 File Metadata Tags ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // Check if NIP-94 metadata emission is enabled int nip94_is_enabled(void) { sqlite3* db; sqlite3_stmt* stmt; int rc, enabled = 1; // Default enabled rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); if (rc) { return 1; // Default enabled on DB error } const char* sql = "SELECT value FROM server_config WHERE key = 'nip94_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; } // Get CDN origin for blob URLs int nip94_get_origin(char* out, size_t out_size) { if (!out || out_size == 0) { return 0; } sqlite3* db; sqlite3_stmt* stmt; int rc; rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); if (rc) { // Default on DB error strncpy(out, "http://localhost:9001", out_size - 1); out[out_size - 1] = '\0'; return 1; } const char* sql = "SELECT value FROM server_config WHERE key = 'cdn_origin'"; 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); if (value) { strncpy(out, value, out_size - 1); out[out_size - 1] = '\0'; sqlite3_finalize(stmt); sqlite3_close(db); return 1; } } sqlite3_finalize(stmt); } sqlite3_close(db); // Default fallback strncpy(out, "http://localhost:9001", out_size - 1); out[out_size - 1] = '\0'; return 1; } // Centralized MIME type to file extension mapping const char* mime_to_extension(const char* mime_type) { if (!mime_type) { return ".bin"; } if (strstr(mime_type, "image/jpeg")) { return ".jpg"; } else if (strstr(mime_type, "image/webp")) { return ".webp"; } else if (strstr(mime_type, "image/png")) { return ".png"; } else if (strstr(mime_type, "image/gif")) { return ".gif"; } else if (strstr(mime_type, "video/mp4")) { return ".mp4"; } else if (strstr(mime_type, "video/webm")) { return ".webm"; } else if (strstr(mime_type, "audio/mpeg")) { return ".mp3"; } else if (strstr(mime_type, "audio/ogg")) { return ".ogg"; } else if (strstr(mime_type, "text/plain")) { return ".txt"; } else if (strstr(mime_type, "application/pdf")) { return ".pdf"; } else { return ".bin"; } } // Build canonical blob URL from origin + sha256 + extension void nip94_build_blob_url(const char* origin, const char* sha256, const char* mime_type, char* out, size_t out_size) { if (!origin || !sha256 || !out || out_size == 0) { return; } const char* extension = mime_to_extension(mime_type); snprintf(out, out_size, "%s/%s%s", origin, sha256, extension); } // Parse PNG dimensions from IHDR chunk int parse_png_dimensions(const unsigned char* data, size_t size, int* width, int* height) { if (!data || size < 24 || !width || !height) { return 0; } // Verify PNG signature if (memcmp(data, "\x89PNG\r\n\x1a\n", 8) != 0) { return 0; } // IHDR chunk should start at offset 8 // Skip chunk length (4 bytes) and chunk type "IHDR" (4 bytes) // Width is at offset 16 (4 bytes, big-endian) // Height is at offset 20 (4 bytes, big-endian) if (size >= 24) { *width = (data[16] << 24) | (data[17] << 16) | (data[18] << 8) | data[19]; *height = (data[20] << 24) | (data[21] << 16) | (data[22] << 8) | data[23]; return 1; } return 0; } // Parse JPEG dimensions from SOF0 or SOF2 markers int parse_jpeg_dimensions(const unsigned char* data, size_t size, int* width, int* height) { if (!data || size < 10 || !width || !height) { return 0; } // Verify JPEG signature if (size < 3 || memcmp(data, "\xff\xd8\xff", 3) != 0) { return 0; } size_t pos = 2; while (pos < size - 1) { // Look for marker if (data[pos] != 0xff) { pos++; continue; } unsigned char marker = data[pos + 1]; pos += 2; // SOF0 (0xc0) or SOF2 (0xc2) if (marker == 0xc0 || marker == 0xc2) { // Skip length (2 bytes) and precision (1 byte) if (pos + 5 < size) { pos += 3; // Height (2 bytes, big-endian) *height = (data[pos] << 8) | data[pos + 1]; pos += 2; // Width (2 bytes, big-endian) *width = (data[pos] << 8) | data[pos + 1]; return 1; } return 0; } else if ((marker >= 0xe0 && marker <= 0xef) || (marker >= 0xc4 && marker <= 0xdf && marker != 0xc8)) { // Skip over other segments if (pos + 1 < size) { size_t seg_len = (data[pos] << 8) | data[pos + 1]; pos += seg_len; } else { break; } } else { pos++; } } return 0; } // Parse WebP dimensions from VP8/VP8L/VP8X chunks int parse_webp_dimensions(const unsigned char* data, size_t size, int* width, int* height) { if (!data || size < 20 || !width || !height) { return 0; } // Verify RIFF/WEBP header if (memcmp(data, "RIFF", 4) != 0 || memcmp(data + 8, "WEBP", 4) != 0) { return 0; } // Check chunk type at offset 12 if (memcmp(data + 12, "VP8 ", 4) == 0) { // VP8 lossy format if (size >= 30) { // Skip to frame header (offset 26) *width = ((data[28] | (data[29] << 8)) & 0x3fff); *height = ((data[26] | (data[27] << 8)) & 0x3fff); return 1; } } else if (memcmp(data + 12, "VP8L", 4) == 0) { // VP8L lossless format if (size >= 25) { // Width and height are in bits 0-13 and 14-27 of a 32-bit value at offset 21 uint32_t dim_data = data[21] | (data[22] << 8) | (data[23] << 16) | (data[24] << 24); *width = (dim_data & 0x3fff) + 1; *height = ((dim_data >> 14) & 0x3fff) + 1; return 1; } } else if (memcmp(data + 12, "VP8X", 4) == 0) { // VP8X extended format if (size >= 30) { // Width (3 bytes, little-endian) at offset 24 // Height (3 bytes, little-endian) at offset 27 *width = (data[24] | (data[25] << 8) | (data[26] << 16)) + 1; *height = (data[27] | (data[28] << 8) | (data[29] << 16)) + 1; return 1; } } return 0; } // Get file dimensions based on MIME type int nip94_get_dimensions(const unsigned char* data, size_t size, const char* mime_type, int* width, int* height) { if (!data || !mime_type || !width || !height) { return 0; } if (strstr(mime_type, "image/png")) { return parse_png_dimensions(data, size, width, height); } else if (strstr(mime_type, "image/jpeg")) { return parse_jpeg_dimensions(data, size, width, height); } else if (strstr(mime_type, "image/webp")) { return parse_webp_dimensions(data, size, width, height); } return 0; } // Emit NIP-94 metadata field to stdout void nip94_emit_field(const char* url, const char* mime, const char* sha256, long size, int width, int height) { if (!url || !mime || !sha256) { return; } printf(" \"nip94\": [\n"); printf(" [\"url\", \"%s\"],\n", url); printf(" [\"m\", \"%s\"],\n", mime); printf(" [\"x\", \"%s\"],\n", sha256); printf(" [\"size\", \"%ld\"]", size); // Add dim tag if dimensions are available if (width > 0 && height > 0) { printf(",\n [\"dim\", \"%dx%d\"]", width, height); } printf("\n ]"); } // Handle GET /list/ requests void handle_list_request(const char* pubkey) { // 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) { // Parse since parameter const char* since_param = strstr(query_string, "since="); if (since_param) { since_timestamp = atol(since_param + 6); } // Parse until parameter const char* until_param = strstr(query_string, "until="); if (until_param) { until_timestamp = atol(until_param + 6); } } // Check for optional authorization const char* auth_header = getenv("HTTP_AUTHORIZATION"); const char* auth_status = "none"; if (auth_header) { nostr_request_t request = { .operation = "list", .auth_header = auth_header, .event = NULL, .resource_hash = NULL, .mime_type = NULL, .file_size = 0, .client_ip = getenv("REMOTE_ADDR"), .app_context = NULL }; nostr_request_result_t result; int auth_result = nostr_validate_request(&request, &result); if (auth_result != NOSTR_SUCCESS || !result.valid) { const char* error_type = "authentication_failed"; const char* message = "Invalid or expired authentication"; const char* details = result.reason[0] ? result.reason : "The provided Nostr event is invalid, expired, or does not authorize this operation"; // Provide more specific error messages based on the reason if (strstr(result.reason, "whitelist")) { error_type = "pubkey_not_whitelisted"; message = "Public key not authorized"; } else if (strstr(result.reason, "blacklist")) { error_type = "access_denied"; message = "Access denied by policy"; } send_error_response(401, error_type, message, details); 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) { 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"); } rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { 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); // Get origin from config for consistent URL generation char origin[256]; nip94_get_origin(origin, sizeof(origin)); // Build canonical blob URL using centralized function char blob_url[512]; nip94_build_blob_url(origin, sha256, type, blob_url, sizeof(blob_url)); // Output blob descriptor JSON printf(" {\n"); printf(" \"url\": \"%s\",\n", blob_url); 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); log_request("GET", "/list", auth_status, 200); } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // BUD 09 - Blob Report (NIP-56 Report Events) ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // Validate NIP-56 report event structure int validate_report_event_structure(cJSON* event) { if (!event) { return 0; } // Must be kind 1984 cJSON* kind_json = cJSON_GetObjectItem(event, "kind"); if (!kind_json || !cJSON_IsNumber(kind_json)) { return 0; } if (cJSON_GetNumberValue(kind_json) != 1984) { return 0; } // Must have tags array cJSON* tags = cJSON_GetObjectItem(event, "tags"); if (!tags || !cJSON_IsArray(tags)) { return 0; } // Must have at least one 'x' tag int has_x_tag = 0; 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)) { const char* tag_name_str = cJSON_GetStringValue(tag_name); if (tag_name_str && strcmp(tag_name_str, "x") == 0) { has_x_tag = 1; break; } } } return has_x_tag; } // Extract SHA-256 blob hashes from 'x' tags int extract_blob_hashes_from_report(cJSON* event, char blob_hashes[][65], int max_hashes) { if (!event || !blob_hashes) { return 0; } cJSON* tags = cJSON_GetObjectItem(event, "tags"); if (!tags || !cJSON_IsArray(tags)) { return 0; } int hash_count = 0; cJSON* tag = NULL; cJSON_ArrayForEach(tag, tags) { if (hash_count >= max_hashes) break; 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 (tag_name_str && strcmp(tag_name_str, "x") == 0) { cJSON* hash_value = cJSON_GetArrayItem(tag, 1); if (hash_value && cJSON_IsString(hash_value)) { const char* hash = cJSON_GetStringValue(hash_value); if (hash && validate_sha256_format(hash)) { strncpy(blob_hashes[hash_count], hash, 64); blob_hashes[hash_count][64] = '\0'; hash_count++; } } } } return hash_count; } // Validate NIP-56 report types in x tags int validate_report_types(cJSON* event) { if (!event) { return 0; } cJSON* tags = cJSON_GetObjectItem(event, "tags"); if (!tags || !cJSON_IsArray(tags)) { return 0; } // Valid NIP-56 report types const char* valid_types[] = { "nudity", "malware", "profanity", "illegal", "spam", "impersonation", "other", NULL }; 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 (tag_name_str && strcmp(tag_name_str, "x") == 0) { // Check if report type is provided and valid (optional) cJSON* report_type = cJSON_GetArrayItem(tag, 2); if (report_type && cJSON_IsString(report_type)) { const char* type_str = cJSON_GetStringValue(report_type); if (type_str) { // Validate against known types (but allow unknown types per spec) for (int i = 0; valid_types[i] != NULL; i++) { if (strcmp(type_str, valid_types[i]) == 0) { break; } } // Note: Allow unknown types as per NIP-56 spec flexibility } } } } return 1; // Always return success - report types are informational } // Store blob report in database (optional server behavior) int store_blob_report(const char* event_json, const char* reporter_pubkey) { // Optional implementation - servers MAY store reports sqlite3* db; sqlite3_stmt* stmt; int rc; rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL); if (rc) { return 0; } // Check if blob_reports table exists, create if not const char* create_table_sql = "CREATE TABLE IF NOT EXISTS blob_reports (" "id INTEGER PRIMARY KEY AUTOINCREMENT, " "report_event TEXT NOT NULL, " "reporter_pubkey TEXT, " "reported_at INTEGER NOT NULL, " "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP" ")"; rc = sqlite3_exec(db, create_table_sql, NULL, NULL, NULL); if (rc != SQLITE_OK) { sqlite3_close(db); return 0; } const char* sql = "INSERT INTO blob_reports (report_event, reporter_pubkey, reported_at) VALUES (?, ?, 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, event_json, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, reporter_pubkey, -1, SQLITE_STATIC); rc = sqlite3_step(stmt); int success = (rc == SQLITE_DONE); sqlite3_finalize(stmt); sqlite3_close(db); return success; } // Handle PUT /report requests (BUD-09) void handle_report_request(void) { // Log the incoming request log_request("PUT", "/report", "pending", 0); // Validate HTTP method (only PUT allowed) const char* request_method = getenv("REQUEST_METHOD"); if (!request_method || strcmp(request_method, "PUT") != 0) { send_error_response(405, "method_not_allowed", "Only PUT method allowed", "The /report endpoint only accepts PUT requests"); log_request(request_method ? request_method : "NULL", "/report", "none", 405); return; } // Validate Content-Type const char* content_type = getenv("CONTENT_TYPE"); if (!content_type || strstr(content_type, "application/json") == NULL) { send_error_response(415, "unsupported_media_type", "Content-Type must be application/json", "Report requests must be JSON"); log_request("PUT", "/report", "none", 415); return; } // Validate Content-Length const char* content_length_str = getenv("CONTENT_LENGTH"); if (!content_length_str) { send_error_response(400, "missing_content_length", "Content-Length header required", "Request body size must be specified"); log_request("PUT", "/report", "none", 400); return; } long content_length = atol(content_length_str); if (content_length <= 0 || content_length > 10240) { // 10KB limit for report events send_error_response(400, "invalid_content_length", "Invalid content length", "Report events must be between 1 byte and 10KB"); log_request("PUT", "/report", "none", 400); return; } // Read JSON request body char* json_body = malloc(content_length + 1); if (!json_body) { send_error_response(500, "memory_error", "Failed to allocate memory", "Internal server error"); log_request("PUT", "/report", "none", 500); return; } size_t bytes_read = fread(json_body, 1, content_length, stdin); if (bytes_read != (size_t)content_length) { free(json_body); send_error_response(400, "incomplete_body", "Failed to read complete request body", "The request body was incomplete"); log_request("PUT", "/report", "none", 400); return; } json_body[content_length] = '\0'; // Parse JSON event cJSON* event = cJSON_Parse(json_body); if (!event) { free(json_body); send_error_response(400, "invalid_json", "Invalid JSON format", "Request body must be valid JSON"); log_request("PUT", "/report", "none", 400); return; } // Validate event structure (NIP-56 kind 1984 with x tags) if (!validate_report_event_structure(event)) { cJSON_Delete(event); free(json_body); send_error_response(400, "invalid_report_event", "Invalid report event structure", "Report must be NIP-56 kind 1984 event with x tags"); log_request("PUT", "/report", "none", 400); return; } // Validate nostr event signature and structure int structure_result = nostr_validate_event_structure(event); if (structure_result != NOSTR_SUCCESS) { cJSON_Delete(event); free(json_body); send_error_response(400, "invalid_event_structure", "Invalid nostr event structure", "Event does not conform to nostr event format"); log_request("PUT", "/report", "structure_invalid", 400); return; } int crypto_result = nostr_verify_event_signature(event); if (crypto_result != NOSTR_SUCCESS) { cJSON_Delete(event); free(json_body); send_error_response(400, "invalid_signature", "Invalid event signature", "Event signature verification failed"); log_request("PUT", "/report", "signature_invalid", 400); return; } // Extract blob hashes from x tags char blob_hashes[10][65]; // Support up to 10 blob hashes per report int hash_count = extract_blob_hashes_from_report(event, blob_hashes, 10); if (hash_count == 0) { cJSON_Delete(event); free(json_body); send_error_response(400, "no_blob_hashes", "No valid blob hashes found", "Report must contain at least one valid SHA-256 hash in x tags"); log_request("PUT", "/report", "no_hashes", 400); return; } // Validate report types (optional validation) validate_report_types(event); // Extract reporter pubkey cJSON* pubkey_json = cJSON_GetObjectItem(event, "pubkey"); const char* reporter_pubkey = NULL; if (pubkey_json && cJSON_IsString(pubkey_json)) { reporter_pubkey = cJSON_GetStringValue(pubkey_json); } // Optional: Store report in database (server behavior) if (reporter_pubkey) { store_blob_report(json_body, reporter_pubkey); } // Clean up cJSON_Delete(event); free(json_body); // Return success response printf("Status: 200 OK\r\n"); printf("Content-Type: application/json\r\n\r\n"); printf("{\n"); printf(" \"message\": \"Report received\",\n"); printf(" \"reported_blobs\": %d,\n", hash_count); printf(" \"reporter\": \"%s\"\n", reporter_pubkey ? reporter_pubkey : "anonymous"); printf("}\n"); log_request("PUT", "/report", reporter_pubkey ? "authenticated" : "anonymous", 200); } // Handle DELETE / requests void handle_delete_request(const char* sha256) { // 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 nostr_request_t request = { .operation = "delete", .auth_header = auth_header, .event = NULL, .resource_hash = sha256, .mime_type = NULL, .file_size = 0, .client_ip = getenv("REMOTE_ADDR"), .app_context = NULL }; nostr_request_result_t result; int auth_result = nostr_validate_request(&request, &result); if (auth_result != NOSTR_SUCCESS || !result.valid) { const char* error_type = "authentication_failed"; const char* message = "Invalid or expired authentication"; const char* details = result.reason[0] ? result.reason : "The provided Nostr event is invalid, expired, or does not authorize this operation"; // Provide more specific error messages based on the reason if (strstr(result.reason, "whitelist")) { error_type = "pubkey_not_whitelisted"; message = "Public key not authorized"; } else if (strstr(result.reason, "blacklist")) { error_type = "access_denied"; message = "Access denied by policy"; } send_error_response(401, error_type, message, details); 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; } // Copy auth_pubkey to local buffer before deleting the cJSON object char auth_pubkey_copy[256] = {0}; const char* temp_auth_pubkey = cJSON_GetStringValue(pubkey_json); if (temp_auth_pubkey) { strncpy(auth_pubkey_copy, temp_auth_pubkey, sizeof(auth_pubkey_copy) - 1); } cJSON_Delete(event); const char* auth_pubkey = auth_pubkey_copy; // Use the copied version // 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) { 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) { 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); // 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); // Check ownership - only the uploader can delete if (!uploader_pubkey_copy[0] || strcmp(uploader_pubkey_copy, auth_pubkey) != 0) { 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 { } // 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) { 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) { send_error_response(500, "database_error", "Failed to delete blob metadata", "Internal server error"); log_request("DELETE", "/delete", "authenticated", 500); return; } // 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); // Delete the physical file if (unlink(filepath) != 0) { // Warning: failed to delete physical file } else { // Successfully deleted physical file } // 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"); 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"); // 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"); // 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) { 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]; 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); fflush(stderr); // Use new unified request validation system fprintf(stderr, "AUTH: About to perform authentication - auth_header present: %s\r\n", auth_header ? "YES" : "NO"); // Create request structure for validation nostr_request_t request = { .operation = "upload", .auth_header = auth_header, .event = NULL, .resource_hash = sha256_hex, .mime_type = content_type, .file_size = content_length, .client_ip = getenv("REMOTE_ADDR"), .app_context = NULL }; nostr_request_result_t result; int auth_result = nostr_validate_request(&request, &result); fprintf(stderr, "AUTH: nostr_validate_request returned: %d, valid: %d, reason: %s\r\n", auth_result, result.valid, result.reason); if (auth_result == NOSTR_SUCCESS && !result.valid) { free(file_data); // Use the detailed reason from the authentication system const char* error_type = "authentication_failed"; const char* message = "Authentication failed"; const char* details = result.reason[0] ? result.reason : "The request failed authentication"; // Provide more specific error types based on the reason content if (strstr(result.reason, "whitelist")) { error_type = "pubkey_not_whitelisted"; message = "Public key not authorized"; } else if (strstr(result.reason, "blacklist")) { error_type = "access_denied"; message = "Access denied by policy"; } else if (strstr(result.reason, "expired")) { error_type = "event_expired"; message = "Authentication event expired"; } else if (strstr(result.reason, "signature")) { error_type = "invalid_signature"; message = "Invalid cryptographic signature"; } else if (strstr(result.reason, "size")) { error_type = "file_too_large"; message = "File size exceeds policy limits"; } else if (strstr(result.reason, "MIME") || strstr(result.reason, "mime")) { error_type = "unsupported_type"; message = "File type not allowed by policy"; } else if (strstr(result.reason, "hash")) { error_type = "hash_blocked"; message = "File hash blocked by policy"; } else if (strstr(result.reason, "format") || strstr(result.reason, "invalid")) { error_type = "invalid_format"; message = "Invalid authorization format"; } send_error_response(401, error_type, message, details); log_request("PUT", "/upload", "auth_failed", 401); return; } // Extract uploader pubkey from validation result if auth was provided if (auth_header && result.pubkey[0]) { static char pubkey_buffer[256]; strncpy(pubkey_buffer, result.pubkey, sizeof(pubkey_buffer)-1); pubkey_buffer[sizeof(pubkey_buffer)-1] = '\0'; uploader_pubkey = pubkey_buffer; } // Get dimensions from in-memory buffer before saving file int width = 0, height = 0; nip94_get_dimensions(file_data, content_length, 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) { 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 { } 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; } // 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 { } } else { } } 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; } else { } } } else { } } else { } // 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 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\": %ld,\n", content_length); 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, content_length, width, height); } printf("\n}\n"); } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // MAIN ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// int main(void) { fprintf(stderr, "STARTUP: FastCGI application starting up\r\n"); fflush(stderr); // Initialize server configuration and identity // Try file-based config first, then fall back to database config char config_path[512]; int config_loaded = 0; if (get_config_file_path(config_path, sizeof(config_path))) { fprintf(stderr, "STARTUP: Checking for config file at: %s\n", config_path); if (load_server_config(config_path)) { fprintf(stderr, "STARTUP: File-based configuration loaded successfully\n"); config_loaded = 1; } else { fprintf(stderr, "STARTUP: No valid file-based config found, trying database config\n"); } } // Fall back to database configuration if file config failed if (!config_loaded && !initialize_server_config()) { fprintf(stderr, "STARTUP: No configuration found - server starting in setup mode\n"); fprintf(stderr, "STARTUP: Run interactive setup with: ginxsom --setup\n"); // For interactive mode (when stdin is available), offer setup if (isatty(STDIN_FILENO) && get_config_file_path(config_path, sizeof(config_path))) { return run_interactive_setup(config_path); } } else if (!config_loaded) { fprintf(stderr, "STARTUP: Database configuration loaded successfully\n"); } // 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"); // Initialize request validator system fprintf(stderr, "STARTUP: Initializing request validator system...\r\n"); if (nostr_request_validator_init(DB_PATH, "ginxsom") != NOSTR_SUCCESS) { fprintf(stderr, "FATAL ERROR: Failed to initialize request validator system\r\n"); return 1; } fprintf(stderr, "STARTUP: Request validator system initialized successfully\r\n"); fflush(stderr); while (FCGI_Accept() >= 0) { 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; } // Route HEAD /upload pre-flight (BUD-06) before generic HEAD blob handler if (strcmp(request_method, "HEAD") == 0 && strcmp(request_uri, "/upload") == 0) { // Handle HEAD /upload requests (BUD-06 pre-flight validation) handle_head_upload_request(); } else if (strcmp(request_method, "HEAD") == 0) { // Handle HEAD requests for blob metadata const char* sha256 = extract_sha256_from_uri(request_uri); 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, "PUT") == 0 && strcmp(request_uri, "/mirror") == 0) { // Handle PUT /mirror requests (BUD-04) handle_mirror_request(); } else if (strcmp(request_method, "PUT") == 0 && strcmp(request_uri, "/report") == 0) { // Handle PUT /report requests (BUD-09) handle_report_request(); } else if (strncmp(request_uri, "/api/", 5) == 0) { // Handle admin API requests handle_admin_api_request(request_method, request_uri); } 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); 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; }