2675 lines
107 KiB
C
2675 lines
107 KiB
C
/*
|
||
* Ginxsom Blossom Server - FastCGI Application
|
||
* Handles HEAD requests and other dynamic operations
|
||
*/
|
||
|
||
#define _GNU_SOURCE
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <strings.h>
|
||
#include <unistd.h>
|
||
#include <fcgi_stdio.h>
|
||
#include <sqlite3.h>
|
||
#include <sys/stat.h>
|
||
#include <time.h>
|
||
#include <stdint.h>
|
||
#include "ginxsom.h"
|
||
|
||
// Detailed debugging macros (matching test_auth_debug.c)
|
||
#define LOG_STEP(step, msg, ...) fprintf(stderr, "STEP %s: " msg "\n", step, ##__VA_ARGS__)
|
||
#define LOG_SUCCESS(msg, ...) fprintf(stderr, "SUCCESS: " msg "\n", ##__VA_ARGS__)
|
||
#define LOG_ERROR(msg, ...) fprintf(stderr, "ERROR: " msg "\n", ##__VA_ARGS__)
|
||
#define LOG_INFO(msg, ...) fprintf(stderr, "ℹINFO: " msg "\n", ##__VA_ARGS__)
|
||
#define LOG_DIVIDER() fprintf(stderr, "═══════════════════════════════════════════════════════════════════\n")
|
||
|
||
#define MAX_SHA256_LEN 65
|
||
#define MAX_PATH_LEN 512
|
||
#define MAX_MIME_LEN 128
|
||
|
||
// Database path
|
||
#define DB_PATH "db/ginxsom.db"
|
||
|
||
// 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);
|
||
|
||
// 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;
|
||
|
||
fprintf(stderr, "DEBUG: get_blob_metadata() called with sha256='%s'\r\n", sha256);
|
||
fprintf(stderr, "DEBUG: Opening database at path: %s\r\n", DB_PATH);
|
||
|
||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
||
if (rc) {
|
||
fprintf(stderr, "DEBUG: Database open FAILED: %s\r\n", sqlite3_errmsg(db));
|
||
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
|
||
return 0;
|
||
}
|
||
|
||
fprintf(stderr, "DEBUG: Database opened successfully\r\n");
|
||
|
||
const char* sql = "SELECT sha256, size, type, uploaded_at, filename FROM blobs WHERE sha256 = ?";
|
||
fprintf(stderr, "DEBUG: Preparing SQL: %s\r\n", sql);
|
||
|
||
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||
if (rc != SQLITE_OK) {
|
||
fprintf(stderr, "DEBUG: SQL prepare FAILED: %s\r\n", sqlite3_errmsg(db));
|
||
fprintf(stderr, "SQL error: %s\n", sqlite3_errmsg(db));
|
||
sqlite3_close(db);
|
||
return 0;
|
||
}
|
||
|
||
fprintf(stderr, "DEBUG: SQL prepared successfully\r\n");
|
||
fprintf(stderr, "DEBUG: Binding parameter sha256='%s'\r\n", sha256);
|
||
|
||
sqlite3_bind_text(stmt, 1, sha256, -1, SQLITE_STATIC);
|
||
|
||
fprintf(stderr, "DEBUG: Executing SQL query...\r\n");
|
||
rc = sqlite3_step(stmt);
|
||
fprintf(stderr, "DEBUG: sqlite3_step() returned: %d (SQLITE_ROW=%d, SQLITE_DONE=%d)\r\n",
|
||
rc, SQLITE_ROW, SQLITE_DONE);
|
||
|
||
if (rc == SQLITE_ROW) {
|
||
fprintf(stderr, "DEBUG: Row found! Extracting metadata...\r\n");
|
||
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;
|
||
fprintf(stderr, "DEBUG: Metadata extracted - size=%ld, type='%s'\r\n",
|
||
metadata->size, metadata->type);
|
||
} else {
|
||
fprintf(stderr, "DEBUG: No row found for sha256='%s'\r\n", sha256);
|
||
metadata->found = 0;
|
||
}
|
||
|
||
sqlite3_finalize(stmt);
|
||
sqlite3_close(db);
|
||
fprintf(stderr, "DEBUG: Database closed, returning %d\r\n", metadata->found);
|
||
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 = "";
|
||
|
||
// Determine file extension based on MIME type
|
||
if (strstr(mime_type, "image/jpeg")) {
|
||
extension = ".jpg";
|
||
} else if (strstr(mime_type, "image/webp")) {
|
||
extension = ".webp";
|
||
} else if (strstr(mime_type, "image/png")) {
|
||
extension = ".png";
|
||
} else if (strstr(mime_type, "image/gif")) {
|
||
extension = ".gif";
|
||
} else if (strstr(mime_type, "video/mp4")) {
|
||
extension = ".mp4";
|
||
} else if (strstr(mime_type, "video/webm")) {
|
||
extension = ".webm";
|
||
} else if (strstr(mime_type, "audio/mpeg")) {
|
||
extension = ".mp3";
|
||
} else if (strstr(mime_type, "audio/ogg")) {
|
||
extension = ".ogg";
|
||
} else if (strstr(mime_type, "text/plain")) {
|
||
extension = ".txt";
|
||
}
|
||
|
||
snprintf(filepath, sizeof(filepath), "blobs/%s%s", sha256, extension);
|
||
|
||
fprintf(stderr, "DEBUG: file_exists_with_type() checking path: '%s' (MIME: %s)\r\n", filepath, mime_type);
|
||
|
||
struct stat st;
|
||
int result = stat(filepath, &st);
|
||
fprintf(stderr, "DEBUG: stat() returned: %d (0=success, -1=fail)\r\n", result);
|
||
|
||
if (result == 0) {
|
||
fprintf(stderr, "DEBUG: File exists! Size: %ld bytes\r\n", st.st_size);
|
||
return 1;
|
||
} else {
|
||
fprintf(stderr, "DEBUG: File does not exist or stat failed\r\n");
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
// Handle HEAD request for blob
|
||
void handle_head_request(const char* sha256) {
|
||
blob_metadata_t metadata = {0};
|
||
|
||
fprintf(stderr, "DEBUG: handle_head_request called with sha256=%s\r\n", sha256);
|
||
|
||
// Validate SHA-256 format (64 hex characters)
|
||
if (strlen(sha256) != 64) {
|
||
fprintf(stderr, "DEBUG: SHA-256 length validation failed: %zu\r\n", strlen(sha256));
|
||
printf("Status: 400 Bad Request\r\n");
|
||
printf("Content-Type: text/plain\r\n\r\n");
|
||
printf("Invalid SHA-256 hash format\n");
|
||
return;
|
||
}
|
||
|
||
fprintf(stderr, "DEBUG: SHA-256 length validation passed\r\n");
|
||
|
||
// Check if blob exists in database - this is the single source of truth
|
||
if (!get_blob_metadata(sha256, &metadata)) {
|
||
fprintf(stderr, "DEBUG: Database lookup failed for sha256=%s\r\n", sha256);
|
||
printf("Status: 404 Not Found\r\n");
|
||
printf("Content-Type: text/plain\r\n\r\n");
|
||
printf("Blob not found\n");
|
||
return;
|
||
}
|
||
|
||
fprintf(stderr, "DEBUG: Database lookup succeeded - blob exists\r\n");
|
||
|
||
// 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) {
|
||
fprintf(stderr, "DEBUG: parse_authorization_header - invalid parameters: auth_header=%p, event_json=%p\n",
|
||
(void*)auth_header, (void*)event_json);
|
||
return NOSTR_ERROR_INVALID_INPUT;
|
||
}
|
||
|
||
fprintf(stderr, "DEBUG: parse_authorization_header called with header: %.50s...\n", auth_header);
|
||
|
||
// Check for "Nostr " prefix (case-insensitive)
|
||
const char* prefix = "nostr ";
|
||
size_t prefix_len = strlen(prefix);
|
||
|
||
if (strncasecmp(auth_header, prefix, prefix_len) != 0) {
|
||
fprintf(stderr, "DEBUG: Authorization header missing 'Nostr ' prefix (found: %.10s)\n", auth_header);
|
||
return NOSTR_ERROR_INVALID_INPUT;
|
||
}
|
||
|
||
// Extract base64 encoded event after "Nostr "
|
||
const char* base64_event = auth_header + prefix_len;
|
||
fprintf(stderr, "DEBUG: Extracted base64 event (length=%zu): %.100s...\n", strlen(base64_event), base64_event);
|
||
|
||
// 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);
|
||
|
||
fprintf(stderr, "DEBUG: Base64 decode result - decoded_len=%zu\n", decoded_len);
|
||
|
||
if (decoded_len == 0) {
|
||
fprintf(stderr, "DEBUG: Failed to decode base64 event - base64_decode returned 0\n");
|
||
return NOSTR_ERROR_INVALID_INPUT;
|
||
}
|
||
|
||
if (decoded_len >= json_size) {
|
||
fprintf(stderr, "DEBUG: Decoded JSON too large for buffer (decoded_len=%zu, json_size=%zu)\n", 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';
|
||
|
||
fprintf(stderr, "DEBUG: Successfully decoded JSON (length=%zu): %s\n", decoded_len, event_json);
|
||
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;
|
||
}
|
||
|
||
fprintf(stderr, "DEBUG: Validating Blossom event\r\n");
|
||
|
||
// Check event kind (must be 24242 for Blossom uploads)
|
||
cJSON* kind_json = cJSON_GetObjectItem(event, "kind");
|
||
if (!kind_json || !cJSON_IsNumber(kind_json)) {
|
||
fprintf(stderr, "DEBUG: Event missing or invalid 'kind' field\r\n");
|
||
return NOSTR_ERROR_EVENT_INVALID_CONTENT;
|
||
}
|
||
|
||
int kind = cJSON_GetNumberValue(kind_json);
|
||
if (kind != 24242) {
|
||
fprintf(stderr, "DEBUG: Event kind %d is not 24242 (Blossom upload)\r\n", kind);
|
||
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)) {
|
||
fprintf(stderr, "DEBUG: Event missing or invalid 'created_at' field\r\n");
|
||
return NOSTR_ERROR_EVENT_INVALID_CONTENT;
|
||
}
|
||
|
||
// Look for expiration in tags
|
||
cJSON* tags = cJSON_GetObjectItem(event, "tags");
|
||
if (!tags || !cJSON_IsArray(tags)) {
|
||
fprintf(stderr, "DEBUG: Event missing or invalid 'tags' field\r\n");
|
||
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;
|
||
fprintf(stderr, "DEBUG: Found matching method tag: %s\r\n", event_method);
|
||
}
|
||
}
|
||
} 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;
|
||
fprintf(stderr, "DEBUG: Found matching hash tag: %s\r\n", event_hash);
|
||
}
|
||
}
|
||
} 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));
|
||
fprintf(stderr, "DEBUG: Found expiration tag: %ld\r\n", expiration);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Check if method matches (required)
|
||
if (!found_method) {
|
||
fprintf(stderr, "DEBUG: Event missing or invalid method tag\r\n");
|
||
return NOSTR_ERROR_EVENT_INVALID_CONTENT;
|
||
}
|
||
|
||
// Check if hash matches (if provided)
|
||
if (expected_hash && !found_hash) {
|
||
fprintf(stderr, "DEBUG: Event hash doesn't match expected hash\r\n");
|
||
return NOSTR_ERROR_EVENT_INVALID_CONTENT;
|
||
}
|
||
|
||
// Check expiration
|
||
time_t now = time(NULL);
|
||
if (expiration > 0 && now > expiration) {
|
||
fprintf(stderr, "DEBUG: Event expired (now: %ld, exp: %ld)\r\n", now, expiration);
|
||
return NOSTR_ERROR_EVENT_INVALID_CONTENT;
|
||
}
|
||
|
||
fprintf(stderr, "DEBUG: Blossom event validation passed\r\n");
|
||
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 with step-by-step logging
|
||
*/
|
||
int detailed_structure_validation(cJSON* event) {
|
||
LOG_DIVIDER();
|
||
LOG_STEP("STRUCT-1", "Starting detailed structure validation");
|
||
|
||
if (!event || !cJSON_IsObject(event)) {
|
||
LOG_ERROR("Event is null or not a JSON object");
|
||
return NOSTR_ERROR_EVENT_INVALID_STRUCTURE;
|
||
}
|
||
LOG_SUCCESS("Event is valid JSON object");
|
||
|
||
// Check each required field existence
|
||
LOG_STEP("STRUCT-2", "Checking required field existence");
|
||
const char* required_fields[] = {"id", "pubkey", "created_at", "kind", "tags", "content", "sig"};
|
||
for (int i = 0; i < 7; i++) {
|
||
cJSON* field = cJSON_GetObjectItem(event, required_fields[i]);
|
||
if (!field) {
|
||
LOG_ERROR("Missing required field: %s", required_fields[i]);
|
||
return NOSTR_ERROR_EVENT_INVALID_STRUCTURE;
|
||
}
|
||
LOG_SUCCESS("Field '%s' exists", required_fields[i]);
|
||
}
|
||
|
||
// Get all fields for detailed validation
|
||
cJSON* id_item = cJSON_GetObjectItem(event, "id");
|
||
cJSON* pubkey_item = cJSON_GetObjectItem(event, "pubkey");
|
||
cJSON* created_at_item = cJSON_GetObjectItem(event, "created_at");
|
||
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
|
||
cJSON* tags_item = cJSON_GetObjectItem(event, "tags");
|
||
cJSON* content_item = cJSON_GetObjectItem(event, "content");
|
||
cJSON* sig_item = cJSON_GetObjectItem(event, "sig");
|
||
|
||
// Validate field types
|
||
LOG_STEP("STRUCT-3", "Validating field types");
|
||
if (!cJSON_IsString(id_item)) {
|
||
LOG_ERROR("Field 'id' is not a string (type: %d)", id_item->type);
|
||
return NOSTR_ERROR_EVENT_INVALID_ID;
|
||
}
|
||
LOG_SUCCESS("Field 'id' is string");
|
||
|
||
if (!cJSON_IsString(pubkey_item)) {
|
||
LOG_ERROR("Field 'pubkey' is not a string (type: %d)", pubkey_item->type);
|
||
return NOSTR_ERROR_EVENT_INVALID_PUBKEY;
|
||
}
|
||
LOG_SUCCESS("Field 'pubkey' is string");
|
||
|
||
if (!cJSON_IsNumber(created_at_item)) {
|
||
LOG_ERROR("Field 'created_at' is not a number (type: %d)", created_at_item->type);
|
||
return NOSTR_ERROR_EVENT_INVALID_CREATED_AT;
|
||
}
|
||
LOG_SUCCESS("Field 'created_at' is number");
|
||
|
||
if (!cJSON_IsNumber(kind_item)) {
|
||
LOG_ERROR("Field 'kind' is not a number (type: %d)", kind_item->type);
|
||
return NOSTR_ERROR_EVENT_INVALID_KIND;
|
||
}
|
||
LOG_SUCCESS("Field 'kind' is number");
|
||
|
||
if (!cJSON_IsArray(tags_item)) {
|
||
LOG_ERROR("Field 'tags' is not an array (type: %d)", tags_item->type);
|
||
return NOSTR_ERROR_EVENT_INVALID_TAGS;
|
||
}
|
||
LOG_SUCCESS("Field 'tags' is array");
|
||
|
||
if (!cJSON_IsString(content_item)) {
|
||
LOG_ERROR("Field 'content' is not a string (type: %d)", content_item->type);
|
||
return NOSTR_ERROR_EVENT_INVALID_CONTENT;
|
||
}
|
||
LOG_SUCCESS("Field 'content' is string");
|
||
|
||
if (!cJSON_IsString(sig_item)) {
|
||
LOG_ERROR("Field 'sig' is not a string (type: %d)", sig_item->type);
|
||
return NOSTR_ERROR_EVENT_INVALID_SIGNATURE;
|
||
}
|
||
LOG_SUCCESS("Field 'sig' is string");
|
||
|
||
// Validate hex string lengths
|
||
LOG_STEP("STRUCT-4", "Validating hex string lengths");
|
||
const char* id_str = cJSON_GetStringValue(id_item);
|
||
const char* pubkey_str = cJSON_GetStringValue(pubkey_item);
|
||
const char* sig_str = cJSON_GetStringValue(sig_item);
|
||
|
||
LOG_INFO("ID string: '%s' (length: %zu)", id_str, id_str ? strlen(id_str) : 0);
|
||
if (!id_str || strlen(id_str) != 64) {
|
||
LOG_ERROR("ID string invalid length (expected 64, got %zu)", id_str ? strlen(id_str) : 0);
|
||
return NOSTR_ERROR_EVENT_INVALID_ID;
|
||
}
|
||
LOG_SUCCESS("ID string length is correct (64 chars)");
|
||
|
||
LOG_INFO("Pubkey string: '%s' (length: %zu)", pubkey_str, pubkey_str ? strlen(pubkey_str) : 0);
|
||
if (!pubkey_str || strlen(pubkey_str) != 64) {
|
||
LOG_ERROR("Pubkey string invalid length (expected 64, got %zu)", pubkey_str ? strlen(pubkey_str) : 0);
|
||
return NOSTR_ERROR_EVENT_INVALID_PUBKEY;
|
||
}
|
||
LOG_SUCCESS("Pubkey string length is correct (64 chars)");
|
||
|
||
LOG_INFO("Signature string: '%s' (length: %zu)", sig_str, sig_str ? strlen(sig_str) : 0);
|
||
if (!sig_str || strlen(sig_str) != 128) {
|
||
LOG_ERROR("Signature string invalid length (expected 128, got %zu)", sig_str ? strlen(sig_str) : 0);
|
||
return NOSTR_ERROR_EVENT_INVALID_SIGNATURE;
|
||
}
|
||
LOG_SUCCESS("Signature string length is correct (128 chars)");
|
||
|
||
// Validate hex characters
|
||
LOG_STEP("STRUCT-5", "Validating hex characters");
|
||
LOG_INFO("Checking ID hex characters...");
|
||
for (int i = 0; i < 64; i++) {
|
||
char c = id_str[i];
|
||
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) {
|
||
LOG_ERROR("Invalid hex character in ID at position %d: '%c' (0x%02x)", i, c, (unsigned char)c);
|
||
return NOSTR_ERROR_EVENT_INVALID_ID;
|
||
}
|
||
}
|
||
LOG_SUCCESS("ID hex characters are valid (lowercase)");
|
||
|
||
LOG_INFO("Checking pubkey hex characters...");
|
||
for (int i = 0; i < 64; i++) {
|
||
char c = pubkey_str[i];
|
||
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) {
|
||
LOG_ERROR("Invalid hex character in pubkey at position %d: '%c' (0x%02x)", i, c, (unsigned char)c);
|
||
return NOSTR_ERROR_EVENT_INVALID_PUBKEY;
|
||
}
|
||
}
|
||
LOG_SUCCESS("Pubkey hex characters are valid (lowercase)");
|
||
|
||
LOG_INFO("Checking signature hex characters...");
|
||
for (int i = 0; i < 128; i++) {
|
||
char c = sig_str[i];
|
||
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) {
|
||
LOG_ERROR("Invalid hex character in signature at position %d: '%c' (0x%02x)", i, c, (unsigned char)c);
|
||
return NOSTR_ERROR_EVENT_INVALID_SIGNATURE;
|
||
}
|
||
}
|
||
LOG_SUCCESS("Signature hex characters are valid (lowercase)");
|
||
|
||
// Validate timestamp
|
||
LOG_STEP("STRUCT-6", "Validating timestamp");
|
||
double created_at = cJSON_GetNumberValue(created_at_item);
|
||
LOG_INFO("Created_at timestamp: %.0f", created_at);
|
||
if (created_at < 0) {
|
||
LOG_ERROR("Invalid timestamp (negative): %.0f", created_at);
|
||
return NOSTR_ERROR_EVENT_INVALID_CREATED_AT;
|
||
}
|
||
|
||
// Convert to human readable time
|
||
time_t timestamp = (time_t)created_at;
|
||
char time_str[100];
|
||
struct tm* tm_info = gmtime(×tamp);
|
||
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S UTC", tm_info);
|
||
LOG_SUCCESS("Timestamp is valid: %s", time_str);
|
||
|
||
// Validate kind
|
||
LOG_STEP("STRUCT-7", "Validating kind");
|
||
double kind = cJSON_GetNumberValue(kind_item);
|
||
LOG_INFO("Event kind: %.0f", kind);
|
||
if (kind < 0 || kind > 65535 || kind != (int)kind) {
|
||
LOG_ERROR("Invalid kind value: %.0f (must be integer 0-65535)", kind);
|
||
return NOSTR_ERROR_EVENT_INVALID_KIND;
|
||
}
|
||
LOG_SUCCESS("Kind is valid: %d", (int)kind);
|
||
|
||
// Validate tags array structure
|
||
LOG_STEP("STRUCT-8", "Validating tags array structure");
|
||
int tag_count = cJSON_GetArraySize(tags_item);
|
||
LOG_INFO("Tags array has %d elements", tag_count);
|
||
|
||
cJSON* tag_item;
|
||
int tag_index = 0;
|
||
cJSON_ArrayForEach(tag_item, tags_item) {
|
||
if (!cJSON_IsArray(tag_item)) {
|
||
LOG_ERROR("Tag at index %d is not an array (type: %d)", tag_index, tag_item->type);
|
||
return NOSTR_ERROR_EVENT_INVALID_TAGS;
|
||
}
|
||
|
||
int tag_element_count = cJSON_GetArraySize(tag_item);
|
||
LOG_INFO("Tag[%d] has %d elements", tag_index, tag_element_count);
|
||
|
||
cJSON* tag_element;
|
||
int element_index = 0;
|
||
cJSON_ArrayForEach(tag_element, tag_item) {
|
||
if (!cJSON_IsString(tag_element)) {
|
||
LOG_ERROR("Tag[%d][%d] is not a string (type: %d)", tag_index, element_index, tag_element->type);
|
||
return NOSTR_ERROR_EVENT_INVALID_TAGS;
|
||
}
|
||
const char* tag_value = cJSON_GetStringValue(tag_element);
|
||
LOG_INFO("Tag[%d][%d]: '%s'", tag_index, element_index, tag_value);
|
||
element_index++;
|
||
}
|
||
tag_index++;
|
||
}
|
||
LOG_SUCCESS("Tags array structure is valid");
|
||
|
||
// Validate content
|
||
LOG_STEP("STRUCT-9", "Validating content");
|
||
const char* content_str = cJSON_GetStringValue(content_item);
|
||
LOG_INFO("Content: '%s' (length: %zu)", content_str, content_str ? strlen(content_str) : 0);
|
||
LOG_SUCCESS("Content is valid string");
|
||
|
||
LOG_SUCCESS("Structure validation completed successfully");
|
||
return NOSTR_SUCCESS;
|
||
}
|
||
|
||
/**
|
||
* Detailed signature validation with step-by-step logging
|
||
*/
|
||
int detailed_signature_validation(cJSON* event) {
|
||
LOG_DIVIDER();
|
||
LOG_STEP("CRYPTO-1", "Starting detailed signature validation");
|
||
|
||
if (!event) {
|
||
LOG_ERROR("Event is null");
|
||
return NOSTR_ERROR_INVALID_INPUT;
|
||
}
|
||
|
||
// Get event fields
|
||
cJSON* id_item = cJSON_GetObjectItem(event, "id");
|
||
cJSON* pubkey_item = cJSON_GetObjectItem(event, "pubkey");
|
||
cJSON* created_at_item = cJSON_GetObjectItem(event, "created_at");
|
||
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
|
||
cJSON* tags_item = cJSON_GetObjectItem(event, "tags");
|
||
cJSON* content_item = cJSON_GetObjectItem(event, "content");
|
||
cJSON* sig_item = cJSON_GetObjectItem(event, "sig");
|
||
|
||
if (!id_item || !pubkey_item || !created_at_item || !kind_item ||
|
||
!tags_item || !content_item || !sig_item) {
|
||
LOG_ERROR("Missing required fields for signature validation");
|
||
return NOSTR_ERROR_EVENT_INVALID_STRUCTURE;
|
||
}
|
||
|
||
// Create serialization array
|
||
LOG_STEP("CRYPTO-2", "Creating serialization array");
|
||
cJSON* serialize_array = cJSON_CreateArray();
|
||
if (!serialize_array) {
|
||
LOG_ERROR("Failed to create serialization array");
|
||
return NOSTR_ERROR_MEMORY_FAILED;
|
||
}
|
||
|
||
cJSON_AddItemToArray(serialize_array, cJSON_CreateNumber(0));
|
||
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(pubkey_item, 1));
|
||
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(created_at_item, 1));
|
||
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(kind_item, 1));
|
||
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(tags_item, 1));
|
||
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(content_item, 1));
|
||
|
||
LOG_SUCCESS("Serialization array created");
|
||
|
||
// Convert to JSON string
|
||
LOG_STEP("CRYPTO-3", "Converting to JSON string");
|
||
char* serialize_string = cJSON_PrintUnformatted(serialize_array);
|
||
cJSON_Delete(serialize_array);
|
||
|
||
if (!serialize_string) {
|
||
LOG_ERROR("Failed to serialize array to JSON string");
|
||
return NOSTR_ERROR_MEMORY_FAILED;
|
||
}
|
||
|
||
LOG_SUCCESS("JSON serialization string created");
|
||
LOG_INFO("Serialization string (length %zu): %s", strlen(serialize_string), serialize_string);
|
||
|
||
// Hash the serialized event
|
||
LOG_STEP("CRYPTO-4", "Computing SHA256 hash");
|
||
unsigned char event_hash[32];
|
||
if (nostr_sha256((const unsigned char*)serialize_string, strlen(serialize_string), event_hash) != 0) {
|
||
LOG_ERROR("SHA256 hashing failed");
|
||
free(serialize_string);
|
||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||
}
|
||
|
||
LOG_SUCCESS("SHA256 hash computed");
|
||
hex_dump("Event hash", event_hash, 32);
|
||
|
||
// Convert hash to hex for event ID verification
|
||
LOG_STEP("CRYPTO-5", "Verifying event ID");
|
||
char calculated_id[65];
|
||
nostr_bytes_to_hex(event_hash, 32, calculated_id);
|
||
|
||
const char* provided_id = cJSON_GetStringValue(id_item);
|
||
LOG_INFO("Calculated ID: %s", calculated_id);
|
||
LOG_INFO("Provided ID: %s", provided_id);
|
||
|
||
if (!provided_id || strcmp(calculated_id, provided_id) != 0) {
|
||
LOG_ERROR("Event ID mismatch!");
|
||
LOG_ERROR(" Expected: %s", calculated_id);
|
||
LOG_ERROR(" Got: %s", provided_id ? provided_id : "NULL");
|
||
free(serialize_string);
|
||
return NOSTR_ERROR_EVENT_INVALID_ID;
|
||
}
|
||
LOG_SUCCESS("Event ID verification passed");
|
||
|
||
// Prepare signature verification
|
||
LOG_STEP("CRYPTO-6", "Preparing signature verification");
|
||
const char* pubkey_str = cJSON_GetStringValue(pubkey_item);
|
||
const char* sig_str = cJSON_GetStringValue(sig_item);
|
||
|
||
if (!pubkey_str || !sig_str) {
|
||
LOG_ERROR("Missing pubkey or signature strings");
|
||
free(serialize_string);
|
||
return NOSTR_ERROR_EVENT_INVALID_STRUCTURE;
|
||
}
|
||
|
||
// Convert hex strings to bytes
|
||
LOG_STEP("CRYPTO-7", "Converting hex strings to bytes");
|
||
unsigned char pubkey_bytes[32];
|
||
unsigned char sig_bytes[64];
|
||
|
||
if (nostr_hex_to_bytes(pubkey_str, pubkey_bytes, 32) != 0) {
|
||
LOG_ERROR("Failed to convert pubkey hex to bytes");
|
||
free(serialize_string);
|
||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||
}
|
||
LOG_SUCCESS("Pubkey hex converted to bytes");
|
||
hex_dump("Pubkey bytes", pubkey_bytes, 32);
|
||
|
||
if (nostr_hex_to_bytes(sig_str, sig_bytes, 64) != 0) {
|
||
LOG_ERROR("Failed to convert signature hex to bytes");
|
||
free(serialize_string);
|
||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||
}
|
||
LOG_SUCCESS("Signature hex converted to bytes");
|
||
hex_dump("Signature bytes", sig_bytes, 64);
|
||
|
||
// Verify signature using nostr_core_lib function (avoiding direct secp256k1 calls)
|
||
LOG_STEP("CRYPTO-8", "Verifying signature using nostr_verify_event_signature()");
|
||
|
||
// Create a temporary event structure for verification
|
||
cJSON* temp_event = cJSON_CreateObject();
|
||
if (!temp_event) {
|
||
LOG_ERROR("Failed to create temporary event for verification");
|
||
free(serialize_string);
|
||
return NOSTR_ERROR_MEMORY_FAILED;
|
||
}
|
||
|
||
// Copy all required fields to temp event
|
||
cJSON_AddItemToObject(temp_event, "id", cJSON_Duplicate(id_item, 1));
|
||
cJSON_AddItemToObject(temp_event, "pubkey", cJSON_Duplicate(pubkey_item, 1));
|
||
cJSON_AddItemToObject(temp_event, "created_at", cJSON_Duplicate(created_at_item, 1));
|
||
cJSON_AddItemToObject(temp_event, "kind", cJSON_Duplicate(kind_item, 1));
|
||
cJSON_AddItemToObject(temp_event, "tags", cJSON_Duplicate(tags_item, 1));
|
||
cJSON_AddItemToObject(temp_event, "content", cJSON_Duplicate(content_item, 1));
|
||
cJSON_AddItemToObject(temp_event, "sig", cJSON_Duplicate(sig_item, 1));
|
||
|
||
LOG_INFO("Calling nostr_verify_event_signature() for detailed crypto validation");
|
||
int crypto_verify_result = nostr_verify_event_signature(temp_event);
|
||
LOG_INFO("nostr_verify_event_signature returned: %d (%s)",
|
||
crypto_verify_result, nostr_strerror(crypto_verify_result));
|
||
|
||
cJSON_Delete(temp_event);
|
||
|
||
if (crypto_verify_result != NOSTR_SUCCESS) {
|
||
LOG_ERROR("Signature verification FAILED!");
|
||
LOG_ERROR("nostr_verify_event_signature returned error: %d (%s)",
|
||
crypto_verify_result, nostr_strerror(crypto_verify_result));
|
||
free(serialize_string);
|
||
return crypto_verify_result;
|
||
}
|
||
|
||
LOG_SUCCESS("Signature verification PASSED using nostr_core_lib!");
|
||
free(serialize_string);
|
||
return NOSTR_SUCCESS;
|
||
}
|
||
|
||
/**
|
||
* Analyze event fields in detail
|
||
*/
|
||
void analyze_event_fields(cJSON* event) {
|
||
LOG_DIVIDER();
|
||
LOG_STEP("ANALYZE-1", "Analyzing event field details");
|
||
|
||
cJSON* field;
|
||
cJSON_ArrayForEach(field, event) {
|
||
if (field->string) {
|
||
LOG_INFO("Field '%s':", field->string);
|
||
if (cJSON_IsString(field)) {
|
||
const char* value = cJSON_GetStringValue(field);
|
||
LOG_INFO(" Type: String");
|
||
LOG_INFO(" Value: '%s'", value);
|
||
LOG_INFO(" Length: %zu", value ? strlen(value) : 0);
|
||
} else if (cJSON_IsNumber(field)) {
|
||
double value = cJSON_GetNumberValue(field);
|
||
LOG_INFO(" Type: Number");
|
||
LOG_INFO(" Value: %.0f", value);
|
||
} else if (cJSON_IsArray(field)) {
|
||
int size = cJSON_GetArraySize(field);
|
||
LOG_INFO(" Type: Array");
|
||
LOG_INFO(" Size: %d", size);
|
||
} else {
|
||
LOG_INFO(" Type: Other (%d)", field->type);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Print hex dump of binary data
|
||
*/
|
||
void hex_dump(const char* label, const unsigned char* data, size_t len) {
|
||
LOG_INFO("%s (%zu bytes):", label, len);
|
||
for (size_t i = 0; i < len; i += 16) {
|
||
fprintf(stderr, " %04zx: ", i);
|
||
for (size_t j = 0; j < 16; j++) {
|
||
if (i + j < len) {
|
||
fprintf(stderr, "%02x ", data[i + j]);
|
||
} else {
|
||
fprintf(stderr, " ");
|
||
}
|
||
}
|
||
fprintf(stderr, " |");
|
||
for (size_t j = 0; j < 16 && i + j < len; j++) {
|
||
unsigned char c = data[i + j];
|
||
fprintf(stderr, "%c", (c >= 32 && c <= 126) ? c : '.');
|
||
}
|
||
fprintf(stderr, "|\n");
|
||
}
|
||
}
|
||
|
||
// Main authentication function with comprehensive step-by-step logging
|
||
int authenticate_request(const char* auth_header, const char* method, const char* file_hash) {
|
||
LOG_DIVIDER();
|
||
LOG_STEP("SERVER-1", "Starting server-style authentication (mirroring test_auth_debug.c)");
|
||
|
||
if (!auth_header) {
|
||
LOG_ERROR("No authorization header provided");
|
||
return NOSTR_ERROR_INVALID_INPUT;
|
||
}
|
||
|
||
LOG_INFO("Server-style auth called with method: %s, hash: %s",
|
||
method ? method : "null", file_hash ? file_hash : "null");
|
||
|
||
// Parse authorization header (same as server)
|
||
char event_json[4096];
|
||
LOG_STEP("SERVER-2", "Calling parse_authorization_header");
|
||
int parse_result = parse_authorization_header(auth_header, event_json, sizeof(event_json));
|
||
if (parse_result != NOSTR_SUCCESS) {
|
||
LOG_ERROR("Authorization header parsing failed: %d (%s)", parse_result, nostr_strerror(parse_result));
|
||
return parse_result;
|
||
}
|
||
LOG_SUCCESS("parse_authorization_header succeeded");
|
||
|
||
// Parse JSON event (same as server)
|
||
LOG_STEP("SERVER-3", "Calling cJSON_Parse on JSON string");
|
||
LOG_INFO("JSON to parse: %s", event_json);
|
||
cJSON* event = cJSON_Parse(event_json);
|
||
if (!event) {
|
||
LOG_ERROR("Failed to parse JSON event with cJSON_Parse");
|
||
return NOSTR_ERROR_EVENT_INVALID_CONTENT;
|
||
}
|
||
LOG_SUCCESS("cJSON_Parse succeeded, event parsed");
|
||
|
||
// Print complete parsed JSON like server does
|
||
char* parsed_json_str = cJSON_Print(event);
|
||
LOG_INFO("Parsed JSON: %s", parsed_json_str ? parsed_json_str : "NULL");
|
||
if (parsed_json_str) free(parsed_json_str);
|
||
|
||
// Debug: Print event fields before validation (same as server)
|
||
cJSON* id_json = cJSON_GetObjectItem(event, "id");
|
||
cJSON* pubkey_json = cJSON_GetObjectItem(event, "pubkey");
|
||
cJSON* sig_json = cJSON_GetObjectItem(event, "sig");
|
||
cJSON* kind_json = cJSON_GetObjectItem(event, "kind");
|
||
cJSON* created_at_json = cJSON_GetObjectItem(event, "created_at");
|
||
|
||
LOG_STEP("SERVER-4", "Event fields before validation");
|
||
LOG_INFO(" id: %s", id_json && cJSON_IsString(id_json) ? cJSON_GetStringValue(id_json) : "MISSING/INVALID");
|
||
LOG_INFO(" pubkey: %s", pubkey_json && cJSON_IsString(pubkey_json) ? cJSON_GetStringValue(pubkey_json) : "MISSING/INVALID");
|
||
LOG_INFO(" sig: %s", sig_json && cJSON_IsString(sig_json) ? cJSON_GetStringValue(sig_json) : "MISSING/INVALID");
|
||
LOG_INFO(" kind: %d", kind_json && cJSON_IsNumber(kind_json) ? (int)cJSON_GetNumberValue(kind_json) : -999);
|
||
LOG_INFO(" created_at: %ld", created_at_json && cJSON_IsNumber(created_at_json) ? (long)cJSON_GetNumberValue(created_at_json) : -999);
|
||
|
||
// Detailed pubkey analysis (same as server)
|
||
if (pubkey_json && cJSON_IsString(pubkey_json)) {
|
||
const char* pubkey_str = cJSON_GetStringValue(pubkey_json);
|
||
LOG_STEP("SERVER-5", "Detailed pubkey analysis");
|
||
LOG_INFO(" Pubkey: %s", pubkey_str ? pubkey_str : "NULL");
|
||
LOG_INFO(" Length: %zu", pubkey_str ? strlen(pubkey_str) : 0);
|
||
if (pubkey_str && strlen(pubkey_str) == 64) {
|
||
LOG_INFO(" Character analysis (first 10): ");
|
||
for (int i = 0; i < 10; i++) {
|
||
char c = pubkey_str[i];
|
||
fprintf(stderr, "%c(0x%02x) ", c, (unsigned char)c);
|
||
}
|
||
fprintf(stderr, "\n");
|
||
}
|
||
}
|
||
|
||
// Pre-validation pubkey analysis (same as server)
|
||
LOG_STEP("SERVER-6", "Pre-validation pubkey analysis");
|
||
if (pubkey_json && cJSON_IsString(pubkey_json)) {
|
||
const char* pubkey_str = cJSON_GetStringValue(pubkey_json);
|
||
LOG_INFO(" Pubkey: %s", pubkey_str ? pubkey_str : "NULL");
|
||
LOG_INFO(" Length: %zu", pubkey_str ? strlen(pubkey_str) : 0);
|
||
if (pubkey_str && strlen(pubkey_str) == 64) {
|
||
LOG_INFO(" Character analysis (first 10): ");
|
||
for (int i = 0; i < 10; i++) {
|
||
char c = pubkey_str[i];
|
||
fprintf(stderr, "%c(%d) ", c, (int)c);
|
||
}
|
||
fprintf(stderr, "\n");
|
||
LOG_INFO(" Character validation test: ");
|
||
int valid_chars = 1;
|
||
for (int i = 0; i < 64; i++) {
|
||
char c = pubkey_str[i];
|
||
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) {
|
||
fprintf(stderr, "INVALID at pos %d: %c(%d) ", i, c, (int)c);
|
||
valid_chars = 0;
|
||
}
|
||
}
|
||
if (valid_chars) {
|
||
fprintf(stderr, "ALL VALID (lowercase hex)\n");
|
||
} else {
|
||
fprintf(stderr, "\n");
|
||
}
|
||
}
|
||
}
|
||
|
||
// Detailed validation analysis (same as server)
|
||
LOG_STEP("SERVER-7", "Starting detailed validation analysis");
|
||
|
||
// Test structure validation first (same as server)
|
||
LOG_INFO("Testing structure validation...");
|
||
int structure_result = nostr_validate_event_structure(event);
|
||
LOG_INFO("nostr_validate_event_structure returned: %d (%s)",
|
||
structure_result, nostr_strerror(structure_result));
|
||
|
||
// EMERGENCY DEBUG: Write structure validation result to file
|
||
FILE* debug_file = fopen("debug_validation.log", "a");
|
||
if (debug_file) {
|
||
fprintf(debug_file, "=== STRUCTURE VALIDATION DEBUG ===\n");
|
||
fprintf(debug_file, "nostr_validate_event_structure result: %d (%s)\n",
|
||
structure_result, nostr_strerror(structure_result));
|
||
if (structure_result != NOSTR_SUCCESS) {
|
||
fprintf(debug_file, "STRUCTURE VALIDATION FAILED!\n");
|
||
// Log the event JSON for debugging
|
||
char* event_str = cJSON_Print(event);
|
||
if (event_str) {
|
||
fprintf(debug_file, "Event JSON: %s\n", event_str);
|
||
free(event_str);
|
||
}
|
||
}
|
||
fprintf(debug_file, "=== END STRUCTURE DEBUG ===\n\n");
|
||
fclose(debug_file);
|
||
}
|
||
|
||
if (structure_result != NOSTR_SUCCESS) {
|
||
LOG_ERROR("STRUCTURE validation failed!");
|
||
cJSON_Delete(event);
|
||
return structure_result;
|
||
}
|
||
LOG_SUCCESS("Structure validation PASSED");
|
||
|
||
// Test crypto validation separately (same as server)
|
||
LOG_INFO("Testing cryptographic verification...");
|
||
int crypto_result = nostr_verify_event_signature(event);
|
||
LOG_INFO("nostr_verify_event_signature returned: %d (%s)",
|
||
crypto_result, nostr_strerror(crypto_result));
|
||
|
||
// EMERGENCY DEBUG: Write crypto validation result to file
|
||
FILE* debug_file2 = fopen("debug_validation.log", "a");
|
||
if (debug_file2) {
|
||
fprintf(debug_file2, "=== CRYPTO VALIDATION DEBUG ===\n");
|
||
fprintf(debug_file2, "nostr_verify_event_signature result: %d (%s)\n",
|
||
crypto_result, nostr_strerror(crypto_result));
|
||
if (crypto_result != NOSTR_SUCCESS) {
|
||
fprintf(debug_file2, "CRYPTO VALIDATION FAILED!\n");
|
||
if (pubkey_json && cJSON_IsString(pubkey_json)) {
|
||
const char* pubkey_str = cJSON_GetStringValue(pubkey_json);
|
||
fprintf(debug_file2, "Failed pubkey: %s (length: %zu)\n",
|
||
pubkey_str ? pubkey_str : "NULL", pubkey_str ? strlen(pubkey_str) : 0);
|
||
}
|
||
}
|
||
fprintf(debug_file2, "=== END CRYPTO DEBUG ===\n\n");
|
||
fclose(debug_file2);
|
||
}
|
||
|
||
if (crypto_result != NOSTR_SUCCESS) {
|
||
LOG_ERROR("CRYPTO verification failed!");
|
||
if (pubkey_json && cJSON_IsString(pubkey_json)) {
|
||
const char* pubkey_str = cJSON_GetStringValue(pubkey_json);
|
||
LOG_ERROR("Failed pubkey: %s (length: %zu)",
|
||
pubkey_str ? pubkey_str : "NULL", pubkey_str ? strlen(pubkey_str) : 0);
|
||
}
|
||
cJSON_Delete(event);
|
||
return crypto_result;
|
||
}
|
||
LOG_SUCCESS("Crypto verification PASSED");
|
||
|
||
// Finally test complete validation (same as server)
|
||
LOG_INFO("Testing complete validation...");
|
||
int validation_result = nostr_validate_event(event);
|
||
LOG_INFO("nostr_validate_event returned: %d (%s)",
|
||
validation_result, nostr_strerror(validation_result));
|
||
|
||
// EMERGENCY DEBUG: Write complete validation result to file
|
||
FILE* debug_file3 = fopen("debug_validation.log", "a");
|
||
if (debug_file3) {
|
||
fprintf(debug_file3, "=== COMPLETE VALIDATION DEBUG ===\n");
|
||
fprintf(debug_file3, "nostr_validate_event result: %d (%s)\n",
|
||
validation_result, nostr_strerror(validation_result));
|
||
if (validation_result != NOSTR_SUCCESS) {
|
||
fprintf(debug_file3, "COMPLETE VALIDATION FAILED!\n");
|
||
if (pubkey_json && cJSON_IsString(pubkey_json)) {
|
||
const char* pubkey_str = cJSON_GetStringValue(pubkey_json);
|
||
fprintf(debug_file3, "Pubkey length: %zu, value: %s\n",
|
||
pubkey_str ? strlen(pubkey_str) : 0, pubkey_str ? pubkey_str : "NULL");
|
||
}
|
||
}
|
||
fprintf(debug_file3, "=== END COMPLETE DEBUG ===\n\n");
|
||
fclose(debug_file3);
|
||
}
|
||
|
||
if (validation_result != NOSTR_SUCCESS) {
|
||
LOG_ERROR("COMPLETE validation failed: %d (%s)",
|
||
validation_result, nostr_strerror(validation_result));
|
||
|
||
// Additional debug: Check specific validation issues (same as server)
|
||
if (pubkey_json && cJSON_IsString(pubkey_json)) {
|
||
const char* pubkey_str = cJSON_GetStringValue(pubkey_json);
|
||
LOG_ERROR("Pubkey length: %zu, value: %s",
|
||
pubkey_str ? strlen(pubkey_str) : 0, pubkey_str ? pubkey_str : "NULL");
|
||
}
|
||
|
||
cJSON_Delete(event);
|
||
return validation_result;
|
||
}
|
||
LOG_SUCCESS("Complete validation PASSED");
|
||
|
||
// Run our detailed validations for additional debugging
|
||
LOG_STEP("SERVER-8", "Running detailed structure validation");
|
||
int detailed_struct_result = detailed_structure_validation(event);
|
||
if (detailed_struct_result != NOSTR_SUCCESS) {
|
||
LOG_ERROR("Detailed structure validation failed: %d (%s)", detailed_struct_result, nostr_strerror(detailed_struct_result));
|
||
cJSON_Delete(event);
|
||
return detailed_struct_result;
|
||
}
|
||
LOG_SUCCESS("Detailed structure validation PASSED");
|
||
|
||
LOG_STEP("SERVER-9", "Running detailed signature validation");
|
||
int detailed_crypto_result = detailed_signature_validation(event);
|
||
if (detailed_crypto_result != NOSTR_SUCCESS) {
|
||
LOG_ERROR("Detailed signature validation failed: %d (%s)", detailed_crypto_result, nostr_strerror(detailed_crypto_result));
|
||
cJSON_Delete(event);
|
||
return detailed_crypto_result;
|
||
}
|
||
LOG_SUCCESS("Detailed signature validation PASSED");
|
||
|
||
// Analyze event fields
|
||
analyze_event_fields(event);
|
||
|
||
// Validate Blossom-specific requirements
|
||
LOG_STEP("SERVER-10", "Validating Blossom-specific requirements");
|
||
int blossom_result = validate_blossom_event(event, file_hash, method);
|
||
if (blossom_result != NOSTR_SUCCESS) {
|
||
LOG_ERROR("Blossom event validation failed: %d (%s)", blossom_result, nostr_strerror(blossom_result));
|
||
cJSON_Delete(event);
|
||
return blossom_result;
|
||
}
|
||
LOG_SUCCESS("Blossom event validation PASSED");
|
||
|
||
cJSON_Delete(event);
|
||
LOG_SUCCESS("Server-style authentication successful, returning NOSTR_SUCCESS");
|
||
return NOSTR_SUCCESS;
|
||
}
|
||
|
||
/////////////////////////////////////////////////////////////////////////////////////////
|
||
/////////////////////////////////////////////////////////////////////////////////////////
|
||
// AUTHENTICATION RULES SYSTEM (4.1.2)
|
||
/////////////////////////////////////////////////////////////////////////////////////////
|
||
/////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
// Authentication rule result structure
|
||
typedef struct {
|
||
int allowed; // 0 = denied, 1 = allowed
|
||
char reason[256]; // Human-readable reason
|
||
int rule_id; // Rule ID that made the decision (0 if no rule)
|
||
int priority; // Priority of the rule that matched
|
||
} auth_rule_result_t;
|
||
|
||
// Check if authentication rules system is enabled
|
||
int auth_rules_enabled(void) {
|
||
sqlite3* db;
|
||
sqlite3_stmt* stmt;
|
||
int rc, enabled = 0;
|
||
|
||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
||
if (rc) {
|
||
fprintf(stderr, "DEBUG: Database open failed in auth_rules_enabled: %s\r\n", sqlite3_errmsg(db));
|
||
return 0; // Disable rules if can't check database
|
||
}
|
||
|
||
const char* sql = "SELECT value FROM server_config WHERE key = 'auth_rules_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;
|
||
}
|
||
|
||
// Check pubkey whitelist rule
|
||
int check_pubkey_whitelist(const char* pubkey, const char* operation, auth_rule_result_t* result) {
|
||
if (!pubkey || !operation || !result) {
|
||
return 0;
|
||
}
|
||
|
||
sqlite3* db;
|
||
sqlite3_stmt* stmt;
|
||
int rc;
|
||
|
||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
||
if (rc) {
|
||
return 0;
|
||
}
|
||
|
||
const char* sql = "SELECT id, priority, description FROM auth_rules "
|
||
"WHERE rule_type = 'pubkey_whitelist' AND rule_target = ? "
|
||
"AND (operation = ? OR operation = '*') AND enabled = 1 "
|
||
"AND (expires_at IS NULL OR expires_at > strftime('%s', 'now')) "
|
||
"ORDER BY priority ASC, created_at ASC LIMIT 1";
|
||
|
||
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||
if (rc != SQLITE_OK) {
|
||
sqlite3_close(db);
|
||
return 0;
|
||
}
|
||
|
||
sqlite3_bind_text(stmt, 1, pubkey, -1, SQLITE_STATIC);
|
||
sqlite3_bind_text(stmt, 2, operation, -1, SQLITE_STATIC);
|
||
|
||
rc = sqlite3_step(stmt);
|
||
if (rc == SQLITE_ROW) {
|
||
result->allowed = 1;
|
||
result->rule_id = sqlite3_column_int(stmt, 0);
|
||
result->priority = sqlite3_column_int(stmt, 1);
|
||
const char* description = (const char*)sqlite3_column_text(stmt, 2);
|
||
snprintf(result->reason, sizeof(result->reason),
|
||
"Allowed by whitelist rule: %s", description ? description : "pubkey whitelisted");
|
||
|
||
sqlite3_finalize(stmt);
|
||
sqlite3_close(db);
|
||
return 1;
|
||
}
|
||
|
||
sqlite3_finalize(stmt);
|
||
sqlite3_close(db);
|
||
return 0;
|
||
}
|
||
|
||
// Check pubkey blacklist rule
|
||
int check_pubkey_blacklist(const char* pubkey, const char* operation, auth_rule_result_t* result) {
|
||
if (!pubkey || !operation || !result) {
|
||
return 0;
|
||
}
|
||
|
||
sqlite3* db;
|
||
sqlite3_stmt* stmt;
|
||
int rc;
|
||
|
||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
||
if (rc) {
|
||
return 0;
|
||
}
|
||
|
||
const char* sql = "SELECT id, priority, description FROM auth_rules "
|
||
"WHERE rule_type = 'pubkey_blacklist' AND rule_target = ? "
|
||
"AND (operation = ? OR operation = '*') AND enabled = 1 "
|
||
"AND (expires_at IS NULL OR expires_at > strftime('%s', 'now')) "
|
||
"ORDER BY priority ASC, created_at ASC LIMIT 1";
|
||
|
||
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||
if (rc != SQLITE_OK) {
|
||
sqlite3_close(db);
|
||
return 0;
|
||
}
|
||
|
||
sqlite3_bind_text(stmt, 1, pubkey, -1, SQLITE_STATIC);
|
||
sqlite3_bind_text(stmt, 2, operation, -1, SQLITE_STATIC);
|
||
|
||
rc = sqlite3_step(stmt);
|
||
if (rc == SQLITE_ROW) {
|
||
result->allowed = 0;
|
||
result->rule_id = sqlite3_column_int(stmt, 0);
|
||
result->priority = sqlite3_column_int(stmt, 1);
|
||
const char* description = (const char*)sqlite3_column_text(stmt, 2);
|
||
snprintf(result->reason, sizeof(result->reason),
|
||
"Denied by blacklist rule: %s", description ? description : "pubkey blacklisted");
|
||
|
||
sqlite3_finalize(stmt);
|
||
sqlite3_close(db);
|
||
return 1;
|
||
}
|
||
|
||
sqlite3_finalize(stmt);
|
||
sqlite3_close(db);
|
||
return 0;
|
||
}
|
||
|
||
// Check hash blacklist rule
|
||
int check_hash_blacklist(const char* hash, const char* operation, auth_rule_result_t* result) {
|
||
if (!hash || !operation || !result) {
|
||
return 0;
|
||
}
|
||
|
||
sqlite3* db;
|
||
sqlite3_stmt* stmt;
|
||
int rc;
|
||
|
||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
||
if (rc) {
|
||
return 0;
|
||
}
|
||
|
||
const char* sql = "SELECT id, priority, description FROM auth_rules "
|
||
"WHERE rule_type = 'hash_blacklist' AND rule_target = ? "
|
||
"AND (operation = ? OR operation = '*') AND enabled = 1 "
|
||
"AND (expires_at IS NULL OR expires_at > strftime('%s', 'now')) "
|
||
"ORDER BY priority ASC, created_at ASC LIMIT 1";
|
||
|
||
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||
if (rc != SQLITE_OK) {
|
||
sqlite3_close(db);
|
||
return 0;
|
||
}
|
||
|
||
sqlite3_bind_text(stmt, 1, hash, -1, SQLITE_STATIC);
|
||
sqlite3_bind_text(stmt, 2, operation, -1, SQLITE_STATIC);
|
||
|
||
rc = sqlite3_step(stmt);
|
||
if (rc == SQLITE_ROW) {
|
||
result->allowed = 0;
|
||
result->rule_id = sqlite3_column_int(stmt, 0);
|
||
result->priority = sqlite3_column_int(stmt, 1);
|
||
const char* description = (const char*)sqlite3_column_text(stmt, 2);
|
||
snprintf(result->reason, sizeof(result->reason),
|
||
"Denied by hash blacklist rule: %s", description ? description : "hash blacklisted");
|
||
|
||
sqlite3_finalize(stmt);
|
||
sqlite3_close(db);
|
||
return 1;
|
||
}
|
||
|
||
sqlite3_finalize(stmt);
|
||
sqlite3_close(db);
|
||
return 0;
|
||
}
|
||
|
||
// Check MIME type whitelist rule
|
||
int check_mime_type_whitelist(const char* mime_type, const char* operation, auth_rule_result_t* result) {
|
||
if (!mime_type || !operation || !result) {
|
||
return 0;
|
||
}
|
||
|
||
sqlite3* db;
|
||
sqlite3_stmt* stmt;
|
||
int rc;
|
||
|
||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
||
if (rc) {
|
||
return 0;
|
||
}
|
||
|
||
// Check for exact match or wildcard patterns (e.g., "image/*")
|
||
const char* sql = "SELECT id, priority, description FROM auth_rules "
|
||
"WHERE rule_type = 'mime_type_whitelist' "
|
||
"AND (rule_target = ? OR (rule_target LIKE '%/*' AND ? LIKE REPLACE(rule_target, '*', '%'))) "
|
||
"AND (operation = ? OR operation = '*') AND enabled = 1 "
|
||
"AND (expires_at IS NULL OR expires_at > strftime('%s', 'now')) "
|
||
"ORDER BY priority ASC, created_at ASC LIMIT 1";
|
||
|
||
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||
if (rc != SQLITE_OK) {
|
||
sqlite3_close(db);
|
||
return 0;
|
||
}
|
||
|
||
sqlite3_bind_text(stmt, 1, mime_type, -1, SQLITE_STATIC);
|
||
sqlite3_bind_text(stmt, 2, mime_type, -1, SQLITE_STATIC);
|
||
sqlite3_bind_text(stmt, 3, operation, -1, SQLITE_STATIC);
|
||
|
||
rc = sqlite3_step(stmt);
|
||
if (rc == SQLITE_ROW) {
|
||
result->allowed = 1;
|
||
result->rule_id = sqlite3_column_int(stmt, 0);
|
||
result->priority = sqlite3_column_int(stmt, 1);
|
||
const char* description = (const char*)sqlite3_column_text(stmt, 2);
|
||
snprintf(result->reason, sizeof(result->reason),
|
||
"Allowed by MIME type whitelist: %s", description ? description : "MIME type whitelisted");
|
||
|
||
sqlite3_finalize(stmt);
|
||
sqlite3_close(db);
|
||
return 1;
|
||
}
|
||
|
||
sqlite3_finalize(stmt);
|
||
sqlite3_close(db);
|
||
return 0;
|
||
}
|
||
|
||
// Check MIME type blacklist rule
|
||
int check_mime_type_blacklist(const char* mime_type, const char* operation, auth_rule_result_t* result) {
|
||
if (!mime_type || !operation || !result) {
|
||
return 0;
|
||
}
|
||
|
||
sqlite3* db;
|
||
sqlite3_stmt* stmt;
|
||
int rc;
|
||
|
||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
||
if (rc) {
|
||
return 0;
|
||
}
|
||
|
||
// Check for exact match or wildcard patterns (e.g., "application/*")
|
||
const char* sql = "SELECT id, priority, description FROM auth_rules "
|
||
"WHERE rule_type = 'mime_type_blacklist' "
|
||
"AND (rule_target = ? OR (rule_target LIKE '%/*' AND ? LIKE REPLACE(rule_target, '*', '%'))) "
|
||
"AND (operation = ? OR operation = '*') AND enabled = 1 "
|
||
"AND (expires_at IS NULL OR expires_at > strftime('%s', 'now')) "
|
||
"ORDER BY priority ASC, created_at ASC LIMIT 1";
|
||
|
||
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||
if (rc != SQLITE_OK) {
|
||
sqlite3_close(db);
|
||
return 0;
|
||
}
|
||
|
||
sqlite3_bind_text(stmt, 1, mime_type, -1, SQLITE_STATIC);
|
||
sqlite3_bind_text(stmt, 2, mime_type, -1, SQLITE_STATIC);
|
||
sqlite3_bind_text(stmt, 3, operation, -1, SQLITE_STATIC);
|
||
|
||
rc = sqlite3_step(stmt);
|
||
if (rc == SQLITE_ROW) {
|
||
result->allowed = 0;
|
||
result->rule_id = sqlite3_column_int(stmt, 0);
|
||
result->priority = sqlite3_column_int(stmt, 1);
|
||
const char* description = (const char*)sqlite3_column_text(stmt, 2);
|
||
snprintf(result->reason, sizeof(result->reason),
|
||
"Denied by MIME type blacklist: %s", description ? description : "MIME type blacklisted");
|
||
|
||
sqlite3_finalize(stmt);
|
||
sqlite3_close(db);
|
||
return 1;
|
||
}
|
||
|
||
sqlite3_finalize(stmt);
|
||
sqlite3_close(db);
|
||
return 0;
|
||
}
|
||
|
||
// Check file size limit rule
|
||
int check_size_limit(long file_size, const char* pubkey, const char* operation, auth_rule_result_t* result) {
|
||
if (!result || file_size < 0) {
|
||
return 0;
|
||
}
|
||
|
||
sqlite3* db;
|
||
sqlite3_stmt* stmt;
|
||
int rc;
|
||
|
||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
||
if (rc) {
|
||
return 0;
|
||
}
|
||
|
||
// Check for pubkey-specific or global size limits
|
||
const char* sql = "SELECT id, priority, rule_value, description FROM auth_rules "
|
||
"WHERE rule_type = 'size_limit' "
|
||
"AND (rule_target = ? OR rule_target = '*') "
|
||
"AND (operation = ? OR operation = '*') AND enabled = 1 "
|
||
"AND (expires_at IS NULL OR expires_at > strftime('%s', 'now')) "
|
||
"ORDER BY CASE WHEN rule_target = ? THEN 0 ELSE 1 END, priority ASC, created_at ASC LIMIT 1";
|
||
|
||
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||
if (rc != SQLITE_OK) {
|
||
sqlite3_close(db);
|
||
return 0;
|
||
}
|
||
|
||
sqlite3_bind_text(stmt, 1, pubkey ? pubkey : "*", -1, SQLITE_STATIC);
|
||
sqlite3_bind_text(stmt, 2, operation, -1, SQLITE_STATIC);
|
||
sqlite3_bind_text(stmt, 3, pubkey ? pubkey : "*", -1, SQLITE_STATIC);
|
||
|
||
rc = sqlite3_step(stmt);
|
||
if (rc == SQLITE_ROW) {
|
||
const char* size_limit_str = (const char*)sqlite3_column_text(stmt, 2);
|
||
long size_limit = size_limit_str ? atol(size_limit_str) : 0;
|
||
|
||
if (size_limit > 0 && file_size > size_limit) {
|
||
result->allowed = 0;
|
||
result->rule_id = sqlite3_column_int(stmt, 0);
|
||
result->priority = sqlite3_column_int(stmt, 1);
|
||
const char* description = (const char*)sqlite3_column_text(stmt, 3);
|
||
snprintf(result->reason, sizeof(result->reason),
|
||
"File size %ld exceeds limit %ld: %s",
|
||
file_size, size_limit, description ? description : "size limit exceeded");
|
||
|
||
sqlite3_finalize(stmt);
|
||
sqlite3_close(db);
|
||
return 1;
|
||
}
|
||
}
|
||
|
||
sqlite3_finalize(stmt);
|
||
sqlite3_close(db);
|
||
return 0;
|
||
}
|
||
|
||
/////////////////////////////////////////////////////////////////////////////////////////
|
||
/////////////////////////////////////////////////////////////////////////////////////////
|
||
// RULE EVALUATION ENGINE (4.1.3)
|
||
/////////////////////////////////////////////////////////////////////////////////////////
|
||
/////////////////////////////////////////////////////////////////////////////////////////
|
||
|
||
// Cache key generation for authentication decisions
|
||
void generate_auth_cache_key(const char* pubkey, const char* operation, const char* hash,
|
||
const char* mime_type, long file_size, char* cache_key, size_t key_size) {
|
||
char temp_buffer[1024];
|
||
snprintf(temp_buffer, sizeof(temp_buffer), "%s|%s|%s|%s|%ld",
|
||
pubkey ? pubkey : "", operation ? operation : "",
|
||
hash ? hash : "", mime_type ? mime_type : "", file_size);
|
||
|
||
// Generate SHA-256 hash of the key components for consistent cache keys
|
||
unsigned char hash_bytes[32];
|
||
if (nostr_sha256((unsigned char*)temp_buffer, strlen(temp_buffer), hash_bytes) == NOSTR_SUCCESS) {
|
||
nostr_bytes_to_hex(hash_bytes, 32, cache_key);
|
||
cache_key[64] = '\0'; // Ensure null termination
|
||
} else {
|
||
// Fallback if hashing fails
|
||
strncpy(cache_key, temp_buffer, key_size - 1);
|
||
cache_key[key_size - 1] = '\0';
|
||
}
|
||
}
|
||
|
||
// Check authentication cache for previous decisions
|
||
int check_auth_cache(const char* cache_key, auth_rule_result_t* result) {
|
||
if (!cache_key || !result) {
|
||
return 0;
|
||
}
|
||
|
||
sqlite3* db;
|
||
sqlite3_stmt* stmt;
|
||
int rc;
|
||
|
||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
||
if (rc) {
|
||
return 0;
|
||
}
|
||
|
||
const char* sql = "SELECT allowed, rule_id, rule_reason FROM auth_cache "
|
||
"WHERE cache_key = ? AND expires_at > 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, cache_key, -1, SQLITE_STATIC);
|
||
|
||
rc = sqlite3_step(stmt);
|
||
if (rc == SQLITE_ROW) {
|
||
result->allowed = sqlite3_column_int(stmt, 0);
|
||
result->rule_id = sqlite3_column_int(stmt, 1);
|
||
result->priority = 0; // Not stored in cache
|
||
const char* reason = (const char*)sqlite3_column_text(stmt, 2);
|
||
if (reason) {
|
||
strncpy(result->reason, reason, sizeof(result->reason) - 1);
|
||
result->reason[sizeof(result->reason) - 1] = '\0';
|
||
}
|
||
|
||
sqlite3_finalize(stmt);
|
||
sqlite3_close(db);
|
||
fprintf(stderr, "DEBUG: Cache hit for key: %.16s... (allowed=%d)\r\n", cache_key, result->allowed);
|
||
return 1;
|
||
}
|
||
|
||
sqlite3_finalize(stmt);
|
||
sqlite3_close(db);
|
||
return 0;
|
||
}
|
||
|
||
// Store authentication decision in cache
|
||
void store_auth_cache(const char* cache_key, const auth_rule_result_t* result) {
|
||
if (!cache_key || !result) {
|
||
return;
|
||
}
|
||
|
||
sqlite3* db;
|
||
sqlite3_stmt* stmt;
|
||
int rc;
|
||
|
||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL);
|
||
if (rc) {
|
||
fprintf(stderr, "DEBUG: Failed to open database for caching: %s\r\n", sqlite3_errmsg(db));
|
||
return;
|
||
}
|
||
|
||
// Get cache TTL from server config (default 5 minutes)
|
||
int cache_ttl = 300;
|
||
const char* ttl_sql = "SELECT value FROM server_config WHERE key = 'auth_cache_ttl'";
|
||
rc = sqlite3_prepare_v2(db, ttl_sql, -1, &stmt, NULL);
|
||
if (rc == SQLITE_OK) {
|
||
rc = sqlite3_step(stmt);
|
||
if (rc == SQLITE_ROW) {
|
||
const char* ttl_value = (const char*)sqlite3_column_text(stmt, 0);
|
||
if (ttl_value) {
|
||
cache_ttl = atoi(ttl_value);
|
||
}
|
||
}
|
||
sqlite3_finalize(stmt);
|
||
}
|
||
|
||
// Insert or replace cache entry
|
||
const char* insert_sql = "INSERT OR REPLACE INTO auth_cache "
|
||
"(cache_key, allowed, rule_id, rule_reason, expires_at) "
|
||
"VALUES (?, ?, ?, ?, strftime('%s', 'now') + ?)";
|
||
|
||
rc = sqlite3_prepare_v2(db, insert_sql, -1, &stmt, NULL);
|
||
if (rc == SQLITE_OK) {
|
||
sqlite3_bind_text(stmt, 1, cache_key, -1, SQLITE_STATIC);
|
||
sqlite3_bind_int(stmt, 2, result->allowed);
|
||
sqlite3_bind_int(stmt, 3, result->rule_id);
|
||
sqlite3_bind_text(stmt, 4, result->reason, -1, SQLITE_STATIC);
|
||
sqlite3_bind_int(stmt, 5, cache_ttl);
|
||
|
||
rc = sqlite3_step(stmt);
|
||
if (rc == SQLITE_DONE) {
|
||
fprintf(stderr, "DEBUG: Cached auth decision for key: %.16s... (TTL=%d)\r\n", cache_key, cache_ttl);
|
||
} else {
|
||
fprintf(stderr, "DEBUG: Failed to cache auth decision: %s\r\n", sqlite3_errmsg(db));
|
||
}
|
||
sqlite3_finalize(stmt);
|
||
}
|
||
|
||
sqlite3_close(db);
|
||
}
|
||
|
||
// Main rule evaluation function
|
||
int evaluate_auth_rules(const char* pubkey, const char* operation, const char* hash,
|
||
const char* mime_type, long file_size, auth_rule_result_t* result) {
|
||
if (!result) {
|
||
return 0;
|
||
}
|
||
|
||
// Initialize result structure
|
||
memset(result, 0, sizeof(auth_rule_result_t));
|
||
result->allowed = 1; // Default allow if no rules apply
|
||
strcpy(result->reason, "No rules matched - default allow");
|
||
|
||
fprintf(stderr, "DEBUG: evaluate_auth_rules called - pubkey=%s, op=%s, hash=%s, mime=%s, size=%ld\r\n",
|
||
pubkey ? pubkey : "NULL", operation ? operation : "NULL",
|
||
hash ? hash : "NULL", mime_type ? mime_type : "NULL", file_size);
|
||
|
||
// Check if authentication rules system is enabled
|
||
if (!auth_rules_enabled()) {
|
||
fprintf(stderr, "DEBUG: Authentication rules system is disabled\r\n");
|
||
strcpy(result->reason, "Authentication rules system disabled - default allow");
|
||
return 1;
|
||
}
|
||
|
||
// Generate cache key for this request
|
||
char cache_key[128];
|
||
generate_auth_cache_key(pubkey, operation, hash, mime_type, file_size, cache_key, sizeof(cache_key));
|
||
|
||
// Check cache first for performance
|
||
if (check_auth_cache(cache_key, result)) {
|
||
fprintf(stderr, "DEBUG: Using cached authentication decision\r\n");
|
||
return 1;
|
||
}
|
||
|
||
fprintf(stderr, "DEBUG: No cache hit - evaluating rules in priority order\r\n");
|
||
|
||
// Evaluate rules in priority order (lower priority number = higher precedence)
|
||
auth_rule_result_t rule_result;
|
||
int highest_priority = 9999;
|
||
int rule_matched = 0;
|
||
|
||
// 1. Check pubkey blacklist first (highest security priority)
|
||
if (pubkey && check_pubkey_blacklist(pubkey, operation, &rule_result)) {
|
||
if (rule_result.priority < highest_priority) {
|
||
*result = rule_result;
|
||
highest_priority = rule_result.priority;
|
||
rule_matched = 1;
|
||
fprintf(stderr, "DEBUG: Pubkey blacklist rule matched (priority %d)\r\n", rule_result.priority);
|
||
}
|
||
}
|
||
|
||
// 2. Check hash blacklist
|
||
if (hash && check_hash_blacklist(hash, operation, &rule_result)) {
|
||
if (rule_result.priority < highest_priority) {
|
||
*result = rule_result;
|
||
highest_priority = rule_result.priority;
|
||
rule_matched = 1;
|
||
fprintf(stderr, "DEBUG: Hash blacklist rule matched (priority %d)\r\n", rule_result.priority);
|
||
}
|
||
}
|
||
|
||
// 3. Check MIME type blacklist
|
||
if (mime_type && check_mime_type_blacklist(mime_type, operation, &rule_result)) {
|
||
if (rule_result.priority < highest_priority) {
|
||
*result = rule_result;
|
||
highest_priority = rule_result.priority;
|
||
rule_matched = 1;
|
||
fprintf(stderr, "DEBUG: MIME type blacklist rule matched (priority %d)\r\n", rule_result.priority);
|
||
}
|
||
}
|
||
|
||
// 4. Check file size limits
|
||
if (file_size > 0 && check_size_limit(file_size, pubkey, operation, &rule_result)) {
|
||
if (rule_result.priority < highest_priority) {
|
||
*result = rule_result;
|
||
highest_priority = rule_result.priority;
|
||
rule_matched = 1;
|
||
fprintf(stderr, "DEBUG: Size limit rule matched (priority %d)\r\n", rule_result.priority);
|
||
}
|
||
}
|
||
|
||
// 5. Check pubkey whitelist (only matters if not already denied)
|
||
if (pubkey && result->allowed && check_pubkey_whitelist(pubkey, operation, &rule_result)) {
|
||
if (rule_result.priority < highest_priority) {
|
||
*result = rule_result;
|
||
highest_priority = rule_result.priority;
|
||
rule_matched = 1;
|
||
fprintf(stderr, "DEBUG: Pubkey whitelist rule matched (priority %d)\r\n", rule_result.priority);
|
||
}
|
||
}
|
||
|
||
// 6. Check MIME type whitelist (only if not already denied)
|
||
if (mime_type && result->allowed && check_mime_type_whitelist(mime_type, operation, &rule_result)) {
|
||
if (rule_result.priority < highest_priority) {
|
||
*result = rule_result;
|
||
highest_priority = rule_result.priority;
|
||
rule_matched = 1;
|
||
fprintf(stderr, "DEBUG: MIME type whitelist rule matched (priority %d)\r\n", rule_result.priority);
|
||
}
|
||
}
|
||
|
||
// Special case: If we have whitelist rules but no whitelist matched, deny by default
|
||
if (result->allowed && pubkey) {
|
||
sqlite3* db;
|
||
sqlite3_stmt* stmt;
|
||
int rc;
|
||
|
||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
||
if (rc == SQLITE_OK) {
|
||
// Check if any pubkey whitelist rules exist for this operation
|
||
const char* sql = "SELECT COUNT(*) FROM auth_rules "
|
||
"WHERE rule_type = 'pubkey_whitelist' AND enabled = 1 "
|
||
"AND (operation = ? OR operation = '*') "
|
||
"AND (expires_at IS NULL OR expires_at > strftime('%s', 'now'))";
|
||
|
||
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||
if (rc == SQLITE_OK) {
|
||
sqlite3_bind_text(stmt, 1, operation, -1, SQLITE_STATIC);
|
||
rc = sqlite3_step(stmt);
|
||
if (rc == SQLITE_ROW) {
|
||
int whitelist_count = sqlite3_column_int(stmt, 0);
|
||
if (whitelist_count > 0) {
|
||
// Whitelist exists but didn't match - deny
|
||
result->allowed = 0;
|
||
result->rule_id = 0;
|
||
result->priority = 0;
|
||
snprintf(result->reason, sizeof(result->reason),
|
||
"Denied - pubkey not in whitelist (found %d whitelist rules)", whitelist_count);
|
||
rule_matched = 1;
|
||
fprintf(stderr, "DEBUG: Denied due to whitelist policy - pubkey not whitelisted\r\n");
|
||
}
|
||
}
|
||
sqlite3_finalize(stmt);
|
||
}
|
||
sqlite3_close(db);
|
||
}
|
||
}
|
||
|
||
// Cache the decision for future requests
|
||
store_auth_cache(cache_key, result);
|
||
|
||
fprintf(stderr, "DEBUG: Rule evaluation complete - allowed=%d, rule_id=%d, reason=%s\r\n",
|
||
result->allowed, result->rule_id, result->reason);
|
||
|
||
return rule_matched;
|
||
}
|
||
|
||
// Enhanced authentication function that integrates rule evaluation
|
||
int authenticate_request_with_rules(const char* auth_header, const char* method, const char* file_hash,
|
||
const char* mime_type, long file_size) {
|
||
fprintf(stderr, "DEBUG: authenticate_request_with_rules called - method: %s, file_hash: %s, mime_type: %s, file_size: %ld\r\n",
|
||
method ? method : "NULL", file_hash ? file_hash : "NULL", mime_type ? mime_type : "NULL", file_size);
|
||
|
||
// Step 1: Basic nostr authentication (if header provided)
|
||
const char* pubkey = NULL;
|
||
static char pubkey_buffer[256];
|
||
|
||
if (auth_header) {
|
||
fprintf(stderr, "DEBUG: Authorization header provided, starting basic nostr authentication\r\n");
|
||
// Parse and validate nostr event first
|
||
int auth_result = authenticate_request(auth_header, method, file_hash);
|
||
if (auth_result != NOSTR_SUCCESS) {
|
||
fprintf(stderr, "DEBUG: Basic nostr authentication failed: %d (%s)\r\n", auth_result, nostr_strerror(auth_result));
|
||
return auth_result;
|
||
}
|
||
fprintf(stderr, "DEBUG: Basic nostr authentication PASSED\r\n");
|
||
|
||
// Extract pubkey from validated event
|
||
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) {
|
||
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';
|
||
pubkey = pubkey_buffer;
|
||
}
|
||
}
|
||
cJSON_Delete(event);
|
||
}
|
||
}
|
||
fprintf(stderr, "DEBUG: Extracted pubkey from auth: %s\r\n", pubkey ? pubkey : "NULL");
|
||
} else {
|
||
fprintf(stderr, "DEBUG: No authorization header - evaluating rules for anonymous request\r\n");
|
||
}
|
||
|
||
// Step 2: Evaluate authentication rules
|
||
auth_rule_result_t rule_result;
|
||
int rule_evaluated = evaluate_auth_rules(pubkey, method, file_hash, mime_type, file_size, &rule_result);
|
||
|
||
if (rule_evaluated && !rule_result.allowed) {
|
||
fprintf(stderr, "DEBUG: Request denied by authentication rules: %s\r\n", rule_result.reason);
|
||
return NOSTR_ERROR_INVALID_INPUT; // Authentication denied by rules
|
||
}
|
||
|
||
fprintf(stderr, "DEBUG: Request allowed - nostr auth + rules passed\r\n");
|
||
return NOSTR_SUCCESS;
|
||
}
|
||
|
||
// 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);
|
||
}
|
||
|
||
// Handle GET /list/<pubkey> requests
|
||
void handle_list_request(const char* pubkey) {
|
||
fprintf(stderr, "DEBUG: handle_list_request called with pubkey=%s\r\n", pubkey ? pubkey : "NULL");
|
||
|
||
// 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) {
|
||
fprintf(stderr, "DEBUG: Query string: %s\r\n", query_string);
|
||
|
||
// Parse since parameter
|
||
const char* since_param = strstr(query_string, "since=");
|
||
if (since_param) {
|
||
since_timestamp = atol(since_param + 6);
|
||
fprintf(stderr, "DEBUG: Since timestamp: %ld\r\n", since_timestamp);
|
||
}
|
||
|
||
// Parse until parameter
|
||
const char* until_param = strstr(query_string, "until=");
|
||
if (until_param) {
|
||
until_timestamp = atol(until_param + 6);
|
||
fprintf(stderr, "DEBUG: Until timestamp: %ld\r\n", until_timestamp);
|
||
}
|
||
}
|
||
|
||
// Check for optional authorization
|
||
const char* auth_header = getenv("HTTP_AUTHORIZATION");
|
||
const char* auth_status = "none";
|
||
|
||
if (auth_header) {
|
||
fprintf(stderr, "DEBUG: Authorization header provided for list request\r\n");
|
||
int auth_result = authenticate_request_with_rules(auth_header, "list", NULL, NULL, 0);
|
||
if (auth_result != NOSTR_SUCCESS) {
|
||
send_error_response(401, "authentication_failed", "Invalid or expired authentication",
|
||
"The provided Nostr event is invalid, expired, or does not authorize this operation");
|
||
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) {
|
||
fprintf(stderr, "DEBUG: Database open failed: %s\r\n", sqlite3_errmsg(db));
|
||
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");
|
||
}
|
||
|
||
fprintf(stderr, "DEBUG: SQL query: %s\r\n", sql);
|
||
|
||
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||
if (rc != SQLITE_OK) {
|
||
fprintf(stderr, "DEBUG: SQL prepare failed: %s\r\n", sqlite3_errmsg(db));
|
||
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);
|
||
|
||
// Determine file extension from MIME type
|
||
const char* extension = "";
|
||
if (strstr(type, "image/jpeg")) {
|
||
extension = ".jpg";
|
||
} else if (strstr(type, "image/webp")) {
|
||
extension = ".webp";
|
||
} else if (strstr(type, "image/png")) {
|
||
extension = ".png";
|
||
} else if (strstr(type, "image/gif")) {
|
||
extension = ".gif";
|
||
} else if (strstr(type, "video/mp4")) {
|
||
extension = ".mp4";
|
||
} else if (strstr(type, "video/webm")) {
|
||
extension = ".webm";
|
||
} else if (strstr(type, "audio/mpeg")) {
|
||
extension = ".mp3";
|
||
} else if (strstr(type, "audio/ogg")) {
|
||
extension = ".ogg";
|
||
} else if (strstr(type, "text/plain")) {
|
||
extension = ".txt";
|
||
} else {
|
||
extension = ".bin";
|
||
}
|
||
|
||
// Output blob descriptor JSON
|
||
printf(" {\n");
|
||
printf(" \"url\": \"http://localhost:9001/%s%s\",\n", sha256, extension);
|
||
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);
|
||
|
||
fprintf(stderr, "DEBUG: List request completed successfully\r\n");
|
||
log_request("GET", "/list", auth_status, 200);
|
||
}
|
||
|
||
// Handle DELETE /<sha256> requests
|
||
void handle_delete_request(const char* sha256) {
|
||
fprintf(stderr, "DEBUG: handle_delete_request called with sha256=%s\r\n", sha256 ? sha256 : "NULL");
|
||
|
||
// 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
|
||
int auth_result = authenticate_request_with_rules(auth_header, "delete", sha256, NULL, 0);
|
||
if (auth_result != NOSTR_SUCCESS) {
|
||
send_error_response(401, "authentication_failed", "Invalid or expired authentication",
|
||
"The provided Nostr event is invalid, expired, or does not authorize this operation");
|
||
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;
|
||
}
|
||
|
||
const char* auth_pubkey = cJSON_GetStringValue(pubkey_json);
|
||
fprintf(stderr, "DEBUG-DELETE: Extracted auth_pubkey from DELETE request: '%s' (length: %zu)\r\n",
|
||
auth_pubkey ? auth_pubkey : "NULL", auth_pubkey ? strlen(auth_pubkey) : 0);
|
||
cJSON_Delete(event);
|
||
|
||
// 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) {
|
||
fprintf(stderr, "DEBUG: Database open failed: %s\r\n", sqlite3_errmsg(db));
|
||
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) {
|
||
fprintf(stderr, "DEBUG: SQL prepare failed: %s\r\n", sqlite3_errmsg(db));
|
||
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);
|
||
|
||
fprintf(stderr, "DEBUG-DELETE: Database query results:\r\n");
|
||
fprintf(stderr, "DEBUG-DELETE: Raw uploader_pubkey from DB: '%s'\r\n", uploader_pubkey ? uploader_pubkey : "NULL");
|
||
fprintf(stderr, "DEBUG-DELETE: Raw blob_type from DB: '%s'\r\n", blob_type ? blob_type : "NULL");
|
||
|
||
// 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);
|
||
|
||
fprintf(stderr, "DEBUG-DELETE: After copying strings:\r\n");
|
||
fprintf(stderr, "DEBUG-DELETE: uploader_pubkey_copy: '%s' (length: %zu)\r\n",
|
||
uploader_pubkey_copy[0] ? uploader_pubkey_copy : "EMPTY", strlen(uploader_pubkey_copy));
|
||
fprintf(stderr, "DEBUG-DELETE: blob_type_copy: '%s'\r\n", blob_type_copy[0] ? blob_type_copy : "EMPTY");
|
||
|
||
// Check ownership - only the uploader can delete
|
||
fprintf(stderr, "DEBUG-DELETE: Ownership verification:\r\n");
|
||
fprintf(stderr, "DEBUG-DELETE: Comparing uploader_pubkey_copy='%s'\r\n", uploader_pubkey_copy);
|
||
fprintf(stderr, "DEBUG-DELETE: Against auth_pubkey='%s'\r\n", auth_pubkey ? auth_pubkey : "NULL");
|
||
fprintf(stderr, "DEBUG-DELETE: uploader_pubkey_copy[0]=%d\r\n", (int)uploader_pubkey_copy[0]);
|
||
fprintf(stderr, "DEBUG-DELETE: strcmp result would be: %d\r\n",
|
||
uploader_pubkey_copy[0] ? strcmp(uploader_pubkey_copy, auth_pubkey) : -999);
|
||
|
||
if (!uploader_pubkey_copy[0] || strcmp(uploader_pubkey_copy, auth_pubkey) != 0) {
|
||
fprintf(stderr, "DEBUG-DELETE: OWNERSHIP CHECK FAILED!\r\n");
|
||
fprintf(stderr, "DEBUG-DELETE: Reason: %s\r\n",
|
||
!uploader_pubkey_copy[0] ? "uploader_pubkey_copy is empty" : "pubkeys don't match");
|
||
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 {
|
||
fprintf(stderr, "DEBUG-DELETE: OWNERSHIP CHECK PASSED!\r\n");
|
||
}
|
||
|
||
fprintf(stderr, "DEBUG: Ownership check passed, proceeding with deletion\r\n");
|
||
|
||
// 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) {
|
||
fprintf(stderr, "DEBUG: Delete SQL prepare failed: %s\r\n", sqlite3_errmsg(db));
|
||
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) {
|
||
fprintf(stderr, "DEBUG: Database delete failed: %d\r\n", rc);
|
||
send_error_response(500, "database_error", "Failed to delete blob metadata", "Internal server error");
|
||
log_request("DELETE", "/delete", "authenticated", 500);
|
||
return;
|
||
}
|
||
|
||
fprintf(stderr, "DEBUG: Blob metadata deleted from database\r\n");
|
||
|
||
// 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);
|
||
|
||
fprintf(stderr, "DEBUG: Attempting to delete file: %s\r\n", filepath);
|
||
|
||
if (unlink(filepath) != 0) {
|
||
fprintf(stderr, "DEBUG: Failed to delete physical file: %s\r\n", filepath);
|
||
// File deletion failed, but database is already updated
|
||
// Log warning but don't fail the request
|
||
fprintf(stderr, "WARNING: Physical file deletion failed, but metadata was removed\r\n");
|
||
} else {
|
||
fprintf(stderr, "DEBUG: Physical file deleted successfully\r\n");
|
||
}
|
||
|
||
// 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");
|
||
|
||
fprintf(stderr, "DEBUG: Delete operation completed successfully\r\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");
|
||
|
||
fprintf(stderr, "DEBUG: content_type=%s\r\n", content_type ? content_type : "NULL");
|
||
fprintf(stderr, "DEBUG: content_length=%s\r\n", content_length_str ? content_length_str : "NULL");
|
||
|
||
// 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");
|
||
fprintf(stderr, "DEBUG: Raw Authorization header: %s\r\n", auth_header ? auth_header : "NULL");
|
||
|
||
// 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) {
|
||
fprintf(stderr, "DEBUG: Expected %ld bytes, read %zu bytes\r\n", content_length, bytes_read);
|
||
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];
|
||
|
||
|
||
// EMERGENCY DEBUG: Write to direct file
|
||
FILE* debug_file = fopen("debug_hash_data.log", "a");
|
||
if (debug_file) {
|
||
fprintf(debug_file, "=== HASH DEBUG SESSION ===\n");
|
||
fprintf(debug_file, "Content length: %ld\n", content_length);
|
||
fprintf(debug_file, "File data to hash: ");
|
||
for (int i = 0; i < content_length; i++) {
|
||
fprintf(debug_file, "%02x", (unsigned char)file_data[i]);
|
||
}
|
||
fprintf(debug_file, "\n");
|
||
fprintf(debug_file, "File data as string: %.*s\n", (int)content_length, file_data);
|
||
fclose(debug_file);
|
||
}
|
||
|
||
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);
|
||
fprintf(stderr, "DEBUG-LAAN: Calculated SHA-256: %s\r\n", sha256_hex);
|
||
fflush(stderr);
|
||
|
||
// EMERGENCY DEBUG: Write calculated hash to direct file
|
||
FILE* debug_file2 = fopen("debug_hash_data.log", "a");
|
||
if (debug_file2) {
|
||
fprintf(debug_file2, "Calculated SHA-256: %s\n", sha256_hex);
|
||
fprintf(debug_file2, "=== END DEBUG SESSION ===\n\n");
|
||
fclose(debug_file2);
|
||
}
|
||
|
||
// TEMPORARY FIX: Bypass rules system and use simple authentication
|
||
fprintf(stderr, "AUTH: About to perform authentication - auth_header present: %s\r\n", auth_header ? "YES" : "NO");
|
||
int auth_result = NOSTR_SUCCESS;
|
||
if (auth_header) {
|
||
fprintf(stderr, "AUTH: Calling authenticate_request with hash: %s\r\n", sha256_hex);
|
||
auth_result = authenticate_request(auth_header, "upload", sha256_hex);
|
||
fprintf(stderr, "AUTH: authenticate_request returned: %d\r\n", auth_result);
|
||
if (auth_result != NOSTR_SUCCESS) {
|
||
free(file_data);
|
||
|
||
// Provide specific error messages based on the authentication failure type
|
||
const char* error_type = "authentication_failed";
|
||
const char* message = "Authentication failed";
|
||
const char* details = "The request failed nostr authentication";
|
||
|
||
switch (auth_result) {
|
||
case NOSTR_ERROR_EVENT_INVALID_CONTENT:
|
||
error_type = "event_expired";
|
||
message = "Authentication event expired";
|
||
details = "The provided nostr event has expired and is no longer valid";
|
||
break;
|
||
case NOSTR_ERROR_EVENT_INVALID_SIGNATURE:
|
||
error_type = "invalid_signature";
|
||
message = "Invalid cryptographic signature";
|
||
details = "The event signature verification failed";
|
||
break;
|
||
case NOSTR_ERROR_EVENT_INVALID_PUBKEY:
|
||
error_type = "invalid_pubkey";
|
||
message = "Invalid public key";
|
||
details = "The event contains an invalid or malformed public key";
|
||
break;
|
||
case NOSTR_ERROR_EVENT_INVALID_ID:
|
||
error_type = "invalid_event_id";
|
||
message = "Invalid event ID";
|
||
details = "The event ID does not match the calculated hash";
|
||
break;
|
||
case NOSTR_ERROR_INVALID_INPUT:
|
||
error_type = "invalid_format";
|
||
message = "Invalid authorization format";
|
||
details = "The authorization header format is invalid or malformed";
|
||
break;
|
||
default:
|
||
error_type = "authentication_failed";
|
||
message = "Authentication failed";
|
||
// Use C-style string formatting for error details
|
||
static char error_details_buffer[256];
|
||
snprintf(error_details_buffer, sizeof(error_details_buffer),
|
||
"The request failed nostr authentication (error code: %d - %s)",
|
||
auth_result, nostr_strerror(auth_result));
|
||
details = error_details_buffer;
|
||
break;
|
||
}
|
||
|
||
send_error_response(401, error_type, message, details);
|
||
log_request("PUT", "/upload", "auth_failed", 401);
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Extract uploader pubkey from authorization if provided
|
||
if (auth_header) {
|
||
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) {
|
||
cJSON* pubkey_json = cJSON_GetObjectItem(event, "pubkey");
|
||
if (pubkey_json && cJSON_IsString(pubkey_json)) {
|
||
static char pubkey_buffer[256];
|
||
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;
|
||
}
|
||
}
|
||
cJSON_Delete(event);
|
||
}
|
||
}
|
||
}
|
||
|
||
fprintf(stderr, "DEBUG: Authentication passed, uploader_pubkey: %s\r\n", uploader_pubkey ? uploader_pubkey : "anonymous");
|
||
|
||
// Determine file extension from Content-Type
|
||
const char* extension = "";
|
||
if (strstr(content_type, "image/jpeg")) {
|
||
extension = ".jpg";
|
||
} else if (strstr(content_type, "image/webp")) {
|
||
extension = ".webp";
|
||
} else if (strstr(content_type, "image/png")) {
|
||
extension = ".png";
|
||
} else if (strstr(content_type, "image/gif")) {
|
||
extension = ".gif";
|
||
} else if (strstr(content_type, "video/mp4")) {
|
||
extension = ".mp4";
|
||
} else if (strstr(content_type, "video/webm")) {
|
||
extension = ".webm";
|
||
} else if (strstr(content_type, "audio/mpeg")) {
|
||
extension = ".mp3";
|
||
} else if (strstr(content_type, "audio/ogg")) {
|
||
extension = ".ogg";
|
||
} else if (strstr(content_type, "text/plain")) {
|
||
extension = ".txt";
|
||
} else {
|
||
// Default to binary extension for unknown types
|
||
extension = ".bin";
|
||
}
|
||
|
||
// Save file to blobs directory with SHA-256 + extension
|
||
char filepath[MAX_PATH_LEN];
|
||
snprintf(filepath, sizeof(filepath), "blobs/%s%s", sha256_hex, extension);
|
||
|
||
fprintf(stderr, "DEBUG: Saving file to: %s\r\n", filepath);
|
||
|
||
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 {
|
||
fprintf(stderr, "DEBUG: File permissions set to 644 for %s\r\n", filepath);
|
||
}
|
||
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;
|
||
}
|
||
|
||
fprintf(stderr, "DEBUG: Successfully saved %zu bytes to %s\r\n", bytes_written, filepath);
|
||
|
||
// Extract filename from Content-Disposition header if present
|
||
const char* filename = NULL;
|
||
const char* content_disposition = getenv("HTTP_CONTENT_DISPOSITION");
|
||
fprintf(stderr, "DEBUG: Content-Disposition header: %s\r\n", content_disposition ? content_disposition : "NULL");
|
||
|
||
if (content_disposition) {
|
||
fprintf(stderr, "DEBUG: Looking for filename= in Content-Disposition header\r\n");
|
||
// Look for filename= in Content-Disposition header
|
||
const char* filename_start = strstr(content_disposition, "filename=");
|
||
if (filename_start) {
|
||
fprintf(stderr, "DEBUG: Found filename= at position %ld\r\n", filename_start - content_disposition);
|
||
filename_start += 9; // Skip "filename="
|
||
fprintf(stderr, "DEBUG: Filename value starts with: %.20s\r\n", filename_start);
|
||
|
||
// Handle quoted filenames
|
||
if (*filename_start == '"') {
|
||
fprintf(stderr, "DEBUG: Processing quoted filename\r\n");
|
||
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;
|
||
fprintf(stderr, "DEBUG: Quoted filename length: %zu\r\n", filename_len);
|
||
if (filename_len < sizeof(filename_buffer)) {
|
||
strncpy(filename_buffer, filename_start, filename_len);
|
||
filename_buffer[filename_len] = '\0';
|
||
filename = filename_buffer;
|
||
fprintf(stderr, "DEBUG: Extracted quoted filename: '%s'\r\n", filename);
|
||
} else {
|
||
fprintf(stderr, "DEBUG: Quoted filename too long, skipping\r\n");
|
||
}
|
||
} else {
|
||
fprintf(stderr, "DEBUG: No closing quote found for filename\r\n");
|
||
}
|
||
} else {
|
||
fprintf(stderr, "DEBUG: Processing unquoted filename\r\n");
|
||
// 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;
|
||
fprintf(stderr, "DEBUG: Unquoted filename length: %zu\r\n", filename_len);
|
||
if (filename_len < sizeof(filename_buffer)) {
|
||
strncpy(filename_buffer, filename_start, filename_len);
|
||
filename_buffer[filename_len] = '\0';
|
||
filename = filename_buffer;
|
||
fprintf(stderr, "DEBUG: Extracted unquoted filename: '%s'\r\n", filename);
|
||
} else {
|
||
fprintf(stderr, "DEBUG: Unquoted filename too long, skipping\r\n");
|
||
}
|
||
}
|
||
} else {
|
||
fprintf(stderr, "DEBUG: No filename= found in Content-Disposition header\r\n");
|
||
}
|
||
} else {
|
||
fprintf(stderr, "DEBUG: No Content-Disposition header provided\r\n");
|
||
}
|
||
|
||
fprintf(stderr, "DEBUG: Final filename after extraction: %s\r\n", filename ? filename : "NULL");
|
||
|
||
// 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
|
||
fprintf(stderr, "DEBUG: Database insertion failed, removing physical file\r\n");
|
||
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;
|
||
}
|
||
|
||
fprintf(stderr, "DEBUG: Blob metadata successfully stored in database\r\n");
|
||
|
||
// 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\": \"http://localhost:9001/%s%s\"\n", sha256_hex, extension);
|
||
printf("}\n");
|
||
|
||
fprintf(stderr, "DEBUG: Upload completed successfully with database storage\r\n");
|
||
}
|
||
|
||
int main(void) {
|
||
fprintf(stderr, "STARTUP: FastCGI application starting up\r\n");
|
||
fflush(stderr);
|
||
|
||
// 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");
|
||
fflush(stderr);
|
||
while (FCGI_Accept() >= 0) {
|
||
// DEBUG: Log every request received
|
||
|
||
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;
|
||
}
|
||
|
||
// Handle HEAD requests for blob metadata
|
||
if (strcmp(request_method, "HEAD") == 0) {
|
||
const char* sha256 = extract_sha256_from_uri(request_uri);
|
||
fprintf(stderr, "DEBUG: Extracted SHA256=%s\r\n", sha256 ? sha256 : "NULL");
|
||
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, "GET") == 0 && strncmp(request_uri, "/list/", 6) == 0) {
|
||
// Handle GET /list/<pubkey> 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 /<sha256> requests
|
||
const char* sha256 = extract_sha256_from_uri(request_uri);
|
||
fprintf(stderr, "DEBUG: DELETE request - extracted SHA256=%s\r\n", sha256 ? sha256 : "NULL");
|
||
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;
|
||
}
|