1 Commits

Author SHA1 Message Date
Your Name
db7621a293 v0.1.10 - In the middle of working on getting admin api working 2025-11-21 11:54:17 -04:00
26 changed files with 1431 additions and 264 deletions

View File

@@ -1,4 +1,4 @@
ADMIN_PRIVKEY='22cc83aa57928a2800234c939240c9a6f0f44a33ea3838a860ed38930b195afd'
ADMIN_PUBKEY='8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e'
SERVER_PRIVKEY='c4e0d2ed7d36277d6698650f68a6e9199f91f3abb476a67f07303e81309c48f1'
SERVER_PUBKEY='52e366edfa4e9cc6a6d4653828e51ccf828a2f5a05227d7a768f33b5a198681a'
SERVER_PUBKEY='ebe82fbff0ff79b2973892eb285cafc767863e434f894838a548580266b70254'

View File

@@ -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

View File

@@ -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');

Binary file not shown.

BIN
build/admin_event.o Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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;

View File

@@ -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');

View File

@@ -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');

View File

@@ -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/<blossom_pubkey>.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/<new_pubkey>.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/<derived_pubkey>.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/<test_pubkey>.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/<pubkey>.db
│ │ └─ Create/overwrite database
│ │
│ ├─ Keys provided (--server-privkey)?
│ │ ├─ Validate privkey format
│ │ ├─ Derive pubkey
│ │ ├─ Set db_path = db/<pubkey>.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/<new_pubkey>.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/<new_pubkey>.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/<derived_pubkey>.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

View File

@@ -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/<new_pubkey>.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)

View File

@@ -11,8 +11,8 @@
#include <unistd.h>
#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);

471
src/admin_event.c Normal file
View File

@@ -0,0 +1,471 @@
// Admin event handler for Kind 23456/23457 admin commands
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#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;
}

View File

@@ -10,8 +10,8 @@
#include <stdint.h>
#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);

View File

@@ -11,8 +11,8 @@
#include <time.h>
#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;
}

View File

@@ -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 <stddef.h>
#include <stdint.h>
@@ -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);

View File

@@ -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/<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");
@@ -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/<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;
@@ -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";

View File

@@ -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");

206
tests/admin_event_test.sh Executable file
View File

@@ -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