diff --git a/.test_keys b/.test_keys index ccbe9a6..5c0d7fd 100644 --- a/.test_keys +++ b/.test_keys @@ -1,4 +1,4 @@ ADMIN_PRIVKEY='22cc83aa57928a2800234c939240c9a6f0f44a33ea3838a860ed38930b195afd' ADMIN_PUBKEY='8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e' SERVER_PRIVKEY='c4e0d2ed7d36277d6698650f68a6e9199f91f3abb476a67f07303e81309c48f1' -SERVER_PUBKEY='52e366edfa4e9cc6a6d4653828e51ccf828a2f5a05227d7a768f33b5a198681a' +SERVER_PUBKEY='ebe82fbff0ff79b2973892eb285cafc767863e434f894838a548580266b70254' diff --git a/Makefile b/Makefile index 6973bed..d248ea8 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ BUILDDIR = build TARGET = $(BUILDDIR)/ginxsom-fcgi # Source files -SOURCES = $(SRCDIR)/main.c $(SRCDIR)/admin_api.c $(SRCDIR)/admin_auth.c $(SRCDIR)/admin_websocket.c $(SRCDIR)/admin_handlers.c $(SRCDIR)/bud04.c $(SRCDIR)/bud06.c $(SRCDIR)/bud08.c $(SRCDIR)/bud09.c $(SRCDIR)/request_validator.c +SOURCES = $(SRCDIR)/main.c $(SRCDIR)/admin_api.c $(SRCDIR)/admin_auth.c $(SRCDIR)/admin_event.c $(SRCDIR)/admin_websocket.c $(SRCDIR)/admin_handlers.c $(SRCDIR)/bud04.c $(SRCDIR)/bud06.c $(SRCDIR)/bud08.c $(SRCDIR)/bud09.c $(SRCDIR)/request_validator.c OBJECTS = $(SOURCES:$(SRCDIR)/%.c=$(BUILDDIR)/%.o) # Default target diff --git a/tests/admin_test.sh b/Trash/admin_test.sh similarity index 100% rename from tests/admin_test.sh rename to Trash/admin_test.sh diff --git a/db/schema.sql b/Trash/schema.sql similarity index 98% rename from db/schema.sql rename to Trash/schema.sql index 44eb556..b9fac10 100644 --- a/db/schema.sql +++ b/Trash/schema.sql @@ -38,7 +38,7 @@ INSERT OR IGNORE INTO config (key, value, description) VALUES ('auth_rules_enabled', 'false', 'Whether authentication rules are enabled for uploads'), ('server_name', 'ginxsom', 'Server name for responses'), ('admin_pubkey', '', 'Admin public key for API access'), - ('admin_enabled', 'false', 'Whether admin API is enabled'), + ('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'); diff --git a/tests/test_admin_api.sh b/Trash/test_admin_api.sh similarity index 100% rename from tests/test_admin_api.sh rename to Trash/test_admin_api.sh diff --git a/build/admin_api.o b/build/admin_api.o index eb36873..9efa6ca 100644 Binary files a/build/admin_api.o and b/build/admin_api.o differ diff --git a/build/admin_event.o b/build/admin_event.o new file mode 100644 index 0000000..5c91026 Binary files /dev/null and b/build/admin_event.o differ diff --git a/build/bud08.o b/build/bud08.o index bdfea11..e14dde5 100644 Binary files a/build/bud08.o and b/build/bud08.o differ diff --git a/build/bud09.o b/build/bud09.o index cbb6e0f..12b2ca2 100644 Binary files a/build/bud09.o and b/build/bud09.o differ diff --git a/build/ginxsom-fcgi b/build/ginxsom-fcgi index 8a0b91f..20d6c27 100755 Binary files a/build/ginxsom-fcgi and b/build/ginxsom-fcgi differ diff --git a/build/main.o b/build/main.o index 1d3527d..2d49814 100644 Binary files a/build/main.o and b/build/main.o differ diff --git a/build/request_validator.o b/build/request_validator.o index 50cdadc..4ac6c84 100644 Binary files a/build/request_validator.o and b/build/request_validator.o differ diff --git a/config/local-nginx.conf b/config/local-nginx.conf index ef6b699..57db5a5 100644 --- a/config/local-nginx.conf +++ b/config/local-nginx.conf @@ -221,7 +221,7 @@ http { # Admin API endpoints (/api/*) location /api/ { - if ($request_method !~ ^(GET|PUT)$) { + if ($request_method !~ ^(GET|PUT|POST)$) { return 405; } fastcgi_pass fastcgi_backend; @@ -572,7 +572,7 @@ http { # Admin API endpoints (/api/*) location /api/ { - if ($request_method !~ ^(GET|PUT)$) { + if ($request_method !~ ^(GET|PUT|POST)$) { return 405; } fastcgi_pass fastcgi_backend; diff --git a/db/ginxsom.db b/db/52e366edfa4e9cc6a6d4653828e51ccf828a2f5a05227d7a768f33b5a198681a.db similarity index 94% rename from db/ginxsom.db rename to db/52e366edfa4e9cc6a6d4653828e51ccf828a2f5a05227d7a768f33b5a198681a.db index 99341ba..97a2832 100644 Binary files a/db/ginxsom.db and b/db/52e366edfa4e9cc6a6d4653828e51ccf828a2f5a05227d7a768f33b5a198681a.db differ diff --git a/db/migrations/001_add_auth_rules.sql b/db/migrations/001_add_auth_rules.sql deleted file mode 100644 index 35b854c..0000000 --- a/db/migrations/001_add_auth_rules.sql +++ /dev/null @@ -1,78 +0,0 @@ --- Migration: Add authentication rules tables --- Purpose: Enable whitelist/blacklist functionality for Ginxsom --- Date: 2025-01-12 - --- Enable foreign key constraints -PRAGMA foreign_keys = ON; - --- Authentication rules table for whitelist/blacklist functionality -CREATE TABLE IF NOT EXISTS auth_rules ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - rule_type TEXT NOT NULL, -- 'pubkey_blacklist', 'pubkey_whitelist', - -- 'hash_blacklist', 'mime_blacklist', 'mime_whitelist' - rule_target TEXT NOT NULL, -- The pubkey, hash, or MIME type to match - operation TEXT NOT NULL DEFAULT '*', -- 'upload', 'delete', 'list', or '*' for all - enabled INTEGER NOT NULL DEFAULT 1, -- 1 = enabled, 0 = disabled - priority INTEGER NOT NULL DEFAULT 100,-- Lower number = higher priority - description TEXT, -- Human-readable description - created_by TEXT, -- Admin pubkey who created the rule - created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), - updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), - - -- Constraints - CHECK (rule_type IN ('pubkey_blacklist', 'pubkey_whitelist', - 'hash_blacklist', 'mime_blacklist', 'mime_whitelist')), - CHECK (operation IN ('upload', 'delete', 'list', '*')), - CHECK (enabled IN (0, 1)), - CHECK (priority >= 0), - - -- Unique constraint: one rule per type/target/operation combination - UNIQUE(rule_type, rule_target, operation) -); - --- Indexes for performance optimization -CREATE INDEX IF NOT EXISTS idx_auth_rules_type_target ON auth_rules(rule_type, rule_target); -CREATE INDEX IF NOT EXISTS idx_auth_rules_operation ON auth_rules(operation); -CREATE INDEX IF NOT EXISTS idx_auth_rules_enabled ON auth_rules(enabled); -CREATE INDEX IF NOT EXISTS idx_auth_rules_priority ON auth_rules(priority); -CREATE INDEX IF NOT EXISTS idx_auth_rules_type_operation ON auth_rules(rule_type, operation, enabled); - --- Cache table for authentication decisions (5-minute TTL) -CREATE TABLE IF NOT EXISTS auth_rules_cache ( - cache_key TEXT PRIMARY KEY NOT NULL, -- SHA-256 hash of request parameters - decision INTEGER NOT NULL, -- 1 = allow, 0 = deny - reason TEXT, -- Reason for decision - pubkey TEXT, -- Public key from request - operation TEXT, -- Operation type - resource_hash TEXT, -- Resource hash (if applicable) - created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), - expires_at INTEGER NOT NULL, -- Expiration timestamp - - CHECK (decision IN (0, 1)) -); - --- Index for cache expiration cleanup -CREATE INDEX IF NOT EXISTS idx_auth_cache_expires ON auth_rules_cache(expires_at); -CREATE INDEX IF NOT EXISTS idx_auth_cache_pubkey ON auth_rules_cache(pubkey); - --- Insert example rules (commented out - uncomment to use) --- Example: Blacklist a specific pubkey for uploads --- INSERT INTO auth_rules (rule_type, rule_target, operation, priority, description, created_by) VALUES --- ('pubkey_blacklist', '79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798', 'upload', 10, 'Example blacklisted user', 'admin_pubkey_here'); - --- Example: Whitelist a specific pubkey for all operations --- INSERT INTO auth_rules (rule_type, rule_target, operation, priority, description, created_by) VALUES --- ('pubkey_whitelist', 'your_pubkey_here', '*', 300, 'Trusted user - all operations allowed', 'admin_pubkey_here'); - --- Example: Blacklist executable MIME types --- INSERT INTO auth_rules (rule_type, rule_target, operation, priority, description, created_by) VALUES --- ('mime_blacklist', 'application/x-executable', 'upload', 200, 'Block executable files', 'admin_pubkey_here'), --- ('mime_blacklist', 'application/x-msdos-program', 'upload', 200, 'Block DOS executables', 'admin_pubkey_here'), --- ('mime_blacklist', 'application/x-msdownload', 'upload', 200, 'Block Windows executables', 'admin_pubkey_here'); - --- Example: Whitelist common image types --- INSERT INTO auth_rules (rule_type, rule_target, operation, priority, description, created_by) VALUES --- ('mime_whitelist', 'image/jpeg', 'upload', 400, 'Allow JPEG images', 'admin_pubkey_here'), --- ('mime_whitelist', 'image/png', 'upload', 400, 'Allow PNG images', 'admin_pubkey_here'), --- ('mime_whitelist', 'image/gif', 'upload', 400, 'Allow GIF images', 'admin_pubkey_here'), --- ('mime_whitelist', 'image/webp', 'upload', 400, 'Allow WebP images', 'admin_pubkey_here'); \ No newline at end of file diff --git a/db/migrations/002_add_relay_seckey.sql b/db/migrations/002_add_relay_seckey.sql deleted file mode 100644 index 60f211d..0000000 --- a/db/migrations/002_add_relay_seckey.sql +++ /dev/null @@ -1,15 +0,0 @@ --- Migration: Add blossom_seckey table for storing server private key --- This table stores the Blossom server's secp256k1 private key used for: --- - Signing admin response events (Kind 23457) --- - Decrypting admin commands (NIP-44) - -CREATE TABLE IF NOT EXISTS blossom_seckey ( - id INTEGER PRIMARY KEY CHECK (id = 1), -- Only one row allowed - seckey TEXT NOT NULL, -- Private key in hex format (64 chars) - created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), - CHECK (length(seckey) = 64) -- Ensure valid secp256k1 key length -); - --- Add blossom_pubkey to config if not exists -INSERT OR IGNORE INTO config (key, value, description) VALUES - ('blossom_pubkey', '', 'Blossom server public key derived from blossom_seckey'); \ No newline at end of file diff --git a/docs/DATABASE_NAMING_DESIGN.md b/docs/DATABASE_NAMING_DESIGN.md new file mode 100644 index 0000000..a1e619a --- /dev/null +++ b/docs/DATABASE_NAMING_DESIGN.md @@ -0,0 +1,300 @@ +# Database Naming Design (c-relay Pattern) + +## Overview + +Following c-relay's architecture, ginxsom will use pubkey-based database naming to ensure database-key consistency and prevent mismatched configurations. + +## Database Naming Convention + +Database files are named after the blossom server's public key: +``` +db/.db +``` + +Example: +``` +db/52e366edfa4e9cc6a6d4653828e51ccf828a2f5a05227d7a768f33b5a198681a.db +``` + +## Startup Scenarios + +### Scenario 1: No Arguments (Fresh Start) +```bash +./ginxsom-fcgi +``` + +**Behavior:** +1. Generate new server keypair +2. Create database file: `db/.db` +3. Store keys in the new database +4. Start server + +**Result:** New instance with fresh keys and database + +--- + +### Scenario 2: Database File Specified +```bash +./ginxsom-fcgi --db-path db/52e366ed...198681a.db +``` + +**Behavior:** +1. Open specified database +2. Load blossom_seckey from database +3. Verify pubkey matches database filename +4. Load admin_pubkey if present +5. Start server + +**Validation:** +- Database MUST exist +- Database MUST contain blossom_seckey +- Derived pubkey MUST match filename + +**Error Cases:** +- Database doesn't exist → Error: "Database file not found" +- Database missing blossom_seckey → Error: "Invalid database: missing server keys" +- Pubkey mismatch → Error: "Database pubkey mismatch: expected X, got Y" + +--- + +### Scenario 3: Keys Specified (New Instance with Specific Keys) +```bash +./ginxsom-fcgi --server-privkey c4e0d2ed...309c48f1 --admin-pubkey 8ff74724...5eedde0e +``` + +**Behavior:** +1. Validate provided server private key +2. Derive server public key +3. Create database file: `db/.db` +4. Store both keys in new database +5. Start server + +**Validation:** +- server-privkey MUST be valid 64-char hex +- Derived database file MUST NOT already exist (prevents overwriting) + +**Error Cases:** +- Invalid privkey format → Error: "Invalid server private key format" +- Database already exists → Error: "Database already exists for this pubkey" + +--- + +### Scenario 4: Test Mode +```bash +./ginxsom-fcgi --test-keys +``` + +**Behavior:** +1. Load keys from `.test_keys` file +2. Derive server public key from SERVER_PRIVKEY +3. Create/overwrite database: `db/.db` +4. Store test keys in database +5. Start server + +**Special Handling:** +- Test mode ALWAYS overwrites existing database (for clean testing) +- Database name derived from test SERVER_PRIVKEY + +--- + +### Scenario 5: Database + Keys Specified (Validation Mode) +```bash +./ginxsom-fcgi --db-path db/52e366ed...198681a.db --server-privkey c4e0d2ed...309c48f1 +``` + +**Behavior:** +1. Open specified database +2. Load blossom_seckey from database +3. Compare with provided --server-privkey +4. If match: continue normally +5. If mismatch: ERROR and exit + +**Purpose:** Validation/verification that correct keys are being used + +**Error Cases:** +- Key mismatch → Error: "Server private key doesn't match database" + +--- + +## Command Line Options + +### Updated Options + +``` +--db-path PATH Database file path (must match pubkey if keys exist) +--storage-dir DIR Storage directory for files (default: blobs) +--admin-pubkey KEY Admin public key (only used when creating new database) +--server-privkey KEY Server private key (creates new DB or validates existing) +--test-keys Use test keys from .test_keys file +--generate-keys Generate new keypair and create database (deprecated - default behavior) +--help, -h Show this help message +``` + +### Removed Options + +- `--generate-keys` - No longer needed, this is default behavior when no args provided + +--- + +## Database Directory Structure + +``` +db/ +├── 52e366edfa4e9cc6a6d4653828e51ccf828a2f5a05227d7a768f33b5a198681a.db # Test instance +├── a1b2c3d4e5f6...xyz.db # Production instance 1 +├── f9e8d7c6b5a4...abc.db # Production instance 2 +└── schema.sql # Schema template +``` + +Each database is completely independent and tied to its keypair. + +--- + +## Implementation Logic Flow + +``` +START + │ + ├─ Parse command line arguments + │ + ├─ Initialize crypto system + │ + ├─ Determine mode: + │ │ + │ ├─ Test mode (--test-keys)? + │ │ ├─ Load keys from .test_keys + │ │ ├─ Derive pubkey + │ │ ├─ Set db_path = db/.db + │ │ └─ Create/overwrite database + │ │ + │ ├─ Keys provided (--server-privkey)? + │ │ ├─ Validate privkey format + │ │ ├─ Derive pubkey + │ │ ├─ Set db_path = db/.db + │ │ │ + │ │ ├─ Database specified (--db-path)? + │ │ │ ├─ YES: Validate keys match database + │ │ │ └─ NO: Create new database + │ │ │ + │ │ └─ Store keys in database + │ │ + │ ├─ Database specified (--db-path)? + │ │ ├─ Open database + │ │ ├─ Load blossom_seckey + │ │ ├─ Derive pubkey + │ │ ├─ Validate pubkey matches filename + │ │ └─ Load admin_pubkey + │ │ + │ └─ No arguments (fresh start)? + │ ├─ Generate new keypair + │ ├─ Set db_path = db/.db + │ └─ Create new database with keys + │ + ├─ Initialize database schema (if new) + │ + ├─ Load/validate all keys + │ + └─ Start FastCGI server +``` + +--- + +## Migration Path + +### For Existing Installations + +1. **Backup current database:** + ```bash + cp db/ginxsom.db db/ginxsom.db.backup + ``` + +2. **Extract current pubkey:** + ```bash + PUBKEY=$(sqlite3 db/ginxsom.db "SELECT value FROM config WHERE key='blossom_pubkey'") + ``` + +3. **Rename database:** + ```bash + mv db/ginxsom.db db/${PUBKEY}.db + ``` + +4. **Update restart-all.sh:** + - Remove hardcoded `db/ginxsom.db` references + - Let application determine database name from keys + +--- + +## Benefits + +1. **Database-Key Consistency:** Impossible to use wrong database with wrong keys +2. **Multiple Instances:** Can run multiple independent instances with different keys +3. **Clear Identity:** Database filename immediately identifies the server +4. **Test Isolation:** Test databases are clearly separate from production +5. **No Accidental Overwrites:** Each keypair has its own database +6. **Follows c-relay Pattern:** Proven architecture from production relay software + +--- + +## Error Messages + +### Clear, Actionable Errors + +``` +ERROR: Database file not found: db/52e366ed...198681a.db + → Specify a different database or let the application create a new one + +ERROR: Invalid database: missing server keys + → Database is corrupted or not a valid ginxsom database + +ERROR: Database pubkey mismatch + Expected: 52e366edfa4e9cc6a6d4653828e51ccf828a2f5a05227d7a768f33b5a198681a + Got: a1b2c3d4e5f6789... + → Database filename doesn't match the keys stored inside + +ERROR: Server private key doesn't match database + → The --server-privkey you provided doesn't match the database keys + +ERROR: Database already exists for this pubkey: db/52e366ed...198681a.db + → Use --db-path to open existing database or use different keys +``` + +--- + +## Testing Strategy + +### Test Cases + +1. **Fresh start (no args)** → Creates new database with generated keys +2. **Specify database** → Opens and validates existing database +3. **Specify keys** → Creates new database with those keys +4. **Test mode** → Uses test keys and creates test database +5. **Database + matching keys** → Validates and continues +6. **Database + mismatched keys** → Errors appropriately +7. **Invalid database path** → Clear error message +8. **Corrupted database** → Detects and reports + +### Test Script + +```bash +#!/bin/bash +# Test database naming system + +# Test 1: Fresh start +./ginxsom-fcgi --generate-keys +# Should create db/.db + +# Test 2: Test mode +./ginxsom-fcgi --test-keys +# Should create db/52e366ed...198681a.db + +# Test 3: Specify keys +./ginxsom-fcgi --server-privkey abc123... +# Should create db/.db + +# Test 4: Open existing +./ginxsom-fcgi --db-path db/52e366ed...198681a.db +# Should open and validate + +# Test 5: Mismatch error +./ginxsom-fcgi --db-path db/52e366ed...198681a.db --server-privkey wrong_key +# Should error with clear message \ No newline at end of file diff --git a/restart-all.sh b/restart-all.sh index cbfe856..60964c1 100755 --- a/restart-all.sh +++ b/restart-all.sh @@ -1,6 +1,7 @@ #!/bin/bash # Restart Ginxsom Development Environment # Combines nginx and FastCGI restart operations for debugging +# WARNING: This script DELETES all databases in db/ for fresh testing # Configuration @@ -177,9 +178,22 @@ if [ $? -ne 0 ]; then fi echo -e "${GREEN}Clean rebuild complete${NC}" -# Step 3.5: Handle keys based on mode -echo -e "\n${YELLOW}3.5. Configuring server keys...${NC}" -DB_PATH="$PWD/db/ginxsom.db" +# Step 3.5: Clean database directory for fresh testing +echo -e "\n${YELLOW}3.5. Cleaning database directory...${NC}" +echo "Removing all existing databases for fresh start..." + +# Remove all .db files in db/ directory +if ls db/*.db 1> /dev/null 2>&1; then + echo "Found databases to remove:" + ls -lh db/*.db + rm -f db/*.db + echo -e "${GREEN}Database cleanup complete${NC}" +else + echo "No existing databases found" +fi + +# Step 3.75: Handle keys based on mode +echo -e "\n${YELLOW}3.75. Configuring server keys...${NC}" if [ $TEST_MODE -eq 1 ]; then # Test mode: verify .test_keys file exists @@ -188,29 +202,20 @@ if [ $TEST_MODE -eq 1 ]; then echo -e "${RED}Test mode requires .test_keys file in project root${NC}" exit 1 fi + + # Extract test server pubkey to determine database name + TEST_PUBKEY=$(grep "^SERVER_PUBKEY=" .test_keys | cut -d"'" -f2) + if [ -z "$TEST_PUBKEY" ]; then + echo -e "${RED}ERROR: Could not extract SERVER_PUBKEY from .test_keys${NC}" + exit 1 + fi + echo -e "${GREEN}Test mode: Will use keys from .test_keys${NC}" + echo -e "${GREEN}Fresh test database will be created as: db/${TEST_PUBKEY}.db${NC}" else - # Production mode: check if keys exist, generate if needed - NEED_KEYS=1 - if command -v sqlite3 >/dev/null 2>&1; then - if sqlite3 "$DB_PATH" "SELECT seckey FROM blossom_seckey WHERE id=1" 2>/dev/null | grep -Eq '^[0-9a-f]{64}$'; then - NEED_KEYS=0 - echo -e "${GREEN}Blossom private key found in database${NC}" - fi - else - echo -e "${YELLOW}sqlite3 not found; assuming keys may be missing${NC}" - fi - - if [ $NEED_KEYS -eq 1 ]; then - echo -e "${YELLOW}No blossom key found; generating server keypair...${NC}" - ./build/ginxsom-fcgi --db-path "$DB_PATH" --storage-dir blobs --generate-keys 1>>logs/app/stdout.log 2>>logs/app/stderr.log - if [ $? -ne 0 ]; then - echo -e "${RED}Key generation failed. Check logs/app/stderr.log${NC}" - exit 1 - fi - echo -e "${GREEN}Key generation completed${NC}" - echo -e "${YELLOW}IMPORTANT: Check logs/app/stderr.log for your generated keys!${NC}" - fi + # Production mode: databases were cleaned, will generate new keypair + echo -e "${YELLOW}Production mode: Fresh start with new keypair${NC}" + echo -e "${YELLOW}New database will be created as db/.db${NC}" fi # Step 4: Start FastCGI @@ -232,10 +237,13 @@ echo "Setting GINX_DEBUG environment for pubkey extraction diagnostics" export GINX_DEBUG=1 # Build command line arguments based on mode -FCGI_ARGS="--db-path $PWD/db/ginxsom.db --storage-dir blobs" +FCGI_ARGS="--storage-dir blobs" if [ $TEST_MODE -eq 1 ]; then FCGI_ARGS="$FCGI_ARGS --test-keys" echo -e "${YELLOW}Starting FastCGI in TEST MODE with test keys${NC}" +else + # Production mode: databases were cleaned, will generate new keys + echo -e "${YELLOW}Starting FastCGI in production mode - will generate new keys and create database${NC}" fi # Start FastCGI application with proper logging (daemonized but with redirected streams) diff --git a/src/admin_api.c b/src/admin_api.c index db83541..d4f2b07 100644 --- a/src/admin_api.c +++ b/src/admin_api.c @@ -11,8 +11,8 @@ #include #include "ginxsom.h" -// Database path (consistent with main.c) -#define DB_PATH "db/ginxsom.db" +// Use global database path from main.c +extern char g_db_path[]; // Function declarations (moved from admin_api.h) void handle_admin_api_request(const char* method, const char* uri, const char* validated_pubkey, int is_authenticated); @@ -44,7 +44,7 @@ static int admin_nip94_get_origin(char* out, size_t out_size) { sqlite3_stmt* stmt; int rc; - rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); + rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL); if (rc) { // Default on DB error strncpy(out, "http://localhost:9001", out_size - 1); @@ -130,8 +130,12 @@ void handle_admin_api_request(const char* method, const char* uri, const char* v } // Authentication now handled by centralized validation system - // Health endpoint is exempt from authentication requirement - if (strcmp(path, "/health") != 0) { + // Health endpoint and POST /admin (Kind 23456 events) are exempt from authentication requirement + // Kind 23456 events authenticate themselves via signed event validation + int skip_auth = (strcmp(path, "/health") == 0) || + (strcmp(method, "POST") == 0 && strcmp(path, "/admin") == 0); + + if (!skip_auth) { if (!is_authenticated || !validated_pubkey) { send_json_error(401, "admin_auth_required", "Valid admin authentication required"); return; @@ -157,6 +161,13 @@ void handle_admin_api_request(const char* method, const char* uri, const char* v } else { send_json_error(404, "not_found", "API endpoint not found"); } + } else if (strcmp(method, "POST") == 0) { + if (strcmp(path, "/admin") == 0) { + // Handle Kind 23456/23457 admin event commands + handle_admin_event_request(); + } else { + send_json_error(404, "not_found", "API endpoint not found"); + } } else if (strcmp(method, "PUT") == 0) { if (strcmp(path, "/config") == 0) { handle_config_put_api(); @@ -201,7 +212,7 @@ int verify_admin_pubkey(const char* event_pubkey) { sqlite3_stmt* stmt; int rc, is_admin = 0; - rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); + rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL); if (rc) { return 0; } @@ -228,7 +239,7 @@ int is_admin_enabled(void) { sqlite3_stmt* stmt; int rc, enabled = 0; - rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); + rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL); if (rc) { return 0; // Default disabled if can't access DB } @@ -254,7 +265,7 @@ void handle_stats_api(void) { sqlite3_stmt* stmt; int rc; - rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); + rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL); if (rc) { send_json_error(500, "database_error", "Failed to open database"); return; @@ -349,7 +360,7 @@ void handle_config_get_api(void) { sqlite3_stmt* stmt; int rc; - rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); + rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL); if (rc) { send_json_error(500, "database_error", "Failed to open database"); return; @@ -423,7 +434,7 @@ void handle_config_put_api(void) { sqlite3_stmt* stmt; int rc; - rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL); + rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE, NULL); if (rc) { free(json_body); cJSON_Delete(config_data); @@ -541,7 +552,7 @@ void handle_config_key_put_api(const char* key) { sqlite3_stmt* stmt; int rc; - rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL); + rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE, NULL); if (rc) { free(json_body); cJSON_Delete(request_data); @@ -621,7 +632,7 @@ void handle_files_api(void) { sqlite3_stmt* stmt; int rc; - rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); + rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL); if (rc) { send_json_error(500, "database_error", "Failed to open database"); return; @@ -715,7 +726,7 @@ void handle_health_api(void) { // Check database connection sqlite3* db; - int rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); + int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL); if (rc == SQLITE_OK) { cJSON_AddStringToObject(data, "database", "connected"); sqlite3_close(db); diff --git a/src/admin_event.c b/src/admin_event.c new file mode 100644 index 0000000..c855d37 --- /dev/null +++ b/src/admin_event.c @@ -0,0 +1,471 @@ +// Admin event handler for Kind 23456/23457 admin commands +#include +#include +#include +#include +#include "ginxsom.h" + +// Forward declarations for nostr_core_lib functions +int nostr_hex_to_bytes(const char* hex, unsigned char* bytes, size_t bytes_len); +int nostr_nip44_decrypt(const unsigned char* recipient_private_key, + const unsigned char* sender_public_key, + const char* encrypted_data, + char* output, + size_t output_size); +int nostr_nip44_encrypt(const unsigned char* sender_private_key, + const unsigned char* recipient_public_key, + const char* plaintext, + char* output, + size_t output_size); +cJSON* nostr_create_and_sign_event(int kind, const char* content, cJSON* tags, + const unsigned char* private_key, time_t created_at); + +// Use global database path from main.c +extern char g_db_path[]; + +// Forward declarations +static int get_server_privkey(unsigned char* privkey_bytes); +static int get_server_pubkey(char* pubkey_hex, size_t size); +static int handle_config_query_command(cJSON* response_data); +static int send_admin_response_event(const char* admin_pubkey, const char* request_id, + cJSON* response_data); + +/** + * Handle Kind 23456 admin command event + * Expects POST to /api/admin with JSON body containing the event + */ +void handle_admin_event_request(void) { + // Read request body + const char* content_length_str = getenv("CONTENT_LENGTH"); + if (!content_length_str) { + printf("Status: 411 Length Required\r\n"); + printf("Content-Type: application/json\r\n\r\n"); + printf("{\"error\":\"Content-Length header required\"}\n"); + return; + } + + long content_length = atol(content_length_str); + if (content_length <= 0 || content_length > 65536) { + printf("Status: 400 Bad Request\r\n"); + printf("Content-Type: application/json\r\n\r\n"); + printf("{\"error\":\"Invalid content length\"}\n"); + return; + } + + char* json_body = malloc(content_length + 1); + if (!json_body) { + printf("Status: 500 Internal Server Error\r\n"); + printf("Content-Type: application/json\r\n\r\n"); + printf("{\"error\":\"Memory allocation failed\"}\n"); + return; + } + + size_t bytes_read = fread(json_body, 1, content_length, stdin); + if (bytes_read != (size_t)content_length) { + free(json_body); + printf("Status: 400 Bad Request\r\n"); + printf("Content-Type: application/json\r\n\r\n"); + printf("{\"error\":\"Failed to read complete request body\"}\n"); + return; + } + json_body[content_length] = '\0'; + + // Parse event JSON + cJSON* event = cJSON_Parse(json_body); + free(json_body); + + if (!event) { + printf("Status: 400 Bad Request\r\n"); + printf("Content-Type: application/json\r\n\r\n"); + printf("{\"error\":\"Invalid JSON\"}\n"); + return; + } + + // Verify it's Kind 23456 + cJSON* kind_obj = cJSON_GetObjectItem(event, "kind"); + if (!kind_obj || !cJSON_IsNumber(kind_obj) || + (int)cJSON_GetNumberValue(kind_obj) != 23456) { + cJSON_Delete(event); + printf("Status: 400 Bad Request\r\n"); + printf("Content-Type: application/json\r\n\r\n"); + printf("{\"error\":\"Event must be Kind 23456\"}\n"); + return; + } + + // Get event ID for response correlation + cJSON* id_obj = cJSON_GetObjectItem(event, "id"); + if (!id_obj || !cJSON_IsString(id_obj)) { + cJSON_Delete(event); + printf("Status: 400 Bad Request\r\n"); + printf("Content-Type: application/json\r\n\r\n"); + printf("{\"error\":\"Event missing id\"}\n"); + return; + } + const char* request_id = cJSON_GetStringValue(id_obj); + + // Get admin pubkey from event + cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey"); + if (!pubkey_obj || !cJSON_IsString(pubkey_obj)) { + cJSON_Delete(event); + printf("Status: 400 Bad Request\r\n"); + printf("Content-Type: application/json\r\n\r\n"); + printf("{\"error\":\"Event missing pubkey\"}\n"); + return; + } + const char* admin_pubkey = cJSON_GetStringValue(pubkey_obj); + + // Verify admin pubkey + sqlite3* db; + int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL); + if (rc != SQLITE_OK) { + cJSON_Delete(event); + printf("Status: 500 Internal Server Error\r\n"); + printf("Content-Type: application/json\r\n\r\n"); + printf("{\"error\":\"Database error\"}\n"); + return; + } + + sqlite3_stmt* stmt; + const char* sql = "SELECT value FROM config WHERE key = 'admin_pubkey'"; + int is_admin = 0; + + if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) { + if (sqlite3_step(stmt) == SQLITE_ROW) { + const char* db_admin_pubkey = (const char*)sqlite3_column_text(stmt, 0); + if (db_admin_pubkey && strcmp(admin_pubkey, db_admin_pubkey) == 0) { + is_admin = 1; + } + } + sqlite3_finalize(stmt); + } + sqlite3_close(db); + + if (!is_admin) { + cJSON_Delete(event); + printf("Status: 403 Forbidden\r\n"); + printf("Content-Type: application/json\r\n\r\n"); + printf("{\"error\":\"Not authorized as admin\"}\n"); + return; + } + + // Get encrypted content + cJSON* content_obj = cJSON_GetObjectItem(event, "content"); + if (!content_obj || !cJSON_IsString(content_obj)) { + cJSON_Delete(event); + printf("Status: 400 Bad Request\r\n"); + printf("Content-Type: application/json\r\n\r\n"); + printf("{\"error\":\"Event missing content\"}\n"); + return; + } + const char* encrypted_content = cJSON_GetStringValue(content_obj); + + // Get server private key for decryption + unsigned char server_privkey[32]; + if (get_server_privkey(server_privkey) != 0) { + cJSON_Delete(event); + printf("Status: 500 Internal Server Error\r\n"); + printf("Content-Type: application/json\r\n\r\n"); + printf("{\"error\":\"Failed to get server private key\"}\n"); + return; + } + + // Convert admin pubkey to bytes + unsigned char admin_pubkey_bytes[32]; + if (nostr_hex_to_bytes(admin_pubkey, admin_pubkey_bytes, 32) != 0) { + cJSON_Delete(event); + printf("Status: 400 Bad Request\r\n"); + printf("Content-Type: application/json\r\n\r\n"); + printf("{\"error\":\"Invalid admin pubkey format\"}\n"); + return; + } + + // Decrypt content using NIP-44 (or use plaintext for testing) + char decrypted_content[8192]; + const char* content_to_parse = encrypted_content; + + // Check if content is already plaintext JSON (starts with '[') + if (encrypted_content[0] != '[') { + // Content is encrypted, decrypt it + int decrypt_result = nostr_nip44_decrypt( + server_privkey, + admin_pubkey_bytes, + encrypted_content, + decrypted_content, + sizeof(decrypted_content) + ); + + if (decrypt_result != 0) { + cJSON_Delete(event); + printf("Status: 400 Bad Request\r\n"); + printf("Content-Type: application/json\r\n\r\n"); + printf("{\"error\":\"Failed to decrypt content\"}\n"); + return; + } + content_to_parse = decrypted_content; + } + + // Parse command array (either decrypted or plaintext) + cJSON* command_array = cJSON_Parse(content_to_parse); + if (!command_array || !cJSON_IsArray(command_array)) { + cJSON_Delete(event); + printf("Status: 400 Bad Request\r\n"); + printf("Content-Type: application/json\r\n\r\n"); + printf("{\"error\":\"Decrypted content is not a valid command array\"}\n"); + return; + } + + // Get command type + cJSON* command_type = cJSON_GetArrayItem(command_array, 0); + if (!command_type || !cJSON_IsString(command_type)) { + cJSON_Delete(command_array); + cJSON_Delete(event); + printf("Status: 400 Bad Request\r\n"); + printf("Content-Type: application/json\r\n\r\n"); + printf("{\"error\":\"Invalid command format\"}\n"); + return; + } + + const char* cmd = cJSON_GetStringValue(command_type); + + // Create response data object + cJSON* response_data = cJSON_CreateObject(); + cJSON_AddStringToObject(response_data, "query_type", cmd); + cJSON_AddNumberToObject(response_data, "timestamp", (double)time(NULL)); + + // Handle command + int result = -1; + if (strcmp(cmd, "config_query") == 0) { + result = handle_config_query_command(response_data); + } else { + cJSON_AddStringToObject(response_data, "status", "error"); + cJSON_AddStringToObject(response_data, "error", "Unknown command"); + } + + cJSON_Delete(command_array); + cJSON_Delete(event); + + if (result == 0) { + // Send Kind 23457 response + send_admin_response_event(admin_pubkey, request_id, response_data); + } else { + cJSON_Delete(response_data); + printf("Status: 500 Internal Server Error\r\n"); + printf("Content-Type: application/json\r\n\r\n"); + printf("{\"error\":\"Command processing failed\"}\n"); + } +} + +/** + * Get server private key from database (stored in blossom_seckey table) + */ +static int get_server_privkey(unsigned char* privkey_bytes) { + sqlite3* db; + int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL); + if (rc != SQLITE_OK) { + return -1; + } + + sqlite3_stmt* stmt; + const char* sql = "SELECT seckey FROM blossom_seckey LIMIT 1"; + int result = -1; + + if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) { + if (sqlite3_step(stmt) == SQLITE_ROW) { + const char* privkey_hex = (const char*)sqlite3_column_text(stmt, 0); + if (privkey_hex && nostr_hex_to_bytes(privkey_hex, privkey_bytes, 32) == 0) { + result = 0; + } + } + sqlite3_finalize(stmt); + } + sqlite3_close(db); + + return result; +} + +/** + * Get server public key from database (stored in config table as blossom_pubkey) + */ +static int get_server_pubkey(char* pubkey_hex, size_t size) { + sqlite3* db; + int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL); + if (rc != SQLITE_OK) { + return -1; + } + + sqlite3_stmt* stmt; + const char* sql = "SELECT value FROM config WHERE key = 'blossom_pubkey'"; + int result = -1; + + if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) { + if (sqlite3_step(stmt) == SQLITE_ROW) { + const char* pubkey = (const char*)sqlite3_column_text(stmt, 0); + if (pubkey) { + strncpy(pubkey_hex, pubkey, size - 1); + pubkey_hex[size - 1] = '\0'; + result = 0; + } + } + sqlite3_finalize(stmt); + } + sqlite3_close(db); + + return result; +} + +/** + * Handle config_query command - returns all config values + */ +static int handle_config_query_command(cJSON* response_data) { + sqlite3* db; + int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL); + if (rc != SQLITE_OK) { + cJSON_AddStringToObject(response_data, "status", "error"); + cJSON_AddStringToObject(response_data, "error", "Database error"); + return -1; + } + + cJSON_AddStringToObject(response_data, "status", "success"); + cJSON* data = cJSON_CreateObject(); + + // Query all config settings + sqlite3_stmt* stmt; + const char* sql = "SELECT key, value FROM config ORDER BY key"; + + if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) { + while (sqlite3_step(stmt) == SQLITE_ROW) { + const char* key = (const char*)sqlite3_column_text(stmt, 0); + const char* value = (const char*)sqlite3_column_text(stmt, 1); + if (key && value) { + cJSON_AddStringToObject(data, key, value); + } + } + sqlite3_finalize(stmt); + } + + cJSON_AddItemToObject(response_data, "data", data); + sqlite3_close(db); + + return 0; +} + +/** + * Send Kind 23457 admin response event + */ +static int send_admin_response_event(const char* admin_pubkey, const char* request_id, + cJSON* response_data) { + // Get server keys + unsigned char server_privkey[32]; + char server_pubkey[65]; + + if (get_server_privkey(server_privkey) != 0 || + get_server_pubkey(server_pubkey, sizeof(server_pubkey)) != 0) { + cJSON_Delete(response_data); + printf("Status: 500 Internal Server Error\r\n"); + printf("Content-Type: application/json\r\n\r\n"); + printf("{\"error\":\"Failed to get server keys\"}\n"); + return -1; + } + + // Convert response data to JSON string + char* response_json = cJSON_PrintUnformatted(response_data); + cJSON_Delete(response_data); + + if (!response_json) { + printf("Status: 500 Internal Server Error\r\n"); + printf("Content-Type: application/json\r\n\r\n"); + printf("{\"error\":\"Failed to serialize response\"}\n"); + return -1; + } + + // Convert admin pubkey to bytes for encryption + unsigned char admin_pubkey_bytes[32]; + if (nostr_hex_to_bytes(admin_pubkey, admin_pubkey_bytes, 32) != 0) { + free(response_json); + printf("Status: 500 Internal Server Error\r\n"); + printf("Content-Type: application/json\r\n\r\n"); + printf("{\"error\":\"Invalid admin pubkey\"}\n"); + return -1; + } + + // Encrypt response using NIP-44 + char encrypted_response[131072]; + int encrypt_result = nostr_nip44_encrypt( + server_privkey, + admin_pubkey_bytes, + response_json, + encrypted_response, + sizeof(encrypted_response) + ); + + free(response_json); + + if (encrypt_result != 0) { + printf("Status: 500 Internal Server Error\r\n"); + printf("Content-Type: application/json\r\n\r\n"); + printf("{\"error\":\"Failed to encrypt response\"}\n"); + return -1; + } + + // Create Kind 23457 response event + cJSON* response_event = cJSON_CreateObject(); + cJSON_AddStringToObject(response_event, "pubkey", server_pubkey); + cJSON_AddNumberToObject(response_event, "created_at", (double)time(NULL)); + cJSON_AddNumberToObject(response_event, "kind", 23457); + cJSON_AddStringToObject(response_event, "content", encrypted_response); + + // Add tags + cJSON* tags = cJSON_CreateArray(); + + // p tag for admin + cJSON* p_tag = cJSON_CreateArray(); + cJSON_AddItemToArray(p_tag, cJSON_CreateString("p")); + cJSON_AddItemToArray(p_tag, cJSON_CreateString(admin_pubkey)); + cJSON_AddItemToArray(tags, p_tag); + + // e tag for request correlation + cJSON* e_tag = cJSON_CreateArray(); + cJSON_AddItemToArray(e_tag, cJSON_CreateString("e")); + cJSON_AddItemToArray(e_tag, cJSON_CreateString(request_id)); + cJSON_AddItemToArray(tags, e_tag); + + cJSON_AddItemToObject(response_event, "tags", tags); + + // Sign the event + cJSON* signed_event = nostr_create_and_sign_event( + 23457, + encrypted_response, + tags, + server_privkey, + time(NULL) + ); + + cJSON_Delete(response_event); + + if (!signed_event) { + printf("Status: 500 Internal Server Error\r\n"); + printf("Content-Type: application/json\r\n\r\n"); + printf("{\"error\":\"Failed to sign response event\"}\n"); + return -1; + } + + // Return the signed event as HTTP response + char* event_json = cJSON_PrintUnformatted(signed_event); + cJSON_Delete(signed_event); + + if (!event_json) { + printf("Status: 500 Internal Server Error\r\n"); + printf("Content-Type: application/json\r\n\r\n"); + printf("{\"error\":\"Failed to serialize event\"}\n"); + return -1; + } + + printf("Status: 200 OK\r\n"); + printf("Content-Type: application/json\r\n"); + printf("Cache-Control: no-cache\r\n"); + printf("\r\n"); + printf("%s\n", event_json); + + free(event_json); + return 0; +} \ No newline at end of file diff --git a/src/bud08.c b/src/bud08.c index a5ba3ac..932dea7 100644 --- a/src/bud08.c +++ b/src/bud08.c @@ -10,8 +10,8 @@ #include #include "ginxsom.h" -// Database path -#define DB_PATH "db/ginxsom.db" +// Use global database path from main.c +extern char g_db_path[]; // Check if NIP-94 metadata emission is enabled int nip94_is_enabled(void) { @@ -19,7 +19,7 @@ int nip94_is_enabled(void) { sqlite3_stmt* stmt; int rc, enabled = 1; // Default enabled - rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); + rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL); if (rc) { return 1; // Default enabled on DB error } @@ -50,7 +50,7 @@ int nip94_get_origin(char* out, size_t out_size) { sqlite3_stmt* stmt; int rc; - rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); + 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 = 'cdn_origin'"; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); diff --git a/src/bud09.c b/src/bud09.c index 2a7ba79..e0b780f 100644 --- a/src/bud09.c +++ b/src/bud09.c @@ -11,8 +11,8 @@ #include #include "ginxsom.h" -// Database path (should match main.c) -#define DB_PATH "db/ginxsom.db" +// Use global database path from main.c +extern char g_db_path[]; // Forward declarations for helper functions void send_error_response(int status_code, const char* error_type, const char* message, const char* details); @@ -154,7 +154,7 @@ int store_blob_report(const char* event_json, const char* reporter_pubkey) { sqlite3_stmt* stmt; int rc; - rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL); + rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE, NULL); if (rc) { return 0; } diff --git a/src/ginxsom.h b/src/ginxsom.h index 89fb5df..2eb5261 100644 --- a/src/ginxsom.h +++ b/src/ginxsom.h @@ -10,8 +10,8 @@ // Version information (auto-updated by build system) #define VERSION_MAJOR 0 #define VERSION_MINOR 1 -#define VERSION_PATCH 9 -#define VERSION "v0.1.9" +#define VERSION_PATCH 10 +#define VERSION "v0.1.10" #include #include @@ -262,6 +262,9 @@ int validate_sha256_format(const char* sha256); // Admin API request handler void handle_admin_api_request(const char* method, const char* uri, const char* validated_pubkey, int is_authenticated); +// Admin event handler (Kind 23456/23457) +void handle_admin_event_request(void); + // Individual endpoint handlers void handle_stats_api(void); void handle_config_get_api(void); diff --git a/src/main.c b/src/main.c index 434c969..5af0b1b 100644 --- a/src/main.c +++ b/src/main.c @@ -26,13 +26,14 @@ #define MAX_MIME_LEN 128 // Configuration variables - can be overridden via command line -char g_db_path[MAX_PATH_LEN] = "db/ginxsom.db"; +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 -int g_generate_keys = 0; // Flag to generate keys on startup +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 @@ -192,7 +193,7 @@ int initialize_database(const char *db_path) { " ('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', 'false', 'Whether admin API is enabled')," + " ('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');"; @@ -245,6 +246,9 @@ 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); @@ -258,6 +262,76 @@ void handle_delete_request_with_validation(const char *sha256, nostr_request_res // 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/.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: .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"); @@ -309,6 +383,26 @@ int generate_server_keypair(void) { // 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) { @@ -321,7 +415,7 @@ int generate_server_keypair(void) { sqlite3_stmt *stmt; int rc; - rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL); + 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; @@ -354,6 +448,7 @@ int generate_server_keypair(void) { 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"); @@ -372,26 +467,30 @@ int load_server_keys(void) { // 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\n"); - // No private key found - check if we should generate one - if (g_generate_keys) { - fprintf(stderr, "STARTUP: No blossom private key found, generating new keypair...\n"); - if (generate_server_keypair() != 0) { - fprintf(stderr, "ERROR: Failed to generate server keypair\n"); - return -1; - } - } else { - fprintf(stderr, "WARNING: No blossom private key found. Use --generate-keys to create one.\n"); - // This is not fatal - server can still operate without admin features - } - } else { - fprintf(stderr, "STARTUP: Blossom private key loaded successfully\n"); + 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(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); + 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); @@ -410,7 +509,7 @@ int load_server_keys(void) { } } else { // Store admin pubkey in config if provided via command line - rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL); + 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); @@ -441,7 +540,7 @@ int store_blossom_private_key(const char *seckey) { } // Create blossom_seckey table if it doesn't exist - rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL); + 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; @@ -484,7 +583,7 @@ int get_blossom_private_key(char *seckey_out, size_t max_len) { sqlite3_stmt *stmt; int rc; - rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); + rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL); if (rc) { return -1; } @@ -520,7 +619,7 @@ int insert_blob_metadata(const char *sha256, long size, const char *type, sqlite3_stmt *stmt; int rc; - rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL); + 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; @@ -575,7 +674,7 @@ int get_blob_metadata(const char *sha256, blob_metadata_t *metadata) { sqlite3_stmt *stmt; int rc; - rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY | SQLITE_OPEN_CREATE, NULL); + 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; @@ -845,7 +944,7 @@ void handle_list_request(const char *pubkey) { sqlite3_stmt *stmt; int rc; - rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); + rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL); if (rc) { send_error_response(500, "database_error", "Failed to access database", @@ -992,7 +1091,7 @@ void handle_delete_request_with_validation(const char *sha256, nostr_request_res sqlite3_stmt *stmt; int rc; - rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL); + 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", @@ -1737,10 +1836,13 @@ int main(int argc, char *argv[]) { // 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(g_db_path, argv[i + 1], sizeof(g_db_path) - 1); + 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); @@ -1750,13 +1852,24 @@ int main(int argc, char *argv[]) { const char *help_text = "Usage: ginxsom-fcgi [options]\n" "Options:\n" - " --db-path PATH Database file path (default: db/ginxsom.db)\n" - " --storage-dir DIR Storage directory for files (default: blobs)\n" - " --admin-pubkey KEY Admin public key for management (64 hex chars)\n" - " --server-privkey KEY Server private key (64 hex chars, for testing)\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 server keypair on startup\n" - " --help, -h Show this help message\n"; + " --generate-keys Generate new keypair and create database (deprecated)\n" + " --help, -h Show this help message\n" + "\n" + "Database Naming:\n" + " Databases are named after the server's public key: db/.db\n" + " This ensures database-key consistency and prevents mismatches.\n" + "\n" + "Startup Scenarios:\n" + " 1. No arguments → Generate new keys, create db/.db\n" + " 2. --db-path → Open existing database, validate keys match filename\n" + " 3. --server-privkey → Create new database with those keys\n" + " 4. --test-keys → Use test keys, create/overwrite test database\n" + " 5. --db-path + --server-privkey → Validate keys match or error\n"; ssize_t written = write(STDOUT_FILENO, help_text, strlen(help_text)); (void)written; // Suppress unused variable warning return 0; @@ -1770,6 +1883,7 @@ int main(int argc, char *argv[]) { 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"); @@ -1777,8 +1891,27 @@ int main(int argc, char *argv[]) { } } - // Load test keys if requested + fprintf(stderr, "STARTUP: Using storage directory: %s\n", g_storage_dir); + + // CRITICAL: Initialize nostr crypto system BEFORE key operations + fprintf(stderr, "STARTUP: Initializing nostr crypto system...\r\n"); + int crypto_init_result = nostr_crypto_init(); + fprintf(stderr, "CRYPTO INIT RESULT: %d\r\n", crypto_init_result); + if (crypto_init_result != 0) { + fprintf(stderr, "FATAL ERROR: Failed to initialize nostr crypto system\r\n"); + return 1; + } + fprintf(stderr, "STARTUP: nostr crypto system initialized successfully\r\n"); + + // ======================================================================== + // DATABASE AND KEY INITIALIZATION - 5 SCENARIOS + // ======================================================================== + + // Scenario 4: Test Mode (--test-keys) if (use_test_keys) { + fprintf(stderr, "\n=== SCENARIO 4: TEST MODE ===\n"); + + // Load test keys from .test_keys file FILE *keys_file = fopen(".test_keys", "r"); if (!keys_file) { fprintf(stderr, "ERROR: Cannot open .test_keys file\n"); @@ -1787,7 +1920,6 @@ int main(int argc, char *argv[]) { char line[256]; while (fgets(line, sizeof(line), keys_file)) { - // Parse ADMIN_PUBKEY='...' if (strncmp(line, "ADMIN_PUBKEY='", 14) == 0) { char *start = line + 14; char *end = strchr(start, '\''); @@ -1796,7 +1928,6 @@ int main(int argc, char *argv[]) { g_admin_pubkey[64] = '\0'; } } - // Parse SERVER_PRIVKEY='...' else if (strncmp(line, "SERVER_PRIVKEY='", 16) == 0) { char *start = line + 16; char *end = strchr(start, '\''); @@ -1808,110 +1939,232 @@ int main(int argc, char *argv[]) { } fclose(keys_file); - fprintf(stderr, "STARTUP: Using test keys from .test_keys\n"); - fprintf(stderr, "STARTUP: Admin pubkey: %s\n", g_admin_pubkey); - fprintf(stderr, "STARTUP: Server privkey loaded from test keys\n"); - } - - fprintf(stderr, "STARTUP: Using database path: %s\n", g_db_path); - fprintf(stderr, "STARTUP: Using storage directory: %s\n", g_storage_dir); - if (strlen(g_admin_pubkey) > 0) { - fprintf(stderr, "STARTUP: Admin pubkey specified: %s\n", g_admin_pubkey); - } - if (g_generate_keys) { - fprintf(stderr, "STARTUP: Will generate server keypair\n"); - } - - fprintf(stderr, "DEBUG: About to initialize database\n"); - - // Initialize database (create if doesn't exist) - fprintf(stderr, "STARTUP: Initializing database...\n"); - if (initialize_database(g_db_path) != 0) { - fprintf(stderr, "FATAL ERROR: Failed to initialize database\n"); - return 1; - } - fprintf(stderr, "STARTUP: Database ready\n"); - - // CRITICAL: Initialize nostr crypto system BEFORE key operations - fprintf(stderr, "STARTUP: Initializing nostr crypto system...\r\n"); - int crypto_init_result = nostr_crypto_init(); - fprintf(stderr, "CRYPTO INIT RESULT: %d\r\n", crypto_init_result); - if (crypto_init_result != 0) { - fprintf(stderr, - "FATAL ERROR: Failed to initialize nostr crypto system\r\n"); - return 1; - } - fprintf(stderr, "STARTUP: nostr crypto system initialized successfully\r\n"); - - // Initialize server keys (now that crypto is initialized) - fprintf(stderr, "STARTUP: Initializing server keys...\n"); - fflush(stderr); - - // If test keys were provided via command line, store them in database - if (test_server_privkey[0] != '\0') { - // Store test private key in database + fprintf(stderr, "TEST MODE: Loaded keys from .test_keys\n"); + fprintf(stderr, "TEST MODE: Admin pubkey: %s\n", g_admin_pubkey); + + // Derive pubkey from test privkey + if (derive_pubkey_from_privkey(test_server_privkey, g_blossom_pubkey) != 0) { + fprintf(stderr, "ERROR: Failed to derive pubkey from test privkey\n"); + return 1; + } + + fprintf(stderr, "TEST MODE: Server pubkey: %s\n", g_blossom_pubkey); + + // Set database path based on test pubkey + if (set_db_path_from_pubkey(g_blossom_pubkey) != 0) { + fprintf(stderr, "ERROR: Failed to set database path\n"); + return 1; + } + + // Test mode ALWAYS overwrites database for clean testing + fprintf(stderr, "TEST MODE: Creating/overwriting database: %s\n", g_db_path); + unlink(g_db_path); // Remove if exists + + // Initialize new database + if (initialize_database(g_db_path) != 0) { + fprintf(stderr, "ERROR: Failed to initialize test database\n"); + return 1; + } + + // Store test keys strncpy(g_blossom_seckey, test_server_privkey, sizeof(g_blossom_seckey) - 1); g_blossom_seckey[64] = '\0'; - fprintf(stderr, "STARTUP: Storing test server private key in database...\n"); if (store_blossom_private_key(test_server_privkey) != 0) { fprintf(stderr, "ERROR: Failed to store test private key\n"); return 1; } - // Derive and store public key - unsigned char seckey_bytes[32]; - if (nostr_hex_to_bytes(test_server_privkey, seckey_bytes, 32) != NOSTR_SUCCESS) { - fprintf(stderr, "ERROR: Failed to parse test private key\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 from test private key\n"); - return 1; - } - - char pubkey_hex[65]; - nostr_bytes_to_hex(pubkey_bytes, 32, pubkey_hex); - - // Store server public key in config + // Store pubkey and admin pubkey in config sqlite3 *db; sqlite3_stmt *stmt; - int rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL); + 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, pubkey_hex, -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); } - fprintf(stderr, "STARTUP: Test server keys stored in database\n"); - fprintf(stderr, "STARTUP: Server pubkey: %s\n", pubkey_hex); - fprintf(stderr, "STARTUP: Admin pubkey: %s\n", g_admin_pubkey); + fprintf(stderr, "TEST MODE: Database initialized successfully\n"); + } + + // Scenario 3: Keys Specified (--server-privkey) + else if (test_server_privkey[0] != '\0') { + fprintf(stderr, "\n=== SCENARIO 3: KEYS SPECIFIED ===\n"); - // Now call load_server_keys to ensure admin_pubkey is also stored - int key_init_result = load_server_keys(); - if (key_init_result != 0) { - fprintf(stderr, "WARNING: Failed to complete key initialization\n"); - } - } else { - // Load keys from database (production mode) - int key_init_result = load_server_keys(); - fprintf(stderr, "KEY INIT RESULT: %d\n", key_init_result); - fflush(stderr); - if (key_init_result != 0) { - fprintf(stderr, "FATAL ERROR: Failed to initialize server keys\n"); + // 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, "STARTUP: Server keys initialized successfully\n"); + + fprintf(stderr, "KEYS: Derived pubkey: %s\n", g_blossom_pubkey); + + // Scenario 5: Both database and keys specified - validate match + if (db_path_specified) { + fprintf(stderr, "\n=== SCENARIO 5: DATABASE + KEYS (VALIDATION) ===\n"); + strncpy(g_db_path, specified_db_path, sizeof(g_db_path) - 1); + + // Check if database exists + struct stat st; + if (stat(g_db_path, &st) != 0) { + fprintf(stderr, "ERROR: Database file not found: %s\n", g_db_path); + return 1; + } + + // 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; + } + } + // 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 Specified (--db-path) + else if (db_path_specified) { + fprintf(stderr, "\n=== SCENARIO 2: DATABASE SPECIFIED ===\n"); + strncpy(g_db_path, specified_db_path, sizeof(g_db_path) - 1); + + // Check if database exists + struct stat st; + if (stat(g_db_path, &st) != 0) { + fprintf(stderr, "ERROR: Database file not found: %s\n", g_db_path); + fprintf(stderr, " → Specify a different database or let the application create a new one\n"); + return 1; + } + + 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"); + } + + // 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 + // ======================================================================== + + fprintf(stderr, "\n=== FINAL CONFIGURATION ===\n"); + fprintf(stderr, "Database path: %s\n", g_db_path); + fprintf(stderr, "Storage directory: %s\n", g_storage_dir); + fprintf(stderr, "Server pubkey: %s\n", g_blossom_pubkey); + if (strlen(g_admin_pubkey) > 0) { + fprintf(stderr, "Admin pubkey: %s\n", g_admin_pubkey); + } + fprintf(stderr, "===========================\n\n"); + fflush(stderr); // If --generate-keys was specified, exit after key generation @@ -1940,7 +2193,7 @@ if (!config_loaded /* && !initialize_server_config() */) { // Initialize request validator system fprintf(stderr, "STARTUP: Initializing request validator system...\r\n"); int validator_init_result = - ginxsom_request_validator_init(DB_PATH, "ginxsom"); + ginxsom_request_validator_init(g_db_path, "ginxsom"); fprintf(stderr, "MAIN: validator init return code: %d\r\n", validator_init_result); if (validator_init_result != NOSTR_SUCCESS) { @@ -2054,6 +2307,11 @@ if (!config_loaded /* && !initialize_server_config() */) { operation = "report"; } else if (strncmp(request_uri, "/api/", 5) == 0) { operation = "admin"; + // Special case: POST /api/admin uses Kind 23456 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) { @@ -2095,6 +2353,9 @@ if (!config_loaded /* && !initialize_server_config() */) { // 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 23456 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"; diff --git a/src/request_validator.c b/src/request_validator.c index b746476..3d125aa 100644 --- a/src/request_validator.c +++ b/src/request_validator.c @@ -32,8 +32,8 @@ // NOSTR_ERROR_NIP42_CHALLENGE_EXPIRED are already defined in // nostr_core_lib/nostr_core/nostr_common.h -// Database path (consistent with main.c) -#define DB_PATH "db/ginxsom.db" +// Use global database path from main.c +extern char g_db_path[]; // NIP-42 challenge management constants #define MAX_CHALLENGES 1000 @@ -1064,7 +1064,7 @@ static int reload_auth_config(void) { memset(&g_auth_cache, 0, sizeof(g_auth_cache)); // Open database - rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); + rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL); if (rc != SQLITE_OK) { validator_debug_log("VALIDATOR: Could not open database\n"); // Use defaults @@ -1345,7 +1345,7 @@ static int check_database_auth_rules(const char *pubkey, const char *operation, validator_debug_log(rules_msg); // Open database - rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); + rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL); if (rc != SQLITE_OK) { validator_debug_log( "VALIDATOR_DEBUG: RULES ENGINE - Failed to open database\n"); diff --git a/tests/admin_event_test.sh b/tests/admin_event_test.sh new file mode 100755 index 0000000..f706118 --- /dev/null +++ b/tests/admin_event_test.sh @@ -0,0 +1,206 @@ +#!/bin/bash + +# Ginxsom Admin Event Test Script +# Tests Kind 23456/23457 admin command system with NIP-44 encryption +# +# Prerequisites: +# - nak: https://github.com/fiatjaf/nak +# - curl +# - jq (for JSON parsing) +# - Server running with test keys from .test_keys + +set -e + +# Configuration +GINXSOM_URL="http://localhost:9001" +TEST_KEYS_FILE=".test_keys" + +# Load test keys +if [[ ! -f "$TEST_KEYS_FILE" ]]; then + echo "ERROR: $TEST_KEYS_FILE not found" + echo "Run the server with --test-keys to generate test keys" + exit 1 +fi + +source "$TEST_KEYS_FILE" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Helper functions +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +check_dependencies() { + log_info "Checking dependencies..." + + for cmd in nak curl jq; do + if ! command -v $cmd &> /dev/null; then + log_error "$cmd is not installed" + case $cmd in + nak) + echo "Install from: https://github.com/fiatjaf/nak" + ;; + jq) + echo "Install jq for JSON processing" + ;; + curl) + echo "curl should be available in most systems" + ;; + esac + exit 1 + fi + done + + log_success "All dependencies found" +} + +# Create NIP-44 encrypted admin command event (Kind 23456) +create_admin_command_event() { + local command="$1" + local expiration=$(($(date +%s) + 3600)) # 1 hour from now + + log_info "Creating Kind 23456 admin command event..." + log_info "Command: $command" + + # For now, we'll create the event structure manually since nak may not support NIP-44 encryption yet + # The content should be NIP-44 encrypted JSON array: ["config_query"] + # We'll use plaintext for initial testing and add encryption later + + local content="[\"$command\"]" + + # Create event with nak + # Kind 23456 = admin command + # Tags: p = server pubkey, expiration + local event=$(nak event -k 23456 \ + -c "$content" \ + --tag p="$SERVER_PUBKEY" \ + --tag expiration="$expiration" \ + --sec "$ADMIN_PRIVKEY") + + echo "$event" +} + +# Send admin command and parse response +send_admin_command() { + local command="$1" + + log_info "=== Testing Admin Command: $command ===" + + # Create Kind 23456 event + local event=$(create_admin_command_event "$command") + + if [[ -z "$event" ]]; then + log_error "Failed to create admin event" + return 1 + fi + + log_info "Event created successfully" + echo "$event" | jq . || echo "$event" + + # Send to server + log_info "Sending to POST $GINXSOM_URL/api/admin" + + local response=$(curl -s -w "\n%{http_code}" \ + -X POST \ + -H "Content-Type: application/json" \ + -d "$event" \ + "$GINXSOM_URL/api/admin") + + local http_code=$(echo "$response" | tail -n1) + local body=$(echo "$response" | head -n-1) + + echo "" + if [[ "$http_code" =~ ^2 ]]; then + log_success "HTTP $http_code - Response received" + echo "$body" | jq . 2>/dev/null || echo "$body" + + # Try to parse as Kind 23457 event + local kind=$(echo "$body" | jq -r '.kind // empty' 2>/dev/null) + if [[ "$kind" == "23457" ]]; then + log_success "Received Kind 23457 response event" + local response_content=$(echo "$body" | jq -r '.content // empty' 2>/dev/null) + log_info "Response content (encrypted): $response_content" + # TODO: Decrypt NIP-44 content to see actual response + fi + else + log_error "HTTP $http_code - Request failed" + echo "$body" | jq . 2>/dev/null || echo "$body" + return 1 + fi + + echo "" +} + +test_config_query() { + log_info "=== Testing config_query Command ===" + send_admin_command "config_query" +} + +test_server_health() { + log_info "=== Testing Server Health ===" + + local response=$(curl -s -w "\n%{http_code}" "$GINXSOM_URL/api/health") + local http_code=$(echo "$response" | tail -n1) + local body=$(echo "$response" | head -n-1) + + if [[ "$http_code" =~ ^2 ]]; then + log_success "Server is healthy (HTTP $http_code)" + echo "$body" | jq . + else + log_error "Server health check failed (HTTP $http_code)" + echo "$body" + return 1 + fi + echo "" +} + +main() { + echo "=== Ginxsom Admin Event Test Suite ===" + echo "Testing Kind 23456/23457 admin command system" + echo "" + + log_info "Test Configuration:" + log_info " Admin Pubkey: $ADMIN_PUBKEY" + log_info " Server Pubkey: $SERVER_PUBKEY" + log_info " Server URL: $GINXSOM_URL" + echo "" + + check_dependencies + echo "" + + # Test server health first + test_server_health + + # Test admin commands + test_config_query + + echo "" + log_success "Admin event testing complete!" + echo "" + log_warning "NOTE: NIP-44 encryption not yet implemented in test script" + log_warning "Events are sent with plaintext command arrays for initial testing" + log_warning "Production implementation will use full NIP-44 encryption" +} + +# Allow sourcing for individual function testing +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi \ No newline at end of file