Files
ginxsom/src/main.c

3155 lines
109 KiB
C

/*
* 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 <getopt.h>
#include <curl/curl.h>
#include <sqlite3.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#include <dirent.h>
#include <sched.h>
#include <signal.h>
// 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 <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
// 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/<pubkey>.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: <pubkey>.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/<pubkey> 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 /<sha256> 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/<pubkey>.db\n"
" This ensures database-key consistency and prevents mismatches.\n"
"\n"
"Startup Scenarios:\n"
" 1. No arguments → Generate new keys, create db/<new_pubkey>.db\n"
" 2. --db-path <path> → Open existing database, validate keys match filename\n"
" 3. --server-privkey <key>→ 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 /<sha256>\",\n");
printf(" \"blob_head\": \"HEAD /<sha256>\",\n");
printf(" \"upload\": \"PUT /upload\",\n");
printf(" \"upload_requirements\": \"HEAD /upload\",\n");
printf(" \"list\": \"GET /list/<pubkey>\",\n");
printf(" \"delete\": \"DELETE /<sha256>\",\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/<pubkey> 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 /<sha256>\",\n");
printf(" \"blob_head\": \"HEAD /<sha256>\",\n");
printf(" \"upload\": \"PUT /upload\",\n");
printf(" \"upload_requirements\": \"HEAD /upload\",\n");
printf(" \"list\": \"GET /list/<pubkey>\",\n");
printf(" \"delete\": \"DELETE /<sha256>\",\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 /<sha256> 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 =====