Files
ginxsom/src/bud09.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);
}