Files
ginxsom/Trash/main copy.c
2025-09-02 17:23:02 -04:00

2675 lines
107 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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(&timestamp);
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;
}