/* * Ginxsom Blossom Server - FastCGI Application * Handles HEAD requests and other dynamic operations */ #define _GNU_SOURCE #include "ginxsom.h" #include "relay_client.h" #include "admin_commands.h" #include "../nostr_core_lib/nostr_core/nostr_common.h" #include "../nostr_core_lib/nostr_core/nostr_log.h" #include "../nostr_core_lib/nostr_core/utils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Shutdown flag set by signal handler - volatile sig_atomic_t is async-signal-safe static volatile sig_atomic_t g_shutdown_requested = 0; static time_t g_server_start_time = 0; static void shutdown_handler(int signum) { (void)signum; g_shutdown_requested = 1; } // Centralized logging level (minimum severity to emit) log_level_t g_log_level = LOG_DEBUG; static nostr_log_level_t app_log_level_to_nostr(log_level_t level) { switch (level) { case LOG_TRACE: return NOSTR_LOG_LEVEL_TRACE; case LOG_DEBUG: return NOSTR_LOG_LEVEL_DEBUG; case LOG_INFO: return NOSTR_LOG_LEVEL_INFO; case LOG_WARN: return NOSTR_LOG_LEVEL_WARN; case LOG_ERROR: default: return NOSTR_LOG_LEVEL_ERROR; } } static void nostr_log_cb(int level, const char *component, const char *message, void *user_data) { (void)user_data; const char *comp = (component && component[0]) ? component : "core"; const char *msg = message ? message : ""; log_level_t app_level = LOG_INFO; switch (level) { case NOSTR_LOG_LEVEL_ERROR: app_level = LOG_ERROR; break; case NOSTR_LOG_LEVEL_WARN: app_level = LOG_WARN; break; case NOSTR_LOG_LEVEL_INFO: app_level = LOG_INFO; break; case NOSTR_LOG_LEVEL_DEBUG: app_level = LOG_DEBUG; break; case NOSTR_LOG_LEVEL_TRACE: app_level = LOG_TRACE; break; default: app_level = LOG_INFO; break; } app_log(app_level, "[nostr:%s] %s", comp, msg); } // Centralized logging system (declaration in ginxsom.h) void app_log(log_level_t level, const char *format, ...) { if (level < g_log_level) { return; } FILE *log_file = fopen("logs/app/app.log", "a"); if (!log_file) { return; // Silently fail if we can't open log file } // Get timestamp 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); // Get log level string const char *level_str; switch (level) { case LOG_TRACE: level_str = "TRACE"; break; case LOG_DEBUG: level_str = "DEBUG"; break; case LOG_INFO: level_str = "INFO"; break; case LOG_WARN: level_str = "WARN"; break; case LOG_ERROR: level_str = "ERROR"; break; default: level_str = "UNKNOWN"; break; } // Write log prefix with timestamp, PID, and level fprintf(log_file, "[%s] [PID:%d] [%s] ", timestamp, getpid(), level_str); // Write formatted message va_list args; va_start(args, format); vfprintf(log_file, format, args); va_end(args); // Ensure newline fprintf(log_file, "\n"); fclose(log_file); } #define MAX_SHA256_LEN 65 #define MAX_PATH_LEN 4096 #define MAX_MIME_LEN 128 // Configuration variables - can be overridden via command line char g_db_path[MAX_PATH_LEN] = ""; // Will be set based on pubkey char g_storage_dir[MAX_PATH_LEN] = "."; // Key management variables char g_admin_pubkey[65] = ""; // Admin public key for authorization char g_blossom_seckey[65] = ""; // Blossom server private key for decryption/signing char g_blossom_pubkey[65] = ""; // Blossom server public key (derived from seckey) int g_generate_keys = 0; // Flag to generate keys on startup (deprecated) // Use global configuration variables #define DB_PATH g_db_path // Configuration system implementation #include #include #include #include // Function to get XDG config directory const char *get_config_dir(char *buffer, size_t buffer_size) { const char *xdg_config = getenv("XDG_CONFIG_HOME"); if (xdg_config) { snprintf(buffer, buffer_size, "%s/ginxsom", xdg_config); return buffer; } const char *home = getenv("HOME"); if (home) { snprintf(buffer, buffer_size, "%s/.config/ginxsom", home); return buffer; } // Fallback return ".config/ginxsom"; } // Database initialization function int initialize_database(const char *db_path) { sqlite3 *db; char *err_msg = NULL; int rc; // Check if database file exists struct stat st; int db_exists = (stat(db_path, &st) == 0); // Open database with CREATE flag rc = sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL); if (rc != SQLITE_OK) { app_log(LOG_ERROR, "Cannot open database: %s\n", sqlite3_errmsg(db)); sqlite3_close(db); return -1; } // If database was just created, initialize schema if (!db_exists) { app_log(LOG_INFO, "Database not found, initializing schema...\n"); // Enable foreign key constraints rc = sqlite3_exec(db, "PRAGMA foreign_keys = ON;", NULL, NULL, &err_msg); if (rc != SQLITE_OK) { app_log(LOG_ERROR, "Failed to enable foreign keys: %s\n", err_msg); sqlite3_free(err_msg); sqlite3_close(db); return -1; } // Create blobs table const char *create_blobs = "CREATE TABLE IF NOT EXISTS blobs (" " sha256 TEXT PRIMARY KEY NOT NULL," " size INTEGER NOT NULL," " type TEXT NOT NULL," " uploaded_at INTEGER NOT NULL," " uploader_pubkey TEXT," " filename TEXT," " CHECK (length(sha256) = 64)," " CHECK (size >= 0)," " CHECK (uploaded_at > 0)" ");"; rc = sqlite3_exec(db, create_blobs, NULL, NULL, &err_msg); if (rc != SQLITE_OK) { app_log(LOG_ERROR, "Failed to create blobs table: %s\n", err_msg); sqlite3_free(err_msg); sqlite3_close(db); return -1; } // Create config table const char *create_config = "CREATE TABLE IF NOT EXISTS config (" " key TEXT PRIMARY KEY NOT NULL," " value TEXT NOT NULL," " description TEXT," " created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))," " updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))" ");"; rc = sqlite3_exec(db, create_config, NULL, NULL, &err_msg); if (rc != SQLITE_OK) { app_log(LOG_ERROR, "Failed to create config table: %s\n", err_msg); sqlite3_free(err_msg); sqlite3_close(db); return -1; } // Create system table for runtime metrics (read-only, updated by server) const char *create_system = "CREATE TABLE IF NOT EXISTS system (" " key TEXT PRIMARY KEY NOT NULL," " value TEXT NOT NULL," " updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))" ");"; rc = sqlite3_exec(db, create_system, NULL, NULL, &err_msg); if (rc != SQLITE_OK) { app_log(LOG_ERROR, "Failed to create system table: %s\n", err_msg); sqlite3_free(err_msg); sqlite3_close(db); return -1; } // Create auth_rules table (c-relay compatible schema) const char *create_auth_rules = "CREATE TABLE IF NOT EXISTS auth_rules (" " id INTEGER PRIMARY KEY AUTOINCREMENT," " rule_type TEXT NOT NULL," " pattern_type TEXT NOT NULL," " pattern_value TEXT NOT NULL," " active INTEGER NOT NULL DEFAULT 1," " created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))," " updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))" ");"; rc = sqlite3_exec(db, create_auth_rules, NULL, NULL, &err_msg); if (rc != SQLITE_OK) { app_log(LOG_ERROR, "Failed to create auth_rules table: %s\n", err_msg); sqlite3_free(err_msg); sqlite3_close(db); return -1; } // Create indexes const char *create_indexes = "CREATE INDEX IF NOT EXISTS idx_blobs_uploaded_at ON blobs(uploaded_at);" "CREATE INDEX IF NOT EXISTS idx_blobs_uploader_pubkey ON blobs(uploader_pubkey);" "CREATE INDEX IF NOT EXISTS idx_blobs_type ON blobs(type);" "CREATE INDEX IF NOT EXISTS idx_config_updated_at ON config(updated_at);" "CREATE INDEX IF NOT EXISTS idx_auth_rules_type ON auth_rules(rule_type);" "CREATE INDEX IF NOT EXISTS idx_auth_rules_pattern ON auth_rules(pattern_type, pattern_value);" "CREATE INDEX IF NOT EXISTS idx_auth_rules_active ON auth_rules(active);"; rc = sqlite3_exec(db, create_indexes, NULL, NULL, &err_msg); if (rc != SQLITE_OK) { app_log(LOG_ERROR, "Failed to create indexes: %s\n", err_msg); sqlite3_free(err_msg); sqlite3_close(db); return -1; } // Insert default configuration const char *insert_config = "INSERT OR IGNORE INTO config (key, value, description) VALUES" " ('max_file_size', '104857600', 'Maximum file size in bytes (100MB)')," " ('auth_rules_enabled', 'true', 'Whether authentication rules are enabled for uploads')," " ('server_name', 'ginxsom', 'Server name for responses')," " ('admin_pubkey', '', 'Admin public key for API access')," " ('admin_enabled', 'true', 'Whether admin API is enabled')," " ('nip42_require_auth', 'false', 'Enable NIP-42 challenge/response authentication')," " ('nip42_challenge_timeout', '600', 'NIP-42 challenge timeout in seconds')," " ('nip42_time_tolerance', '300', 'NIP-42 timestamp tolerance in seconds')," " ('enable_relay_connect', 'false', 'Enable connection to Nostr relays')," " ('kind_0_content', '{\"name\":\"Ginxsom Blossom Server\",\"about\":\"A Nostr-enabled Blossom media server\",\"picture\":\"\"}', 'JSON content for Kind 0 profile event')," " ('kind_10002_tags', '[\"wss://relay.laantungir.net\"]', 'JSON array of relay URLs for Kind 10002');"; rc = sqlite3_exec(db, insert_config, NULL, NULL, &err_msg); if (rc != SQLITE_OK) { app_log(LOG_ERROR, "Failed to insert default config: %s\n", err_msg); sqlite3_free(err_msg); sqlite3_close(db); return -1; } // Create storage_stats view (legacy - kept for backward compatibility) const char *create_view = "CREATE VIEW IF NOT EXISTS storage_stats AS " "SELECT " " COUNT(*) as total_blobs, " " SUM(size) as total_bytes, " " AVG(size) as avg_blob_size, " " MIN(uploaded_at) as first_upload, " " MAX(uploaded_at) as last_upload, " " COUNT(DISTINCT uploader_pubkey) as unique_uploaders " "FROM blobs;"; rc = sqlite3_exec(db, create_view, NULL, NULL, &err_msg); if (rc != SQLITE_OK) { app_log(LOG_ERROR, "Failed to create storage_stats view: %s\n", err_msg); sqlite3_free(err_msg); sqlite3_close(db); return -1; } // Create blob_overview view for admin dashboard with system metrics const char *create_overview_view = "CREATE VIEW IF NOT EXISTS blob_overview AS " "SELECT " " COUNT(*) as total_blobs, " " COALESCE(SUM(size), 0) as total_bytes, " " MIN(uploaded_at) as first_upload, " " MAX(uploaded_at) as last_upload, " " (SELECT value FROM system WHERE key = 'version') as version, " " (SELECT value FROM system WHERE key = 'process_id') as process_id, " " (SELECT value FROM system WHERE key = 'memory_mb') as memory_mb, " " (SELECT value FROM system WHERE key = 'cpu_core') as cpu_core, " " (SELECT value FROM system WHERE key = 'fs_blob_count') as fs_blob_count, " " (SELECT value FROM system WHERE key = 'fs_blob_size_mb') as fs_blob_size_mb " "FROM blobs;"; rc = sqlite3_exec(db, create_overview_view, NULL, NULL, &err_msg); if (rc != SQLITE_OK) { app_log(LOG_ERROR, "Failed to create blob_overview view: %s\n", err_msg); sqlite3_free(err_msg); sqlite3_close(db); return -1; } // Create blob_type_distribution view for MIME type statistics const char *create_type_view = "CREATE VIEW IF NOT EXISTS blob_type_distribution AS " "SELECT " " type as mime_type, " " COUNT(*) as blob_count, " " SUM(size) as total_bytes, " " ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM blobs), 2) as percentage " "FROM blobs " "GROUP BY type " "ORDER BY blob_count DESC;"; rc = sqlite3_exec(db, create_type_view, NULL, NULL, &err_msg); if (rc != SQLITE_OK) { app_log(LOG_ERROR, "Failed to create blob_type_distribution view: %s\n", err_msg); sqlite3_free(err_msg); sqlite3_close(db); return -1; } // Create blob_time_stats view for time-based statistics const char *create_time_view = "CREATE VIEW IF NOT EXISTS blob_time_stats AS " "SELECT " " COUNT(CASE WHEN uploaded_at >= strftime('%s', 'now', '-1 day') THEN 1 END) as blobs_24h, " " COUNT(CASE WHEN uploaded_at >= strftime('%s', 'now', '-7 days') THEN 1 END) as blobs_7d, " " COUNT(CASE WHEN uploaded_at >= strftime('%s', 'now', '-30 days') THEN 1 END) as blobs_30d " "FROM blobs;"; rc = sqlite3_exec(db, create_time_view, NULL, NULL, &err_msg); if (rc != SQLITE_OK) { app_log(LOG_ERROR, "Failed to create blob_time_stats view: %s\n", err_msg); sqlite3_free(err_msg); sqlite3_close(db); return -1; } // Create top_uploaders view for pubkey statistics const char *create_uploaders_view = "CREATE VIEW IF NOT EXISTS top_uploaders AS " "SELECT " " uploader_pubkey, " " COUNT(*) as blob_count, " " SUM(size) as total_bytes, " " ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM blobs), 2) as percentage, " " MIN(uploaded_at) as first_upload, " " MAX(uploaded_at) as last_upload " "FROM blobs " "WHERE uploader_pubkey IS NOT NULL " "GROUP BY uploader_pubkey " "ORDER BY blob_count DESC " "LIMIT 20;"; rc = sqlite3_exec(db, create_uploaders_view, NULL, NULL, &err_msg); if (rc != SQLITE_OK) { app_log(LOG_ERROR, "Failed to create top_uploaders view: %s\n", err_msg); sqlite3_free(err_msg); sqlite3_close(db); return -1; } app_log(LOG_INFO, "Database schema initialized successfully\n"); } sqlite3_close(db); return 0; } // Function declarations void handle_options_request(void); 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); // Key management functions int generate_random_private_key_bytes(unsigned char *key_bytes, size_t len); int generate_server_keypair(void); int load_server_keys(void); int store_blossom_private_key(const char *seckey); int get_blossom_private_key(char *seckey_out, size_t max_len); int derive_pubkey_from_privkey(const char *privkey_hex, char *pubkey_hex_out); int validate_database_pubkey_match(const char *db_path, const char *expected_pubkey); int set_db_path_from_pubkey(const char *pubkey); // External validator function declarations const char *nostr_request_validator_get_last_violation_type(void); int nostr_generate_nip42_challenge(char *challenge_out, size_t challenge_size, const char *client_ip); // NIP-42 function declarations void handle_auth_challenge_request(void); // Handler function declarations with validation support void handle_delete_request_with_validation(const char *sha256, nostr_request_result_t *validation_result); // Key management function implementations // Derive public key from private key hex string int derive_pubkey_from_privkey(const char *privkey_hex, char *pubkey_hex_out) { if (!privkey_hex || strlen(privkey_hex) != 64) { app_log(LOG_ERROR, "ERROR: Invalid private key format\n"); return -1; } unsigned char seckey_bytes[32]; if (nostr_hex_to_bytes(privkey_hex, seckey_bytes, 32) != NOSTR_SUCCESS) { app_log(LOG_ERROR, "ERROR: Failed to parse private key hex\n"); return -1; } unsigned char pubkey_bytes[32]; if (nostr_ec_public_key_from_private_key(seckey_bytes, pubkey_bytes) != NOSTR_SUCCESS) { app_log(LOG_ERROR, "ERROR: Failed to derive public key\n"); return -1; } nostr_bytes_to_hex(pubkey_bytes, 32, pubkey_hex_out); return 0; } // Set database path based on pubkey (db/.db) int set_db_path_from_pubkey(const char *pubkey) { if (!pubkey || strlen(pubkey) != 64) { app_log(LOG_ERROR, "ERROR: Invalid pubkey for database naming\n"); return -1; } snprintf(g_db_path, sizeof(g_db_path), "db/%s.db", pubkey); app_log(LOG_INFO, "DATABASE: Set database path to %s\n", g_db_path); return 0; } // Validate that database filename matches the pubkey stored inside int validate_database_pubkey_match(const char *db_path, const char *expected_pubkey) { // Extract pubkey from database filename const char *filename = strrchr(db_path, '/'); if (!filename) { filename = db_path; } else { filename++; // Skip the '/' } // Check if filename matches pattern: .db size_t filename_len = strlen(filename); if (filename_len != 67) { // 64 chars + ".db" = 67 app_log(LOG_WARN, "WARNING: Database filename doesn't match pubkey pattern: %s\n", filename); return 0; // Don't enforce for non-standard names (backward compatibility) } // Extract pubkey from filename (first 64 chars) char filename_pubkey[65]; strncpy(filename_pubkey, filename, 64); filename_pubkey[64] = '\0'; // Compare with expected pubkey if (strcasecmp(filename_pubkey, expected_pubkey) != 0) { app_log(LOG_ERROR, "ERROR: Database pubkey mismatch!\n"); app_log(LOG_ERROR, " Database filename: %s\n", filename_pubkey); app_log(LOG_ERROR, " Expected pubkey: %s\n", expected_pubkey); app_log(LOG_ERROR, " → Database filename doesn't match the keys stored inside\n"); return -1; } app_log(LOG_INFO, "DATABASE: Pubkey validation passed: %s\n", filename_pubkey); return 0; } // Generate random private key bytes using /dev/urandom int generate_random_private_key_bytes(unsigned char *key_bytes, size_t len) { FILE *fp = fopen("/dev/urandom", "rb"); if (!fp) { app_log(LOG_ERROR, "ERROR: Cannot open /dev/urandom for key generation\n"); return -1; } size_t bytes_read = fread(key_bytes, 1, len, fp); fclose(fp); if (bytes_read != len) { app_log(LOG_ERROR, "ERROR: Failed to read %zu bytes from /dev/urandom\n", len); return -1; } return 0; } // Generate server keypair and store in database int generate_server_keypair(void) { app_log(LOG_DEBUG, "DEBUG: generate_server_keypair() called\n"); unsigned char seckey_bytes[32]; char seckey_hex[65]; char pubkey_hex[65]; // Generate random private key app_log(LOG_DEBUG, "DEBUG: Generating random private key...\n"); if (generate_random_private_key_bytes(seckey_bytes, 32) != 0) { app_log(LOG_DEBUG, "DEBUG: Failed to generate random bytes\n"); return -1; } // Validate the private key if (nostr_ec_private_key_verify(seckey_bytes) != NOSTR_SUCCESS) { app_log(LOG_ERROR, "ERROR: Generated invalid private key\n"); return -1; } // Convert to hex nostr_bytes_to_hex(seckey_bytes, 32, seckey_hex); // Derive public key unsigned char pubkey_bytes[32]; if (nostr_ec_public_key_from_private_key(seckey_bytes, pubkey_bytes) != NOSTR_SUCCESS) { app_log(LOG_ERROR, "ERROR: Failed to derive public key\n"); return -1; } // Convert public key to hex nostr_bytes_to_hex(pubkey_bytes, 32, pubkey_hex); // Store pubkey in global variable strncpy(g_blossom_pubkey, pubkey_hex, sizeof(g_blossom_pubkey) - 1); g_blossom_pubkey[64] = '\0'; // Store seckey in global variable strncpy(g_blossom_seckey, seckey_hex, sizeof(g_blossom_seckey) - 1); g_blossom_seckey[64] = '\0'; // Set database path based on pubkey (MUST be done before storing keys) if (set_db_path_from_pubkey(pubkey_hex) != 0) { app_log(LOG_ERROR, "ERROR: Failed to set database path\n"); return -1; } // Initialize database with new path if (initialize_database(g_db_path) != 0) { app_log(LOG_ERROR, "ERROR: Failed to initialize database at %s\n", g_db_path); return -1; } // Store private key securely if (store_blossom_private_key(seckey_hex) != 0) { app_log(LOG_ERROR, "ERROR: Failed to store blossom private key\n"); return -1; } // Store public key in config sqlite3 *db; sqlite3_stmt *stmt; int rc; rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE, NULL); if (rc) { app_log(LOG_ERROR, "ERROR: Can't open database for config: %s\n", sqlite3_errmsg(db)); return -1; } const char *sql = "INSERT OR REPLACE INTO config (key, value, description) VALUES (?, ?, ?)"; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { app_log(LOG_ERROR, "ERROR: SQL prepare failed: %s\n", sqlite3_errmsg(db)); sqlite3_close(db); return -1; } sqlite3_bind_text(stmt, 1, "blossom_pubkey", -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, pubkey_hex, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 3, "Blossom server's public key for Nostr communication", -1, SQLITE_STATIC); rc = sqlite3_step(stmt); sqlite3_finalize(stmt); sqlite3_close(db); if (rc != SQLITE_DONE) { app_log(LOG_ERROR, "ERROR: Failed to store blossom public key in config\n"); return -1; } // Display keys for admin setup app_log(LOG_INFO, "========================================\n"); app_log(LOG_INFO, "SERVER KEYPAIR GENERATED SUCCESSFULLY\n"); app_log(LOG_INFO, "========================================\n"); app_log(LOG_INFO, "Blossom Public Key: %s\n", pubkey_hex); app_log(LOG_INFO, "Blossom Private Key: %s\n", seckey_hex); app_log(LOG_INFO, "Database Path: %s\n", g_db_path); app_log(LOG_INFO, "========================================\n"); app_log(LOG_WARN, "IMPORTANT: Save the private key securely!\n"); app_log(LOG_INFO, "This key is used for decrypting admin messages.\n"); app_log(LOG_INFO, "========================================\n"); return 0; } // Load server keys from database int load_server_keys(void) { app_log(LOG_DEBUG, "DEBUG: load_server_keys() called\n"); sqlite3 *db; sqlite3_stmt *stmt; int rc; // Try to load blossom private key app_log(LOG_DEBUG, "DEBUG: Trying to load blossom private key...\n"); if (get_blossom_private_key(g_blossom_seckey, sizeof(g_blossom_seckey)) != 0) { app_log(LOG_DEBUG, "DEBUG: No blossom private key found in database\n"); return -1; // Keys must exist in database } app_log(LOG_INFO, "STARTUP: Blossom private key loaded successfully\n"); // Derive public key from private key if (derive_pubkey_from_privkey(g_blossom_seckey, g_blossom_pubkey) != 0) { app_log(LOG_ERROR, "ERROR: Failed to derive public key from private key\n"); return -1; } app_log(LOG_INFO, "STARTUP: Derived blossom pubkey: %s\n", g_blossom_pubkey); // Validate database filename matches pubkey if (validate_database_pubkey_match(g_db_path, g_blossom_pubkey) != 0) { app_log(LOG_ERROR, "ERROR: Database pubkey validation failed\n"); return -1; } // Load admin pubkey from command line or config if (strlen(g_admin_pubkey) == 0) { // Try to load from database config rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL); if (rc == SQLITE_OK) { const char *sql = "SELECT value FROM config WHERE key = 'admin_pubkey'"; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) { const char *pubkey = (const char *)sqlite3_column_text(stmt, 0); if (pubkey && strlen(pubkey) == 64) { strncpy(g_admin_pubkey, pubkey, sizeof(g_admin_pubkey) - 1); app_log(LOG_INFO, "STARTUP: Admin pubkey loaded from config: %s\n", g_admin_pubkey); } } sqlite3_finalize(stmt); } sqlite3_close(db); } } else { // Store admin pubkey in config if provided via command line rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE, NULL); if (rc == SQLITE_OK) { const char *sql = "INSERT OR REPLACE INTO config (key, value, description) VALUES (?, ?, ?)"; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_text(stmt, 1, "admin_pubkey", -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, g_admin_pubkey, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 3, "Admin public key for management authentication", -1, SQLITE_STATIC); sqlite3_step(stmt); sqlite3_finalize(stmt); } sqlite3_close(db); } } return 0; } // Store blossom private key in dedicated table int store_blossom_private_key(const char *seckey) { sqlite3 *db; sqlite3_stmt *stmt; int rc; // Validate key format if (!seckey || strlen(seckey) != 64) { app_log(LOG_ERROR, "ERROR: Invalid blossom private key format\n"); return -1; } // Create blossom_seckey table if it doesn't exist rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL); if (rc) { app_log(LOG_ERROR, "ERROR: Can't open database: %s\n", sqlite3_errmsg(db)); return -1; } // Create table const char *create_sql = "CREATE TABLE IF NOT EXISTS blossom_seckey (id INTEGER PRIMARY KEY CHECK (id = 1), seckey TEXT NOT NULL, created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), CHECK (length(seckey) = 64))"; rc = sqlite3_exec(db, create_sql, NULL, NULL, NULL); if (rc != SQLITE_OK) { app_log(LOG_ERROR, "ERROR: Failed to create blossom_seckey table: %s\n", sqlite3_errmsg(db)); sqlite3_close(db); return -1; } // Store key const char *sql = "INSERT OR REPLACE INTO blossom_seckey (id, seckey) VALUES (1, ?)"; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { app_log(LOG_ERROR, "ERROR: SQL prepare failed: %s\n", sqlite3_errmsg(db)); sqlite3_close(db); return -1; } sqlite3_bind_text(stmt, 1, seckey, -1, SQLITE_STATIC); rc = sqlite3_step(stmt); sqlite3_finalize(stmt); sqlite3_close(db); if (rc != SQLITE_DONE) { app_log(LOG_ERROR, "ERROR: Failed to store blossom private key\n"); return -1; } return 0; } // Get blossom private key from database int get_blossom_private_key(char *seckey_out, size_t max_len) { sqlite3 *db; sqlite3_stmt *stmt; int rc; rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL); if (rc) { return -1; } const char *sql = "SELECT seckey FROM blossom_seckey WHERE id = 1 LIMIT 1"; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { sqlite3_close(db); return -1; } rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) { const char *seckey = (const char *)sqlite3_column_text(stmt, 0); if (seckey && strlen(seckey) == 64 && strlen(seckey) < max_len) { strcpy(seckey_out, seckey); sqlite3_finalize(stmt); sqlite3_close(db); return 0; } } sqlite3_finalize(stmt); sqlite3_close(db); return -1; } // Helper function to count filesystem blobs static int count_filesystem_blobs(long *total_count, long *total_size_bytes) { DIR *dir = opendir(g_storage_dir); if (!dir) { return -1; } *total_count = 0; *total_size_bytes = 0; struct dirent *entry; while ((entry = readdir(dir)) != NULL) { // Skip . and .. if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { continue; } // Build full path char filepath[MAX_PATH_LEN]; snprintf(filepath, sizeof(filepath), "%s/%s", g_storage_dir, entry->d_name); // Get file stats struct stat st; if (stat(filepath, &st) == 0 && S_ISREG(st.st_mode)) { (*total_count)++; *total_size_bytes += st.st_size; } } closedir(dir); return 0; } // Helper function to get memory usage in MB from /proc/self/status static long get_memory_usage_mb(void) { FILE *fp = fopen("/proc/self/status", "r"); if (!fp) { return -1; } char line[256]; long vmrss_kb = -1; while (fgets(line, sizeof(line), fp)) { if (strncmp(line, "VmRSS:", 6) == 0) { // Parse VmRSS value (in kB) char *p = line + 6; while (*p == ' ' || *p == '\t') p++; vmrss_kb = atol(p); break; } } fclose(fp); if (vmrss_kb > 0) { return vmrss_kb / 1024; // Convert kB to MB } return -1; } // Helper function to get CPU core static int get_cpu_core(void) { #ifdef __linux__ return sched_getcpu(); #else return -1; #endif } // Update system metrics in system table (key-value pairs) static int update_system_metrics(void) { sqlite3 *db; sqlite3_stmt *stmt; int rc; rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE, NULL); if (rc != SQLITE_OK) { return -1; } // Get system metrics int pid = getpid(); long memory_mb = get_memory_usage_mb(); int cpu_core = get_cpu_core(); long fs_blob_count = 0; long fs_blob_size = 0; count_filesystem_blobs(&fs_blob_count, &fs_blob_size); long fs_blob_size_mb = fs_blob_size / (1024 * 1024); // Prepare INSERT OR REPLACE statement for key-value updates const char *sql = "INSERT OR REPLACE INTO system (key, value, updated_at) VALUES (?, ?, strftime('%s', 'now'))"; // Update version rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_text(stmt, 1, "version", -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, VERSION, -1, SQLITE_STATIC); sqlite3_step(stmt); sqlite3_finalize(stmt); } // Update process_id rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { char pid_str[32]; snprintf(pid_str, sizeof(pid_str), "%d", pid); sqlite3_bind_text(stmt, 1, "process_id", -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, pid_str, -1, SQLITE_TRANSIENT); sqlite3_step(stmt); sqlite3_finalize(stmt); } // Update memory_mb if (memory_mb > 0) { rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { char mem_str[32]; snprintf(mem_str, sizeof(mem_str), "%ld", memory_mb); sqlite3_bind_text(stmt, 1, "memory_mb", -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, mem_str, -1, SQLITE_TRANSIENT); sqlite3_step(stmt); sqlite3_finalize(stmt); } } // Update cpu_core if (cpu_core >= 0) { rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { char core_str[32]; snprintf(core_str, sizeof(core_str), "%d", cpu_core); sqlite3_bind_text(stmt, 1, "cpu_core", -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, core_str, -1, SQLITE_TRANSIENT); sqlite3_step(stmt); sqlite3_finalize(stmt); } } // Update fs_blob_count rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { char count_str[32]; snprintf(count_str, sizeof(count_str), "%ld", fs_blob_count); sqlite3_bind_text(stmt, 1, "fs_blob_count", -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, count_str, -1, SQLITE_TRANSIENT); sqlite3_step(stmt); sqlite3_finalize(stmt); } // Update fs_blob_size_mb rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { char size_str[32]; snprintf(size_str, sizeof(size_str), "%ld", fs_blob_size_mb); sqlite3_bind_text(stmt, 1, "fs_blob_size_mb", -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, size_str, -1, SQLITE_TRANSIENT); sqlite3_step(stmt); sqlite3_finalize(stmt); } sqlite3_close(db); return 0; } // 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(g_db_path, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL); if (rc) { app_log(LOG_ERROR, "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) { app_log(LOG_ERROR, "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; rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY | SQLITE_OPEN_CREATE, NULL); if (rc) { app_log(LOG_ERROR, "Can't open database: %s\n", sqlite3_errmsg(db)); return 0; } const char *sql = "SELECT sha256, size, type, uploaded_at, filename FROM " "blobs WHERE sha256 = ?"; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { app_log(LOG_ERROR, "SQL error: %s\n", sqlite3_errmsg(db)); sqlite3_close(db); return 0; } sqlite3_bind_text(stmt, 1, sha256, -1, SQLITE_STATIC); rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) { 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; } else { metadata->found = 0; } sqlite3_finalize(stmt); sqlite3_close(db); 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 = mime_to_extension(mime_type); // Construct path safely size_t dir_len = strlen(g_storage_dir); size_t sha_len = strlen(sha256); size_t ext_len = strlen(extension); size_t total_len = dir_len + 1 + sha_len + ext_len + 1; // +1 for /, +1 for null if (total_len > sizeof(filepath)) { app_log(LOG_WARN, "WARNING: File path too long for buffer: %s/%s%s\n", g_storage_dir, sha256, extension); return 0; } // Build path manually to avoid compiler warnings memcpy(filepath, g_storage_dir, dir_len); filepath[dir_len] = '/'; memcpy(filepath + dir_len + 1, sha256, sha_len); memcpy(filepath + dir_len + 1 + sha_len, extension, ext_len); filepath[total_len - 1] = '\0'; struct stat st; int result = stat(filepath, &st); if (result == 0) { return 1; } else { return 0; } } // Handle HEAD request for blob void handle_head_request(const char *sha256) { blob_metadata_t metadata = {0}; // Validate SHA-256 format (64 hex characters) if (strlen(sha256) != 64) { printf("Status: 400 Bad Request\r\n"); printf("Content-Type: text/plain\r\n\r\n"); printf("Invalid SHA-256 hash format\n"); return; } // Check if blob exists in database - this is the single source of truth if (!get_blob_metadata(sha256, &metadata)) { printf("Status: 404 Not Found\r\n"); printf("Content-Type: text/plain\r\n\r\n"); printf("Blob not found\n"); return; } // 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-01 CORS Compliance System (Now handled by nginx) ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // 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 403: status_text = "Forbidden"; 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 app_log(LOG_INFO, "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/ requests void handle_list_request(const char *pubkey) { // 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) { // Parse since parameter const char *since_param = strstr(query_string, "since="); if (since_param) { since_timestamp = atol(since_param + 6); } // Parse until parameter const char *until_param = strstr(query_string, "until="); if (until_param) { until_timestamp = atol(until_param + 6); } } // Authentication is handled by centralized validation system // Check if auth header was provided to set appropriate status const char *auth_header = getenv("HTTP_AUTHORIZATION"); const char *auth_status = auth_header ? "authenticated" : "none"; // Query database for blobs uploaded by this pubkey sqlite3 *db; sqlite3_stmt *stmt; int rc; rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL); if (rc) { 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"); } rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc != SQLITE_OK) { 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); // Get origin from config for consistent URL generation char origin[256]; nip94_get_origin(origin, sizeof(origin)); // Build canonical blob URL using centralized function char blob_url[512]; nip94_build_blob_url(origin, sha256, type, blob_url, sizeof(blob_url)); // Output blob descriptor JSON printf(" {\n"); printf(" \"url\": \"%s\",\n", blob_url); 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); log_request("GET", "/list", auth_status, 200); } // Handle DELETE / requests with validation result void handle_delete_request_with_validation(const char *sha256, nostr_request_result_t *validation_result) { // 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; } } // Get authenticated pubkey from centralized validation result const char *auth_pubkey = NULL; if (validation_result && validation_result->valid && strlen(validation_result->pubkey) == 64) { auth_pubkey = validation_result->pubkey; } else { // No valid authentication - this should have been caught by centralized validation send_error_response(401, "unauthorized", "Authentication required for delete operations", "Valid authentication is required to delete blobs"); log_request("DELETE", "/delete", "unauthenticated", 401); return; } // Check if blob exists in database sqlite3 *db; sqlite3_stmt *stmt; int rc; rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL); if (rc) { 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) { 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); // 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); // Check ownership - only the uploader can delete if (!uploader_pubkey_copy[0] || strcmp(uploader_pubkey_copy, auth_pubkey) != 0) { 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 { } // 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) { 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) { send_error_response(500, "database_error", "Failed to delete blob metadata", "Internal server error"); log_request("DELETE", "/delete", "authenticated", 500); return; } // Determine file extension from MIME type and delete physical file const char *extension = mime_to_extension(blob_type_copy); char filepath[MAX_PATH_LEN]; // Construct path safely size_t dir_len = strlen(g_storage_dir); size_t sha_len = strlen(sha256); size_t ext_len = strlen(extension); size_t total_len = dir_len + 1 + sha_len + ext_len + 1; // +1 for /, +1 for null if (total_len > sizeof(filepath)) { app_log(LOG_WARN, "WARNING: File path too long for buffer: %s/%s%s\n", g_storage_dir, sha256, extension); // Continue anyway - unlink will fail gracefully } else { // Build path manually to avoid compiler warnings memcpy(filepath, g_storage_dir, dir_len); filepath[dir_len] = '/'; memcpy(filepath + dir_len + 1, sha256, sha_len); memcpy(filepath + dir_len + 1 + sha_len, extension, ext_len); filepath[total_len - 1] = '\0'; } // Delete the physical file if (unlink(filepath) != 0) { // Warning: failed to delete physical file } else { // Successfully deleted physical file } // 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"); 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"); // 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"); // 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) { 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]; 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); fflush(stderr); // Get dimensions from in-memory buffer before saving file int width = 0, height = 0; nip94_get_dimensions(file_data, content_length, content_type, &width, &height); // Determine file extension from Content-Type using centralized mapping const char *extension = mime_to_extension(content_type); // Save file to storage directory with SHA-256 + extension char filepath[MAX_PATH_LEN]; // Construct path safely size_t dir_len = strlen(g_storage_dir); size_t sha_len = strlen(sha256_hex); size_t ext_len = strlen(extension); size_t total_len = dir_len + 1 + sha_len + ext_len + 1; // +1 for /, +1 for null if (total_len > sizeof(filepath)) { app_log(LOG_WARN, "WARNING: File path too long for buffer: %s/%s%s\n", g_storage_dir, sha256_hex, extension); printf("Status: 500 Internal Server Error\r\n"); printf("Content-Type: text/plain\r\n\r\n"); printf("File path too long\n"); return; } // Build path manually to avoid compiler warnings memcpy(filepath, g_storage_dir, dir_len); filepath[dir_len] = '/'; memcpy(filepath + dir_len + 1, sha256_hex, sha_len); memcpy(filepath + dir_len + 1 + sha_len, extension, ext_len); filepath[total_len - 1] = '\0'; 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) { app_log(LOG_WARN, "WARNING: Failed to set file permissions for %s\r\n", filepath); // Continue anyway - this is not a fatal error } else { } 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; } // Extract filename from Content-Disposition header if present const char *filename = NULL; const char *content_disposition = getenv("HTTP_CONTENT_DISPOSITION"); if (content_disposition) { // Look for filename= in Content-Disposition header const char *filename_start = strstr(content_disposition, "filename="); if (filename_start) { filename_start += 9; // Skip "filename=" // Handle quoted filenames if (*filename_start == '"') { 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; if (filename_len < sizeof(filename_buffer)) { strncpy(filename_buffer, filename_start, filename_len); filename_buffer[filename_len] = '\0'; filename = filename_buffer; } else { } } else { } } else { // 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; if (filename_len < sizeof(filename_buffer)) { strncpy(filename_buffer, filename_start, filename_len); filename_buffer[filename_len] = '\0'; filename = filename_buffer; } else { } } } else { } } else { } // 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 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; } // Get origin from config char origin[256]; nip94_get_origin(origin, sizeof(origin)); // Build canonical blob URL char blob_url[512]; nip94_build_blob_url(origin, sha256_hex, content_type, blob_url, sizeof(blob_url)); // 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\": \"%s\"", blob_url); // Add NIP-94 metadata if enabled if (nip94_is_enabled()) { printf(",\n"); nip94_emit_field(blob_url, content_type, sha256_hex, content_length, width, height); } printf("\n}\n"); } // Handle PUT /upload requests with pre-validated file data from validator void handle_upload_request_with_validation(nostr_request_result_t* validation_result) { // 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"); // 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"); // Extract uploader pubkey from validation result const char *uploader_pubkey = NULL; if (validation_result && strlen(validation_result->pubkey) == 64) { uploader_pubkey = validation_result->pubkey; log_request("PUT", "/upload", "authenticated", 0); } else if (auth_header) { log_request("PUT", "/upload", "auth_provided", 0); } else { log_request("PUT", "/upload", "anonymous", 0); } // Use file data from validator if available, otherwise read from stdin unsigned char *file_data = NULL; size_t file_size = 0; int should_free_file_data = 0; if (validation_result && validation_result->file_data && validation_result->file_size > 0) { // Use file data provided by validator file_data = validation_result->file_data; file_size = validation_result->file_size; should_free_file_data = 0; // Validator owns the memory app_log(LOG_DEBUG, "UPLOAD: Using file data from validator (%zu bytes)\n", file_size); } else { // Fallback: read from stdin (for non-Blossom uploads or when validation didn't provide file data) 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) { 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; } file_size = bytes_read; should_free_file_data = 1; // We own the memory app_log(LOG_DEBUG, "UPLOAD: Read file data from stdin (%zu bytes)\n", file_size); } // Calculate SHA-256 hash using nostr_core function unsigned char hash[32]; if (nostr_sha256(file_data, file_size, hash) != NOSTR_SUCCESS) { if (should_free_file_data) 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); fflush(stderr); // Get dimensions from in-memory buffer before saving file int width = 0, height = 0; nip94_get_dimensions(file_data, file_size, content_type, &width, &height); // Determine file extension from Content-Type using centralized mapping const char *extension = mime_to_extension(content_type); // Save file to storage directory with SHA-256 + extension char filepath[MAX_PATH_LEN]; // Construct path safely size_t dir_len = strlen(g_storage_dir); size_t sha_len = strlen(sha256_hex); size_t ext_len = strlen(extension); size_t total_len = dir_len + 1 + sha_len + ext_len + 1; // +1 for /, +1 for null if (total_len > sizeof(filepath)) { app_log(LOG_WARN, "WARNING: File path too long for buffer: %s/%s%s\n", g_storage_dir, sha256_hex, extension); printf("Status: 500 Internal Server Error\r\n"); printf("Content-Type: text/plain\r\n\r\n"); printf("File path too long\n"); return; } // Build path manually to avoid compiler warnings memcpy(filepath, g_storage_dir, dir_len); filepath[dir_len] = '/'; memcpy(filepath + dir_len + 1, sha256_hex, sha_len); memcpy(filepath + dir_len + 1 + sha_len, extension, ext_len); filepath[total_len - 1] = '\0'; FILE *outfile = fopen(filepath, "wb"); if (!outfile) { if (should_free_file_data) 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, file_size, 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) { app_log(LOG_WARN, "WARNING: Failed to set file permissions for %s\r\n", filepath); // Continue anyway - this is not a fatal error } // Don't free file_data here if validator owns it - main() will handle cleanup if (should_free_file_data) { free(file_data); } if (bytes_written != file_size) { // 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; } // Extract filename from Content-Disposition header if present const char *filename = NULL; const char *content_disposition = getenv("HTTP_CONTENT_DISPOSITION"); if (content_disposition) { // Look for filename= in Content-Disposition header const char *filename_start = strstr(content_disposition, "filename="); if (filename_start) { filename_start += 9; // Skip "filename=" // Handle quoted filenames if (*filename_start == '"') { 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; if (filename_len < sizeof(filename_buffer)) { strncpy(filename_buffer, filename_start, filename_len); filename_buffer[filename_len] = '\0'; filename = filename_buffer; } } } else { // 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; if (filename_len < sizeof(filename_buffer)) { strncpy(filename_buffer, filename_start, filename_len); filename_buffer[filename_len] = '\0'; filename = filename_buffer; } } } } // Store blob metadata in database time_t uploaded_time = time(NULL); if (!insert_blob_metadata(sha256_hex, file_size, content_type, uploaded_time, uploader_pubkey, filename)) { // Database insertion failed - clean up the physical file to maintain // consistency 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; } // Update system metrics after successful blob upload update_system_metrics(); // Get origin from config char origin[256]; nip94_get_origin(origin, sizeof(origin)); // Build canonical blob URL char blob_url[512]; nip94_build_blob_url(origin, sha256_hex, content_type, blob_url, sizeof(blob_url)); // 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", (long)file_size); printf(" \"type\": \"%s\",\n", content_type); printf(" \"uploaded\": %ld,\n", uploaded_time); printf(" \"url\": \"%s\"", blob_url); // Add NIP-94 metadata if enabled if (nip94_is_enabled()) { printf(",\n"); nip94_emit_field(blob_url, content_type, sha256_hex, file_size, width, height); } printf("\n}\n"); } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // NIP-42 Authentication Support ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // OPTIONS Preflight Request Handler (BUD-01 CORS) ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // Handle OPTIONS requests for CORS preflight void handle_options_request(void) { log_request("OPTIONS", "*", "cors_preflight", 0); printf("Status: 200 OK\r\n"); printf("Content-Length: 0\r\n"); printf("\r\n"); // No body for OPTIONS response log_request("OPTIONS", "*", "cors_preflight", 200); } // Handle GET /auth requests to provide NIP-42 challenges void handle_auth_challenge_request(void) { // Log the incoming request log_request("GET", "/auth", "challenge_request", 0); // Use the validator's challenge generation system char challenge_buffer[65]; const char *client_ip = getenv("REMOTE_ADDR"); // Generate and store challenge using validator system int result = nostr_generate_nip42_challenge(challenge_buffer, sizeof(challenge_buffer), client_ip); if (result != NOSTR_SUCCESS) { printf("Status: 500 Internal Server Error\r\n"); printf("Content-Type: application/json\r\n\r\n"); printf("{\n"); printf(" \"error\": \"challenge_generation_failed\",\n"); printf(" \"message\": \"Failed to generate authentication challenge\"\n"); printf("}\n"); log_request("GET", "/auth", "challenge_failed", 500); return; } // Calculate expiration (current time + challenge timeout) // Default timeout is 600 seconds (10 minutes) as per NIP-42 challenge manager time_t expires_at = time(NULL) + 600; // Return the challenge as JSON printf("Status: 200 OK\r\n"); printf("Content-Type: application/json\r\n\r\n"); printf("{\n"); printf(" \"challenge\": \"%s\",\n", challenge_buffer); printf(" \"relay\": \"ginxsom\",\n"); printf(" \"expires\": %ld\n", expires_at); printf("}\n"); log_request("GET", "/auth", "challenge_generated", 200); } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // MAIN ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// int main(int argc, char *argv[]) { // Initialize application logging app_log(LOG_INFO, "=== Ginxsom FastCGI Application Starting ==="); app_log(LOG_INFO, "Process ID: %d", getpid()); g_server_start_time = time(NULL); // Parse command line arguments int use_test_keys = 0; char test_server_privkey[65] = ""; char specified_db_path[MAX_PATH_LEN] = ""; int db_path_specified = 0; for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "--db-path") == 0 && i + 1 < argc) { strncpy(specified_db_path, argv[i + 1], sizeof(specified_db_path) - 1); db_path_specified = 1; i++; // Skip next argument } else if (strcmp(argv[i], "--storage-dir") == 0 && i + 1 < argc) { strncpy(g_storage_dir, argv[i + 1], sizeof(g_storage_dir) - 1); i++; // Skip next argument } else if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) { // Use write() directly to avoid FCGI printf redefinition const char *help_text = "Usage: ginxsom-fcgi [options]\n" "Options:\n" " --db-path PATH Database file path (must match pubkey if keys exist)\n" " --storage-dir DIR Storage directory for files (default: .)\n" " --admin-pubkey KEY Admin public key (only used when creating new database)\n" " --server-privkey KEY Server private key (creates new DB or validates existing)\n" " --test-keys Use test keys from .test_keys file\n" " --generate-keys Generate new keypair and create database (deprecated)\n" " --help, -h Show this help message\n" "\n" "Database Naming:\n" " Databases are named after the server's public key: db/.db\n" " This ensures database-key consistency and prevents mismatches.\n" "\n" "Startup Scenarios:\n" " 1. No arguments → Generate new keys, create db/.db\n" " 2. --db-path → Open existing database, validate keys match filename\n" " 3. --server-privkey → Create new database with those keys\n" " 4. --test-keys → Use test keys, create/overwrite test database\n" " 5. --db-path + --server-privkey → Validate keys match or error\n"; ssize_t written = write(STDOUT_FILENO, help_text, strlen(help_text)); (void)written; // Suppress unused variable warning return 0; } else if (strcmp(argv[i], "--admin-pubkey") == 0 && i + 1 < argc) { strncpy(g_admin_pubkey, argv[i + 1], sizeof(g_admin_pubkey) - 1); i++; // Skip next argument } else if (strcmp(argv[i], "--server-privkey") == 0 && i + 1 < argc) { strncpy(test_server_privkey, argv[i + 1], sizeof(test_server_privkey) - 1); i++; // Skip next argument } else if (strcmp(argv[i], "--test-keys") == 0) { use_test_keys = 1; } else if (strcmp(argv[i], "--generate-keys") == 0) { g_generate_keys = 1; app_log(LOG_WARN, "WARNING: --generate-keys is deprecated. Keys are generated automatically when needed.\n"); } else { app_log(LOG_ERROR, "Unknown option: %s\n", argv[i]); app_log(LOG_ERROR, "Use --help for usage information\n"); return 1; } } app_log(LOG_INFO, "Storage directory: %s", g_storage_dir); // Initialize nostr core system BEFORE key operations and relay activity app_log(LOG_INFO, "Initializing nostr core system..."); int nostr_init_result = nostr_init(); if (nostr_init_result != NOSTR_SUCCESS) { app_log(LOG_ERROR, "Failed to initialize nostr core system (result: %d)", nostr_init_result); return 1; } nostr_set_log_callback(nostr_log_cb, NULL); nostr_set_log_level(app_log_level_to_nostr(g_log_level)); app_log(LOG_INFO, "Nostr core system initialized successfully"); // ======================================================================== // DATABASE AND KEY INITIALIZATION - 5 SCENARIOS // ======================================================================== // Scenario 4: Test Mode (--test-keys) if (use_test_keys) { app_log(LOG_INFO, "=== SCENARIO 4: TEST MODE ==="); // Load test keys from .test_keys file FILE *keys_file = fopen(".test_keys", "r"); if (!keys_file) { app_log(LOG_ERROR, "Cannot open .test_keys file"); return 1; } char line[256]; while (fgets(line, sizeof(line), keys_file)) { if (strncmp(line, "ADMIN_PUBKEY='", 14) == 0) { char *start = line + 14; char *end = strchr(start, '\''); if (end && (end - start) == 64) { strncpy(g_admin_pubkey, start, 64); g_admin_pubkey[64] = '\0'; } } else if (strncmp(line, "SERVER_PRIVKEY='", 16) == 0) { char *start = line + 16; char *end = strchr(start, '\''); if (end && (end - start) == 64) { strncpy(test_server_privkey, start, 64); test_server_privkey[64] = '\0'; app_log(LOG_DEBUG, "Parsed SERVER_PRIVKEY from .test_keys"); } else { app_log(LOG_ERROR, "Failed to parse SERVER_PRIVKEY (length: %ld)", end ? (long)(end - start) : -1L); } } } fclose(keys_file); app_log(LOG_INFO, "Loaded keys from .test_keys"); app_log(LOG_INFO, "Admin pubkey: %s", g_admin_pubkey); // Derive pubkey from test privkey if (derive_pubkey_from_privkey(test_server_privkey, g_blossom_pubkey) != 0) { app_log(LOG_ERROR, "Failed to derive pubkey from test privkey"); return 1; } app_log(LOG_INFO, "Server pubkey: %s", g_blossom_pubkey); // Set database path based on test pubkey if (set_db_path_from_pubkey(g_blossom_pubkey) != 0) { app_log(LOG_ERROR, "Failed to set database path"); return 1; } // Test mode ALWAYS overwrites database for clean testing app_log(LOG_INFO, "Creating/overwriting test database: %s", g_db_path); unlink(g_db_path); // Remove if exists // Initialize new database if (initialize_database(g_db_path) != 0) { app_log(LOG_ERROR, "Failed to initialize test database"); return 1; } // Store test keys strncpy(g_blossom_seckey, test_server_privkey, sizeof(g_blossom_seckey) - 1); g_blossom_seckey[64] = '\0'; if (store_blossom_private_key(test_server_privkey) != 0) { app_log(LOG_ERROR, "Failed to store test private key"); return 1; } // Store pubkey and admin pubkey in config sqlite3 *db; sqlite3_stmt *stmt; int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE, NULL); if (rc == SQLITE_OK) { const char *sql = "INSERT OR REPLACE INTO config (key, value, description) VALUES (?, ?, ?)"; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_text(stmt, 1, "blossom_pubkey", -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, g_blossom_pubkey, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 3, "Blossom server's public key (TEST MODE)", -1, SQLITE_STATIC); sqlite3_step(stmt); sqlite3_finalize(stmt); } if (strlen(g_admin_pubkey) > 0) { rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_text(stmt, 1, "admin_pubkey", -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, g_admin_pubkey, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 3, "Admin public key (TEST MODE)", -1, SQLITE_STATIC); sqlite3_step(stmt); sqlite3_finalize(stmt); } } sqlite3_close(db); } app_log(LOG_INFO, "Test database initialized successfully"); } // Scenario 3: Keys Specified (--server-privkey) else if (test_server_privkey[0] != '\0') { app_log(LOG_INFO, "=== SCENARIO 3: KEYS SPECIFIED ==="); // Derive pubkey from provided privkey if (derive_pubkey_from_privkey(test_server_privkey, g_blossom_pubkey) != 0) { app_log(LOG_ERROR, "ERROR: Invalid server private key\n"); return 1; } app_log(LOG_INFO, "KEYS: Derived pubkey: %s\n", g_blossom_pubkey); // Scenario 5: Both database path and keys specified if (db_path_specified) { app_log(LOG_INFO, "\n=== SCENARIO 5: DATABASE PATH + KEYS ===\n"); // Check if specified path is a directory or file struct stat st; int is_directory = 0; if (stat(specified_db_path, &st) == 0) { is_directory = S_ISDIR(st.st_mode); } else { // Path doesn't exist - assume it's meant to be a directory is_directory = (specified_db_path[strlen(specified_db_path) - 1] == '/' || strstr(specified_db_path, ".db") == NULL); } if (is_directory) { // Build database path from directory + derived pubkey snprintf(g_db_path, sizeof(g_db_path), "%s/%s.db", specified_db_path, g_blossom_pubkey); app_log(LOG_INFO, "DATABASE: Using directory path, derived database: %s\n", g_db_path); } else { // Use specified file path directly strncpy(g_db_path, specified_db_path, sizeof(g_db_path) - 1); g_db_path[sizeof(g_db_path) - 1] = '\0'; app_log(LOG_INFO, "DATABASE: Using file path: %s\n", g_db_path); } // Check if database exists if (stat(g_db_path, &st) == 0) { // Database exists - validate keys match app_log(LOG_INFO, "DATABASE: Found existing database, validating keys...\n"); // Load keys from database if (get_blossom_private_key(g_blossom_seckey, sizeof(g_blossom_seckey)) != 0) { app_log(LOG_ERROR, "ERROR: Invalid database: missing server keys\n"); return 1; } // Compare with provided key if (strcmp(g_blossom_seckey, test_server_privkey) != 0) { app_log(LOG_ERROR, "ERROR: Server private key doesn't match database\n"); app_log(LOG_ERROR, " Provided key and database keys are different\n"); return 1; } app_log(LOG_INFO, "VALIDATION: Keys match database - continuing\n"); // Validate pubkey matches filename if (validate_database_pubkey_match(g_db_path, g_blossom_pubkey) != 0) { return 1; } } else { // Database doesn't exist - create it with provided keys app_log(LOG_INFO, "DATABASE: No existing database, creating new one...\n"); // Initialize new database if (initialize_database(g_db_path) != 0) { app_log(LOG_ERROR, "ERROR: Failed to initialize database\n"); return 1; } // Store keys strncpy(g_blossom_seckey, test_server_privkey, sizeof(g_blossom_seckey) - 1); g_blossom_seckey[64] = '\0'; if (store_blossom_private_key(test_server_privkey) != 0) { app_log(LOG_ERROR, "ERROR: Failed to store private key\n"); return 1; } // Store pubkey in config sqlite3 *db; sqlite3_stmt *stmt; int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE, NULL); if (rc == SQLITE_OK) { const char *sql = "INSERT OR REPLACE INTO config (key, value, description) VALUES (?, ?, ?)"; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_text(stmt, 1, "blossom_pubkey", -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, g_blossom_pubkey, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 3, "Blossom server's public key", -1, SQLITE_STATIC); sqlite3_step(stmt); sqlite3_finalize(stmt); } if (strlen(g_admin_pubkey) > 0) { rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_text(stmt, 1, "admin_pubkey", -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, g_admin_pubkey, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 3, "Admin public key", -1, SQLITE_STATIC); sqlite3_step(stmt); sqlite3_finalize(stmt); } } sqlite3_close(db); } app_log(LOG_INFO, "DATABASE: New database created successfully\n"); } } // Scenario 3 continued: Create new database with provided keys else { // Set database path based on derived pubkey if (set_db_path_from_pubkey(g_blossom_pubkey) != 0) { app_log(LOG_ERROR, "ERROR: Failed to set database path\n"); return 1; } // Check if database already exists struct stat st; if (stat(g_db_path, &st) == 0) { app_log(LOG_ERROR, "ERROR: Database already exists for this pubkey: %s\n", g_db_path); app_log(LOG_ERROR, " Use --db-path to open existing database or use different keys\n"); return 1; } // Initialize new database if (initialize_database(g_db_path) != 0) { app_log(LOG_ERROR, "ERROR: Failed to initialize database\n"); return 1; } // Store keys strncpy(g_blossom_seckey, test_server_privkey, sizeof(g_blossom_seckey) - 1); g_blossom_seckey[64] = '\0'; if (store_blossom_private_key(test_server_privkey) != 0) { app_log(LOG_ERROR, "ERROR: Failed to store private key\n"); return 1; } // Store pubkey in config sqlite3 *db; sqlite3_stmt *stmt; int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE, NULL); if (rc == SQLITE_OK) { const char *sql = "INSERT OR REPLACE INTO config (key, value, description) VALUES (?, ?, ?)"; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_text(stmt, 1, "blossom_pubkey", -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, g_blossom_pubkey, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 3, "Blossom server's public key", -1, SQLITE_STATIC); sqlite3_step(stmt); sqlite3_finalize(stmt); } if (strlen(g_admin_pubkey) > 0) { rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_text(stmt, 1, "admin_pubkey", -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, g_admin_pubkey, -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 3, "Admin public key", -1, SQLITE_STATIC); sqlite3_step(stmt); sqlite3_finalize(stmt); } } sqlite3_close(db); } app_log(LOG_INFO, "KEYS: New database created successfully\n"); } } // Scenario 2: Database Path Specified (--db-path) // Note: --db-path should specify a DIRECTORY, not a full file path // The actual database filename will be derived from the server's pubkey else if (db_path_specified) { app_log(LOG_INFO, "\n=== SCENARIO 2: DATABASE DIRECTORY SPECIFIED ===\n"); // Check if specified path is a directory or file struct stat st; int is_directory = 0; if (stat(specified_db_path, &st) == 0) { is_directory = S_ISDIR(st.st_mode); } else { // Path doesn't exist - assume it's meant to be a directory is_directory = (specified_db_path[strlen(specified_db_path) - 1] == '/' || strstr(specified_db_path, ".db") == NULL); } if (is_directory) { // Treat as directory - will derive filename from pubkey after loading keys app_log(LOG_INFO, "DATABASE: Directory specified: %s\n", specified_db_path); app_log(LOG_INFO, "DATABASE: Will derive filename from server pubkey\n"); // Look for any .db file that matches the pubkey pattern DIR *dir = opendir(specified_db_path); int found_db = 0; if (dir) { struct dirent *entry; while ((entry = readdir(dir)) != NULL) { // Check if filename matches pattern: <64-hex-chars>.db size_t name_len = strlen(entry->d_name); if (name_len == 67 && strcmp(entry->d_name + 64, ".db") == 0) { // Found a potential database file snprintf(g_db_path, sizeof(g_db_path), "%s/%s", specified_db_path, entry->d_name); found_db = 1; app_log(LOG_INFO, "DATABASE: Found existing database: %s\n", g_db_path); break; } } closedir(dir); } if (!found_db) { // No database found - this is OK, we'll create one if we have keys app_log(LOG_INFO, "DATABASE: No existing database found in directory\n"); // g_db_path will be set later based on pubkey } } else { // Treat as full file path (legacy behavior) strncpy(g_db_path, specified_db_path, sizeof(g_db_path) - 1); g_db_path[sizeof(g_db_path) - 1] = '\0'; } // If we found a database file, try to load it if (g_db_path[0] != '\0' && stat(g_db_path, &st) == 0) { app_log(LOG_INFO, "DATABASE: Opening existing database: %s\n", g_db_path); // Load keys from database if (load_server_keys() != 0) { app_log(LOG_ERROR, "ERROR: Failed to load keys from database\n"); app_log(LOG_ERROR, " → Database may be corrupted or not a valid ginxsom database\n"); return 1; } app_log(LOG_INFO, "DATABASE: Keys loaded and validated successfully\n"); } else { // No database file exists - we need keys to create one app_log(LOG_ERROR, "ERROR: No database found and no --server-privkey provided\n"); app_log(LOG_ERROR, " → Use --server-privkey to create a new database\n"); return 1; } } // Scenario 1: No Arguments (Fresh Start) else { app_log(LOG_INFO, "\n=== SCENARIO 1: FRESH START (NO ARGUMENTS) ===\n"); app_log(LOG_INFO, "FRESH START: Generating new server keypair...\n"); // Generate new keypair (this will set g_db_path based on pubkey) if (generate_server_keypair() != 0) { app_log(LOG_ERROR, "ERROR: Failed to generate server keypair\n"); return 1; } app_log(LOG_INFO, "FRESH START: New instance created successfully\n"); } // ======================================================================== // END DATABASE AND KEY INITIALIZATION // ======================================================================== app_log(LOG_INFO, "=== FINAL CONFIGURATION ==="); app_log(LOG_INFO, "Database path: %s", g_db_path); app_log(LOG_INFO, "Storage directory: %s", g_storage_dir); app_log(LOG_INFO, "Server pubkey: %s", g_blossom_pubkey); if (strlen(g_admin_pubkey) > 0) { app_log(LOG_INFO, "Admin pubkey: %s", g_admin_pubkey); } app_log(LOG_INFO, "==========================="); // If --generate-keys was specified, exit after key generation if (g_generate_keys) { app_log(LOG_INFO, "Key generation completed, exiting"); return 0; } // Initialize server configuration and identity // Try file-based config first, then fall back to database config int config_loaded = 0; // Fall back to database configuration if file config failed if (!config_loaded /* && !initialize_server_config() */) { app_log(LOG_WARN, "STARTUP: No configuration found - server starting in setup mode"); app_log(LOG_WARN, "STARTUP: Run interactive setup with: ginxsom --setup"); // For interactive mode (when stdin is available), offer setup } else if (!config_loaded) { app_log(LOG_INFO, "STARTUP: Database configuration loaded successfully"); } // Initialize request validator system app_log(LOG_INFO, "Initializing request validator system..."); int validator_init_result = ginxsom_request_validator_init(g_db_path, "ginxsom"); if (validator_init_result != NOSTR_SUCCESS) { app_log(LOG_ERROR, "Failed to initialize request validator system (result: %d)", validator_init_result); return 1; } app_log(LOG_INFO, "Request validator system initialized successfully"); // Initialize relay client system app_log(LOG_INFO, "Initializing relay client system..."); int relay_init_result = relay_client_init(g_db_path); if (relay_init_result != 0) { app_log(LOG_WARN, "Failed to initialize relay client system (result: %d)", relay_init_result); app_log(LOG_WARN, "Continuing without relay client functionality"); } else { app_log(LOG_INFO, "Relay client system initialized successfully"); // Start relay connections (this will check enable_relay_connect config) app_log(LOG_INFO, "Starting relay client connections..."); int relay_start_result = relay_client_start(); if (relay_start_result != 0) { app_log(LOG_WARN, "Failed to start relay client (result: %d)", relay_start_result); app_log(LOG_WARN, "Relay client disabled - check configuration"); } else { app_log(LOG_INFO, "Relay client started successfully"); } } // Initialize admin commands system app_log(LOG_INFO, "Initializing admin commands system..."); int admin_cmd_result = admin_commands_init(g_db_path); if (admin_cmd_result != 0) { app_log(LOG_WARN, "Failed to initialize admin commands system (result: %d)", admin_cmd_result); app_log(LOG_WARN, "Continuing without admin commands functionality"); } else { app_log(LOG_INFO, "Admin commands system initialized successfully"); } // Initialize system metrics at startup app_log(LOG_INFO, "Initializing system metrics..."); if (update_system_metrics() == 0) { app_log(LOG_INFO, "System metrics initialized successfully"); } else { app_log(LOG_WARN, "Failed to initialize system metrics"); } ///////////////////////////////////////////////////////////////////// // THIS IS WHERE THE REQUESTS ENTER THE FastCGI ///////////////////////////////////////////////////////////////////// app_log(LOG_INFO, "FastCGI request loop starting - ready to accept requests"); // Register signal handlers for graceful shutdown signal(SIGTERM, shutdown_handler); signal(SIGINT, shutdown_handler); int first_request = 1; while (FCGI_Accept() >= 0 && !g_shutdown_requested) { if (first_request) { app_log(LOG_DEBUG, "FCGI: First request received"); first_request = 0; } const char *request_method = getenv("REQUEST_METHOD"); const char *request_uri = getenv("REQUEST_URI"); const char *auth_header = getenv("HTTP_AUTHORIZATION"); 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 OPTIONS PREFLIGHT REQUESTS (BUD-01 CORS) ///////////////////////////////////////////////////////////////////// if (strcmp(request_method, "OPTIONS") == 0) { handle_options_request(); continue; } ///////////////////////////////////////////////////////////////////// // CENTRALIZED REQUEST VALIDATION SYSTEM ///////////////////////////////////////////////////////////////////// // Special case: Root endpoint is public and doesn't require authentication if (strcmp(request_method, "GET") == 0 && strcmp(request_uri, "/") == 0) { // Handle GET / requests - Server info endpoint (NIP-11) printf("Status: 200 OK\r\n"); printf("Content-Type: application/nostr+json\r\n\r\n"); printf("{\n"); printf(" \"server\": \"ginxsom\",\n"); printf(" \"version\": \"%s\",\n", VERSION); printf(" \"description\": \"Ginxsom Blossom Server\",\n"); printf(" \"pubkey\": \"%s\",\n", g_blossom_pubkey); printf(" \"endpoints\": {\n"); printf(" \"blob_get\": \"GET /\",\n"); printf(" \"blob_head\": \"HEAD /\",\n"); printf(" \"upload\": \"PUT /upload\",\n"); printf(" \"upload_requirements\": \"HEAD /upload\",\n"); printf(" \"list\": \"GET /list/\",\n"); printf(" \"delete\": \"DELETE /\",\n"); printf(" \"mirror\": \"PUT /mirror\",\n"); printf(" \"report\": \"PUT /report\",\n"); printf(" \"health\": \"GET /health\",\n"); printf(" \"auth\": \"GET /auth\"\n"); printf(" },\n"); printf(" \"supported_buds\": [\n"); printf(" \"BUD-01\",\n"); printf(" \"BUD-02\",\n"); printf(" \"BUD-04\",\n"); printf(" \"BUD-06\",\n"); printf(" \"BUD-08\",\n"); printf(" \"BUD-09\"\n"); printf(" ],\n"); printf(" \"limits\": {\n"); printf(" \"max_upload_size\": 104857600,\n"); printf(" \"supported_mime_types\": [\n"); size_t supported_mime_count = 0; const char* const* supported_mimes = ginxsom_supported_mime_types(&supported_mime_count); for (size_t i = 0; i < supported_mime_count; i++) { printf(" \"%s\"%s\n", supported_mimes[i], (i + 1 < supported_mime_count) ? "," : ""); } printf(" ]\n"); printf(" },\n"); printf(" \"authentication\": {\n"); printf(" \"required_for_upload\": false,\n"); printf(" \"required_for_delete\": true,\n"); printf(" \"required_for_list\": false,\n"); printf(" \"nip42_enabled\": true\n"); printf(" }\n"); printf("}\n"); log_request("GET", "/", "server_info", 200); continue; } // Determine operation from request method and URI const char *operation = "unknown"; const char *resource_hash = NULL; if (strcmp(request_method, "HEAD") == 0 && strcmp(request_uri, "/upload") == 0) { operation = "head_upload"; } else if (strcmp(request_method, "HEAD") == 0) { operation = "head"; resource_hash = extract_sha256_from_uri(request_uri); } else if (strcmp(request_method, "PUT") == 0 && strcmp(request_uri, "/upload") == 0) { operation = "upload"; } else if (strcmp(request_method, "PUT") == 0 && strcmp(request_uri, "/mirror") == 0) { operation = "mirror"; } else if (strcmp(request_method, "PUT") == 0 && strcmp(request_uri, "/report") == 0) { operation = "report"; } else if (strncmp(request_uri, "/admin", 6) == 0) { operation = "admin_interface"; // Public static files - no auth required } else if (strncmp(request_uri, "/api/", 5) == 0) { // Check if this is a static file request or API request const char *path = request_uri + 5; // Skip "/api/" int is_static_file = 0; // Check for static file extensions or root /api path if (strstr(path, ".html") || strstr(path, ".css") || strstr(path, ".js") || strlen(path) == 0 || strcmp(path, "/") == 0) { is_static_file = 1; } if (is_static_file) { operation = "admin_interface"; // Public static files - no auth required } else { operation = "admin"; // Special case: POST /api/admin uses Kind 23458 events for authentication // Skip centralized validation for these requests if (strcmp(request_method, "POST") == 0 && strcmp(request_uri, "/api/admin") == 0) { operation = "admin_event"; // Mark as special case } } } else if (strcmp(request_method, "GET") == 0 && strncmp(request_uri, "/list/", 6) == 0) { operation = "list"; } else if (strcmp(request_method, "GET") == 0 && strcmp(request_uri, "/auth") == 0) { operation = "challenge"; } else if (strcmp(request_method, "GET") == 0 && strcmp(request_uri, "/health") == 0) { operation = "health"; } else if (strcmp(request_method, "DELETE") == 0) { operation = "delete"; resource_hash = extract_sha256_from_uri(request_uri); } // Declare result structure nostr_request_result_t result; // Create unified request structure nostr_unified_request_t request = { .operation = operation, .auth_header = auth_header, .event = NULL, .resource_hash = resource_hash, .mime_type = getenv("CONTENT_TYPE"), .file_size = getenv("CONTENT_LENGTH") ? atol(getenv("CONTENT_LENGTH")) : 0, .request_url = "ginxsom", .challenge_id = NULL, // Validator will extract from NIP-42 event tags .nip42_enabled = 1, // Let validator check actual config .client_ip = getenv("REMOTE_ADDR"), .app_context = NULL }; // Validate the request through unified system int validation_result = nostr_validate_unified_request(&request, &result); // Handle validation failures (except for certain cases) if (validation_result != NOSTR_SUCCESS || !result.valid) { // Special case: challenge generation failure should be handled by the endpoint if (strcmp(operation, "challenge") == 0) { // Let the /auth endpoint handle this - it will generate its own error response } else if (strcmp(operation, "admin_interface") == 0) { // Admin interface serves public static files - no auth required } else if (strcmp(operation, "head") == 0 || strcmp(operation, "head_upload") == 0) { // HEAD requests might not require auth depending on config - let handler decide } else if (strcmp(operation, "list") == 0) { // List operation might be optional auth - let handler decide } else if (strcmp(operation, "health") == 0) { // Health endpoint is public and doesn't require authentication - let handler decide } else if (strcmp(operation, "admin") == 0 && strcmp(request_uri, "/api/health") == 0) { // Health endpoint is public and doesn't require authentication - let handler decide } else if (strcmp(operation, "admin_event") == 0) { // POST /api/admin uses Kind 23458 events - authentication handled by admin_event.c // Skip centralized validation and let the handler validate the event } else { // For other operations, validation failure means auth failure const char *message = result.reason[0] ? result.reason : "Authentication failed"; const char *details = "Authentication validation failed"; // Determine appropriate status code based on violation type int status_code = 401; // Default: Unauthorized (no auth or invalid auth) const char *violation_type = nostr_request_validator_get_last_violation_type(); // If auth rules denied the request, use 403 Forbidden instead of 401 Unauthorized if (violation_type && ( strcmp(violation_type, "pubkey_blacklist") == 0 || strcmp(violation_type, "hash_blacklist") == 0 || strcmp(violation_type, "whitelist_violation") == 0 || strcmp(violation_type, "mime_blacklist") == 0 || strcmp(violation_type, "mime_whitelist_violation") == 0)) { status_code = 403; // Forbidden: authenticated but not authorized } // Always include event JSON in details when auth header is provided for debugging if (auth_header) { // Parse event JSON from auth header to show in details for debugging static char event_json[8192]; // Increased buffer size if (strncasecmp(auth_header, "nostr ", 6) == 0) { // Decode base64 to get event JSON const char *base64_event = auth_header + 6; unsigned char decoded_buffer[8192]; // Increased buffer size size_t decoded_len = base64_decode(base64_event, decoded_buffer); if (decoded_len > 0 && decoded_len < sizeof(event_json)) { memcpy(event_json, decoded_buffer, decoded_len); event_json[decoded_len] = '\0'; details = event_json; // Use the event JSON as details for debugging } } } send_error_response(status_code, "authentication_failed", message, details); log_request(request_method, request_uri, "auth_failed", status_code); continue; } } ///////////////////////////////////////////////////////////////////// // ROUTE THE NOW VALID REQUEST TO HANDLERS ///////////////////////////////////////////////////////////////////// // Route HEAD /upload pre-flight (BUD-06) before generic HEAD blob handler if (strcmp(request_method, "HEAD") == 0 && strcmp(request_uri, "/upload") == 0) { // Handle HEAD /upload requests (BUD-06 pre-flight validation) handle_head_upload_request(); } else if (strcmp(request_method, "HEAD") == 0) { // Handle HEAD requests for blob metadata const char *sha256 = extract_sha256_from_uri(request_uri); if (sha256) { handle_head_request(sha256); log_request("HEAD", request_uri, "public", 200); } 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 pre-validated auth handle_upload_request_with_validation(&result); // Clean up file data allocated by validator nostr_request_result_free_file_data(&result); } else if (strcmp(request_method, "PUT") == 0 && strcmp(request_uri, "/mirror") == 0) { // Handle PUT /mirror requests (BUD-04) handle_mirror_request(); } else if (strcmp(request_method, "PUT") == 0 && strcmp(request_uri, "/report") == 0) { // Handle PUT /report requests (BUD-09) handle_report_request(); } else if (strcmp(request_method, "POST") == 0 && strcmp(request_uri, "/api/admin") == 0) { // Handle POST /api/admin requests (Kind 23458 admin events) handle_admin_event_request(); } else if (strncmp(request_uri, "/admin", 6) == 0) { // Handle admin web interface requests (embedded files) handle_admin_interface_request(request_uri); } else if (strncmp(request_uri, "/api/", 5) == 0) { // Check if this is a static file request (no auth required) or API request (auth required) const char *path = request_uri + 5; // Skip "/api/" int is_static_file = 0; // Check for static file extensions if (strstr(path, ".html") || strstr(path, ".css") || strstr(path, ".js") || strcmp(request_uri, "/api") == 0 || strcmp(request_uri, "/api/") == 0) { is_static_file = 1; } if (is_static_file) { // Serve static files without authentication handle_admin_interface_request(request_uri); } else { // Handle admin API requests with pre-validated auth const char *validated_pubkey = (result.valid && strlen(result.pubkey) == 64) ? result.pubkey : NULL; handle_admin_api_request(request_method, request_uri, validated_pubkey, result.valid); } } else if (strcmp(request_method, "GET") == 0 && strncmp(request_uri, "/list/", 6) == 0) { // Handle GET /list/ requests with pre-validated auth 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'; // TODO: Pass validated result to existing handler 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, "GET") == 0 && strcmp(request_uri, "/") == 0) { // Handle GET / requests - Server info endpoint (NIP-11) printf("Status: 200 OK\r\n"); printf("Content-Type: application/nostr+json\r\n\r\n"); printf("{\n"); printf(" \"server\": \"ginxsom\",\n"); printf(" \"version\": \"%s\",\n", VERSION); printf(" \"description\": \"Ginxsom Blossom Server\",\n"); printf(" \"pubkey\": \"%s\",\n", g_blossom_pubkey); printf(" \"endpoints\": {\n"); printf(" \"blob_get\": \"GET /\",\n"); printf(" \"blob_head\": \"HEAD /\",\n"); printf(" \"upload\": \"PUT /upload\",\n"); printf(" \"upload_requirements\": \"HEAD /upload\",\n"); printf(" \"list\": \"GET /list/\",\n"); printf(" \"delete\": \"DELETE /\",\n"); printf(" \"mirror\": \"PUT /mirror\",\n"); printf(" \"report\": \"PUT /report\",\n"); printf(" \"health\": \"GET /health\",\n"); printf(" \"auth\": \"GET /auth\"\n"); printf(" },\n"); printf(" \"supported_buds\": [\n"); printf(" \"BUD-01\",\n"); printf(" \"BUD-02\",\n"); printf(" \"BUD-04\",\n"); printf(" \"BUD-06\",\n"); printf(" \"BUD-08\",\n"); printf(" \"BUD-09\"\n"); printf(" ],\n"); printf(" \"limits\": {\n"); printf(" \"max_upload_size\": 104857600,\n"); printf(" \"supported_mime_types\": [\n"); size_t supported_mime_count = 0; const char* const* supported_mimes = ginxsom_supported_mime_types(&supported_mime_count); for (size_t i = 0; i < supported_mime_count; i++) { printf(" \"%s\"%s\n", supported_mimes[i], (i + 1 < supported_mime_count) ? "," : ""); } printf(" ]\n"); printf(" },\n"); printf(" \"authentication\": {\n"); printf(" \"required_for_upload\": false,\n"); printf(" \"required_for_delete\": true,\n"); printf(" \"required_for_list\": false,\n"); printf(" \"nip42_enabled\": true\n"); printf(" }\n"); printf("}\n"); log_request("GET", "/", "server_info", 200); } else if (strcmp(request_method, "GET") == 0 && strcmp(request_uri, "/auth") == 0) { // Handle GET /auth requests using the existing handler handle_auth_challenge_request(); } else if (strcmp(request_method, "GET") == 0 && strcmp(request_uri, "/health") == 0) { // Handle GET /health requests - simple public health response time_t now = time(NULL); long uptime_seconds = 0; if (g_server_start_time > 0 && now >= g_server_start_time) { uptime_seconds = (long)(now - g_server_start_time); } printf("Status: 200 OK\r\n"); printf("Content-Type: application/json\r\n\r\n"); printf("{\n"); printf(" \"status\": \"ok\",\n"); printf(" \"version\": \"%s\",\n", VERSION); printf(" \"uptime\": %ld\n", uptime_seconds); printf("}\n"); log_request("GET", "/health", "public", 200); } else if (strcmp(request_method, "DELETE") == 0) { // Handle DELETE / requests with pre-validated auth const char *sha256 = extract_sha256_from_uri(request_uri); if (sha256) { // Pass validated result to handler for ownership check handle_delete_request_with_validation(sha256, &result); } 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); } } // Graceful shutdown - stop relay client thread cleanly app_log(LOG_INFO, "Shutting down ginxsom (shutdown_requested=%d)...", (int)g_shutdown_requested); relay_client_stop(); nostr_set_log_callback(NULL, NULL); nostr_cleanup(); app_log(LOG_INFO, "Ginxsom shutdown complete"); return 0; } // ===== END UNUSED CODE =====