/* * BUD-09 - Blob Report (NIP-56 Report Events) * Handles reporting of inappropriate or harmful blob content */ #include #include #include #include #include #include #include "ginxsom.h" // Use global database path from main.c extern char g_db_path[]; // Forward declarations for helper functions 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); int validate_sha256_format(const char* sha256); ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // 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(g_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); }