328 lines
12 KiB
C
328 lines
12 KiB
C
/*
|
|
* BUD-09 - Blob Report (NIP-56 Report Events)
|
|
* Handles reporting of inappropriate or harmful blob content
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sqlite3.h>
|
|
#include <fcgi_stdio.h>
|
|
#include <time.h>
|
|
#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);
|
|
} |