bud-09 implemented
This commit is contained in:
318
src/main.c
318
src/main.c
@@ -62,6 +62,13 @@ int parse_webp_dimensions(const unsigned char* data, size_t size, int* width, in
|
||||
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];
|
||||
@@ -2393,6 +2400,314 @@ void handle_list_request(const char* pubkey) {
|
||||
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 /<sha256> requests
|
||||
void handle_delete_request(const char* sha256) {
|
||||
|
||||
@@ -2983,6 +3298,9 @@ int main(void) {
|
||||
} 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 (strcmp(request_method, "GET") == 0 && strncmp(request_uri, "/list/", 6) == 0) {
|
||||
// Handle GET /list/<pubkey> requests
|
||||
const char* pubkey = request_uri + 6; // Skip "/list/"
|
||||
|
||||
Reference in New Issue
Block a user