3071 lines
106 KiB
C
3071 lines
106 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/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>
|
|
|
|
// Centralized logging system (declaration in ginxsom.h)
|
|
void app_log(log_level_t level, const char *format, ...) {
|
|
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_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) {
|
|
fprintf(stderr, "Cannot open database: %s\n", sqlite3_errmsg(db));
|
|
sqlite3_close(db);
|
|
return -1;
|
|
}
|
|
|
|
// If database was just created, initialize schema
|
|
if (!db_exists) {
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "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', 'true', '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) {
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "Failed to create top_uploaders view: %s\n", err_msg);
|
|
sqlite3_free(err_msg);
|
|
sqlite3_close(db);
|
|
return -1;
|
|
}
|
|
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "ERROR: Invalid pubkey for database naming\n");
|
|
return -1;
|
|
}
|
|
|
|
snprintf(g_db_path, sizeof(g_db_path), "db/%s.db", pubkey);
|
|
fprintf(stderr, "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
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "ERROR: Database pubkey mismatch!\n");
|
|
fprintf(stderr, " Database filename: %s\n", filename_pubkey);
|
|
fprintf(stderr, " Expected pubkey: %s\n", expected_pubkey);
|
|
fprintf(stderr, " → Database filename doesn't match the keys stored inside\n");
|
|
return -1;
|
|
}
|
|
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "DEBUG: generate_server_keypair() called\n");
|
|
unsigned char seckey_bytes[32];
|
|
char seckey_hex[65];
|
|
char pubkey_hex[65];
|
|
|
|
// Generate random private key
|
|
fprintf(stderr, "DEBUG: Generating random private key...\n");
|
|
if (generate_random_private_key_bytes(seckey_bytes, 32) != 0) {
|
|
fprintf(stderr, "DEBUG: Failed to generate random bytes\n");
|
|
return -1;
|
|
}
|
|
|
|
// Validate the private key
|
|
if (nostr_ec_private_key_verify(seckey_bytes) != NOSTR_SUCCESS) {
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "ERROR: Failed to set database path\n");
|
|
return -1;
|
|
}
|
|
|
|
// Initialize database with new path
|
|
if (initialize_database(g_db_path) != 0) {
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "ERROR: Failed to store blossom public key in config\n");
|
|
return -1;
|
|
}
|
|
|
|
// Display keys for admin setup
|
|
fprintf(stderr, "========================================\n");
|
|
fprintf(stderr, "SERVER KEYPAIR GENERATED SUCCESSFULLY\n");
|
|
fprintf(stderr, "========================================\n");
|
|
fprintf(stderr, "Blossom Public Key: %s\n", pubkey_hex);
|
|
fprintf(stderr, "Blossom Private Key: %s\n", seckey_hex);
|
|
fprintf(stderr, "Database Path: %s\n", g_db_path);
|
|
fprintf(stderr, "========================================\n");
|
|
fprintf(stderr, "IMPORTANT: Save the private key securely!\n");
|
|
fprintf(stderr, "This key is used for decrypting admin messages.\n");
|
|
fprintf(stderr, "========================================\n");
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Load server keys from database
|
|
int load_server_keys(void) {
|
|
fprintf(stderr, "DEBUG: load_server_keys() called\n");
|
|
sqlite3 *db;
|
|
sqlite3_stmt *stmt;
|
|
int rc;
|
|
|
|
// Try to load blossom private key
|
|
fprintf(stderr, "DEBUG: Trying to load blossom private key...\n");
|
|
if (get_blossom_private_key(g_blossom_seckey, sizeof(g_blossom_seckey)) != 0) {
|
|
fprintf(stderr, "DEBUG: No blossom private key found in database\n");
|
|
return -1; // Keys must exist in database
|
|
}
|
|
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "ERROR: Failed to derive public key from private key\n");
|
|
return -1;
|
|
}
|
|
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "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);
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
|
|
return 0;
|
|
}
|
|
|
|
const char *sql = "INSERT INTO blobs (sha256, size, type, uploaded_at, "
|
|
"uploader_pubkey, filename) VALUES (?, ?, ?, ?, ?, ?)";
|
|
|
|
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
|
if (rc != SQLITE_OK) {
|
|
fprintf(stderr, "SQL error: %s\n", sqlite3_errmsg(db));
|
|
sqlite3_close(db);
|
|
return 0;
|
|
}
|
|
|
|
// Bind parameters
|
|
sqlite3_bind_text(stmt, 1, sha256, -1, SQLITE_STATIC);
|
|
sqlite3_bind_int64(stmt, 2, size);
|
|
sqlite3_bind_text(stmt, 3, type, -1, SQLITE_STATIC);
|
|
sqlite3_bind_int64(stmt, 4, uploaded_at);
|
|
if (uploader_pubkey) {
|
|
sqlite3_bind_text(stmt, 5, uploader_pubkey, -1, SQLITE_STATIC);
|
|
} else {
|
|
sqlite3_bind_null(stmt, 5);
|
|
}
|
|
if (filename) {
|
|
sqlite3_bind_text(stmt, 6, filename, -1, SQLITE_STATIC);
|
|
} else {
|
|
sqlite3_bind_null(stmt, 6);
|
|
}
|
|
|
|
rc = sqlite3_step(stmt);
|
|
|
|
int success = 0;
|
|
if (rc == SQLITE_DONE) {
|
|
success = 1;
|
|
} else if (rc == SQLITE_CONSTRAINT) {
|
|
// This is actually OK - blob already exists with same hash
|
|
success = 1;
|
|
} else {
|
|
success = 0;
|
|
}
|
|
|
|
sqlite3_finalize(stmt);
|
|
sqlite3_close(db);
|
|
return success;
|
|
}
|
|
|
|
// Get blob metadata from database
|
|
int get_blob_metadata(const char *sha256, blob_metadata_t *metadata) {
|
|
sqlite3 *db;
|
|
sqlite3_stmt *stmt;
|
|
int rc;
|
|
|
|
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY | SQLITE_OPEN_CREATE, NULL);
|
|
if (rc) {
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "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)) {
|
|
fprintf(stderr, "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
|
|
fprintf(stderr, "LOG: [%s] %s %s - Auth: %s - Status: %d\r\n", timestamp,
|
|
method ? method : "NULL", uri ? uri : "NULL",
|
|
auth_status ? auth_status : "none", status_code);
|
|
}
|
|
|
|
// Handle GET /list/<pubkey> requests
|
|
void handle_list_request(const char *pubkey) {
|
|
|
|
// 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 = "";
|
|
if (strstr(blob_type_copy, "image/jpeg")) {
|
|
extension = ".jpg";
|
|
} else if (strstr(blob_type_copy, "image/webp")) {
|
|
extension = ".webp";
|
|
} else if (strstr(blob_type_copy, "image/png")) {
|
|
extension = ".png";
|
|
} else if (strstr(blob_type_copy, "image/gif")) {
|
|
extension = ".gif";
|
|
} else if (strstr(blob_type_copy, "video/mp4")) {
|
|
extension = ".mp4";
|
|
} else if (strstr(blob_type_copy, "video/webm")) {
|
|
extension = ".webm";
|
|
} else if (strstr(blob_type_copy, "audio/mpeg")) {
|
|
extension = ".mp3";
|
|
} else if (strstr(blob_type_copy, "audio/ogg")) {
|
|
extension = ".ogg";
|
|
} else if (strstr(blob_type_copy, "text/plain")) {
|
|
extension = ".txt";
|
|
} else {
|
|
extension = ".bin";
|
|
}
|
|
|
|
char filepath[MAX_PATH_LEN];
|
|
// 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)) {
|
|
fprintf(stderr, "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)) {
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "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
|
|
fprintf(stderr, "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
|
|
fprintf(stderr, "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)) {
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "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());
|
|
|
|
// 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;
|
|
fprintf(stderr, "WARNING: --generate-keys is deprecated. Keys are generated automatically when needed.\n");
|
|
} else {
|
|
fprintf(stderr, "Unknown option: %s\n", argv[i]);
|
|
fprintf(stderr, "Use --help for usage information\n");
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
app_log(LOG_INFO, "Storage directory: %s", g_storage_dir);
|
|
|
|
// CRITICAL: Initialize nostr crypto system BEFORE key operations
|
|
app_log(LOG_INFO, "Initializing nostr crypto system...");
|
|
int crypto_init_result = nostr_crypto_init();
|
|
if (crypto_init_result != 0) {
|
|
app_log(LOG_ERROR, "Failed to initialize nostr crypto system (result: %d)", crypto_init_result);
|
|
return 1;
|
|
}
|
|
app_log(LOG_INFO, "Nostr crypto 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) {
|
|
fprintf(stderr, "ERROR: Invalid server private key\n");
|
|
return 1;
|
|
}
|
|
|
|
fprintf(stderr, "KEYS: Derived pubkey: %s\n", g_blossom_pubkey);
|
|
|
|
// Scenario 5: Both database path and keys specified
|
|
if (db_path_specified) {
|
|
fprintf(stderr, "\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);
|
|
fprintf(stderr, "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';
|
|
fprintf(stderr, "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
|
|
fprintf(stderr, "DATABASE: Found existing database, validating keys...\n");
|
|
|
|
// Load keys from database
|
|
if (get_blossom_private_key(g_blossom_seckey, sizeof(g_blossom_seckey)) != 0) {
|
|
fprintf(stderr, "ERROR: Invalid database: missing server keys\n");
|
|
return 1;
|
|
}
|
|
|
|
// Compare with provided key
|
|
if (strcmp(g_blossom_seckey, test_server_privkey) != 0) {
|
|
fprintf(stderr, "ERROR: Server private key doesn't match database\n");
|
|
fprintf(stderr, " Provided key and database keys are different\n");
|
|
return 1;
|
|
}
|
|
|
|
fprintf(stderr, "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
|
|
fprintf(stderr, "DATABASE: No existing database, creating new one...\n");
|
|
|
|
// Initialize new database
|
|
if (initialize_database(g_db_path) != 0) {
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "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);
|
|
}
|
|
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "ERROR: Failed to set database path\n");
|
|
return 1;
|
|
}
|
|
|
|
// Check if database already exists
|
|
struct stat st;
|
|
if (stat(g_db_path, &st) == 0) {
|
|
fprintf(stderr, "ERROR: Database already exists for this pubkey: %s\n", g_db_path);
|
|
fprintf(stderr, " Use --db-path to open existing database or use different keys\n");
|
|
return 1;
|
|
}
|
|
|
|
// Initialize new database
|
|
if (initialize_database(g_db_path) != 0) {
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "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);
|
|
}
|
|
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "\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
|
|
fprintf(stderr, "DATABASE: Directory specified: %s\n", specified_db_path);
|
|
fprintf(stderr, "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;
|
|
fprintf(stderr, "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
|
|
fprintf(stderr, "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) {
|
|
fprintf(stderr, "DATABASE: Opening existing database: %s\n", g_db_path);
|
|
|
|
// Load keys from database
|
|
if (load_server_keys() != 0) {
|
|
fprintf(stderr, "ERROR: Failed to load keys from database\n");
|
|
fprintf(stderr, " → Database may be corrupted or not a valid ginxsom database\n");
|
|
return 1;
|
|
}
|
|
|
|
fprintf(stderr, "DATABASE: Keys loaded and validated successfully\n");
|
|
} else {
|
|
// No database file exists - we need keys to create one
|
|
fprintf(stderr, "ERROR: No database found and no --server-privkey provided\n");
|
|
fprintf(stderr, " → Use --server-privkey to create a new database\n");
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
// Scenario 1: No Arguments (Fresh Start)
|
|
else {
|
|
fprintf(stderr, "\n=== SCENARIO 1: FRESH START (NO ARGUMENTS) ===\n");
|
|
fprintf(stderr, "FRESH START: Generating new server keypair...\n");
|
|
|
|
// Generate new keypair (this will set g_db_path based on pubkey)
|
|
if (generate_server_keypair() != 0) {
|
|
fprintf(stderr, "ERROR: Failed to generate server keypair\n");
|
|
return 1;
|
|
}
|
|
|
|
fprintf(stderr, "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() */) {
|
|
fprintf(
|
|
stderr,
|
|
"STARTUP: No configuration found - server starting in setup mode\n");
|
|
fprintf(stderr, "STARTUP: Run interactive setup with: ginxsom --setup\n");
|
|
// For interactive mode (when stdin is available), offer setup
|
|
|
|
} else if (!config_loaded) {
|
|
fprintf(stderr, "STARTUP: Database configuration loaded successfully\n");
|
|
}
|
|
|
|
// 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");
|
|
|
|
int first_request = 1;
|
|
while (FCGI_Accept() >= 0) {
|
|
// Test stderr capture on first request
|
|
if (first_request) {
|
|
fprintf(stderr, "FCGI: First request received - testing nginx stderr capture\n");
|
|
fflush(stderr);
|
|
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");
|
|
printf(" \"image/jpeg\",\n");
|
|
printf(" \"image/png\",\n");
|
|
printf(" \"image/webp\",\n");
|
|
printf(" \"image/gif\",\n");
|
|
printf(" \"video/mp4\",\n");
|
|
printf(" \"video/webm\",\n");
|
|
printf(" \"audio/mpeg\",\n");
|
|
printf(" \"audio/ogg\",\n");
|
|
printf(" \"text/plain\",\n");
|
|
printf(" \"application/pdf\"\n");
|
|
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, "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, "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");
|
|
printf(" \"image/jpeg\",\n");
|
|
printf(" \"image/png\",\n");
|
|
printf(" \"image/webp\",\n");
|
|
printf(" \"image/gif\",\n");
|
|
printf(" \"video/mp4\",\n");
|
|
printf(" \"video/webm\",\n");
|
|
printf(" \"audio/mpeg\",\n");
|
|
printf(" \"audio/ogg\",\n");
|
|
printf(" \"text/plain\",\n");
|
|
printf(" \"application/pdf\"\n");
|
|
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, "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);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
// ===== END UNUSED CODE =====
|