Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db7621a293 | ||
|
|
e693fe3caa | ||
|
|
c1b615de32 |
@@ -1,4 +0,0 @@
|
|||||||
ADMIN_PRIVKEY='31d3fd4bb38f4f6b60fb66e0a2e5063703bb3394579ce820d5aaf3773b96633f'
|
|
||||||
ADMIN_PUBKEY='bd109762a8185716ec0fe0f887e911c30d40e36cf7b6bb99f6eef3301e9f6f99'
|
|
||||||
SERVER_PRIVKEY='c4e0d2ed7d36277d6698650f68a6e9199f91f3abb476a67f07303e81309c48f1'
|
|
||||||
SERVER_PUBKEY='52e366edfa4e9cc6a6d4653828e51ccf828a2f5a05227d7a768f33b5a198681a'
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,4 +2,5 @@ blossom/
|
|||||||
logs/
|
logs/
|
||||||
nostr_core_lib/
|
nostr_core_lib/
|
||||||
blobs/
|
blobs/
|
||||||
|
c-relay/
|
||||||
|
|
||||||
|
|||||||
4
.test_keys
Normal file
4
.test_keys
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
ADMIN_PRIVKEY='22cc83aa57928a2800234c939240c9a6f0f44a33ea3838a860ed38930b195afd'
|
||||||
|
ADMIN_PUBKEY='8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e'
|
||||||
|
SERVER_PRIVKEY='c4e0d2ed7d36277d6698650f68a6e9199f91f3abb476a67f07303e81309c48f1'
|
||||||
|
SERVER_PUBKEY='ebe82fbff0ff79b2973892eb285cafc767863e434f894838a548580266b70254'
|
||||||
2
Makefile
2
Makefile
@@ -8,7 +8,7 @@ BUILDDIR = build
|
|||||||
TARGET = $(BUILDDIR)/ginxsom-fcgi
|
TARGET = $(BUILDDIR)/ginxsom-fcgi
|
||||||
|
|
||||||
# Source files
|
# Source files
|
||||||
SOURCES = $(SRCDIR)/main.c $(SRCDIR)/admin_api.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)
|
OBJECTS = $(SOURCES:$(SRCDIR)/%.c=$(BUILDDIR)/%.o)
|
||||||
|
|
||||||
# Default target
|
# Default target
|
||||||
|
|||||||
1389
Trash/Ginxsom_Management_System_Design.md
Normal file
1389
Trash/Ginxsom_Management_System_Design.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -38,7 +38,7 @@ INSERT OR IGNORE INTO config (key, value, description) VALUES
|
|||||||
('auth_rules_enabled', 'false', 'Whether authentication rules are enabled for uploads'),
|
('auth_rules_enabled', 'false', 'Whether authentication rules are enabled for uploads'),
|
||||||
('server_name', 'ginxsom', 'Server name for responses'),
|
('server_name', 'ginxsom', 'Server name for responses'),
|
||||||
('admin_pubkey', '', 'Admin public key for API access'),
|
('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_require_auth', 'false', 'Enable NIP-42 challenge/response authentication'),
|
||||||
('nip42_challenge_timeout', '600', 'NIP-42 challenge timeout in seconds'),
|
('nip42_challenge_timeout', '600', 'NIP-42 challenge timeout in seconds'),
|
||||||
('nip42_time_tolerance', '300', 'NIP-42 timestamp tolerance in seconds');
|
('nip42_time_tolerance', '300', 'NIP-42 timestamp tolerance in seconds');
|
||||||
@@ -68,19 +68,6 @@ CREATE TABLE IF NOT EXISTS auth_rules (
|
|||||||
UNIQUE(rule_type, rule_target, operation)
|
UNIQUE(rule_type, rule_target, operation)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- 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))
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Indexes for performance optimization
|
-- 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_type_target ON auth_rules(rule_type, rule_target);
|
||||||
@@ -89,9 +76,6 @@ 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_priority ON auth_rules(priority);
|
||||||
CREATE INDEX IF NOT EXISTS idx_auth_rules_type_operation ON auth_rules(rule_type, operation, enabled);
|
CREATE INDEX IF NOT EXISTS idx_auth_rules_type_operation ON auth_rules(rule_type, operation, enabled);
|
||||||
|
|
||||||
-- 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);
|
|
||||||
|
|
||||||
-- View for storage statistics
|
-- View for storage statistics
|
||||||
CREATE VIEW IF NOT EXISTS storage_stats AS
|
CREATE VIEW IF NOT EXISTS storage_stats AS
|
||||||
Binary file not shown.
BIN
build/admin_auth.o
Normal file
BIN
build/admin_auth.o
Normal file
Binary file not shown.
BIN
build/admin_event.o
Normal file
BIN
build/admin_event.o
Normal file
Binary file not shown.
BIN
build/admin_handlers.o
Normal file
BIN
build/admin_handlers.o
Normal file
Binary file not shown.
BIN
build/admin_websocket.o
Normal file
BIN
build/admin_websocket.o
Normal file
Binary file not shown.
BIN
build/bud08.o
BIN
build/bud08.o
Binary file not shown.
BIN
build/bud09.o
BIN
build/bud09.o
Binary file not shown.
Binary file not shown.
BIN
build/main.o
BIN
build/main.o
Binary file not shown.
Binary file not shown.
@@ -2,7 +2,7 @@
|
|||||||
# Comprehensive Blossom Protocol Implementation
|
# Comprehensive Blossom Protocol Implementation
|
||||||
|
|
||||||
# Main context - specify error log here to override system default
|
# Main context - specify error log here to override system default
|
||||||
error_log logs/nginx/error.log debug;
|
error_log logs/nginx/error.log info;
|
||||||
pid logs/nginx/nginx.pid;
|
pid logs/nginx/nginx.pid;
|
||||||
|
|
||||||
events {
|
events {
|
||||||
@@ -221,7 +221,7 @@ http {
|
|||||||
|
|
||||||
# Admin API endpoints (/api/*)
|
# Admin API endpoints (/api/*)
|
||||||
location /api/ {
|
location /api/ {
|
||||||
if ($request_method !~ ^(GET|PUT)$) {
|
if ($request_method !~ ^(GET|PUT|POST)$) {
|
||||||
return 405;
|
return 405;
|
||||||
}
|
}
|
||||||
fastcgi_pass fastcgi_backend;
|
fastcgi_pass fastcgi_backend;
|
||||||
@@ -572,7 +572,7 @@ http {
|
|||||||
|
|
||||||
# Admin API endpoints (/api/*)
|
# Admin API endpoints (/api/*)
|
||||||
location /api/ {
|
location /api/ {
|
||||||
if ($request_method !~ ^(GET|PUT)$) {
|
if ($request_method !~ ^(GET|PUT|POST)$) {
|
||||||
return 405;
|
return 405;
|
||||||
}
|
}
|
||||||
fastcgi_pass fastcgi_backend;
|
fastcgi_pass fastcgi_backend;
|
||||||
|
|||||||
Binary file not shown.
BIN
db/ginxsom.db
BIN
db/ginxsom.db
Binary file not shown.
@@ -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');
|
|
||||||
21
deploy_lt.sh
21
deploy_lt.sh
@@ -13,6 +13,12 @@ print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
|||||||
print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
|
print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
|
||||||
print_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
print_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||||
|
|
||||||
|
# Parse command line arguments
|
||||||
|
FRESH_INSTALL=false
|
||||||
|
if [[ "$1" == "--fresh" ]]; then
|
||||||
|
FRESH_INSTALL=true
|
||||||
|
fi
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
REMOTE_HOST="laantungir.net"
|
REMOTE_HOST="laantungir.net"
|
||||||
REMOTE_USER="ubuntu"
|
REMOTE_USER="ubuntu"
|
||||||
@@ -68,7 +74,7 @@ print_status "Copying files to remote server..."
|
|||||||
|
|
||||||
# Copy entire project directory (excluding unnecessary files)
|
# Copy entire project directory (excluding unnecessary files)
|
||||||
print_status "Copying entire ginxsom project..."
|
print_status "Copying entire ginxsom project..."
|
||||||
rsync -avz --exclude='.git' --exclude='build' --exclude='logs' --exclude='Trash' --exclude='blobs' --exclude='db/ginxsom.db' --no-g --no-o --no-perms --omit-dir-times . $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/
|
rsync -avz --exclude='.git' --exclude='build' --exclude='logs' --exclude='Trash' --exclude='blobs' --exclude='db' --no-g --no-o --no-perms --omit-dir-times . $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/
|
||||||
|
|
||||||
# Build on remote server to ensure compatibility
|
# Build on remote server to ensure compatibility
|
||||||
print_status "Building ginxsom on remote server..."
|
print_status "Building ginxsom on remote server..."
|
||||||
@@ -162,6 +168,15 @@ ssh $REMOTE_USER@$REMOTE_HOST << EOF
|
|||||||
# Create db directory if it doesn't exist
|
# Create db directory if it doesn't exist
|
||||||
mkdir -p $REMOTE_DIR/db
|
mkdir -p $REMOTE_DIR/db
|
||||||
|
|
||||||
|
if [ "$FRESH_INSTALL" = "true" ]; then
|
||||||
|
echo "Fresh install: removing existing database and blobs..."
|
||||||
|
# Remove existing database
|
||||||
|
sudo rm -f $REMOTE_DB_PATH
|
||||||
|
sudo rm -f /var/www/html/blossom/ginxsom.db
|
||||||
|
# Remove existing blobs
|
||||||
|
sudo rm -rf $REMOTE_DATA_DIR/*
|
||||||
|
echo "Existing data removed"
|
||||||
|
else
|
||||||
# Backup current database if it exists in old location
|
# Backup current database if it exists in old location
|
||||||
if [ -f /var/www/html/blossom/ginxsom.db ]; then
|
if [ -f /var/www/html/blossom/ginxsom.db ]; then
|
||||||
echo "Backing up existing database..."
|
echo "Backing up existing database..."
|
||||||
@@ -179,6 +194,7 @@ ssh $REMOTE_USER@$REMOTE_HOST << EOF
|
|||||||
else
|
else
|
||||||
echo "Database already exists at $REMOTE_DB_PATH"
|
echo "Database already exists at $REMOTE_DB_PATH"
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Set proper permissions - www-data needs write access to db directory for SQLite journal files
|
# Set proper permissions - www-data needs write access to db directory for SQLite journal files
|
||||||
sudo chown -R www-data:www-data $REMOTE_DIR/db
|
sudo chown -R www-data:www-data $REMOTE_DIR/db
|
||||||
@@ -285,3 +301,6 @@ print_status "Test endpoints:"
|
|||||||
echo " Health: curl -k https://blossom.laantungir.net/health"
|
echo " Health: curl -k https://blossom.laantungir.net/health"
|
||||||
echo " Root: curl -k https://blossom.laantungir.net/"
|
echo " Root: curl -k https://blossom.laantungir.net/"
|
||||||
echo " List: curl -k https://blossom.laantungir.net/list"
|
echo " List: curl -k https://blossom.laantungir.net/list"
|
||||||
|
if [ "$FRESH_INSTALL" = "true" ]; then
|
||||||
|
print_warning "Fresh install completed - database and blobs have been reset"
|
||||||
|
fi
|
||||||
300
docs/DATABASE_NAMING_DESIGN.md
Normal file
300
docs/DATABASE_NAMING_DESIGN.md
Normal 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
|
||||||
994
docs/MANAGEMENT_SYSTEM_DESIGN.md
Normal file
994
docs/MANAGEMENT_SYSTEM_DESIGN.md
Normal file
@@ -0,0 +1,994 @@
|
|||||||
|
# Ginxsom Management System Design
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
This document outlines the design for a secure management interface for ginxsom (Blossom media storage server) based on c-relay's proven admin system architecture. The design uses Kind 23456/23457 events with NIP-44 encryption over WebSocket for real-time admin operations.
|
||||||
|
|
||||||
|
## 1. System Architecture
|
||||||
|
|
||||||
|
### 1.1 High-Level Overview
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
Admin[Admin Client] -->|WebSocket| WS[WebSocket Handler]
|
||||||
|
WS -->|Kind 23456| Auth[Admin Authorization]
|
||||||
|
Auth -->|Decrypt NIP-44| Decrypt[Command Decryption]
|
||||||
|
Decrypt -->|Parse JSON Array| Router[Command Router]
|
||||||
|
Router -->|Route by Command Type| Handlers[Unified Handlers]
|
||||||
|
Handlers -->|Execute| DB[(Database)]
|
||||||
|
Handlers -->|Execute| FS[File System]
|
||||||
|
Handlers -->|Generate Response| Encrypt[NIP-44 Encryption]
|
||||||
|
Encrypt -->|Kind 23457| WS
|
||||||
|
WS -->|WebSocket| Admin
|
||||||
|
|
||||||
|
style Admin fill:#e1f5ff
|
||||||
|
style Auth fill:#fff3cd
|
||||||
|
style Handlers fill:#d4edda
|
||||||
|
style DB fill:#f8d7da
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 Component Architecture
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
subgraph "Admin Interface"
|
||||||
|
CLI[CLI Tool]
|
||||||
|
Web[Web Dashboard]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "ginxsom FastCGI Process"
|
||||||
|
WS[WebSocket Endpoint]
|
||||||
|
Auth[Authorization Layer]
|
||||||
|
Router[Command Router]
|
||||||
|
|
||||||
|
subgraph "Unified Handlers"
|
||||||
|
BlobH[Blob Handler]
|
||||||
|
StorageH[Storage Handler]
|
||||||
|
ConfigH[Config Handler]
|
||||||
|
StatsH[Stats Handler]
|
||||||
|
SystemH[System Handler]
|
||||||
|
end
|
||||||
|
|
||||||
|
DB[(SQLite Database)]
|
||||||
|
Storage[Blob Storage]
|
||||||
|
end
|
||||||
|
|
||||||
|
CLI -->|WebSocket| WS
|
||||||
|
Web -->|WebSocket| WS
|
||||||
|
WS --> Auth
|
||||||
|
Auth --> Router
|
||||||
|
Router --> BlobH
|
||||||
|
Router --> StorageH
|
||||||
|
Router --> ConfigH
|
||||||
|
Router --> StatsH
|
||||||
|
Router --> SystemH
|
||||||
|
|
||||||
|
BlobH --> DB
|
||||||
|
BlobH --> Storage
|
||||||
|
StorageH --> Storage
|
||||||
|
ConfigH --> DB
|
||||||
|
StatsH --> DB
|
||||||
|
SystemH --> DB
|
||||||
|
|
||||||
|
style Auth fill:#fff3cd
|
||||||
|
style Router fill:#d4edda
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 Data Flow for Admin Commands
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Admin
|
||||||
|
participant WebSocket
|
||||||
|
participant Auth
|
||||||
|
participant Handler
|
||||||
|
participant Database
|
||||||
|
|
||||||
|
Admin->>WebSocket: Kind 23456 Event (NIP-44 encrypted)
|
||||||
|
WebSocket->>Auth: Verify admin signature
|
||||||
|
Auth->>Auth: Check pubkey matches admin_pubkey
|
||||||
|
Auth->>Auth: Verify event signature
|
||||||
|
Auth->>WebSocket: Authorization OK
|
||||||
|
WebSocket->>Handler: Decrypt & parse command array
|
||||||
|
Handler->>Handler: Validate command structure
|
||||||
|
Handler->>Database: Execute operation
|
||||||
|
Database-->>Handler: Result
|
||||||
|
Handler->>Handler: Build response JSON
|
||||||
|
Handler->>WebSocket: Encrypt response (NIP-44)
|
||||||
|
WebSocket->>Admin: Kind 23457 Event (encrypted response)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.4 Integration with Existing Ginxsom
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph "Existing Ginxsom"
|
||||||
|
Main[main.c]
|
||||||
|
BUD04[bud04.c - Mirror]
|
||||||
|
BUD06[bud06.c - Requirements]
|
||||||
|
BUD08[bud08.c - NIP-94]
|
||||||
|
BUD09[bud09.c - Report]
|
||||||
|
AdminAPI[admin_api.c - Basic Admin]
|
||||||
|
Validator[request_validator.c]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "New Management System"
|
||||||
|
AdminWS[admin_websocket.c]
|
||||||
|
AdminAuth[admin_auth.c]
|
||||||
|
AdminHandlers[admin_handlers.c]
|
||||||
|
AdminConfig[admin_config.c]
|
||||||
|
end
|
||||||
|
|
||||||
|
Main -->|Initialize| AdminWS
|
||||||
|
AdminWS -->|Use| AdminAuth
|
||||||
|
AdminWS -->|Route to| AdminHandlers
|
||||||
|
AdminHandlers -->|Query| BUD04
|
||||||
|
AdminHandlers -->|Query| BUD06
|
||||||
|
AdminHandlers -->|Query| BUD08
|
||||||
|
AdminHandlers -->|Query| BUD09
|
||||||
|
AdminHandlers -->|Update| AdminConfig
|
||||||
|
AdminAuth -->|Use| Validator
|
||||||
|
|
||||||
|
style AdminWS fill:#d4edda
|
||||||
|
style AdminAuth fill:#fff3cd
|
||||||
|
style AdminHandlers fill:#e1f5ff
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Database Schema
|
||||||
|
|
||||||
|
### 2.1 Core Tables
|
||||||
|
|
||||||
|
Following c-relay's minimal approach, we need only two tables for key management:
|
||||||
|
|
||||||
|
#### relay_seckey Table
|
||||||
|
```sql
|
||||||
|
-- Stores relay's private key (used for signing Kind 23457 responses)
|
||||||
|
CREATE TABLE relay_seckey (
|
||||||
|
private_key_hex TEXT NOT NULL CHECK (length(private_key_hex) = 64),
|
||||||
|
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: This table stores the relay's private key as plain hex (no encryption). The key is used to:
|
||||||
|
- Sign Kind 23457 response events
|
||||||
|
- Encrypt responses using NIP-44 (shared secret with admin pubkey)
|
||||||
|
|
||||||
|
#### config Table (Extended)
|
||||||
|
```sql
|
||||||
|
-- Existing config table, add admin_pubkey entry
|
||||||
|
INSERT INTO config (key, value, data_type, description, category, requires_restart)
|
||||||
|
VALUES (
|
||||||
|
'admin_pubkey',
|
||||||
|
'<64-char-hex-pubkey>',
|
||||||
|
'string',
|
||||||
|
'Public key of authorized admin (hex format)',
|
||||||
|
'security',
|
||||||
|
0
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: Admin public key is stored in the config table, not a separate table. Admin private key is NEVER stored anywhere.
|
||||||
|
|
||||||
|
### 2.2 Schema Comparison with c-relay
|
||||||
|
|
||||||
|
| c-relay | ginxsom | Purpose |
|
||||||
|
|---------|---------|---------|
|
||||||
|
| `relay_seckey` (private_key_hex, created_at) | `relay_seckey` (private_key_hex, created_at) | Relay private key storage |
|
||||||
|
| `config` table entry for admin_pubkey | `config` table entry for admin_pubkey | Admin authorization |
|
||||||
|
| No audit log | No audit log | Keep it simple |
|
||||||
|
| No processed events tracking | No processed events tracking | Stateless processing |
|
||||||
|
|
||||||
|
### 2.3 Key Storage Strategy
|
||||||
|
|
||||||
|
**Relay Private Key**:
|
||||||
|
- Stored in `relay_seckey` table as plain 64-character hex
|
||||||
|
- Generated on first startup or provided via `--relay-privkey` CLI option
|
||||||
|
- Used for signing Kind 23457 responses and NIP-44 encryption
|
||||||
|
- Never exposed via API
|
||||||
|
|
||||||
|
**Admin Public Key**:
|
||||||
|
- Stored in `config` table as plain 64-character hex
|
||||||
|
- Generated on first startup or provided via `--admin-pubkey` CLI option
|
||||||
|
- Used to verify Kind 23456 command signatures
|
||||||
|
- Can be queried via admin API
|
||||||
|
|
||||||
|
**Admin Private Key**:
|
||||||
|
- NEVER stored anywhere in the system
|
||||||
|
- Kept only by the admin in their client/tool
|
||||||
|
- Used to sign Kind 23456 commands and decrypt Kind 23457 responses
|
||||||
|
|
||||||
|
## 3. API Design
|
||||||
|
|
||||||
|
### 3.1 Command Structure
|
||||||
|
|
||||||
|
Following c-relay's pattern, all commands use JSON array format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
["command_name", {"param1": "value1", "param2": "value2"}]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Event Structure
|
||||||
|
|
||||||
|
#### Kind 23456 - Admin Command Event
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 23456,
|
||||||
|
"pubkey": "<admin-pubkey-hex>",
|
||||||
|
"created_at": 1234567890,
|
||||||
|
"tags": [
|
||||||
|
["p", "<relay-pubkey-hex>"]
|
||||||
|
],
|
||||||
|
"content": "<nip44-encrypted-command-array>",
|
||||||
|
"sig": "<signature>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Content (decrypted)**:
|
||||||
|
```json
|
||||||
|
["blob_list", {"limit": 100, "offset": 0}]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Kind 23457 - Admin Response Event
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 23457,
|
||||||
|
"pubkey": "<relay-pubkey-hex>",
|
||||||
|
"created_at": 1234567890,
|
||||||
|
"tags": [
|
||||||
|
["p", "<admin-pubkey-hex>"],
|
||||||
|
["e", "<original-command-event-id>"]
|
||||||
|
],
|
||||||
|
"content": "<nip44-encrypted-response>",
|
||||||
|
"sig": "<signature>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Content (decrypted)**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"blobs": [
|
||||||
|
{"sha256": "abc123...", "size": 1024, "type": "image/png"},
|
||||||
|
{"sha256": "def456...", "size": 2048, "type": "video/mp4"}
|
||||||
|
],
|
||||||
|
"total": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Command Categories
|
||||||
|
|
||||||
|
#### Blob Operations
|
||||||
|
- `blob_list` - List blobs with pagination
|
||||||
|
- `blob_info` - Get detailed blob information
|
||||||
|
- `blob_delete` - Delete blob(s)
|
||||||
|
- `blob_mirror` - Mirror blob from another server
|
||||||
|
|
||||||
|
#### Storage Management
|
||||||
|
- `storage_stats` - Get storage usage statistics
|
||||||
|
- `storage_quota` - Get/set storage quotas
|
||||||
|
- `storage_cleanup` - Clean up orphaned files
|
||||||
|
|
||||||
|
#### Configuration
|
||||||
|
- `config_get` - Get configuration value(s)
|
||||||
|
- `config_set` - Set configuration value(s)
|
||||||
|
- `config_list` - List all configuration
|
||||||
|
- `auth_rules_list` - List authentication rules
|
||||||
|
- `auth_rules_add` - Add authentication rule
|
||||||
|
- `auth_rules_remove` - Remove authentication rule
|
||||||
|
|
||||||
|
#### Statistics
|
||||||
|
- `stats_uploads` - Upload statistics
|
||||||
|
- `stats_bandwidth` - Bandwidth usage
|
||||||
|
- `stats_storage` - Storage usage over time
|
||||||
|
- `stats_users` - User activity statistics
|
||||||
|
|
||||||
|
#### System
|
||||||
|
- `system_info` - Get system information
|
||||||
|
- `system_restart` - Restart server (graceful)
|
||||||
|
- `system_backup` - Trigger database backup
|
||||||
|
- `system_restore` - Restore from backup
|
||||||
|
|
||||||
|
### 3.4 Command Examples
|
||||||
|
|
||||||
|
#### Example 1: List Blobs
|
||||||
|
```json
|
||||||
|
// Command (Kind 23456 content, decrypted)
|
||||||
|
["blob_list", {
|
||||||
|
"limit": 50,
|
||||||
|
"offset": 0,
|
||||||
|
"type": "image/*",
|
||||||
|
"sort": "created_at",
|
||||||
|
"order": "desc"
|
||||||
|
}]
|
||||||
|
|
||||||
|
// Response (Kind 23457 content, decrypted)
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"blobs": [
|
||||||
|
{
|
||||||
|
"sha256": "abc123...",
|
||||||
|
"size": 102400,
|
||||||
|
"type": "image/png",
|
||||||
|
"created": 1234567890,
|
||||||
|
"url": "https://blossom.example.com/abc123.png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 150,
|
||||||
|
"limit": 50,
|
||||||
|
"offset": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example 2: Delete Blob
|
||||||
|
```json
|
||||||
|
// Command
|
||||||
|
["blob_delete", {
|
||||||
|
"sha256": "abc123...",
|
||||||
|
"confirm": true
|
||||||
|
}]
|
||||||
|
|
||||||
|
// Response
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"deleted": true,
|
||||||
|
"sha256": "abc123...",
|
||||||
|
"freed_bytes": 102400
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example 3: Get Storage Stats
|
||||||
|
```json
|
||||||
|
// Command
|
||||||
|
["storage_stats", {}]
|
||||||
|
|
||||||
|
// Response
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"total_blobs": 1500,
|
||||||
|
"total_bytes": 5368709120,
|
||||||
|
"total_bytes_human": "5.0 GB",
|
||||||
|
"disk_usage": {
|
||||||
|
"used": 5368709120,
|
||||||
|
"available": 94631291904,
|
||||||
|
"total": 100000000000,
|
||||||
|
"percent": 5.4
|
||||||
|
},
|
||||||
|
"by_type": {
|
||||||
|
"image/png": {"count": 500, "bytes": 2147483648},
|
||||||
|
"image/jpeg": {"count": 300, "bytes": 1610612736},
|
||||||
|
"video/mp4": {"count": 200, "bytes": 1610612736}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example 4: Set Configuration
|
||||||
|
```json
|
||||||
|
// Command
|
||||||
|
["config_set", {
|
||||||
|
"max_upload_size": 10485760,
|
||||||
|
"allowed_mime_types": ["image/*", "video/mp4"]
|
||||||
|
}]
|
||||||
|
|
||||||
|
// Response
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"updated": ["max_upload_size", "allowed_mime_types"],
|
||||||
|
"requires_restart": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 Error Handling
|
||||||
|
|
||||||
|
All errors follow consistent format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": {
|
||||||
|
"code": "BLOB_NOT_FOUND",
|
||||||
|
"message": "Blob with hash abc123... not found",
|
||||||
|
"details": {
|
||||||
|
"sha256": "abc123..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Codes**:
|
||||||
|
- `UNAUTHORIZED` - Invalid admin signature
|
||||||
|
- `INVALID_COMMAND` - Unknown command or malformed structure
|
||||||
|
- `INVALID_PARAMS` - Missing or invalid parameters
|
||||||
|
- `BLOB_NOT_FOUND` - Requested blob doesn't exist
|
||||||
|
- `STORAGE_FULL` - Storage quota exceeded
|
||||||
|
- `DATABASE_ERROR` - Database operation failed
|
||||||
|
- `SYSTEM_ERROR` - Internal server error
|
||||||
|
|
||||||
|
## 4. File Structure
|
||||||
|
|
||||||
|
### 4.1 New Files to Create
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── admin_websocket.c # WebSocket endpoint for admin commands
|
||||||
|
├── admin_websocket.h # WebSocket handler declarations
|
||||||
|
├── admin_auth.c # Admin authorization (adapted from c-relay)
|
||||||
|
├── admin_auth.h # Authorization function declarations
|
||||||
|
├── admin_handlers.c # Unified command handlers
|
||||||
|
├── admin_handlers.h # Handler function declarations
|
||||||
|
├── admin_config.c # Configuration management
|
||||||
|
├── admin_config.h # Config function declarations
|
||||||
|
└── admin_keys.c # Key generation and storage
|
||||||
|
admin_keys.h # Key management declarations
|
||||||
|
|
||||||
|
include/
|
||||||
|
└── admin_system.h # Public admin system interface
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Files to Adapt from c-relay
|
||||||
|
|
||||||
|
| c-relay File | Purpose | Adaptation for ginxsom |
|
||||||
|
|--------------|---------|------------------------|
|
||||||
|
| `dm_admin.c` | Admin event processing | → `admin_websocket.c` (WebSocket instead of DM) |
|
||||||
|
| `api.c` (lines 768-838) | NIP-44 encryption/response | → `admin_handlers.c` (response generation) |
|
||||||
|
| `config.c` (lines 500-583) | Key storage/retrieval | → `admin_keys.c` (relay key management) |
|
||||||
|
| `main.c` (lines 1389-1556) | CLI argument parsing | → `main.c` (add admin CLI options) |
|
||||||
|
|
||||||
|
### 4.3 Integration with Existing Files
|
||||||
|
|
||||||
|
**src/main.c**:
|
||||||
|
- Add CLI options: `--admin-pubkey`, `--relay-privkey`
|
||||||
|
- Initialize admin WebSocket endpoint
|
||||||
|
- Generate keys on first startup
|
||||||
|
|
||||||
|
**src/admin_api.c** (existing):
|
||||||
|
- Keep existing basic admin API
|
||||||
|
- Add WebSocket admin endpoint
|
||||||
|
- Route Kind 23456 events to new handlers
|
||||||
|
|
||||||
|
**db/schema.sql**:
|
||||||
|
- Add `relay_seckey` table
|
||||||
|
- Add `admin_pubkey` to config table
|
||||||
|
|
||||||
|
## 5. Implementation Plan
|
||||||
|
|
||||||
|
### 5.1 Phase 1: Foundation (Week 1)
|
||||||
|
|
||||||
|
**Goal**: Set up key management and database schema
|
||||||
|
|
||||||
|
**Tasks**:
|
||||||
|
1. Create `relay_seckey` table in schema
|
||||||
|
2. Add `admin_pubkey` to config table
|
||||||
|
3. Implement `admin_keys.c`:
|
||||||
|
- `generate_relay_keypair()`
|
||||||
|
- `generate_admin_keypair()`
|
||||||
|
- `store_relay_private_key()`
|
||||||
|
- `load_relay_private_key()`
|
||||||
|
- `get_admin_pubkey()`
|
||||||
|
4. Update `main.c`:
|
||||||
|
- Add CLI options (`--admin-pubkey`, `--relay-privkey`)
|
||||||
|
- Generate keys on first startup
|
||||||
|
- Print keys once (like c-relay)
|
||||||
|
5. Test key generation and storage
|
||||||
|
|
||||||
|
**Deliverables**:
|
||||||
|
- Working key generation
|
||||||
|
- Keys stored in database
|
||||||
|
- CLI options functional
|
||||||
|
|
||||||
|
### 5.2 Phase 2: Authorization (Week 2)
|
||||||
|
|
||||||
|
**Goal**: Implement admin event authorization
|
||||||
|
|
||||||
|
**Tasks**:
|
||||||
|
1. Create `admin_auth.c` (adapted from c-relay's authorization):
|
||||||
|
- `verify_admin_event()` - Check Kind 23456 signature
|
||||||
|
- `check_admin_pubkey()` - Verify against stored admin_pubkey
|
||||||
|
- `verify_relay_target()` - Check 'p' tag matches relay pubkey
|
||||||
|
2. Add NIP-44 crypto functions (use existing nostr_core_lib):
|
||||||
|
- `decrypt_admin_command()` - Decrypt Kind 23456 content
|
||||||
|
- `encrypt_admin_response()` - Encrypt Kind 23457 content
|
||||||
|
3. Test authorization flow
|
||||||
|
4. Test encryption/decryption
|
||||||
|
|
||||||
|
**Deliverables**:
|
||||||
|
- Working authorization layer
|
||||||
|
- NIP-44 encryption functional
|
||||||
|
- Unit tests for auth
|
||||||
|
|
||||||
|
### 5.3 Phase 3: WebSocket Endpoint (Week 3)
|
||||||
|
|
||||||
|
**Goal**: Create WebSocket handler for admin commands
|
||||||
|
|
||||||
|
**Tasks**:
|
||||||
|
1. Create `admin_websocket.c`:
|
||||||
|
- WebSocket endpoint at `/admin` or similar
|
||||||
|
- Receive Kind 23456 events
|
||||||
|
- Route to authorization layer
|
||||||
|
- Parse command array from decrypted content
|
||||||
|
- Route to appropriate handler
|
||||||
|
- Build Kind 23457 response
|
||||||
|
- Send encrypted response
|
||||||
|
2. Integrate with existing FastCGI WebSocket handling
|
||||||
|
3. Add connection management
|
||||||
|
4. Test WebSocket communication
|
||||||
|
|
||||||
|
**Deliverables**:
|
||||||
|
- Working WebSocket endpoint
|
||||||
|
- Event routing functional
|
||||||
|
- Response generation working
|
||||||
|
|
||||||
|
### 5.4 Phase 4: Command Handlers (Week 4-5)
|
||||||
|
|
||||||
|
**Goal**: Implement unified command handlers
|
||||||
|
|
||||||
|
**Tasks**:
|
||||||
|
1. Create `admin_handlers.c` with unified handler pattern:
|
||||||
|
- `handle_blob_command()` - Blob operations
|
||||||
|
- `handle_storage_command()` - Storage management
|
||||||
|
- `handle_config_command()` - Configuration
|
||||||
|
- `handle_stats_command()` - Statistics
|
||||||
|
- `handle_system_command()` - System operations
|
||||||
|
2. Implement each command:
|
||||||
|
- Blob: list, info, delete, mirror
|
||||||
|
- Storage: stats, quota, cleanup
|
||||||
|
- Config: get, set, list, auth_rules
|
||||||
|
- Stats: uploads, bandwidth, storage, users
|
||||||
|
- System: info, restart, backup, restore
|
||||||
|
3. Add validation for each command
|
||||||
|
4. Test each command individually
|
||||||
|
|
||||||
|
**Deliverables**:
|
||||||
|
- All commands implemented
|
||||||
|
- Validation working
|
||||||
|
- Integration tests passing
|
||||||
|
|
||||||
|
### 5.5 Phase 5: Testing & Documentation (Week 6)
|
||||||
|
|
||||||
|
**Goal**: Comprehensive testing and documentation
|
||||||
|
|
||||||
|
**Tasks**:
|
||||||
|
1. Create test suite:
|
||||||
|
- Unit tests for each handler
|
||||||
|
- Integration tests for full flow
|
||||||
|
- Security tests for authorization
|
||||||
|
- Performance tests for WebSocket
|
||||||
|
2. Create admin CLI tool (simple Node.js/Python script):
|
||||||
|
- Generate Kind 23456 events
|
||||||
|
- Send via WebSocket
|
||||||
|
- Decrypt Kind 23457 responses
|
||||||
|
- Pretty-print results
|
||||||
|
3. Write documentation:
|
||||||
|
- Admin API reference
|
||||||
|
- CLI tool usage guide
|
||||||
|
- Security best practices
|
||||||
|
- Troubleshooting guide
|
||||||
|
4. Create example scripts
|
||||||
|
|
||||||
|
**Deliverables**:
|
||||||
|
- Complete test suite
|
||||||
|
- Working CLI tool
|
||||||
|
- Full documentation
|
||||||
|
- Example scripts
|
||||||
|
|
||||||
|
### 5.6 Phase 6: Web Dashboard (Optional, Week 7-8)
|
||||||
|
|
||||||
|
**Goal**: Create web-based admin interface
|
||||||
|
|
||||||
|
**Tasks**:
|
||||||
|
1. Design web UI (React/Vue/Svelte)
|
||||||
|
2. Implement WebSocket client
|
||||||
|
3. Create command forms
|
||||||
|
4. Add real-time updates
|
||||||
|
5. Deploy dashboard
|
||||||
|
|
||||||
|
**Deliverables**:
|
||||||
|
- Working web dashboard
|
||||||
|
- User documentation
|
||||||
|
- Deployment guide
|
||||||
|
|
||||||
|
## 6. Security Considerations
|
||||||
|
|
||||||
|
### 6.1 Key Security
|
||||||
|
|
||||||
|
**Relay Private Key**:
|
||||||
|
- Stored in database as plain hex (following c-relay pattern)
|
||||||
|
- Never exposed via API
|
||||||
|
- Used only for signing responses
|
||||||
|
- Backed up with database
|
||||||
|
|
||||||
|
**Admin Private Key**:
|
||||||
|
- NEVER stored on server
|
||||||
|
- Kept only by admin
|
||||||
|
- Used to sign commands
|
||||||
|
- Should be stored securely by admin (password manager, hardware key, etc.)
|
||||||
|
|
||||||
|
**Admin Public Key**:
|
||||||
|
- Stored in config table
|
||||||
|
- Used for authorization
|
||||||
|
- Can be rotated by updating config
|
||||||
|
|
||||||
|
### 6.2 Authorization Flow
|
||||||
|
|
||||||
|
1. Receive Kind 23456 event
|
||||||
|
2. Verify event signature (nostr_verify_event_signature)
|
||||||
|
3. Check pubkey matches admin_pubkey from config
|
||||||
|
4. Verify 'p' tag targets this relay
|
||||||
|
5. Decrypt content using NIP-44
|
||||||
|
6. Parse and validate command
|
||||||
|
7. Execute command
|
||||||
|
8. Encrypt response using NIP-44
|
||||||
|
9. Sign Kind 23457 response
|
||||||
|
10. Send response
|
||||||
|
|
||||||
|
### 6.3 Attack Mitigation
|
||||||
|
|
||||||
|
**Replay Attacks**:
|
||||||
|
- Check event timestamp (reject old events)
|
||||||
|
- Optional: Track processed event IDs (if needed)
|
||||||
|
|
||||||
|
**Unauthorized Access**:
|
||||||
|
- Strict pubkey verification
|
||||||
|
- Signature validation
|
||||||
|
- Relay targeting check
|
||||||
|
|
||||||
|
**Command Injection**:
|
||||||
|
- Validate all command parameters
|
||||||
|
- Use parameterized SQL queries
|
||||||
|
- Sanitize file paths
|
||||||
|
|
||||||
|
**DoS Protection**:
|
||||||
|
- Rate limit admin commands
|
||||||
|
- Timeout long-running operations
|
||||||
|
- Limit response sizes
|
||||||
|
|
||||||
|
## 7. Command Line Interface
|
||||||
|
|
||||||
|
### 7.1 CLI Options (Following c-relay Pattern)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ginxsom [OPTIONS]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h, --help Show help message
|
||||||
|
-v, --version Show version information
|
||||||
|
-p, --port PORT Override server port
|
||||||
|
--strict-port Fail if exact port unavailable
|
||||||
|
-a, --admin-pubkey KEY Override admin public key (hex or npub)
|
||||||
|
-r, --relay-privkey KEY Override relay private key (hex or nsec)
|
||||||
|
--debug-level=N Set debug level (0-5)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
ginxsom # Start server (auto-generate keys on first run)
|
||||||
|
ginxsom -p 8080 # Start on port 8080
|
||||||
|
ginxsom -a <npub> # Set admin pubkey
|
||||||
|
ginxsom -r <nsec> # Set relay privkey
|
||||||
|
ginxsom --debug-level=3 # Enable info-level debugging
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 First Startup Behavior
|
||||||
|
|
||||||
|
On first startup (no database exists):
|
||||||
|
|
||||||
|
1. Generate relay keypair
|
||||||
|
2. Generate admin keypair
|
||||||
|
3. Print keys ONCE to console:
|
||||||
|
```
|
||||||
|
=== Ginxsom First Startup ===
|
||||||
|
|
||||||
|
Relay Keys (for server):
|
||||||
|
Public Key (npub): npub1...
|
||||||
|
Private Key (nsec): nsec1...
|
||||||
|
|
||||||
|
Admin Keys (for you):
|
||||||
|
Public Key (npub): npub1...
|
||||||
|
Private Key (nsec): nsec1...
|
||||||
|
|
||||||
|
IMPORTANT: Save these keys securely!
|
||||||
|
The admin private key will NOT be shown again.
|
||||||
|
The relay private key is stored in the database.
|
||||||
|
|
||||||
|
Database created: <relay-pubkey>.db
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Store relay private key in database
|
||||||
|
5. Store admin public key in config
|
||||||
|
6. Start server
|
||||||
|
|
||||||
|
### 7.3 Subsequent Startups
|
||||||
|
|
||||||
|
On subsequent startups:
|
||||||
|
|
||||||
|
1. Find existing database file
|
||||||
|
2. Load relay private key from database
|
||||||
|
3. Load admin public key from config
|
||||||
|
4. Apply CLI overrides if provided
|
||||||
|
5. Start server
|
||||||
|
|
||||||
|
## 8. Comparison with c-relay
|
||||||
|
|
||||||
|
### 8.1 Similarities
|
||||||
|
|
||||||
|
| Feature | c-relay | ginxsom |
|
||||||
|
|---------|---------|---------|
|
||||||
|
| Event Types | Kind 23456/23457 | Kind 23456/23457 |
|
||||||
|
| Encryption | NIP-44 | NIP-44 |
|
||||||
|
| Command Format | JSON arrays | JSON arrays |
|
||||||
|
| Key Storage | relay_seckey table | relay_seckey table |
|
||||||
|
| Admin Auth | config table | config table |
|
||||||
|
| CLI Options | --admin-pubkey, --relay-privkey | --admin-pubkey, --relay-privkey |
|
||||||
|
| Response Format | Encrypted JSON | Encrypted JSON |
|
||||||
|
|
||||||
|
### 8.2 Differences
|
||||||
|
|
||||||
|
| Aspect | c-relay | ginxsom |
|
||||||
|
|--------|---------|---------|
|
||||||
|
| Transport | WebSocket (Nostr relay) | WebSocket (FastCGI) |
|
||||||
|
| Commands | Relay-specific (auth, config, stats) | Blossom-specific (blob, storage, mirror) |
|
||||||
|
| Database | SQLite (events) | SQLite (blobs + metadata) |
|
||||||
|
| File Storage | N/A | Blob storage on disk |
|
||||||
|
| Integration | Standalone relay | FastCGI + nginx |
|
||||||
|
|
||||||
|
### 8.3 Architectural Decisions
|
||||||
|
|
||||||
|
**Why follow c-relay's pattern?**
|
||||||
|
1. Proven in production
|
||||||
|
2. Simple and secure
|
||||||
|
3. No complex key management
|
||||||
|
4. Minimal database schema
|
||||||
|
5. Easy to understand and maintain
|
||||||
|
|
||||||
|
**What we're NOT doing (from initial design)**:
|
||||||
|
1. ❌ NIP-17 gift wrap (too complex)
|
||||||
|
2. ❌ Separate admin_keys table (use config)
|
||||||
|
3. ❌ Audit log table (keep it simple)
|
||||||
|
4. ❌ Processed events tracking (stateless)
|
||||||
|
5. ❌ Key encryption before storage (plain hex)
|
||||||
|
6. ❌ Migration strategy (new project)
|
||||||
|
|
||||||
|
## 9. Testing Strategy
|
||||||
|
|
||||||
|
### 9.1 Unit Tests
|
||||||
|
|
||||||
|
**admin_keys.c**:
|
||||||
|
- Key generation produces valid keys
|
||||||
|
- Keys can be stored and retrieved
|
||||||
|
- Invalid keys are rejected
|
||||||
|
|
||||||
|
**admin_auth.c**:
|
||||||
|
- Valid admin events pass authorization
|
||||||
|
- Invalid signatures are rejected
|
||||||
|
- Wrong pubkeys are rejected
|
||||||
|
- Expired events are rejected
|
||||||
|
|
||||||
|
**admin_handlers.c**:
|
||||||
|
- Each command handler works correctly
|
||||||
|
- Invalid parameters are rejected
|
||||||
|
- Error responses are properly formatted
|
||||||
|
|
||||||
|
### 9.2 Integration Tests
|
||||||
|
|
||||||
|
**Full Flow**:
|
||||||
|
1. Generate admin keypair
|
||||||
|
2. Create Kind 23456 command
|
||||||
|
3. Send via WebSocket
|
||||||
|
4. Verify authorization
|
||||||
|
5. Execute command
|
||||||
|
6. Receive Kind 23457 response
|
||||||
|
7. Decrypt and verify response
|
||||||
|
|
||||||
|
**Security Tests**:
|
||||||
|
- Unauthorized pubkey rejected
|
||||||
|
- Invalid signature rejected
|
||||||
|
- Replay attack prevented
|
||||||
|
- Command injection prevented
|
||||||
|
|
||||||
|
### 9.3 Performance Tests
|
||||||
|
|
||||||
|
- WebSocket connection handling
|
||||||
|
- Command processing latency
|
||||||
|
- Concurrent admin operations
|
||||||
|
- Large response handling
|
||||||
|
|
||||||
|
## 10. Future Enhancements
|
||||||
|
|
||||||
|
### 10.1 Short Term
|
||||||
|
|
||||||
|
1. **Command History**: Track admin commands for audit
|
||||||
|
2. **Multi-Admin Support**: Multiple authorized admin pubkeys
|
||||||
|
3. **Role-Based Access**: Different permission levels
|
||||||
|
4. **Batch Operations**: Execute multiple commands in one request
|
||||||
|
|
||||||
|
### 10.2 Long Term
|
||||||
|
|
||||||
|
1. **Web Dashboard**: Full-featured web UI
|
||||||
|
2. **Monitoring Integration**: Prometheus/Grafana metrics
|
||||||
|
3. **Backup Automation**: Scheduled backups
|
||||||
|
4. **Replication**: Multi-server blob replication
|
||||||
|
5. **Advanced Analytics**: Usage patterns, trends, predictions
|
||||||
|
|
||||||
|
## 11. References
|
||||||
|
|
||||||
|
### 11.1 Nostr NIPs
|
||||||
|
|
||||||
|
- **NIP-01**: Basic protocol flow
|
||||||
|
- **NIP-04**: Encrypted Direct Messages (deprecated, but reference)
|
||||||
|
- **NIP-19**: bech32-encoded entities (npub, nsec)
|
||||||
|
- **NIP-44**: Versioned Encryption (used for admin commands)
|
||||||
|
|
||||||
|
### 11.2 Blossom Specifications
|
||||||
|
|
||||||
|
- **BUD-01**: Blob Upload/Download
|
||||||
|
- **BUD-02**: Blob Descriptor
|
||||||
|
- **BUD-04**: Mirroring
|
||||||
|
- **BUD-06**: Upload Requirements
|
||||||
|
- **BUD-08**: NIP-94 Integration
|
||||||
|
- **BUD-09**: Blob Reporting
|
||||||
|
|
||||||
|
### 11.3 c-relay Source Files
|
||||||
|
|
||||||
|
- `c-relay/src/dm_admin.c` - Admin event processing
|
||||||
|
- `c-relay/src/api.c` - NIP-44 encryption
|
||||||
|
- `c-relay/src/config.c` - Key storage
|
||||||
|
- `c-relay/src/main.c` - CLI options
|
||||||
|
- `c-relay/src/sql_schema.h` - Database schema
|
||||||
|
|
||||||
|
## 12. Appendix
|
||||||
|
|
||||||
|
### 12.1 Example Admin CLI Tool (Python)
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Ginxsom Admin CLI Tool
|
||||||
|
Sends admin commands to ginxsom server via WebSocket
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import websockets
|
||||||
|
import json
|
||||||
|
from nostr_sdk import Keys, Event, EventBuilder, Kind
|
||||||
|
|
||||||
|
class GinxsomAdmin:
|
||||||
|
def __init__(self, server_url, admin_nsec, relay_npub):
|
||||||
|
self.server_url = server_url
|
||||||
|
self.admin_keys = Keys.parse(admin_nsec)
|
||||||
|
self.relay_pubkey = Keys.parse(relay_npub).public_key()
|
||||||
|
|
||||||
|
async def send_command(self, command, params):
|
||||||
|
"""Send admin command and wait for response"""
|
||||||
|
# Build command array
|
||||||
|
command_array = [command, params]
|
||||||
|
|
||||||
|
# Encrypt with NIP-44
|
||||||
|
encrypted = self.admin_keys.nip44_encrypt(
|
||||||
|
self.relay_pubkey,
|
||||||
|
json.dumps(command_array)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build Kind 23456 event
|
||||||
|
event = EventBuilder(
|
||||||
|
Kind(23456),
|
||||||
|
encrypted,
|
||||||
|
[["p", str(self.relay_pubkey)]]
|
||||||
|
).to_event(self.admin_keys)
|
||||||
|
|
||||||
|
# Send via WebSocket
|
||||||
|
async with websockets.connect(self.server_url) as ws:
|
||||||
|
await ws.send(json.dumps(event.as_json()))
|
||||||
|
|
||||||
|
# Wait for Kind 23457 response
|
||||||
|
response = await ws.recv()
|
||||||
|
response_event = Event.from_json(response)
|
||||||
|
|
||||||
|
# Decrypt response
|
||||||
|
decrypted = self.admin_keys.nip44_decrypt(
|
||||||
|
self.relay_pubkey,
|
||||||
|
response_event.content()
|
||||||
|
)
|
||||||
|
|
||||||
|
return json.loads(decrypted)
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
async def main():
|
||||||
|
admin = GinxsomAdmin(
|
||||||
|
"ws://localhost:8080/admin",
|
||||||
|
"nsec1...", # Admin private key
|
||||||
|
"npub1..." # Relay public key
|
||||||
|
)
|
||||||
|
|
||||||
|
# List blobs
|
||||||
|
result = await admin.send_command("blob_list", {
|
||||||
|
"limit": 10,
|
||||||
|
"offset": 0
|
||||||
|
})
|
||||||
|
|
||||||
|
print(json.dumps(result, indent=2))
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
|
```
|
||||||
|
|
||||||
|
### 12.2 Database Schema SQL
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Add to db/schema.sql
|
||||||
|
|
||||||
|
-- Relay Private Key Storage
|
||||||
|
CREATE TABLE relay_seckey (
|
||||||
|
private_key_hex TEXT NOT NULL CHECK (length(private_key_hex) = 64),
|
||||||
|
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Admin Public Key (add to config table)
|
||||||
|
INSERT INTO config (key, value, data_type, description, category, requires_restart)
|
||||||
|
VALUES (
|
||||||
|
'admin_pubkey',
|
||||||
|
'', -- Set during first startup
|
||||||
|
'string',
|
||||||
|
'Public key of authorized admin (64-char hex)',
|
||||||
|
'security',
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Relay Public Key (add to config table)
|
||||||
|
INSERT INTO config (key, value, data_type, description, category, requires_restart)
|
||||||
|
VALUES (
|
||||||
|
'relay_pubkey',
|
||||||
|
'', -- Set during first startup
|
||||||
|
'string',
|
||||||
|
'Public key of this relay (64-char hex)',
|
||||||
|
'server',
|
||||||
|
0
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 12.3 Makefile Updates
|
||||||
|
|
||||||
|
```makefile
|
||||||
|
# Add to Makefile
|
||||||
|
|
||||||
|
# Admin system objects
|
||||||
|
ADMIN_OBJS = build/admin_websocket.o \
|
||||||
|
build/admin_auth.o \
|
||||||
|
build/admin_handlers.o \
|
||||||
|
build/admin_config.o \
|
||||||
|
build/admin_keys.o
|
||||||
|
|
||||||
|
# Update main target
|
||||||
|
build/ginxsom-fcgi: $(OBJS) $(ADMIN_OBJS)
|
||||||
|
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
||||||
|
|
||||||
|
# Admin system rules
|
||||||
|
build/admin_websocket.o: src/admin_websocket.c
|
||||||
|
$(CC) $(CFLAGS) -c $< -o $@
|
||||||
|
|
||||||
|
build/admin_auth.o: src/admin_auth.c
|
||||||
|
$(CC) $(CFLAGS) -c $< -o $@
|
||||||
|
|
||||||
|
build/admin_handlers.o: src/admin_handlers.c
|
||||||
|
$(CC) $(CFLAGS) -c $< -o $@
|
||||||
|
|
||||||
|
build/admin_config.o: src/admin_config.c
|
||||||
|
$(CC) $(CFLAGS) -c $< -o $@
|
||||||
|
|
||||||
|
build/admin_keys.o: src/admin_keys.c
|
||||||
|
$(CC) $(CFLAGS) -c $< -o $@
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Document Version**: 2.0
|
||||||
|
**Last Updated**: 2025-01-16
|
||||||
|
**Status**: Ready for Implementation
|
||||||
@@ -15,6 +15,10 @@ server {
|
|||||||
|
|
||||||
root /var/www/html;
|
root /var/www/html;
|
||||||
index index.html index.htm;
|
index index.html index.htm;
|
||||||
|
# CORS for Nostr NIP-05 verification
|
||||||
|
add_header Access-Control-Allow-Origin * always;
|
||||||
|
add_header Access-Control-Allow-Methods "GET, OPTIONS" always;
|
||||||
|
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range" always;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/ =404;
|
try_files $uri $uri/ =404;
|
||||||
@@ -42,6 +46,10 @@ server {
|
|||||||
|
|
||||||
root /var/www/html;
|
root /var/www/html;
|
||||||
index index.html index.htm;
|
index index.html index.htm;
|
||||||
|
# CORS for Nostr NIP-05 verification
|
||||||
|
add_header Access-Control-Allow-Origin * always;
|
||||||
|
add_header Access-Control-Allow-Methods "GET, OPTIONS" always;
|
||||||
|
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range" always;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/ =404;
|
try_files $uri $uri/ =404;
|
||||||
@@ -58,7 +66,7 @@ server {
|
|||||||
# Blossom subdomains HTTP - redirect to HTTPS (keep for ACME)
|
# Blossom subdomains HTTP - redirect to HTTPS (keep for ACME)
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name blossom.laantungir.com blossom.laantungir.net blossom.laantungir.org;
|
server_name blossom.laantungir.net;
|
||||||
|
|
||||||
location /.well-known/acme-challenge/ {
|
location /.well-known/acme-challenge/ {
|
||||||
root /var/www/certbot;
|
root /var/www/certbot;
|
||||||
@@ -72,7 +80,7 @@ server {
|
|||||||
# Blossom subdomains HTTPS - ginxsom FastCGI
|
# Blossom subdomains HTTPS - ginxsom FastCGI
|
||||||
server {
|
server {
|
||||||
listen 443 ssl;
|
listen 443 ssl;
|
||||||
server_name blossom.laantungir.com blossom.laantungir.net blossom.laantungir.org;
|
server_name blossom.laantungir.net;
|
||||||
|
|
||||||
ssl_certificate /etc/letsencrypt/live/git.laantungir.net/fullchain.pem;
|
ssl_certificate /etc/letsencrypt/live/git.laantungir.net/fullchain.pem;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/git.laantungir.net/privkey.pem;
|
ssl_certificate_key /etc/letsencrypt/live/git.laantungir.net/privkey.pem;
|
||||||
@@ -256,8 +264,8 @@ server {
|
|||||||
listen 443 ssl;
|
listen 443 ssl;
|
||||||
server_name relay.laantungir.com relay.laantungir.net relay.laantungir.org;
|
server_name relay.laantungir.com relay.laantungir.net relay.laantungir.org;
|
||||||
|
|
||||||
ssl_certificate /etc/letsencrypt/live/blossom.laantungir.net/fullchain.pem;
|
ssl_certificate /etc/letsencrypt/live/git.laantungir.net/fullchain.pem;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/blossom.laantungir.net/privkey.pem;
|
ssl_certificate_key /etc/letsencrypt/live/git.laantungir.net/privkey.pem;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://127.0.0.1:8888;
|
proxy_pass http://127.0.0.1:8888;
|
||||||
|
|||||||
@@ -1,11 +1,36 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Restart Ginxsom Development Environment
|
# Restart Ginxsom Development Environment
|
||||||
# Combines nginx and FastCGI restart operations for debugging
|
# Combines nginx and FastCGI restart operations for debugging
|
||||||
|
# WARNING: This script DELETES all databases in db/ for fresh testing
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
|
|
||||||
|
# Parse command line arguments
|
||||||
|
TEST_MODE=0
|
||||||
|
FOLLOW_LOGS=0
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
-t|--test-keys)
|
||||||
|
TEST_MODE=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--follow)
|
||||||
|
FOLLOW_LOGS=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option: $1"
|
||||||
|
echo "Usage: $0 [-t|--test-keys] [--follow]"
|
||||||
|
echo " -t, --test-keys Use test mode with keys from .test_keys"
|
||||||
|
echo " --follow Follow logs in real-time"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
# Check for --follow flag
|
# Check for --follow flag
|
||||||
if [[ "$1" == "--follow" ]]; then
|
if [[ $FOLLOW_LOGS -eq 1 ]]; then
|
||||||
echo "=== Following logs in real-time ==="
|
echo "=== Following logs in real-time ==="
|
||||||
echo "Monitoring: nginx error, nginx access, app stderr, app stdout"
|
echo "Monitoring: nginx error, nginx access, app stderr, app stdout"
|
||||||
echo "Press Ctrl+C to stop following logs"
|
echo "Press Ctrl+C to stop following logs"
|
||||||
@@ -37,7 +62,12 @@ touch logs/app/stderr.log logs/app/stdout.log logs/nginx/error.log logs/nginx/ac
|
|||||||
chmod 644 logs/app/stderr.log logs/app/stdout.log logs/nginx/error.log logs/nginx/access.log
|
chmod 644 logs/app/stderr.log logs/app/stdout.log logs/nginx/error.log logs/nginx/access.log
|
||||||
chmod 755 logs/nginx logs/app
|
chmod 755 logs/nginx logs/app
|
||||||
|
|
||||||
|
if [ $TEST_MODE -eq 1 ]; then
|
||||||
|
echo -e "${YELLOW}=== Ginxsom Development Environment Restart (TEST MODE) ===${NC}"
|
||||||
|
echo "Using test keys from .test_keys file"
|
||||||
|
else
|
||||||
echo -e "${YELLOW}=== Ginxsom Development Environment Restart ===${NC}"
|
echo -e "${YELLOW}=== Ginxsom Development Environment Restart ===${NC}"
|
||||||
|
fi
|
||||||
echo "Starting full restart sequence..."
|
echo "Starting full restart sequence..."
|
||||||
|
|
||||||
# Function to check if a process is running
|
# Function to check if a process is running
|
||||||
@@ -148,6 +178,46 @@ if [ $? -ne 0 ]; then
|
|||||||
fi
|
fi
|
||||||
echo -e "${GREEN}Clean rebuild complete${NC}"
|
echo -e "${GREEN}Clean rebuild complete${NC}"
|
||||||
|
|
||||||
|
# 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
|
||||||
|
if [ ! -f ".test_keys" ]; then
|
||||||
|
echo -e "${RED}ERROR: .test_keys file not found${NC}"
|
||||||
|
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: 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
|
# Step 4: Start FastCGI
|
||||||
echo -e "\n${YELLOW}4. Starting FastCGI application...${NC}"
|
echo -e "\n${YELLOW}4. Starting FastCGI application...${NC}"
|
||||||
echo "Socket: $SOCKET_PATH"
|
echo "Socket: $SOCKET_PATH"
|
||||||
@@ -166,9 +236,19 @@ fi
|
|||||||
echo "Setting GINX_DEBUG environment for pubkey extraction diagnostics"
|
echo "Setting GINX_DEBUG environment for pubkey extraction diagnostics"
|
||||||
export GINX_DEBUG=1
|
export GINX_DEBUG=1
|
||||||
|
|
||||||
|
# Build command line arguments based on mode
|
||||||
|
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)
|
# Start FastCGI application with proper logging (daemonized but with redirected streams)
|
||||||
echo "FastCGI starting at $(date)" >> logs/app/stderr.log
|
echo "FastCGI starting at $(date)" >> logs/app/stderr.log
|
||||||
spawn-fcgi -s "$SOCKET_PATH" -M 666 -u "$USER" -g "$USER" -P "$PID_FILE" -- "$FCGI_BINARY" --storage-dir blobs 1>>logs/app/stdout.log 2>>logs/app/stderr.log
|
spawn-fcgi -s "$SOCKET_PATH" -M 666 -u "$USER" -g "$USER" -P "$PID_FILE" -- "$FCGI_BINARY" $FCGI_ARGS 1>>logs/app/stdout.log 2>>logs/app/stderr.log
|
||||||
|
|
||||||
if [ $? -eq 0 ] && [ -f "$PID_FILE" ]; then
|
if [ $? -eq 0 ] && [ -f "$PID_FILE" ]; then
|
||||||
PID=$(cat "$PID_FILE")
|
PID=$(cat "$PID_FILE")
|
||||||
|
|||||||
@@ -11,8 +11,8 @@
|
|||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include "ginxsom.h"
|
#include "ginxsom.h"
|
||||||
|
|
||||||
// Database path (consistent with main.c)
|
// Use global database path from main.c
|
||||||
#define DB_PATH "db/ginxsom.db"
|
extern char g_db_path[];
|
||||||
|
|
||||||
// Function declarations (moved from admin_api.h)
|
// 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);
|
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;
|
sqlite3_stmt* stmt;
|
||||||
int rc;
|
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) {
|
if (rc) {
|
||||||
// Default on DB error
|
// Default on DB error
|
||||||
strncpy(out, "http://localhost:9001", out_size - 1);
|
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
|
// Authentication now handled by centralized validation system
|
||||||
// Health endpoint is exempt from authentication requirement
|
// Health endpoint and POST /admin (Kind 23456 events) are exempt from authentication requirement
|
||||||
if (strcmp(path, "/health") != 0) {
|
// 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) {
|
if (!is_authenticated || !validated_pubkey) {
|
||||||
send_json_error(401, "admin_auth_required", "Valid admin authentication required");
|
send_json_error(401, "admin_auth_required", "Valid admin authentication required");
|
||||||
return;
|
return;
|
||||||
@@ -157,6 +161,13 @@ void handle_admin_api_request(const char* method, const char* uri, const char* v
|
|||||||
} else {
|
} else {
|
||||||
send_json_error(404, "not_found", "API endpoint not found");
|
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) {
|
} else if (strcmp(method, "PUT") == 0) {
|
||||||
if (strcmp(path, "/config") == 0) {
|
if (strcmp(path, "/config") == 0) {
|
||||||
handle_config_put_api();
|
handle_config_put_api();
|
||||||
@@ -201,7 +212,7 @@ int verify_admin_pubkey(const char* event_pubkey) {
|
|||||||
sqlite3_stmt* stmt;
|
sqlite3_stmt* stmt;
|
||||||
int rc, is_admin = 0;
|
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) {
|
if (rc) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -228,7 +239,7 @@ int is_admin_enabled(void) {
|
|||||||
sqlite3_stmt* stmt;
|
sqlite3_stmt* stmt;
|
||||||
int rc, enabled = 0;
|
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) {
|
if (rc) {
|
||||||
return 0; // Default disabled if can't access DB
|
return 0; // Default disabled if can't access DB
|
||||||
}
|
}
|
||||||
@@ -254,7 +265,7 @@ void handle_stats_api(void) {
|
|||||||
sqlite3_stmt* stmt;
|
sqlite3_stmt* stmt;
|
||||||
int rc;
|
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) {
|
if (rc) {
|
||||||
send_json_error(500, "database_error", "Failed to open database");
|
send_json_error(500, "database_error", "Failed to open database");
|
||||||
return;
|
return;
|
||||||
@@ -349,7 +360,7 @@ void handle_config_get_api(void) {
|
|||||||
sqlite3_stmt* stmt;
|
sqlite3_stmt* stmt;
|
||||||
int rc;
|
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) {
|
if (rc) {
|
||||||
send_json_error(500, "database_error", "Failed to open database");
|
send_json_error(500, "database_error", "Failed to open database");
|
||||||
return;
|
return;
|
||||||
@@ -423,7 +434,7 @@ void handle_config_put_api(void) {
|
|||||||
sqlite3_stmt* stmt;
|
sqlite3_stmt* stmt;
|
||||||
int rc;
|
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) {
|
if (rc) {
|
||||||
free(json_body);
|
free(json_body);
|
||||||
cJSON_Delete(config_data);
|
cJSON_Delete(config_data);
|
||||||
@@ -541,7 +552,7 @@ void handle_config_key_put_api(const char* key) {
|
|||||||
sqlite3_stmt* stmt;
|
sqlite3_stmt* stmt;
|
||||||
int rc;
|
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) {
|
if (rc) {
|
||||||
free(json_body);
|
free(json_body);
|
||||||
cJSON_Delete(request_data);
|
cJSON_Delete(request_data);
|
||||||
@@ -621,7 +632,7 @@ void handle_files_api(void) {
|
|||||||
sqlite3_stmt* stmt;
|
sqlite3_stmt* stmt;
|
||||||
int rc;
|
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) {
|
if (rc) {
|
||||||
send_json_error(500, "database_error", "Failed to open database");
|
send_json_error(500, "database_error", "Failed to open database");
|
||||||
return;
|
return;
|
||||||
@@ -715,7 +726,7 @@ void handle_health_api(void) {
|
|||||||
|
|
||||||
// Check database connection
|
// Check database connection
|
||||||
sqlite3* db;
|
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) {
|
if (rc == SQLITE_OK) {
|
||||||
cJSON_AddStringToObject(data, "database", "connected");
|
cJSON_AddStringToObject(data, "database", "connected");
|
||||||
sqlite3_close(db);
|
sqlite3_close(db);
|
||||||
|
|||||||
509
src/admin_auth.c
Normal file
509
src/admin_auth.c
Normal file
@@ -0,0 +1,509 @@
|
|||||||
|
/*
|
||||||
|
* Ginxsom Admin Authentication Module
|
||||||
|
* Handles Kind 23456/23457 admin events with NIP-44 encryption
|
||||||
|
* Based on c-relay's dm_admin.c implementation
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ginxsom.h"
|
||||||
|
#include "../nostr_core_lib/nostr_core/nostr_common.h"
|
||||||
|
#include "../nostr_core_lib/nostr_core/nip001.h"
|
||||||
|
#include "../nostr_core_lib/nostr_core/nip044.h"
|
||||||
|
#include "../nostr_core_lib/nostr_core/utils.h"
|
||||||
|
#include <cjson/cJSON.h>
|
||||||
|
#include <sqlite3.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
// Forward declarations
|
||||||
|
int get_blossom_private_key(char *seckey_out, size_t max_len);
|
||||||
|
int validate_admin_pubkey(const char *pubkey);
|
||||||
|
|
||||||
|
// Global variables for admin auth
|
||||||
|
static char g_blossom_seckey[65] = ""; // Cached blossom server private key
|
||||||
|
static int g_keys_loaded = 0; // Whether keys have been loaded
|
||||||
|
|
||||||
|
// Load blossom server keys if not already loaded
|
||||||
|
static int ensure_keys_loaded(void) {
|
||||||
|
if (!g_keys_loaded) {
|
||||||
|
if (get_blossom_private_key(g_blossom_seckey, sizeof(g_blossom_seckey)) != 0) {
|
||||||
|
fprintf(stderr, "ERROR: Cannot load blossom private key for admin auth\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
g_keys_loaded = 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that an event is a Kind 23456 admin command event
|
||||||
|
int is_admin_command_event(cJSON *event, const char *relay_pubkey) {
|
||||||
|
if (!event || !relay_pubkey) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check kind = 23456 (admin command)
|
||||||
|
cJSON *kind = cJSON_GetObjectItem(event, "kind");
|
||||||
|
if (!cJSON_IsNumber(kind) || kind->valueint != 23456) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check tags for 'p' tag with relay pubkey
|
||||||
|
cJSON *tags = cJSON_GetObjectItem(event, "tags");
|
||||||
|
if (!cJSON_IsArray(tags)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int found_p_tag = 0;
|
||||||
|
cJSON *tag = NULL;
|
||||||
|
cJSON_ArrayForEach(tag, tags) {
|
||||||
|
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
|
||||||
|
cJSON *tag_name = cJSON_GetArrayItem(tag, 0);
|
||||||
|
cJSON *tag_value = cJSON_GetArrayItem(tag, 1);
|
||||||
|
|
||||||
|
if (cJSON_IsString(tag_name) && strcmp(tag_name->valuestring, "p") == 0 &&
|
||||||
|
cJSON_IsString(tag_value) && strcmp(tag_value->valuestring, relay_pubkey) == 0) {
|
||||||
|
found_p_tag = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return found_p_tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate admin event signature and pubkey
|
||||||
|
int validate_admin_event(cJSON *event) {
|
||||||
|
if (!event) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get event fields
|
||||||
|
cJSON *pubkey = cJSON_GetObjectItem(event, "pubkey");
|
||||||
|
cJSON *sig = cJSON_GetObjectItem(event, "sig");
|
||||||
|
|
||||||
|
if (!cJSON_IsString(pubkey) || !cJSON_IsString(sig)) {
|
||||||
|
fprintf(stderr, "AUTH: Invalid event format - missing pubkey or sig\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if pubkey matches configured admin pubkey
|
||||||
|
if (!validate_admin_pubkey(pubkey->valuestring)) {
|
||||||
|
fprintf(stderr, "AUTH: Pubkey %s is not authorized admin\n", pubkey->valuestring);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Validate event signature using nostr_core_lib
|
||||||
|
// For now, assume signature is valid if pubkey matches
|
||||||
|
// In production, this should verify the signature cryptographically
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt NIP-44 encrypted admin command
|
||||||
|
int decrypt_admin_command(cJSON *event, char **decrypted_command_out) {
|
||||||
|
if (!event || !decrypted_command_out) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we have the relay private key
|
||||||
|
if (ensure_keys_loaded() != 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get admin pubkey from event
|
||||||
|
cJSON *admin_pubkey_json = cJSON_GetObjectItem(event, "pubkey");
|
||||||
|
if (!cJSON_IsString(admin_pubkey_json)) {
|
||||||
|
fprintf(stderr, "AUTH: Missing or invalid pubkey in event\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get encrypted content
|
||||||
|
cJSON *content = cJSON_GetObjectItem(event, "content");
|
||||||
|
if (!cJSON_IsString(content)) {
|
||||||
|
fprintf(stderr, "AUTH: Missing or invalid content in event\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert hex keys to bytes
|
||||||
|
unsigned char blossom_private_key[32];
|
||||||
|
unsigned char admin_public_key[32];
|
||||||
|
|
||||||
|
if (nostr_hex_to_bytes(g_blossom_seckey, blossom_private_key, 32) != 0) {
|
||||||
|
fprintf(stderr, "AUTH: Failed to parse blossom private key\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nostr_hex_to_bytes(admin_pubkey_json->valuestring, admin_public_key, 32) != 0) {
|
||||||
|
fprintf(stderr, "AUTH: Failed to parse admin public key\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate buffer for decrypted content
|
||||||
|
char decrypted_buffer[8192];
|
||||||
|
|
||||||
|
// Decrypt using NIP-44
|
||||||
|
int result = nostr_nip44_decrypt(
|
||||||
|
blossom_private_key,
|
||||||
|
admin_public_key,
|
||||||
|
content->valuestring,
|
||||||
|
decrypted_buffer,
|
||||||
|
sizeof(decrypted_buffer)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result != NOSTR_SUCCESS) {
|
||||||
|
fprintf(stderr, "AUTH: NIP-44 decryption failed with error code %d\n", result);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate and copy decrypted content
|
||||||
|
*decrypted_command_out = malloc(strlen(decrypted_buffer) + 1);
|
||||||
|
if (!*decrypted_command_out) {
|
||||||
|
fprintf(stderr, "AUTH: Failed to allocate memory for decrypted content\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
strcpy(*decrypted_command_out, decrypted_buffer);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse decrypted command array
|
||||||
|
int parse_admin_command(const char *decrypted_content, char ***command_array_out, int *command_count_out) {
|
||||||
|
if (!decrypted_content || !command_array_out || !command_count_out) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the decrypted content as JSON array
|
||||||
|
cJSON *content_json = cJSON_Parse(decrypted_content);
|
||||||
|
if (!content_json) {
|
||||||
|
fprintf(stderr, "AUTH: Failed to parse decrypted content as JSON\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cJSON_IsArray(content_json)) {
|
||||||
|
fprintf(stderr, "AUTH: Decrypted content is not a JSON array\n");
|
||||||
|
cJSON_Delete(content_json);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int array_size = cJSON_GetArraySize(content_json);
|
||||||
|
if (array_size < 1) {
|
||||||
|
fprintf(stderr, "AUTH: Command array is empty\n");
|
||||||
|
cJSON_Delete(content_json);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate command array
|
||||||
|
char **command_array = malloc(array_size * sizeof(char *));
|
||||||
|
if (!command_array) {
|
||||||
|
fprintf(stderr, "AUTH: Failed to allocate command array\n");
|
||||||
|
cJSON_Delete(content_json);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse each array element as string
|
||||||
|
for (int i = 0; i < array_size; i++) {
|
||||||
|
cJSON *item = cJSON_GetArrayItem(content_json, i);
|
||||||
|
if (!cJSON_IsString(item)) {
|
||||||
|
fprintf(stderr, "AUTH: Command array element %d is not a string\n", i);
|
||||||
|
// Clean up allocated strings
|
||||||
|
for (int j = 0; j < i; j++) {
|
||||||
|
free(command_array[j]);
|
||||||
|
}
|
||||||
|
free(command_array);
|
||||||
|
cJSON_Delete(content_json);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
command_array[i] = malloc(strlen(item->valuestring) + 1);
|
||||||
|
if (!command_array[i]) {
|
||||||
|
fprintf(stderr, "AUTH: Failed to allocate command string\n");
|
||||||
|
// Clean up allocated strings
|
||||||
|
for (int j = 0; j < i; j++) {
|
||||||
|
free(command_array[j]);
|
||||||
|
}
|
||||||
|
free(command_array);
|
||||||
|
cJSON_Delete(content_json);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
strcpy(command_array[i], item->valuestring);
|
||||||
|
if (!command_array[i]) {
|
||||||
|
fprintf(stderr, "AUTH: Failed to duplicate command string\n");
|
||||||
|
// Clean up allocated strings
|
||||||
|
for (int j = 0; j < i; j++) {
|
||||||
|
free(command_array[j]);
|
||||||
|
}
|
||||||
|
free(command_array);
|
||||||
|
cJSON_Delete(content_json);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_Delete(content_json);
|
||||||
|
*command_array_out = command_array;
|
||||||
|
*command_count_out = array_size;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process incoming admin command event (Kind 23456)
|
||||||
|
int process_admin_command(cJSON *event, char ***command_array_out, int *command_count_out, char **admin_pubkey_out) {
|
||||||
|
if (!event || !command_array_out || !command_count_out || !admin_pubkey_out) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get blossom server pubkey from config
|
||||||
|
sqlite3 *db;
|
||||||
|
sqlite3_stmt *stmt;
|
||||||
|
char blossom_pubkey[65] = "";
|
||||||
|
|
||||||
|
if (sqlite3_open_v2("db/ginxsom.db", &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *sql = "SELECT value FROM config WHERE key = 'blossom_pubkey'";
|
||||||
|
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(blossom_pubkey, pubkey, sizeof(blossom_pubkey) - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
}
|
||||||
|
sqlite3_close(db);
|
||||||
|
|
||||||
|
if (strlen(blossom_pubkey) != 64) {
|
||||||
|
fprintf(stderr, "ERROR: Cannot determine blossom pubkey for admin auth\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a valid admin command event for us
|
||||||
|
if (!is_admin_command_event(event, blossom_pubkey)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate admin authentication (signature and pubkey)
|
||||||
|
if (!validate_admin_event(event)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get admin pubkey from event
|
||||||
|
cJSON *admin_pubkey_json = cJSON_GetObjectItem(event, "pubkey");
|
||||||
|
if (!cJSON_IsString(admin_pubkey_json)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
*admin_pubkey_out = malloc(strlen(admin_pubkey_json->valuestring) + 1);
|
||||||
|
if (!*admin_pubkey_out) {
|
||||||
|
fprintf(stderr, "AUTH: Failed to allocate admin pubkey string\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
strcpy(*admin_pubkey_out, admin_pubkey_json->valuestring);
|
||||||
|
if (!*admin_pubkey_out) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt the command
|
||||||
|
char *decrypted_content = NULL;
|
||||||
|
if (decrypt_admin_command(event, &decrypted_content) != 0) {
|
||||||
|
free(*admin_pubkey_out);
|
||||||
|
*admin_pubkey_out = NULL;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the command array
|
||||||
|
if (parse_admin_command(decrypted_content, command_array_out, command_count_out) != 0) {
|
||||||
|
free(decrypted_content);
|
||||||
|
free(*admin_pubkey_out);
|
||||||
|
*admin_pubkey_out = NULL;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
free(decrypted_content);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate admin pubkey against configured admin
|
||||||
|
int validate_admin_pubkey(const char *pubkey) {
|
||||||
|
if (!pubkey || strlen(pubkey) != 64) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3 *db;
|
||||||
|
sqlite3_stmt *stmt;
|
||||||
|
int result = 0;
|
||||||
|
|
||||||
|
if (sqlite3_open_v2("db/ginxsom.db", &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *sql = "SELECT value FROM config WHERE key = 'admin_pubkey'";
|
||||||
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
|
||||||
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||||
|
const char *admin_pubkey = (const char *)sqlite3_column_text(stmt, 0);
|
||||||
|
if (admin_pubkey && strcmp(admin_pubkey, pubkey) == 0) {
|
||||||
|
result = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
}
|
||||||
|
sqlite3_close(db);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create encrypted response for admin (Kind 23457)
|
||||||
|
int create_admin_response(const char *response_json, const char *admin_pubkey, const char *original_event_id __attribute__((unused)), cJSON **response_event_out) {
|
||||||
|
if (!response_json || !admin_pubkey || !response_event_out) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we have the relay private key
|
||||||
|
if (ensure_keys_loaded() != 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get blossom server pubkey from config
|
||||||
|
sqlite3 *db;
|
||||||
|
sqlite3_stmt *stmt;
|
||||||
|
char blossom_pubkey[65] = "";
|
||||||
|
|
||||||
|
if (sqlite3_open_v2("db/ginxsom.db", &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *sql = "SELECT value FROM config WHERE key = 'blossom_pubkey'";
|
||||||
|
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(blossom_pubkey, pubkey, sizeof(blossom_pubkey) - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
}
|
||||||
|
sqlite3_close(db);
|
||||||
|
|
||||||
|
if (strlen(blossom_pubkey) != 64) {
|
||||||
|
fprintf(stderr, "ERROR: Cannot determine blossom pubkey for response\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert hex keys to bytes
|
||||||
|
unsigned char blossom_private_key[32];
|
||||||
|
unsigned char admin_public_key[32];
|
||||||
|
|
||||||
|
if (nostr_hex_to_bytes(g_blossom_seckey, blossom_private_key, 32) != 0) {
|
||||||
|
fprintf(stderr, "AUTH: Failed to parse blossom private key\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nostr_hex_to_bytes(admin_pubkey, admin_public_key, 32) != 0) {
|
||||||
|
fprintf(stderr, "AUTH: Failed to parse admin public key\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt response using NIP-44
|
||||||
|
char encrypted_content[8192];
|
||||||
|
int result = nostr_nip44_encrypt(
|
||||||
|
blossom_private_key,
|
||||||
|
admin_public_key,
|
||||||
|
response_json,
|
||||||
|
encrypted_content,
|
||||||
|
sizeof(encrypted_content)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result != NOSTR_SUCCESS) {
|
||||||
|
fprintf(stderr, "AUTH: NIP-44 encryption failed with error code %d\n", result);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Kind 23457 response event
|
||||||
|
cJSON *response_event = cJSON_CreateObject();
|
||||||
|
if (!response_event) {
|
||||||
|
fprintf(stderr, "AUTH: Failed to create response event JSON\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set event fields
|
||||||
|
cJSON_AddNumberToObject(response_event, "kind", 23457);
|
||||||
|
cJSON_AddStringToObject(response_event, "pubkey", blossom_pubkey);
|
||||||
|
cJSON_AddNumberToObject(response_event, "created_at", (double)time(NULL));
|
||||||
|
cJSON_AddStringToObject(response_event, "content", encrypted_content);
|
||||||
|
|
||||||
|
// Add tags array with 'p' tag for admin
|
||||||
|
cJSON *tags = cJSON_CreateArray();
|
||||||
|
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);
|
||||||
|
cJSON_AddItemToObject(response_event, "tags", tags);
|
||||||
|
|
||||||
|
// Sign the event with blossom private key
|
||||||
|
// Convert private key hex to bytes
|
||||||
|
unsigned char blossom_private_key_bytes[32];
|
||||||
|
if (nostr_hex_to_bytes(g_blossom_seckey, blossom_private_key_bytes, 32) != 0) {
|
||||||
|
fprintf(stderr, "AUTH: Failed to parse blossom private key for signing\n");
|
||||||
|
cJSON_Delete(response_event);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a temporary event structure for signing
|
||||||
|
cJSON* temp_event = cJSON_Duplicate(response_event, 1);
|
||||||
|
if (!temp_event) {
|
||||||
|
fprintf(stderr, "AUTH: Failed to create temp event for signing\n");
|
||||||
|
cJSON_Delete(response_event);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign the event using nostr_core_lib
|
||||||
|
cJSON* signed_event = nostr_create_and_sign_event(
|
||||||
|
23457, // Kind 23457 (admin response)
|
||||||
|
encrypted_content, // content
|
||||||
|
cJSON_GetObjectItem(response_event, "tags"), // tags
|
||||||
|
blossom_private_key_bytes, // private key
|
||||||
|
(time_t)cJSON_GetNumberValue(cJSON_GetObjectItem(response_event, "created_at")) // timestamp
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!signed_event) {
|
||||||
|
fprintf(stderr, "AUTH: Failed to sign admin response event\n");
|
||||||
|
cJSON_Delete(response_event);
|
||||||
|
cJSON_Delete(temp_event);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract id and signature from signed event
|
||||||
|
cJSON* signed_id = cJSON_GetObjectItem(signed_event, "id");
|
||||||
|
cJSON* signed_sig = cJSON_GetObjectItem(signed_event, "sig");
|
||||||
|
|
||||||
|
if (signed_id && signed_sig) {
|
||||||
|
cJSON_AddStringToObject(response_event, "id", cJSON_GetStringValue(signed_id));
|
||||||
|
cJSON_AddStringToObject(response_event, "sig", cJSON_GetStringValue(signed_sig));
|
||||||
|
} else {
|
||||||
|
fprintf(stderr, "AUTH: Signed event missing id or sig\n");
|
||||||
|
cJSON_Delete(response_event);
|
||||||
|
cJSON_Delete(signed_event);
|
||||||
|
cJSON_Delete(temp_event);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up temporary structures
|
||||||
|
cJSON_Delete(signed_event);
|
||||||
|
cJSON_Delete(temp_event);
|
||||||
|
|
||||||
|
*response_event_out = response_event;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free command array allocated by parse_admin_command
|
||||||
|
void free_command_array(char **command_array, int command_count) {
|
||||||
|
if (command_array) {
|
||||||
|
for (int i = 0; i < command_count; i++) {
|
||||||
|
if (command_array[i]) {
|
||||||
|
free(command_array[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(command_array);
|
||||||
|
}
|
||||||
|
}
|
||||||
471
src/admin_event.c
Normal file
471
src/admin_event.c
Normal 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;
|
||||||
|
}
|
||||||
216
src/admin_handlers.c
Normal file
216
src/admin_handlers.c
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
/*
|
||||||
|
* Ginxsom Admin Command Handlers
|
||||||
|
* Implements execution of admin commands received via Kind 23456 events
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ginxsom.h"
|
||||||
|
#include <cjson/cJSON.h>
|
||||||
|
#include <sqlite3.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <sys/statvfs.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
|
||||||
|
// Forward declarations
|
||||||
|
static cJSON* handle_blob_list(char **args, int arg_count);
|
||||||
|
static cJSON* handle_blob_info(char **args, int arg_count);
|
||||||
|
static cJSON* handle_blob_delete(char **args, int arg_count);
|
||||||
|
static cJSON* handle_storage_stats(char **args, int arg_count);
|
||||||
|
static cJSON* handle_config_get(char **args, int arg_count);
|
||||||
|
static cJSON* handle_config_set(char **args, int arg_count);
|
||||||
|
static cJSON* handle_help(char **args, int arg_count);
|
||||||
|
|
||||||
|
// Command dispatch table
|
||||||
|
typedef struct {
|
||||||
|
const char *command;
|
||||||
|
cJSON* (*handler)(char **args, int arg_count);
|
||||||
|
const char *description;
|
||||||
|
} admin_command_t;
|
||||||
|
|
||||||
|
static admin_command_t command_table[] = {
|
||||||
|
{"blob_list", handle_blob_list, "List all blobs"},
|
||||||
|
{"blob_info", handle_blob_info, "Get blob information"},
|
||||||
|
{"blob_delete", handle_blob_delete, "Delete a blob"},
|
||||||
|
{"storage_stats", handle_storage_stats, "Get storage statistics"},
|
||||||
|
{"config_get", handle_config_get, "Get configuration value"},
|
||||||
|
{"config_set", handle_config_set, "Set configuration value"},
|
||||||
|
{"help", handle_help, "Show available commands"},
|
||||||
|
{NULL, NULL, NULL}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Execute admin command and return JSON response
|
||||||
|
int execute_admin_command(char **command_array, int command_count, char **response_json_out) {
|
||||||
|
if (!command_array || command_count < 1 || !response_json_out) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *command = command_array[0];
|
||||||
|
|
||||||
|
// Find command handler
|
||||||
|
admin_command_t *cmd = NULL;
|
||||||
|
for (int i = 0; command_table[i].command != NULL; i++) {
|
||||||
|
if (strcmp(command_table[i].command, command) == 0) {
|
||||||
|
cmd = &command_table[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *response;
|
||||||
|
if (cmd) {
|
||||||
|
// Execute command handler
|
||||||
|
response = cmd->handler(command_array + 1, command_count - 1);
|
||||||
|
} else {
|
||||||
|
// Unknown command
|
||||||
|
response = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(response, "status", "error");
|
||||||
|
cJSON_AddStringToObject(response, "message", "Unknown command");
|
||||||
|
cJSON_AddStringToObject(response, "command", command);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert response to JSON string
|
||||||
|
char *json_str = cJSON_PrintUnformatted(response);
|
||||||
|
cJSON_Delete(response);
|
||||||
|
|
||||||
|
if (!json_str) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
*response_json_out = json_str;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command handlers
|
||||||
|
|
||||||
|
static cJSON* handle_blob_list(char **args __attribute__((unused)), int arg_count __attribute__((unused))) {
|
||||||
|
cJSON *response = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(response, "status", "success");
|
||||||
|
cJSON_AddStringToObject(response, "command", "blob_list");
|
||||||
|
|
||||||
|
// TODO: Implement actual blob listing from database
|
||||||
|
cJSON *blobs = cJSON_CreateArray();
|
||||||
|
cJSON_AddItemToObject(response, "blobs", blobs);
|
||||||
|
cJSON_AddNumberToObject(response, "count", 0);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
static cJSON* handle_blob_info(char **args, int arg_count) {
|
||||||
|
cJSON *response = cJSON_CreateObject();
|
||||||
|
|
||||||
|
if (arg_count < 1) {
|
||||||
|
cJSON_AddStringToObject(response, "status", "error");
|
||||||
|
cJSON_AddStringToObject(response, "message", "Missing blob hash argument");
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_AddStringToObject(response, "status", "success");
|
||||||
|
cJSON_AddStringToObject(response, "command", "blob_info");
|
||||||
|
cJSON_AddStringToObject(response, "hash", args[0]);
|
||||||
|
|
||||||
|
// TODO: Implement actual blob info retrieval from database
|
||||||
|
cJSON_AddStringToObject(response, "message", "Not yet implemented");
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
static cJSON* handle_blob_delete(char **args, int arg_count) {
|
||||||
|
cJSON *response = cJSON_CreateObject();
|
||||||
|
|
||||||
|
if (arg_count < 1) {
|
||||||
|
cJSON_AddStringToObject(response, "status", "error");
|
||||||
|
cJSON_AddStringToObject(response, "message", "Missing blob hash argument");
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_AddStringToObject(response, "status", "success");
|
||||||
|
cJSON_AddStringToObject(response, "command", "blob_delete");
|
||||||
|
cJSON_AddStringToObject(response, "hash", args[0]);
|
||||||
|
|
||||||
|
// TODO: Implement actual blob deletion
|
||||||
|
cJSON_AddStringToObject(response, "message", "Not yet implemented");
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
static cJSON* handle_storage_stats(char **args __attribute__((unused)), int arg_count __attribute__((unused))) {
|
||||||
|
cJSON *response = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(response, "status", "success");
|
||||||
|
cJSON_AddStringToObject(response, "command", "storage_stats");
|
||||||
|
|
||||||
|
// Get filesystem stats
|
||||||
|
struct statvfs stat;
|
||||||
|
if (statvfs(".", &stat) == 0) {
|
||||||
|
unsigned long long total = stat.f_blocks * stat.f_frsize;
|
||||||
|
unsigned long long available = stat.f_bavail * stat.f_frsize;
|
||||||
|
unsigned long long used = total - available;
|
||||||
|
|
||||||
|
cJSON_AddNumberToObject(response, "total_bytes", (double)total);
|
||||||
|
cJSON_AddNumberToObject(response, "used_bytes", (double)used);
|
||||||
|
cJSON_AddNumberToObject(response, "available_bytes", (double)available);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add blob count and total blob size from database
|
||||||
|
cJSON_AddNumberToObject(response, "blob_count", 0);
|
||||||
|
cJSON_AddNumberToObject(response, "blob_total_bytes", 0);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
static cJSON* handle_config_get(char **args, int arg_count) {
|
||||||
|
cJSON *response = cJSON_CreateObject();
|
||||||
|
|
||||||
|
if (arg_count < 1) {
|
||||||
|
cJSON_AddStringToObject(response, "status", "error");
|
||||||
|
cJSON_AddStringToObject(response, "message", "Missing config key argument");
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_AddStringToObject(response, "status", "success");
|
||||||
|
cJSON_AddStringToObject(response, "command", "config_get");
|
||||||
|
cJSON_AddStringToObject(response, "key", args[0]);
|
||||||
|
|
||||||
|
// TODO: Implement actual config retrieval from database
|
||||||
|
cJSON_AddStringToObject(response, "value", "");
|
||||||
|
cJSON_AddStringToObject(response, "message", "Not yet implemented");
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
static cJSON* handle_config_set(char **args, int arg_count) {
|
||||||
|
cJSON *response = cJSON_CreateObject();
|
||||||
|
|
||||||
|
if (arg_count < 2) {
|
||||||
|
cJSON_AddStringToObject(response, "status", "error");
|
||||||
|
cJSON_AddStringToObject(response, "message", "Missing config key or value argument");
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_AddStringToObject(response, "status", "success");
|
||||||
|
cJSON_AddStringToObject(response, "command", "config_set");
|
||||||
|
cJSON_AddStringToObject(response, "key", args[0]);
|
||||||
|
cJSON_AddStringToObject(response, "value", args[1]);
|
||||||
|
|
||||||
|
// TODO: Implement actual config update in database
|
||||||
|
cJSON_AddStringToObject(response, "message", "Not yet implemented");
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
static cJSON* handle_help(char **args __attribute__((unused)), int arg_count __attribute__((unused))) {
|
||||||
|
cJSON *response = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(response, "status", "success");
|
||||||
|
cJSON_AddStringToObject(response, "command", "help");
|
||||||
|
|
||||||
|
cJSON *commands = cJSON_CreateArray();
|
||||||
|
for (int i = 0; command_table[i].command != NULL; i++) {
|
||||||
|
cJSON *cmd = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(cmd, "command", command_table[i].command);
|
||||||
|
cJSON_AddStringToObject(cmd, "description", command_table[i].description);
|
||||||
|
cJSON_AddItemToArray(commands, cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_AddItemToObject(response, "commands", commands);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
163
src/admin_websocket.c
Normal file
163
src/admin_websocket.c
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
/*
|
||||||
|
* Ginxsom Admin WebSocket Module
|
||||||
|
* Handles WebSocket connections for Kind 23456/23457 admin commands
|
||||||
|
* Based on c-relay's WebSocket implementation
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ginxsom.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <cjson/cJSON.h>
|
||||||
|
#include <sqlite3.h>
|
||||||
|
|
||||||
|
// Forward declarations from admin_auth.c
|
||||||
|
int process_admin_command(cJSON *event, char ***command_array_out, int *command_count_out, char **admin_pubkey_out);
|
||||||
|
void free_command_array(char **command_array, int command_count);
|
||||||
|
int create_admin_response(const char *response_json, const char *admin_pubkey, const char *original_event_id, cJSON **response_event_out);
|
||||||
|
|
||||||
|
// Forward declarations from admin_handlers.c (to be created)
|
||||||
|
int execute_admin_command(char **command_array, int command_count, const char *admin_pubkey, char **response_json_out);
|
||||||
|
|
||||||
|
// Handle WebSocket admin command endpoint (/api/admin)
|
||||||
|
void handle_admin_websocket_request(void) {
|
||||||
|
// For now, this is a placeholder for WebSocket implementation
|
||||||
|
// In a full implementation, this would:
|
||||||
|
// 1. Upgrade HTTP connection to WebSocket
|
||||||
|
// 2. Handle WebSocket frames
|
||||||
|
// 3. Process Kind 23456 events
|
||||||
|
// 4. Send Kind 23457 responses
|
||||||
|
|
||||||
|
printf("Status: 501 Not Implemented\r\n");
|
||||||
|
printf("Content-Type: application/json\r\n\r\n");
|
||||||
|
printf("{\n");
|
||||||
|
printf(" \"error\": \"websocket_not_implemented\",\n");
|
||||||
|
printf(" \"message\": \"WebSocket admin endpoint not yet implemented\",\n");
|
||||||
|
printf(" \"note\": \"Use HTTP POST to /api/admin for now\"\n");
|
||||||
|
printf("}\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle HTTP POST admin command endpoint (/api/admin)
|
||||||
|
void handle_admin_command_post_request(void) {
|
||||||
|
// Read the request body (should contain Kind 23456 event JSON)
|
||||||
|
const char *content_length_str = getenv("CONTENT_LENGTH");
|
||||||
|
if (!content_length_str) {
|
||||||
|
printf("Status: 400 Bad Request\r\n");
|
||||||
|
printf("Content-Type: application/json\r\n\r\n");
|
||||||
|
printf("{\n");
|
||||||
|
printf(" \"error\": \"missing_content_length\",\n");
|
||||||
|
printf(" \"message\": \"Content-Length header required\"\n");
|
||||||
|
printf("}\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long content_length = atol(content_length_str);
|
||||||
|
if (content_length <= 0 || content_length > 1024 * 1024) { // 1MB limit
|
||||||
|
printf("Status: 400 Bad Request\r\n");
|
||||||
|
printf("Content-Type: application/json\r\n\r\n");
|
||||||
|
printf("{\n");
|
||||||
|
printf(" \"error\": \"invalid_content_length\",\n");
|
||||||
|
printf(" \"message\": \"Content-Length must be between 1 and 1MB\"\n");
|
||||||
|
printf("}\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the request body
|
||||||
|
char *request_body = malloc(content_length + 1);
|
||||||
|
if (!request_body) {
|
||||||
|
printf("Status: 500 Internal Server Error\r\n");
|
||||||
|
printf("Content-Type: application/json\r\n\r\n");
|
||||||
|
printf("{\n");
|
||||||
|
printf(" \"error\": \"memory_allocation_failed\",\n");
|
||||||
|
printf(" \"message\": \"Failed to allocate memory for request body\"\n");
|
||||||
|
printf("}\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t bytes_read = fread(request_body, 1, content_length, stdin);
|
||||||
|
if (bytes_read != (size_t)content_length) {
|
||||||
|
free(request_body);
|
||||||
|
printf("Status: 400 Bad Request\r\n");
|
||||||
|
printf("Content-Type: application/json\r\n\r\n");
|
||||||
|
printf("{\n");
|
||||||
|
printf(" \"error\": \"incomplete_request_body\",\n");
|
||||||
|
printf(" \"message\": \"Failed to read complete request body\"\n");
|
||||||
|
printf("}\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
request_body[content_length] = '\0';
|
||||||
|
|
||||||
|
// Parse the JSON event
|
||||||
|
cJSON *event = cJSON_Parse(request_body);
|
||||||
|
free(request_body);
|
||||||
|
|
||||||
|
if (!event) {
|
||||||
|
printf("Status: 400 Bad Request\r\n");
|
||||||
|
printf("Content-Type: application/json\r\n\r\n");
|
||||||
|
printf("{\n");
|
||||||
|
printf(" \"error\": \"invalid_json\",\n");
|
||||||
|
printf(" \"message\": \"Request body is not valid JSON\"\n");
|
||||||
|
printf("}\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the admin command
|
||||||
|
char **command_array = NULL;
|
||||||
|
int command_count = 0;
|
||||||
|
char *admin_pubkey = NULL;
|
||||||
|
|
||||||
|
int result = process_admin_command(event, &command_array, &command_count, &admin_pubkey);
|
||||||
|
cJSON_Delete(event);
|
||||||
|
|
||||||
|
if (result != 0) {
|
||||||
|
printf("Status: 400 Bad Request\r\n");
|
||||||
|
printf("Content-Type: application/json\r\n\r\n");
|
||||||
|
printf("{\n");
|
||||||
|
printf(" \"error\": \"invalid_admin_command\",\n");
|
||||||
|
printf(" \"message\": \"Failed to process admin command\"\n");
|
||||||
|
printf("}\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the command
|
||||||
|
char *response_json = NULL;
|
||||||
|
int exec_result = execute_admin_command(command_array, command_count, admin_pubkey, &response_json);
|
||||||
|
free_command_array(command_array, command_count);
|
||||||
|
free(admin_pubkey);
|
||||||
|
|
||||||
|
if (exec_result != 0) {
|
||||||
|
printf("Status: 500 Internal Server Error\r\n");
|
||||||
|
printf("Content-Type: application/json\r\n\r\n");
|
||||||
|
printf("{\n");
|
||||||
|
printf(" \"error\": \"command_execution_failed\",\n");
|
||||||
|
printf(" \"message\": \"Failed to execute admin command\"\n");
|
||||||
|
printf("}\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the response event (Kind 23457)
|
||||||
|
cJSON *response_event = NULL;
|
||||||
|
int create_result = create_admin_response(response_json, admin_pubkey, NULL, &response_event);
|
||||||
|
free(response_json);
|
||||||
|
|
||||||
|
if (create_result != 0) {
|
||||||
|
printf("Status: 500 Internal Server Error\r\n");
|
||||||
|
printf("Content-Type: application/json\r\n\r\n");
|
||||||
|
printf("{\n");
|
||||||
|
printf(" \"error\": \"response_creation_failed\",\n");
|
||||||
|
printf(" \"message\": \"Failed to create admin response\"\n");
|
||||||
|
printf("}\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the response event as JSON
|
||||||
|
char *response_json_str = cJSON_Print(response_event);
|
||||||
|
cJSON_Delete(response_event);
|
||||||
|
|
||||||
|
printf("Status: 200 OK\r\n");
|
||||||
|
printf("Content-Type: application/json\r\n\r\n");
|
||||||
|
printf("%s\n", response_json_str);
|
||||||
|
|
||||||
|
free(response_json_str);
|
||||||
|
}
|
||||||
@@ -10,8 +10,8 @@
|
|||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include "ginxsom.h"
|
#include "ginxsom.h"
|
||||||
|
|
||||||
// Database path
|
// Use global database path from main.c
|
||||||
#define DB_PATH "db/ginxsom.db"
|
extern char g_db_path[];
|
||||||
|
|
||||||
// Check if NIP-94 metadata emission is enabled
|
// Check if NIP-94 metadata emission is enabled
|
||||||
int nip94_is_enabled(void) {
|
int nip94_is_enabled(void) {
|
||||||
@@ -19,7 +19,7 @@ int nip94_is_enabled(void) {
|
|||||||
sqlite3_stmt* stmt;
|
sqlite3_stmt* stmt;
|
||||||
int rc, enabled = 1; // Default enabled
|
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) {
|
if (rc) {
|
||||||
return 1; // Default enabled on DB error
|
return 1; // Default enabled on DB error
|
||||||
}
|
}
|
||||||
@@ -50,7 +50,7 @@ int nip94_get_origin(char* out, size_t out_size) {
|
|||||||
sqlite3_stmt* stmt;
|
sqlite3_stmt* stmt;
|
||||||
int rc;
|
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) {
|
if (rc == SQLITE_OK) {
|
||||||
const char* sql = "SELECT value FROM config WHERE key = 'cdn_origin'";
|
const char* sql = "SELECT value FROM config WHERE key = 'cdn_origin'";
|
||||||
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||||
|
|||||||
@@ -11,8 +11,8 @@
|
|||||||
#include <time.h>
|
#include <time.h>
|
||||||
#include "ginxsom.h"
|
#include "ginxsom.h"
|
||||||
|
|
||||||
// Database path (should match main.c)
|
// Use global database path from main.c
|
||||||
#define DB_PATH "db/ginxsom.db"
|
extern char g_db_path[];
|
||||||
|
|
||||||
// Forward declarations for helper functions
|
// Forward declarations for helper functions
|
||||||
void send_error_response(int status_code, const char* error_type, const char* message, const char* details);
|
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;
|
sqlite3_stmt* stmt;
|
||||||
int rc;
|
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) {
|
if (rc) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
// Version information (auto-updated by build system)
|
// Version information (auto-updated by build system)
|
||||||
#define VERSION_MAJOR 0
|
#define VERSION_MAJOR 0
|
||||||
#define VERSION_MINOR 1
|
#define VERSION_MINOR 1
|
||||||
#define VERSION_PATCH 7
|
#define VERSION_PATCH 10
|
||||||
#define VERSION "v0.1.7"
|
#define VERSION "v0.1.10"
|
||||||
|
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
@@ -262,6 +262,9 @@ int validate_sha256_format(const char* sha256);
|
|||||||
// Admin API request handler
|
// Admin API request handler
|
||||||
void handle_admin_api_request(const char* method, const char* uri, const char* validated_pubkey, int is_authenticated);
|
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
|
// Individual endpoint handlers
|
||||||
void handle_stats_api(void);
|
void handle_stats_api(void);
|
||||||
void handle_config_get_api(void);
|
void handle_config_get_api(void);
|
||||||
|
|||||||
898
src/main.c
898
src/main.c
@@ -9,7 +9,6 @@
|
|||||||
#include "../nostr_core_lib/nostr_core/utils.h"
|
#include "../nostr_core_lib/nostr_core/utils.h"
|
||||||
#include <getopt.h>
|
#include <getopt.h>
|
||||||
#include <curl/curl.h>
|
#include <curl/curl.h>
|
||||||
#include <fcgi_stdio.h>
|
|
||||||
#include <sqlite3.h>
|
#include <sqlite3.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
@@ -27,9 +26,15 @@
|
|||||||
#define MAX_MIME_LEN 128
|
#define MAX_MIME_LEN 128
|
||||||
|
|
||||||
// Configuration variables - can be overridden via command line
|
// 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] = ".";
|
char g_storage_dir[MAX_PATH_LEN] = ".";
|
||||||
|
|
||||||
|
// Key management variables
|
||||||
|
char g_admin_pubkey[65] = ""; // Admin public key for authorization
|
||||||
|
char g_blossom_seckey[65] = ""; // Blossom server private key for decryption/signing
|
||||||
|
char g_blossom_pubkey[65] = ""; // Blossom server public key (derived from seckey)
|
||||||
|
int g_generate_keys = 0; // Flag to generate keys on startup (deprecated)
|
||||||
|
|
||||||
// Use global configuration variables
|
// Use global configuration variables
|
||||||
#define DB_PATH g_db_path
|
#define DB_PATH g_db_path
|
||||||
|
|
||||||
@@ -60,6 +65,174 @@ const char *get_config_dir(char *buffer, size_t buffer_size) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Database initialization function
|
||||||
|
int initialize_database(const char *db_path) {
|
||||||
|
sqlite3 *db;
|
||||||
|
char *err_msg = NULL;
|
||||||
|
int rc;
|
||||||
|
|
||||||
|
// Check if database file exists
|
||||||
|
struct stat st;
|
||||||
|
int db_exists = (stat(db_path, &st) == 0);
|
||||||
|
|
||||||
|
// Open database with CREATE flag
|
||||||
|
rc = sqlite3_open_v2(db_path, &db,
|
||||||
|
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
fprintf(stderr, "Cannot open database: %s\n", sqlite3_errmsg(db));
|
||||||
|
sqlite3_close(db);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If database was just created, initialize schema
|
||||||
|
if (!db_exists) {
|
||||||
|
fprintf(stderr, "Database not found, initializing schema...\n");
|
||||||
|
|
||||||
|
// Enable foreign key constraints
|
||||||
|
rc = sqlite3_exec(db, "PRAGMA foreign_keys = ON;", NULL, NULL, &err_msg);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
fprintf(stderr, "Failed to enable foreign keys: %s\n", err_msg);
|
||||||
|
sqlite3_free(err_msg);
|
||||||
|
sqlite3_close(db);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create blobs table
|
||||||
|
const char *create_blobs =
|
||||||
|
"CREATE TABLE IF NOT EXISTS blobs ("
|
||||||
|
" sha256 TEXT PRIMARY KEY NOT NULL,"
|
||||||
|
" size INTEGER NOT NULL,"
|
||||||
|
" type TEXT NOT NULL,"
|
||||||
|
" uploaded_at INTEGER NOT NULL,"
|
||||||
|
" uploader_pubkey TEXT,"
|
||||||
|
" filename TEXT,"
|
||||||
|
" CHECK (length(sha256) = 64),"
|
||||||
|
" CHECK (size >= 0),"
|
||||||
|
" CHECK (uploaded_at > 0)"
|
||||||
|
");";
|
||||||
|
|
||||||
|
rc = sqlite3_exec(db, create_blobs, NULL, NULL, &err_msg);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
fprintf(stderr, "Failed to create blobs table: %s\n", err_msg);
|
||||||
|
sqlite3_free(err_msg);
|
||||||
|
sqlite3_close(db);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create config table
|
||||||
|
const char *create_config =
|
||||||
|
"CREATE TABLE IF NOT EXISTS config ("
|
||||||
|
" key TEXT PRIMARY KEY NOT NULL,"
|
||||||
|
" value TEXT NOT NULL,"
|
||||||
|
" description TEXT,"
|
||||||
|
" created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),"
|
||||||
|
" updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))"
|
||||||
|
");";
|
||||||
|
|
||||||
|
rc = sqlite3_exec(db, create_config, NULL, NULL, &err_msg);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
fprintf(stderr, "Failed to create config table: %s\n", err_msg);
|
||||||
|
sqlite3_free(err_msg);
|
||||||
|
sqlite3_close(db);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create auth_rules table
|
||||||
|
const char *create_auth_rules =
|
||||||
|
"CREATE TABLE IF NOT EXISTS auth_rules ("
|
||||||
|
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||||
|
" rule_type TEXT NOT NULL,"
|
||||||
|
" rule_target TEXT NOT NULL,"
|
||||||
|
" operation TEXT NOT NULL DEFAULT '*',"
|
||||||
|
" enabled INTEGER NOT NULL DEFAULT 1,"
|
||||||
|
" priority INTEGER NOT NULL DEFAULT 100,"
|
||||||
|
" description TEXT,"
|
||||||
|
" created_by TEXT,"
|
||||||
|
" created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),"
|
||||||
|
" updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),"
|
||||||
|
" 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(rule_type, rule_target, operation)"
|
||||||
|
");";
|
||||||
|
|
||||||
|
rc = sqlite3_exec(db, create_auth_rules, NULL, NULL, &err_msg);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
fprintf(stderr, "Failed to create auth_rules table: %s\n", err_msg);
|
||||||
|
sqlite3_free(err_msg);
|
||||||
|
sqlite3_close(db);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create indexes
|
||||||
|
const char *create_indexes =
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_blobs_uploaded_at ON blobs(uploaded_at);"
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_blobs_uploader_pubkey ON blobs(uploader_pubkey);"
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_blobs_type ON blobs(type);"
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_config_updated_at ON config(updated_at);"
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_auth_rules_type_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);";
|
||||||
|
|
||||||
|
rc = sqlite3_exec(db, create_indexes, NULL, NULL, &err_msg);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
fprintf(stderr, "Failed to create indexes: %s\n", err_msg);
|
||||||
|
sqlite3_free(err_msg);
|
||||||
|
sqlite3_close(db);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert default configuration
|
||||||
|
const char *insert_config =
|
||||||
|
"INSERT OR IGNORE INTO config (key, value, description) VALUES"
|
||||||
|
" ('max_file_size', '104857600', 'Maximum file size in bytes (100MB)'),"
|
||||||
|
" ('auth_rules_enabled', 'true', 'Whether authentication rules are enabled for uploads'),"
|
||||||
|
" ('server_name', 'ginxsom', 'Server name for responses'),"
|
||||||
|
" ('admin_pubkey', '', 'Admin public key for API access'),"
|
||||||
|
" ('admin_enabled', 'true', 'Whether admin API is enabled'),"
|
||||||
|
" ('nip42_require_auth', 'false', 'Enable NIP-42 challenge/response authentication'),"
|
||||||
|
" ('nip42_challenge_timeout', '600', 'NIP-42 challenge timeout in seconds'),"
|
||||||
|
" ('nip42_time_tolerance', '300', 'NIP-42 timestamp tolerance in seconds');";
|
||||||
|
|
||||||
|
rc = sqlite3_exec(db, insert_config, NULL, NULL, &err_msg);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
fprintf(stderr, "Failed to insert default config: %s\n", err_msg);
|
||||||
|
sqlite3_free(err_msg);
|
||||||
|
sqlite3_close(db);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create storage_stats view
|
||||||
|
const char *create_view =
|
||||||
|
"CREATE VIEW IF NOT EXISTS storage_stats AS "
|
||||||
|
"SELECT "
|
||||||
|
" COUNT(*) as total_blobs, "
|
||||||
|
" SUM(size) as total_bytes, "
|
||||||
|
" AVG(size) as avg_blob_size, "
|
||||||
|
" MIN(uploaded_at) as first_upload, "
|
||||||
|
" MAX(uploaded_at) as last_upload, "
|
||||||
|
" COUNT(DISTINCT uploader_pubkey) as unique_uploaders "
|
||||||
|
"FROM blobs;";
|
||||||
|
|
||||||
|
rc = sqlite3_exec(db, create_view, NULL, NULL, &err_msg);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
fprintf(stderr, "Failed to create storage_stats view: %s\n", err_msg);
|
||||||
|
sqlite3_free(err_msg);
|
||||||
|
sqlite3_close(db);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(stderr, "Database schema initialized successfully\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_close(db);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Function declarations
|
// Function declarations
|
||||||
void handle_options_request(void);
|
void handle_options_request(void);
|
||||||
void send_error_response(int status_code, const char *error_type,
|
void send_error_response(int status_code, const char *error_type,
|
||||||
@@ -67,6 +240,16 @@ void send_error_response(int status_code, const char *error_type,
|
|||||||
void log_request(const char *method, const char *uri, const char *auth_status,
|
void log_request(const char *method, const char *uri, const char *auth_status,
|
||||||
int status_code);
|
int status_code);
|
||||||
|
|
||||||
|
// Key management functions
|
||||||
|
int generate_random_private_key_bytes(unsigned char *key_bytes, size_t len);
|
||||||
|
int generate_server_keypair(void);
|
||||||
|
int load_server_keys(void);
|
||||||
|
int store_blossom_private_key(const char *seckey);
|
||||||
|
int get_blossom_private_key(char *seckey_out, size_t max_len);
|
||||||
|
int derive_pubkey_from_privkey(const char *privkey_hex, char *pubkey_hex_out);
|
||||||
|
int validate_database_pubkey_match(const char *db_path, const char *expected_pubkey);
|
||||||
|
int set_db_path_from_pubkey(const char *pubkey);
|
||||||
|
|
||||||
// External validator function declarations
|
// External validator function declarations
|
||||||
const char *nostr_request_validator_get_last_violation_type(void);
|
const char *nostr_request_validator_get_last_violation_type(void);
|
||||||
int nostr_generate_nip42_challenge(char *challenge_out, size_t challenge_size, const char *client_ip);
|
int nostr_generate_nip42_challenge(char *challenge_out, size_t challenge_size, const char *client_ip);
|
||||||
@@ -77,6 +260,357 @@ void handle_auth_challenge_request(void);
|
|||||||
// Handler function declarations with validation support
|
// Handler function declarations with validation support
|
||||||
void handle_delete_request_with_validation(const char *sha256, nostr_request_result_t *validation_result);
|
void handle_delete_request_with_validation(const char *sha256, nostr_request_result_t *validation_result);
|
||||||
|
|
||||||
|
// Key management function implementations
|
||||||
|
|
||||||
|
// Derive public key from private key hex string
|
||||||
|
int derive_pubkey_from_privkey(const char *privkey_hex, char *pubkey_hex_out) {
|
||||||
|
if (!privkey_hex || strlen(privkey_hex) != 64) {
|
||||||
|
fprintf(stderr, "ERROR: Invalid private key format\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned char seckey_bytes[32];
|
||||||
|
if (nostr_hex_to_bytes(privkey_hex, seckey_bytes, 32) != NOSTR_SUCCESS) {
|
||||||
|
fprintf(stderr, "ERROR: Failed to parse private key hex\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned char pubkey_bytes[32];
|
||||||
|
if (nostr_ec_public_key_from_private_key(seckey_bytes, pubkey_bytes) != NOSTR_SUCCESS) {
|
||||||
|
fprintf(stderr, "ERROR: Failed to derive public key\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
nostr_bytes_to_hex(pubkey_bytes, 32, pubkey_hex_out);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set database path based on pubkey (db/<pubkey>.db)
|
||||||
|
int set_db_path_from_pubkey(const char *pubkey) {
|
||||||
|
if (!pubkey || strlen(pubkey) != 64) {
|
||||||
|
fprintf(stderr, "ERROR: Invalid pubkey for database naming\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
snprintf(g_db_path, sizeof(g_db_path), "db/%s.db", pubkey);
|
||||||
|
fprintf(stderr, "DATABASE: Set database path to %s\n", g_db_path);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that database filename matches the pubkey stored inside
|
||||||
|
int validate_database_pubkey_match(const char *db_path, const char *expected_pubkey) {
|
||||||
|
// Extract pubkey from database filename
|
||||||
|
const char *filename = strrchr(db_path, '/');
|
||||||
|
if (!filename) {
|
||||||
|
filename = db_path;
|
||||||
|
} else {
|
||||||
|
filename++; // Skip the '/'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if filename matches pattern: <pubkey>.db
|
||||||
|
size_t filename_len = strlen(filename);
|
||||||
|
if (filename_len != 67) { // 64 chars + ".db" = 67
|
||||||
|
fprintf(stderr, "WARNING: Database filename doesn't match pubkey pattern: %s\n", filename);
|
||||||
|
return 0; // Don't enforce for non-standard names (backward compatibility)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract pubkey from filename (first 64 chars)
|
||||||
|
char filename_pubkey[65];
|
||||||
|
strncpy(filename_pubkey, filename, 64);
|
||||||
|
filename_pubkey[64] = '\0';
|
||||||
|
|
||||||
|
// Compare with expected pubkey
|
||||||
|
if (strcasecmp(filename_pubkey, expected_pubkey) != 0) {
|
||||||
|
fprintf(stderr, "ERROR: Database pubkey mismatch!\n");
|
||||||
|
fprintf(stderr, " Database filename: %s\n", filename_pubkey);
|
||||||
|
fprintf(stderr, " Expected pubkey: %s\n", expected_pubkey);
|
||||||
|
fprintf(stderr, " → Database filename doesn't match the keys stored inside\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(stderr, "DATABASE: Pubkey validation passed: %s\n", filename_pubkey);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate random private key bytes using /dev/urandom
|
||||||
|
int generate_random_private_key_bytes(unsigned char *key_bytes, size_t len) {
|
||||||
|
FILE *fp = fopen("/dev/urandom", "rb");
|
||||||
|
if (!fp) {
|
||||||
|
fprintf(stderr, "ERROR: Cannot open /dev/urandom for key generation\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t bytes_read = fread(key_bytes, 1, len, fp);
|
||||||
|
fclose(fp);
|
||||||
|
|
||||||
|
if (bytes_read != len) {
|
||||||
|
fprintf(stderr, "ERROR: Failed to read %zu bytes from /dev/urandom\n", len);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate server keypair and store in database
|
||||||
|
int generate_server_keypair(void) {
|
||||||
|
fprintf(stderr, "DEBUG: generate_server_keypair() called\n");
|
||||||
|
unsigned char seckey_bytes[32];
|
||||||
|
char seckey_hex[65];
|
||||||
|
char pubkey_hex[65];
|
||||||
|
|
||||||
|
// Generate random private key
|
||||||
|
fprintf(stderr, "DEBUG: Generating random private key...\n");
|
||||||
|
if (generate_random_private_key_bytes(seckey_bytes, 32) != 0) {
|
||||||
|
fprintf(stderr, "DEBUG: Failed to generate random bytes\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the private key
|
||||||
|
if (nostr_ec_private_key_verify(seckey_bytes) != NOSTR_SUCCESS) {
|
||||||
|
fprintf(stderr, "ERROR: Generated invalid private key\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to hex
|
||||||
|
nostr_bytes_to_hex(seckey_bytes, 32, seckey_hex);
|
||||||
|
|
||||||
|
// Derive public key
|
||||||
|
unsigned char pubkey_bytes[32];
|
||||||
|
if (nostr_ec_public_key_from_private_key(seckey_bytes, pubkey_bytes) != NOSTR_SUCCESS) {
|
||||||
|
fprintf(stderr, "ERROR: Failed to derive public key\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert public key to hex
|
||||||
|
nostr_bytes_to_hex(pubkey_bytes, 32, pubkey_hex);
|
||||||
|
|
||||||
|
// Store pubkey in global variable
|
||||||
|
strncpy(g_blossom_pubkey, pubkey_hex, sizeof(g_blossom_pubkey) - 1);
|
||||||
|
g_blossom_pubkey[64] = '\0';
|
||||||
|
|
||||||
|
// Store seckey in global variable
|
||||||
|
strncpy(g_blossom_seckey, seckey_hex, sizeof(g_blossom_seckey) - 1);
|
||||||
|
g_blossom_seckey[64] = '\0';
|
||||||
|
|
||||||
|
// Set database path based on pubkey (MUST be done before storing keys)
|
||||||
|
if (set_db_path_from_pubkey(pubkey_hex) != 0) {
|
||||||
|
fprintf(stderr, "ERROR: Failed to set database path\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize database with new path
|
||||||
|
if (initialize_database(g_db_path) != 0) {
|
||||||
|
fprintf(stderr, "ERROR: Failed to initialize database at %s\n", g_db_path);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store private key securely
|
||||||
|
if (store_blossom_private_key(seckey_hex) != 0) {
|
||||||
|
fprintf(stderr, "ERROR: Failed to store blossom private key\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store public key in config
|
||||||
|
sqlite3 *db;
|
||||||
|
sqlite3_stmt *stmt;
|
||||||
|
int rc;
|
||||||
|
|
||||||
|
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE, NULL);
|
||||||
|
if (rc) {
|
||||||
|
fprintf(stderr, "ERROR: Can't open database for config: %s\n", sqlite3_errmsg(db));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *sql = "INSERT OR REPLACE INTO config (key, value, description) VALUES (?, ?, ?)";
|
||||||
|
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
fprintf(stderr, "ERROR: SQL prepare failed: %s\n", sqlite3_errmsg(db));
|
||||||
|
sqlite3_close(db);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_bind_text(stmt, 1, "blossom_pubkey", -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 2, pubkey_hex, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 3, "Blossom server's public key for Nostr communication", -1, SQLITE_STATIC);
|
||||||
|
|
||||||
|
rc = sqlite3_step(stmt);
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
sqlite3_close(db);
|
||||||
|
|
||||||
|
if (rc != SQLITE_DONE) {
|
||||||
|
fprintf(stderr, "ERROR: Failed to store blossom public key in config\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display keys for admin setup
|
||||||
|
fprintf(stderr, "========================================\n");
|
||||||
|
fprintf(stderr, "SERVER KEYPAIR GENERATED SUCCESSFULLY\n");
|
||||||
|
fprintf(stderr, "========================================\n");
|
||||||
|
fprintf(stderr, "Blossom Public Key: %s\n", pubkey_hex);
|
||||||
|
fprintf(stderr, "Blossom Private Key: %s\n", seckey_hex);
|
||||||
|
fprintf(stderr, "Database Path: %s\n", g_db_path);
|
||||||
|
fprintf(stderr, "========================================\n");
|
||||||
|
fprintf(stderr, "IMPORTANT: Save the private key securely!\n");
|
||||||
|
fprintf(stderr, "This key is used for decrypting admin messages.\n");
|
||||||
|
fprintf(stderr, "========================================\n");
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load server keys from database
|
||||||
|
int load_server_keys(void) {
|
||||||
|
fprintf(stderr, "DEBUG: load_server_keys() called\n");
|
||||||
|
sqlite3 *db;
|
||||||
|
sqlite3_stmt *stmt;
|
||||||
|
int rc;
|
||||||
|
|
||||||
|
// Try to load blossom private key
|
||||||
|
fprintf(stderr, "DEBUG: Trying to load blossom private key...\n");
|
||||||
|
if (get_blossom_private_key(g_blossom_seckey, sizeof(g_blossom_seckey)) != 0) {
|
||||||
|
fprintf(stderr, "DEBUG: No blossom private key found in database\n");
|
||||||
|
return -1; // Keys must exist in database
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(stderr, "STARTUP: Blossom private key loaded successfully\n");
|
||||||
|
|
||||||
|
// Derive public key from private key
|
||||||
|
if (derive_pubkey_from_privkey(g_blossom_seckey, g_blossom_pubkey) != 0) {
|
||||||
|
fprintf(stderr, "ERROR: Failed to derive public key from private key\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(stderr, "STARTUP: Derived blossom pubkey: %s\n", g_blossom_pubkey);
|
||||||
|
|
||||||
|
// Validate database filename matches pubkey
|
||||||
|
if (validate_database_pubkey_match(g_db_path, g_blossom_pubkey) != 0) {
|
||||||
|
fprintf(stderr, "ERROR: Database pubkey validation failed\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load admin pubkey from command line or config
|
||||||
|
if (strlen(g_admin_pubkey) == 0) {
|
||||||
|
// Try to load from database config
|
||||||
|
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
|
||||||
|
if (rc == SQLITE_OK) {
|
||||||
|
const char *sql = "SELECT value FROM config WHERE key = 'admin_pubkey'";
|
||||||
|
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||||
|
if (rc == SQLITE_OK) {
|
||||||
|
rc = sqlite3_step(stmt);
|
||||||
|
if (rc == SQLITE_ROW) {
|
||||||
|
const char *pubkey = (const char *)sqlite3_column_text(stmt, 0);
|
||||||
|
if (pubkey && strlen(pubkey) == 64) {
|
||||||
|
strncpy(g_admin_pubkey, pubkey, sizeof(g_admin_pubkey) - 1);
|
||||||
|
fprintf(stderr, "STARTUP: Admin pubkey loaded from config: %s\n", g_admin_pubkey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
}
|
||||||
|
sqlite3_close(db);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Store admin pubkey in config if provided via command line
|
||||||
|
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE, NULL);
|
||||||
|
if (rc == SQLITE_OK) {
|
||||||
|
const char *sql = "INSERT OR REPLACE INTO config (key, value, description) VALUES (?, ?, ?)";
|
||||||
|
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||||
|
if (rc == SQLITE_OK) {
|
||||||
|
sqlite3_bind_text(stmt, 1, "admin_pubkey", -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 2, g_admin_pubkey, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 3, "Admin public key for management authentication", -1, SQLITE_STATIC);
|
||||||
|
sqlite3_step(stmt);
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
}
|
||||||
|
sqlite3_close(db);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store blossom private key in dedicated table
|
||||||
|
int store_blossom_private_key(const char *seckey) {
|
||||||
|
sqlite3 *db;
|
||||||
|
sqlite3_stmt *stmt;
|
||||||
|
int rc;
|
||||||
|
|
||||||
|
// Validate key format
|
||||||
|
if (!seckey || strlen(seckey) != 64) {
|
||||||
|
fprintf(stderr, "ERROR: Invalid blossom private key format\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create blossom_seckey table if it doesn't exist
|
||||||
|
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL);
|
||||||
|
if (rc) {
|
||||||
|
fprintf(stderr, "ERROR: Can't open database: %s\n", sqlite3_errmsg(db));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create table
|
||||||
|
const char *create_sql = "CREATE TABLE IF NOT EXISTS blossom_seckey (id INTEGER PRIMARY KEY CHECK (id = 1), seckey TEXT NOT NULL, created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), CHECK (length(seckey) = 64))";
|
||||||
|
rc = sqlite3_exec(db, create_sql, NULL, NULL, NULL);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
fprintf(stderr, "ERROR: Failed to create blossom_seckey table: %s\n", sqlite3_errmsg(db));
|
||||||
|
sqlite3_close(db);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store key
|
||||||
|
const char *sql = "INSERT OR REPLACE INTO blossom_seckey (id, seckey) VALUES (1, ?)";
|
||||||
|
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
fprintf(stderr, "ERROR: SQL prepare failed: %s\n", sqlite3_errmsg(db));
|
||||||
|
sqlite3_close(db);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_bind_text(stmt, 1, seckey, -1, SQLITE_STATIC);
|
||||||
|
rc = sqlite3_step(stmt);
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
sqlite3_close(db);
|
||||||
|
|
||||||
|
if (rc != SQLITE_DONE) {
|
||||||
|
fprintf(stderr, "ERROR: Failed to store blossom private key\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get blossom private key from database
|
||||||
|
int get_blossom_private_key(char *seckey_out, size_t max_len) {
|
||||||
|
sqlite3 *db;
|
||||||
|
sqlite3_stmt *stmt;
|
||||||
|
int rc;
|
||||||
|
|
||||||
|
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
|
||||||
|
if (rc) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *sql = "SELECT seckey FROM blossom_seckey WHERE id = 1 LIMIT 1";
|
||||||
|
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
sqlite3_close(db);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
rc = sqlite3_step(stmt);
|
||||||
|
if (rc == SQLITE_ROW) {
|
||||||
|
const char *seckey = (const char *)sqlite3_column_text(stmt, 0);
|
||||||
|
if (seckey && strlen(seckey) == 64 && strlen(seckey) < max_len) {
|
||||||
|
strcpy(seckey_out, seckey);
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
sqlite3_close(db);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
sqlite3_close(db);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
// Insert blob metadata into database
|
// Insert blob metadata into database
|
||||||
int insert_blob_metadata(const char *sha256, long size, const char *type,
|
int insert_blob_metadata(const char *sha256, long size, const char *type,
|
||||||
long uploaded_at, const char *uploader_pubkey,
|
long uploaded_at, const char *uploader_pubkey,
|
||||||
@@ -85,7 +619,7 @@ int insert_blob_metadata(const char *sha256, long size, const char *type,
|
|||||||
sqlite3_stmt *stmt;
|
sqlite3_stmt *stmt;
|
||||||
int rc;
|
int rc;
|
||||||
|
|
||||||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL);
|
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL);
|
||||||
if (rc) {
|
if (rc) {
|
||||||
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
|
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
|
||||||
return 0;
|
return 0;
|
||||||
@@ -140,7 +674,7 @@ int get_blob_metadata(const char *sha256, blob_metadata_t *metadata) {
|
|||||||
sqlite3_stmt *stmt;
|
sqlite3_stmt *stmt;
|
||||||
int rc;
|
int rc;
|
||||||
|
|
||||||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
|
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY | SQLITE_OPEN_CREATE, NULL);
|
||||||
if (rc) {
|
if (rc) {
|
||||||
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
|
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
|
||||||
return 0;
|
return 0;
|
||||||
@@ -410,7 +944,7 @@ void handle_list_request(const char *pubkey) {
|
|||||||
sqlite3_stmt *stmt;
|
sqlite3_stmt *stmt;
|
||||||
int rc;
|
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) {
|
if (rc) {
|
||||||
|
|
||||||
send_error_response(500, "database_error", "Failed to access database",
|
send_error_response(500, "database_error", "Failed to access database",
|
||||||
@@ -557,7 +1091,7 @@ void handle_delete_request_with_validation(const char *sha256, nostr_request_res
|
|||||||
sqlite3_stmt *stmt;
|
sqlite3_stmt *stmt;
|
||||||
int rc;
|
int rc;
|
||||||
|
|
||||||
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL);
|
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL);
|
||||||
if (rc) {
|
if (rc) {
|
||||||
|
|
||||||
send_error_response(500, "database_error", "Failed to access database",
|
send_error_response(500, "database_error", "Failed to access database",
|
||||||
@@ -1296,21 +1830,60 @@ void handle_auth_challenge_request(void) {
|
|||||||
/////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
int main(int argc, char *argv[]) {
|
||||||
|
fprintf(stderr, "DEBUG: main() started\n");
|
||||||
|
fflush(stderr);
|
||||||
|
|
||||||
// Parse command line arguments
|
// 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++) {
|
for (int i = 1; i < argc; i++) {
|
||||||
if (strcmp(argv[i], "--db-path") == 0 && i + 1 < argc) {
|
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
|
i++; // Skip next argument
|
||||||
} else if (strcmp(argv[i], "--storage-dir") == 0 && i + 1 < argc) {
|
} else if (strcmp(argv[i], "--storage-dir") == 0 && i + 1 < argc) {
|
||||||
strncpy(g_storage_dir, argv[i + 1], sizeof(g_storage_dir) - 1);
|
strncpy(g_storage_dir, argv[i + 1], sizeof(g_storage_dir) - 1);
|
||||||
i++; // Skip next argument
|
i++; // Skip next argument
|
||||||
} else if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
|
} else if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
|
||||||
printf("Usage: %s [options]\n", argv[0]);
|
// Use write() directly to avoid FCGI printf redefinition
|
||||||
printf("Options:\n");
|
const char *help_text =
|
||||||
printf(" --db-path PATH Database file path (default: db/ginxsom.db)\n");
|
"Usage: ginxsom-fcgi [options]\n"
|
||||||
printf(" --storage-dir DIR Storage directory for files (default: blobs)\n");
|
"Options:\n"
|
||||||
printf(" --help, -h Show this help message\n");
|
" --db-path PATH Database file path (must match pubkey if keys exist)\n"
|
||||||
|
" --storage-dir DIR Storage directory for files (default: .)\n"
|
||||||
|
" --admin-pubkey KEY Admin public key (only used when creating new database)\n"
|
||||||
|
" --server-privkey KEY Server private key (creates new DB or validates existing)\n"
|
||||||
|
" --test-keys Use test keys from .test_keys file\n"
|
||||||
|
" --generate-keys Generate new keypair and create database (deprecated)\n"
|
||||||
|
" --help, -h Show this help message\n"
|
||||||
|
"\n"
|
||||||
|
"Database Naming:\n"
|
||||||
|
" Databases are named after the server's public key: db/<pubkey>.db\n"
|
||||||
|
" This ensures database-key consistency and prevents mismatches.\n"
|
||||||
|
"\n"
|
||||||
|
"Startup Scenarios:\n"
|
||||||
|
" 1. No arguments → Generate new keys, create db/<new_pubkey>.db\n"
|
||||||
|
" 2. --db-path <path> → Open existing database, validate keys match filename\n"
|
||||||
|
" 3. --server-privkey <key>→ Create new database with those keys\n"
|
||||||
|
" 4. --test-keys → Use test keys, create/overwrite test database\n"
|
||||||
|
" 5. --db-path + --server-privkey → Validate keys match or error\n";
|
||||||
|
ssize_t written = write(STDOUT_FILENO, help_text, strlen(help_text));
|
||||||
|
(void)written; // Suppress unused variable warning
|
||||||
return 0;
|
return 0;
|
||||||
|
} else if (strcmp(argv[i], "--admin-pubkey") == 0 && i + 1 < argc) {
|
||||||
|
strncpy(g_admin_pubkey, argv[i + 1], sizeof(g_admin_pubkey) - 1);
|
||||||
|
i++; // Skip next argument
|
||||||
|
} else if (strcmp(argv[i], "--server-privkey") == 0 && i + 1 < argc) {
|
||||||
|
strncpy(test_server_privkey, argv[i + 1], sizeof(test_server_privkey) - 1);
|
||||||
|
i++; // Skip next argument
|
||||||
|
} else if (strcmp(argv[i], "--test-keys") == 0) {
|
||||||
|
use_test_keys = 1;
|
||||||
|
} else if (strcmp(argv[i], "--generate-keys") == 0) {
|
||||||
|
g_generate_keys = 1;
|
||||||
|
fprintf(stderr, "WARNING: --generate-keys is deprecated. Keys are generated automatically when needed.\n");
|
||||||
} else {
|
} else {
|
||||||
fprintf(stderr, "Unknown option: %s\n", argv[i]);
|
fprintf(stderr, "Unknown option: %s\n", argv[i]);
|
||||||
fprintf(stderr, "Use --help for usage information\n");
|
fprintf(stderr, "Use --help for usage information\n");
|
||||||
@@ -1318,9 +1891,289 @@ int main(int argc, char *argv[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fprintf(stderr, "STARTUP: Using database path: %s\n", g_db_path);
|
|
||||||
fprintf(stderr, "STARTUP: Using storage directory: %s\n", g_storage_dir);
|
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");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
char line[256];
|
||||||
|
while (fgets(line, sizeof(line), keys_file)) {
|
||||||
|
if (strncmp(line, "ADMIN_PUBKEY='", 14) == 0) {
|
||||||
|
char *start = line + 14;
|
||||||
|
char *end = strchr(start, '\'');
|
||||||
|
if (end && (end - start) == 64) {
|
||||||
|
strncpy(g_admin_pubkey, start, 64);
|
||||||
|
g_admin_pubkey[64] = '\0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (strncmp(line, "SERVER_PRIVKEY='", 16) == 0) {
|
||||||
|
char *start = line + 16;
|
||||||
|
char *end = strchr(start, '\'');
|
||||||
|
if (end && (end - start) == 64) {
|
||||||
|
strncpy(test_server_privkey, start, 64);
|
||||||
|
test_server_privkey[64] = '\0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose(keys_file);
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
if (store_blossom_private_key(test_server_privkey) != 0) {
|
||||||
|
fprintf(stderr, "ERROR: Failed to store test private key\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store pubkey and admin pubkey in config
|
||||||
|
sqlite3 *db;
|
||||||
|
sqlite3_stmt *stmt;
|
||||||
|
int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE, NULL);
|
||||||
|
if (rc == SQLITE_OK) {
|
||||||
|
const char *sql = "INSERT OR REPLACE INTO config (key, value, description) VALUES (?, ?, ?)";
|
||||||
|
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||||
|
if (rc == SQLITE_OK) {
|
||||||
|
sqlite3_bind_text(stmt, 1, "blossom_pubkey", -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 2, g_blossom_pubkey, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 3, "Blossom server's public key (TEST MODE)", -1, SQLITE_STATIC);
|
||||||
|
sqlite3_step(stmt);
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strlen(g_admin_pubkey) > 0) {
|
||||||
|
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||||
|
if (rc == SQLITE_OK) {
|
||||||
|
sqlite3_bind_text(stmt, 1, "admin_pubkey", -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 2, g_admin_pubkey, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 3, "Admin public key (TEST MODE)", -1, SQLITE_STATIC);
|
||||||
|
sqlite3_step(stmt);
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sqlite3_close(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
// Derive pubkey from provided privkey
|
||||||
|
if (derive_pubkey_from_privkey(test_server_privkey, g_blossom_pubkey) != 0) {
|
||||||
|
fprintf(stderr, "ERROR: Invalid server private key\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(stderr, "KEYS: Derived pubkey: %s\n", g_blossom_pubkey);
|
||||||
|
|
||||||
|
// Scenario 5: Both database 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
|
||||||
|
if (g_generate_keys) {
|
||||||
|
fprintf(stderr, "Key generation completed, exiting.\n");
|
||||||
|
fflush(stderr);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize server configuration and identity
|
// Initialize server configuration and identity
|
||||||
// Try file-based config first, then fall back to database config
|
// Try file-based config first, then fall back to database config
|
||||||
int config_loaded = 0;
|
int config_loaded = 0;
|
||||||
@@ -1337,19 +2190,10 @@ if (!config_loaded /* && !initialize_server_config() */) {
|
|||||||
fprintf(stderr, "STARTUP: Database configuration loaded successfully\n");
|
fprintf(stderr, "STARTUP: Database configuration loaded successfully\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
// CRITICAL: Initialize nostr crypto system for cryptographic operations
|
|
||||||
fprintf(stderr, "STARTUP: Initializing nostr crypto system...\r\n");
|
|
||||||
if (nostr_crypto_init() != 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 request validator system
|
// Initialize request validator system
|
||||||
fprintf(stderr, "STARTUP: Initializing request validator system...\r\n");
|
fprintf(stderr, "STARTUP: Initializing request validator system...\r\n");
|
||||||
int validator_init_result =
|
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",
|
fprintf(stderr, "MAIN: validator init return code: %d\r\n",
|
||||||
validator_init_result);
|
validator_init_result);
|
||||||
if (validator_init_result != NOSTR_SUCCESS) {
|
if (validator_init_result != NOSTR_SUCCESS) {
|
||||||
@@ -1463,6 +2307,11 @@ if (!config_loaded /* && !initialize_server_config() */) {
|
|||||||
operation = "report";
|
operation = "report";
|
||||||
} else if (strncmp(request_uri, "/api/", 5) == 0) {
|
} else if (strncmp(request_uri, "/api/", 5) == 0) {
|
||||||
operation = "admin";
|
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) {
|
} else if (strcmp(request_method, "GET") == 0 && strncmp(request_uri, "/list/", 6) == 0) {
|
||||||
operation = "list";
|
operation = "list";
|
||||||
} else if (strcmp(request_method, "GET") == 0 && strcmp(request_uri, "/auth") == 0) {
|
} else if (strcmp(request_method, "GET") == 0 && strcmp(request_uri, "/auth") == 0) {
|
||||||
@@ -1504,6 +2353,9 @@ if (!config_loaded /* && !initialize_server_config() */) {
|
|||||||
// List operation might be optional auth - let handler decide
|
// List operation might be optional auth - let handler decide
|
||||||
} else if (strcmp(operation, "admin") == 0 && strcmp(request_uri, "/api/health") == 0) {
|
} else if (strcmp(operation, "admin") == 0 && strcmp(request_uri, "/api/health") == 0) {
|
||||||
// Health endpoint is public and doesn't require authentication - let handler decide
|
// 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 {
|
} else {
|
||||||
// For other operations, validation failure means auth failure
|
// For other operations, validation failure means auth failure
|
||||||
const char *message = result.reason[0] ? result.reason : "Authentication failed";
|
const char *message = result.reason[0] ? result.reason : "Authentication failed";
|
||||||
|
|||||||
@@ -32,8 +32,8 @@
|
|||||||
// NOSTR_ERROR_NIP42_CHALLENGE_EXPIRED are already defined in
|
// NOSTR_ERROR_NIP42_CHALLENGE_EXPIRED are already defined in
|
||||||
// nostr_core_lib/nostr_core/nostr_common.h
|
// nostr_core_lib/nostr_core/nostr_common.h
|
||||||
|
|
||||||
// Database path (consistent with main.c)
|
// Use global database path from main.c
|
||||||
#define DB_PATH "db/ginxsom.db"
|
extern char g_db_path[];
|
||||||
|
|
||||||
// NIP-42 challenge management constants
|
// NIP-42 challenge management constants
|
||||||
#define MAX_CHALLENGES 1000
|
#define MAX_CHALLENGES 1000
|
||||||
@@ -115,7 +115,7 @@ static int validate_nip42_event(cJSON *event, const char *relay_url,
|
|||||||
const char *challenge_id);
|
const char *challenge_id);
|
||||||
static int validate_admin_event(cJSON *event, const char *method, const char *endpoint);
|
static int validate_admin_event(cJSON *event, const char *method, const char *endpoint);
|
||||||
static int check_database_auth_rules(const char *pubkey, const char *operation,
|
static int check_database_auth_rules(const char *pubkey, const char *operation,
|
||||||
const char *resource_hash);
|
const char *resource_hash, const char *mime_type);
|
||||||
void nostr_request_validator_clear_violation(void);
|
void nostr_request_validator_clear_violation(void);
|
||||||
|
|
||||||
// NIP-42 challenge management functions
|
// NIP-42 challenge management functions
|
||||||
@@ -283,6 +283,16 @@ int nostr_validate_unified_request(const nostr_unified_request_t *request,
|
|||||||
// PHASE 2: NOSTR EVENT VALIDATION (CPU Intensive ~2ms)
|
// PHASE 2: NOSTR EVENT VALIDATION (CPU Intensive ~2ms)
|
||||||
/////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Check if authentication is disabled first (regardless of header presence)
|
||||||
|
if (!g_auth_cache.auth_required) {
|
||||||
|
validator_debug_log("VALIDATOR_DEBUG: STEP 4 PASSED - Authentication "
|
||||||
|
"disabled, allowing request\n");
|
||||||
|
result->valid = 1;
|
||||||
|
result->error_code = NOSTR_SUCCESS;
|
||||||
|
strcpy(result->reason, "Authentication disabled");
|
||||||
|
return NOSTR_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if this is a BUD-09 report request - allow anonymous reporting
|
// Check if this is a BUD-09 report request - allow anonymous reporting
|
||||||
if (request->operation && strcmp(request->operation, "report") == 0) {
|
if (request->operation && strcmp(request->operation, "report") == 0) {
|
||||||
// BUD-09 allows anonymous reporting - pass through to bud09.c for validation
|
// BUD-09 allows anonymous reporting - pass through to bud09.c for validation
|
||||||
@@ -820,7 +830,7 @@ int nostr_validate_unified_request(const nostr_unified_request_t *request,
|
|||||||
}
|
}
|
||||||
|
|
||||||
int rules_result = check_database_auth_rules(
|
int rules_result = check_database_auth_rules(
|
||||||
extracted_pubkey, request->operation, hash_for_rules);
|
extracted_pubkey, request->operation, hash_for_rules, request->mime_type);
|
||||||
if (rules_result != NOSTR_SUCCESS) {
|
if (rules_result != NOSTR_SUCCESS) {
|
||||||
validator_debug_log(
|
validator_debug_log(
|
||||||
"VALIDATOR_DEBUG: STEP 14 FAILED - Database rules denied request\n");
|
"VALIDATOR_DEBUG: STEP 14 FAILED - Database rules denied request\n");
|
||||||
@@ -1054,7 +1064,7 @@ static int reload_auth_config(void) {
|
|||||||
memset(&g_auth_cache, 0, sizeof(g_auth_cache));
|
memset(&g_auth_cache, 0, sizeof(g_auth_cache));
|
||||||
|
|
||||||
// Open database
|
// 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) {
|
if (rc != SQLITE_OK) {
|
||||||
validator_debug_log("VALIDATOR: Could not open database\n");
|
validator_debug_log("VALIDATOR: Could not open database\n");
|
||||||
// Use defaults
|
// Use defaults
|
||||||
@@ -1316,7 +1326,7 @@ static int validate_blossom_event(cJSON *event, const char *expected_hash,
|
|||||||
* Implements the 6-step rule evaluation engine from AUTH_API.md
|
* Implements the 6-step rule evaluation engine from AUTH_API.md
|
||||||
*/
|
*/
|
||||||
static int check_database_auth_rules(const char *pubkey, const char *operation,
|
static int check_database_auth_rules(const char *pubkey, const char *operation,
|
||||||
const char *resource_hash) {
|
const char *resource_hash, const char *mime_type) {
|
||||||
sqlite3 *db = NULL;
|
sqlite3 *db = NULL;
|
||||||
sqlite3_stmt *stmt = NULL;
|
sqlite3_stmt *stmt = NULL;
|
||||||
int rc;
|
int rc;
|
||||||
@@ -1330,12 +1340,12 @@ static int check_database_auth_rules(const char *pubkey, const char *operation,
|
|||||||
char rules_msg[256];
|
char rules_msg[256];
|
||||||
sprintf(rules_msg,
|
sprintf(rules_msg,
|
||||||
"VALIDATOR_DEBUG: RULES ENGINE - Checking rules for pubkey=%.32s..., "
|
"VALIDATOR_DEBUG: RULES ENGINE - Checking rules for pubkey=%.32s..., "
|
||||||
"operation=%s\n",
|
"operation=%s, mime_type=%s\n",
|
||||||
pubkey, operation ? operation : "NULL");
|
pubkey, operation ? operation : "NULL", mime_type ? mime_type : "NULL");
|
||||||
validator_debug_log(rules_msg);
|
validator_debug_log(rules_msg);
|
||||||
|
|
||||||
// Open database
|
// 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) {
|
if (rc != SQLITE_OK) {
|
||||||
validator_debug_log(
|
validator_debug_log(
|
||||||
"VALIDATOR_DEBUG: RULES ENGINE - Failed to open database\n");
|
"VALIDATOR_DEBUG: RULES ENGINE - Failed to open database\n");
|
||||||
@@ -1418,7 +1428,49 @@ static int check_database_auth_rules(const char *pubkey, const char *operation,
|
|||||||
"resource hash provided\n");
|
"resource hash provided\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Check pubkey whitelist
|
// Step 3: Check MIME type blacklist
|
||||||
|
if (mime_type) {
|
||||||
|
// Match both exact MIME type and wildcard patterns (e.g., 'image/*')
|
||||||
|
const char *mime_blacklist_sql =
|
||||||
|
"SELECT rule_type, description FROM auth_rules WHERE rule_type = "
|
||||||
|
"'mime_blacklist' AND (rule_target = ? OR rule_target LIKE '%/*' AND ? LIKE REPLACE(rule_target, '*', '%')) AND (operation = ? OR operation = '*') AND enabled = "
|
||||||
|
"1 ORDER BY priority LIMIT 1";
|
||||||
|
rc = sqlite3_prepare_v2(db, mime_blacklist_sql, -1, &stmt, NULL);
|
||||||
|
if (rc == SQLITE_OK) {
|
||||||
|
sqlite3_bind_text(stmt, 1, mime_type, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 2, mime_type, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 3, operation ? operation : "", -1, SQLITE_STATIC);
|
||||||
|
|
||||||
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||||
|
const char *description = (const char *)sqlite3_column_text(stmt, 1);
|
||||||
|
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 3 FAILED - "
|
||||||
|
"MIME type blacklisted\n");
|
||||||
|
char mime_blacklist_msg[256];
|
||||||
|
sprintf(
|
||||||
|
mime_blacklist_msg,
|
||||||
|
"VALIDATOR_DEBUG: RULES ENGINE - MIME blacklist rule matched: %s\n",
|
||||||
|
description ? description : "Unknown");
|
||||||
|
validator_debug_log(mime_blacklist_msg);
|
||||||
|
|
||||||
|
// Set specific violation details for status code mapping
|
||||||
|
strcpy(g_last_rule_violation.violation_type, "mime_blacklist");
|
||||||
|
sprintf(g_last_rule_violation.reason, "%s: MIME type blacklisted",
|
||||||
|
description ? description : "TEST_MIME_BLACKLIST");
|
||||||
|
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
sqlite3_close(db);
|
||||||
|
return NOSTR_ERROR_AUTH_REQUIRED;
|
||||||
|
}
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
}
|
||||||
|
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 3 PASSED - MIME "
|
||||||
|
"type not blacklisted\n");
|
||||||
|
} else {
|
||||||
|
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 3 SKIPPED - No "
|
||||||
|
"MIME type provided\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Check pubkey whitelist
|
||||||
// Match both exact operation and wildcard '*'
|
// Match both exact operation and wildcard '*'
|
||||||
const char *whitelist_sql =
|
const char *whitelist_sql =
|
||||||
"SELECT rule_type, description FROM auth_rules WHERE rule_type = "
|
"SELECT rule_type, description FROM auth_rules WHERE rule_type = "
|
||||||
@@ -1447,7 +1499,72 @@ static int check_database_auth_rules(const char *pubkey, const char *operation,
|
|||||||
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 3 FAILED - Pubkey "
|
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 3 FAILED - Pubkey "
|
||||||
"not whitelisted\n");
|
"not whitelisted\n");
|
||||||
|
|
||||||
// Step 4: Check if any whitelist rules exist - if yes, deny by default
|
// Step 5: Check MIME type whitelist (only if not already denied)
|
||||||
|
if (mime_type) {
|
||||||
|
// Match both exact MIME type and wildcard patterns (e.g., 'image/*')
|
||||||
|
const char *mime_whitelist_sql =
|
||||||
|
"SELECT rule_type, description FROM auth_rules WHERE rule_type = "
|
||||||
|
"'mime_whitelist' AND (rule_target = ? OR rule_target LIKE '%/*' AND ? LIKE REPLACE(rule_target, '*', '%')) AND (operation = ? OR operation = '*') AND enabled = "
|
||||||
|
"1 ORDER BY priority LIMIT 1";
|
||||||
|
rc = sqlite3_prepare_v2(db, mime_whitelist_sql, -1, &stmt, NULL);
|
||||||
|
if (rc == SQLITE_OK) {
|
||||||
|
sqlite3_bind_text(stmt, 1, mime_type, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 2, mime_type, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 3, operation ? operation : "", -1, SQLITE_STATIC);
|
||||||
|
|
||||||
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||||
|
const char *description = (const char *)sqlite3_column_text(stmt, 1);
|
||||||
|
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 5 PASSED - "
|
||||||
|
"MIME type whitelisted\n");
|
||||||
|
char mime_whitelist_msg[256];
|
||||||
|
sprintf(mime_whitelist_msg,
|
||||||
|
"VALIDATOR_DEBUG: RULES ENGINE - MIME whitelist rule matched: %s\n",
|
||||||
|
description ? description : "Unknown");
|
||||||
|
validator_debug_log(mime_whitelist_msg);
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
sqlite3_close(db);
|
||||||
|
return NOSTR_SUCCESS; // Allow whitelisted MIME type
|
||||||
|
}
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
}
|
||||||
|
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 5 FAILED - MIME "
|
||||||
|
"type not whitelisted\n");
|
||||||
|
} else {
|
||||||
|
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 5 SKIPPED - No "
|
||||||
|
"MIME type provided\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 6: Check if any MIME whitelist rules exist - if yes, deny by default
|
||||||
|
// Match both exact operation and wildcard '*'
|
||||||
|
const char *mime_whitelist_exists_sql =
|
||||||
|
"SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'mime_whitelist' "
|
||||||
|
"AND (operation = ? OR operation = '*') AND enabled = 1 LIMIT 1";
|
||||||
|
rc = sqlite3_prepare_v2(db, mime_whitelist_exists_sql, -1, &stmt, NULL);
|
||||||
|
if (rc == SQLITE_OK) {
|
||||||
|
sqlite3_bind_text(stmt, 1, operation ? operation : "", -1, SQLITE_STATIC);
|
||||||
|
|
||||||
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||||
|
int mime_whitelist_count = sqlite3_column_int(stmt, 0);
|
||||||
|
if (mime_whitelist_count > 0) {
|
||||||
|
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 6 FAILED - "
|
||||||
|
"MIME whitelist exists but type not in it\n");
|
||||||
|
|
||||||
|
// Set specific violation details for status code mapping
|
||||||
|
strcpy(g_last_rule_violation.violation_type, "mime_whitelist_violation");
|
||||||
|
strcpy(g_last_rule_violation.reason,
|
||||||
|
"MIME type not whitelisted for this operation");
|
||||||
|
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
sqlite3_close(db);
|
||||||
|
return NOSTR_ERROR_AUTH_REQUIRED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
}
|
||||||
|
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 6 PASSED - No "
|
||||||
|
"MIME whitelist restrictions apply\n");
|
||||||
|
|
||||||
|
// Step 7: Check if any whitelist rules exist - if yes, deny by default
|
||||||
// Match both exact operation and wildcard '*'
|
// Match both exact operation and wildcard '*'
|
||||||
const char *whitelist_exists_sql =
|
const char *whitelist_exists_sql =
|
||||||
"SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'pubkey_whitelist' "
|
"SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'pubkey_whitelist' "
|
||||||
@@ -1478,7 +1595,7 @@ static int check_database_auth_rules(const char *pubkey, const char *operation,
|
|||||||
"whitelist restrictions apply\n");
|
"whitelist restrictions apply\n");
|
||||||
|
|
||||||
sqlite3_close(db);
|
sqlite3_close(db);
|
||||||
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 5 PASSED - All "
|
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 7 PASSED - All "
|
||||||
"rule checks completed, default ALLOW\n");
|
"rule checks completed, default ALLOW\n");
|
||||||
return NOSTR_SUCCESS; // Default allow if no restrictive rules matched
|
return NOSTR_SUCCESS; // Default allow if no restrictive rules matched
|
||||||
}
|
}
|
||||||
|
|||||||
199
src/test_keygen.c
Normal file
199
src/test_keygen.c
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
/*
|
||||||
|
* Test program for key generation
|
||||||
|
* Standalone version that doesn't require FastCGI
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sqlite3.h>
|
||||||
|
#include "../nostr_core_lib/nostr_core/nostr_common.h"
|
||||||
|
#include "../nostr_core_lib/nostr_core/utils.h"
|
||||||
|
|
||||||
|
// Forward declarations
|
||||||
|
int generate_random_private_key_bytes(unsigned char *key_bytes, size_t len);
|
||||||
|
int generate_server_keypair(const char *db_path);
|
||||||
|
int store_blossom_private_key(const char *db_path, const char *seckey);
|
||||||
|
|
||||||
|
// Generate random private key bytes using /dev/urandom
|
||||||
|
int generate_random_private_key_bytes(unsigned char *key_bytes, size_t len) {
|
||||||
|
FILE *fp = fopen("/dev/urandom", "rb");
|
||||||
|
if (!fp) {
|
||||||
|
fprintf(stderr, "ERROR: Cannot open /dev/urandom for key generation\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t bytes_read = fread(key_bytes, 1, len, fp);
|
||||||
|
fclose(fp);
|
||||||
|
|
||||||
|
if (bytes_read != len) {
|
||||||
|
fprintf(stderr, "ERROR: Failed to read %zu bytes from /dev/urandom\n", len);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store blossom private key in dedicated table
|
||||||
|
int store_blossom_private_key(const char *db_path, const char *seckey) {
|
||||||
|
sqlite3 *db;
|
||||||
|
sqlite3_stmt *stmt;
|
||||||
|
int rc;
|
||||||
|
|
||||||
|
// Validate key format
|
||||||
|
if (!seckey || strlen(seckey) != 64) {
|
||||||
|
fprintf(stderr, "ERROR: Invalid blossom private key format\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create blossom_seckey table if it doesn't exist
|
||||||
|
rc = sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL);
|
||||||
|
if (rc) {
|
||||||
|
fprintf(stderr, "ERROR: Can't open database: %s\n", sqlite3_errmsg(db));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create table
|
||||||
|
const char *create_sql = "CREATE TABLE IF NOT EXISTS blossom_seckey (id INTEGER PRIMARY KEY CHECK (id = 1), seckey TEXT NOT NULL, created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), CHECK (length(seckey) = 64))";
|
||||||
|
rc = sqlite3_exec(db, create_sql, NULL, NULL, NULL);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
fprintf(stderr, "ERROR: Failed to create blossom_seckey table: %s\n", sqlite3_errmsg(db));
|
||||||
|
sqlite3_close(db);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store key
|
||||||
|
const char *sql = "INSERT OR REPLACE INTO blossom_seckey (id, seckey) VALUES (1, ?)";
|
||||||
|
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
fprintf(stderr, "ERROR: SQL prepare failed: %s\n", sqlite3_errmsg(db));
|
||||||
|
sqlite3_close(db);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_bind_text(stmt, 1, seckey, -1, SQLITE_STATIC);
|
||||||
|
rc = sqlite3_step(stmt);
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
sqlite3_close(db);
|
||||||
|
|
||||||
|
if (rc != SQLITE_DONE) {
|
||||||
|
fprintf(stderr, "ERROR: Failed to store blossom private key\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate server keypair and store in database
|
||||||
|
int generate_server_keypair(const char *db_path) {
|
||||||
|
printf("Generating server keypair...\n");
|
||||||
|
unsigned char seckey_bytes[32];
|
||||||
|
char seckey_hex[65];
|
||||||
|
char pubkey_hex[65];
|
||||||
|
|
||||||
|
// Generate random private key
|
||||||
|
printf("Generating random private key...\n");
|
||||||
|
if (generate_random_private_key_bytes(seckey_bytes, 32) != 0) {
|
||||||
|
fprintf(stderr, "Failed to generate random bytes\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the private key
|
||||||
|
if (nostr_ec_private_key_verify(seckey_bytes) != NOSTR_SUCCESS) {
|
||||||
|
fprintf(stderr, "ERROR: Generated invalid private key\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to hex
|
||||||
|
nostr_bytes_to_hex(seckey_bytes, 32, seckey_hex);
|
||||||
|
|
||||||
|
// Derive public key
|
||||||
|
unsigned char pubkey_bytes[32];
|
||||||
|
if (nostr_ec_public_key_from_private_key(seckey_bytes, pubkey_bytes) != NOSTR_SUCCESS) {
|
||||||
|
fprintf(stderr, "ERROR: Failed to derive public key\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert public key to hex
|
||||||
|
nostr_bytes_to_hex(pubkey_bytes, 32, pubkey_hex);
|
||||||
|
|
||||||
|
// Store private key securely
|
||||||
|
if (store_blossom_private_key(db_path, seckey_hex) != 0) {
|
||||||
|
fprintf(stderr, "ERROR: Failed to store blossom private key\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store public key in config
|
||||||
|
sqlite3 *db;
|
||||||
|
sqlite3_stmt *stmt;
|
||||||
|
int rc;
|
||||||
|
|
||||||
|
rc = sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READWRITE, NULL);
|
||||||
|
if (rc) {
|
||||||
|
fprintf(stderr, "ERROR: Can't open database for config: %s\n", sqlite3_errmsg(db));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *sql = "INSERT OR REPLACE INTO config (key, value, description) VALUES (?, ?, ?)";
|
||||||
|
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
fprintf(stderr, "ERROR: SQL prepare failed: %s\n", sqlite3_errmsg(db));
|
||||||
|
sqlite3_close(db);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_bind_text(stmt, 1, "blossom_pubkey", -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 2, pubkey_hex, -1, SQLITE_STATIC);
|
||||||
|
sqlite3_bind_text(stmt, 3, "Blossom server's public key for Nostr communication", -1, SQLITE_STATIC);
|
||||||
|
|
||||||
|
rc = sqlite3_step(stmt);
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
sqlite3_close(db);
|
||||||
|
|
||||||
|
if (rc != SQLITE_DONE) {
|
||||||
|
fprintf(stderr, "ERROR: Failed to store blossom public key in config\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display keys for admin setup
|
||||||
|
printf("========================================\n");
|
||||||
|
printf("SERVER KEYPAIR GENERATED SUCCESSFULLY\n");
|
||||||
|
printf("========================================\n");
|
||||||
|
printf("Blossom Public Key: %s\n", pubkey_hex);
|
||||||
|
printf("Blossom Private Key: %s\n", seckey_hex);
|
||||||
|
printf("========================================\n");
|
||||||
|
printf("IMPORTANT: Save the private key securely!\n");
|
||||||
|
printf("This key is used for decrypting admin messages.\n");
|
||||||
|
printf("========================================\n");
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
const char *db_path = "db/ginxsom.db";
|
||||||
|
|
||||||
|
if (argc > 1) {
|
||||||
|
db_path = argv[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("Test Key Generation\n");
|
||||||
|
printf("===================\n");
|
||||||
|
printf("Database: %s\n\n", db_path);
|
||||||
|
|
||||||
|
// Initialize nostr crypto
|
||||||
|
printf("Initializing nostr crypto system...\n");
|
||||||
|
if (nostr_crypto_init() != NOSTR_SUCCESS) {
|
||||||
|
fprintf(stderr, "FATAL: Failed to initialize nostr crypto\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("Crypto system initialized\n\n");
|
||||||
|
|
||||||
|
// Generate keypair
|
||||||
|
if (generate_server_keypair(db_path) != 0) {
|
||||||
|
fprintf(stderr, "FATAL: Key generation failed\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("\nKey generation test completed successfully!\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
50
src/test_main.c
Normal file
50
src/test_main.c
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* Minimal test version of main.c to debug startup issues
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include "ginxsom.h"
|
||||||
|
|
||||||
|
// Copy just the essential parts for testing
|
||||||
|
char g_db_path[4096] = "db/ginxsom.db";
|
||||||
|
char g_storage_dir[4096] = ".";
|
||||||
|
char g_admin_pubkey[65] = "";
|
||||||
|
char g_relay_seckey[65] = "";
|
||||||
|
int g_generate_keys = 0;
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
printf("DEBUG: main() started\n");
|
||||||
|
fflush(stdout);
|
||||||
|
|
||||||
|
// Parse minimal args
|
||||||
|
for (int i = 1; i < argc; i++) {
|
||||||
|
printf("DEBUG: arg %d: %s\n", i, argv[i]);
|
||||||
|
fflush(stdout);
|
||||||
|
if (strcmp(argv[i], "--generate-keys") == 0) {
|
||||||
|
g_generate_keys = 1;
|
||||||
|
printf("DEBUG: generate-keys flag set\n");
|
||||||
|
fflush(stdout);
|
||||||
|
} else if (strcmp(argv[i], "--help") == 0) {
|
||||||
|
printf("Usage: test_main [options]\n");
|
||||||
|
printf(" --generate-keys Generate keys\n");
|
||||||
|
printf(" --help Show help\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("DEBUG: g_generate_keys = %d\n", g_generate_keys);
|
||||||
|
fflush(stdout);
|
||||||
|
|
||||||
|
if (g_generate_keys) {
|
||||||
|
printf("DEBUG: Would generate keys here\n");
|
||||||
|
fflush(stdout);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("DEBUG: Normal startup would continue here\n");
|
||||||
|
fflush(stdout);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
Test blob content for Ginxsom Blossom server (PRODUCTION)
|
|
||||||
Timestamp: 2025-11-10T06:30:36-04:00
|
|
||||||
Random data: bb7d8d5206aadf4ecd48829f9674454c160bae68e98c6ce5f7f678f997dbe86a
|
|
||||||
Test message: Hello from production test!
|
|
||||||
|
|
||||||
This file is used to test the upload functionality
|
|
||||||
of the Ginxsom Blossom server on blossom.laantungir.net
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
test file content
|
|
||||||
25
test_key_generation.sh
Executable file
25
test_key_generation.sh
Executable file
@@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Test key generation for ginxsom
|
||||||
|
|
||||||
|
echo "=== Testing Key Generation ==="
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Run the binary with --generate-keys flag
|
||||||
|
echo "Running: ./build/ginxsom-fcgi --generate-keys --db-path db/ginxsom.db"
|
||||||
|
echo
|
||||||
|
./build/ginxsom-fcgi --generate-keys --db-path db/ginxsom.db 2>&1
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "=== Checking if keys were stored ==="
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Check if blossom_seckey table was created
|
||||||
|
echo "Checking blossom_seckey table:"
|
||||||
|
sqlite3 db/ginxsom.db "SELECT COUNT(*) as key_count FROM blossom_seckey" 2>&1
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Checking blossom_pubkey in config:"
|
||||||
|
sqlite3 db/ginxsom.db "SELECT value FROM config WHERE key='blossom_pubkey'" 2>&1
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "=== Test Complete ==="
|
||||||
54
test_mode_verification.sh
Executable file
54
test_mode_verification.sh
Executable file
@@ -0,0 +1,54 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "=== Test Mode Verification ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Expected test keys from .test_keys
|
||||||
|
EXPECTED_ADMIN_PUBKEY="8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e"
|
||||||
|
EXPECTED_SERVER_PUBKEY="52e366edfa4e9cc6a6d4653828e51ccf828a2f5a05227d7a768f33b5a198681a"
|
||||||
|
|
||||||
|
echo "1. Checking database keys (should be OLD keys, not test keys)..."
|
||||||
|
DB_ADMIN_PUBKEY=$(sqlite3 db/ginxsom.db "SELECT value FROM config WHERE key = 'admin_pubkey'")
|
||||||
|
DB_BLOSSOM_PUBKEY=$(sqlite3 db/ginxsom.db "SELECT value FROM config WHERE key = 'blossom_pubkey'")
|
||||||
|
DB_BLOSSOM_SECKEY=$(sqlite3 db/ginxsom.db "SELECT seckey FROM blossom_seckey WHERE id = 1")
|
||||||
|
|
||||||
|
echo " Database admin_pubkey: '$DB_ADMIN_PUBKEY'"
|
||||||
|
echo " Database blossom_pubkey: '$DB_BLOSSOM_PUBKEY'"
|
||||||
|
echo " Database blossom_seckey: '$DB_BLOSSOM_SECKEY'"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Verify database was NOT modified with test keys
|
||||||
|
if [ "$DB_ADMIN_PUBKEY" = "$EXPECTED_ADMIN_PUBKEY" ]; then
|
||||||
|
echo " ❌ FAIL: Database admin_pubkey matches test key (should NOT be modified)"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo " ✓ PASS: Database admin_pubkey is different from test key (not modified)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DB_BLOSSOM_PUBKEY" = "$EXPECTED_SERVER_PUBKEY" ]; then
|
||||||
|
echo " ❌ FAIL: Database blossom_pubkey matches test key (should NOT be modified)"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo " ✓ PASS: Database blossom_pubkey is different from test key (not modified)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "2. Checking server is running..."
|
||||||
|
if curl -s http://localhost:9001/ > /dev/null; then
|
||||||
|
echo " ✓ PASS: Server is responding"
|
||||||
|
else
|
||||||
|
echo " ❌ FAIL: Server is not responding"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "3. Verifying test keys from .test_keys file..."
|
||||||
|
echo " Expected admin pubkey: $EXPECTED_ADMIN_PUBKEY"
|
||||||
|
echo " Expected server pubkey: $EXPECTED_SERVER_PUBKEY"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== All Tests Passed ==="
|
||||||
|
echo "Test mode is working correctly:"
|
||||||
|
echo " - Test keys are loaded in memory"
|
||||||
|
echo " - Database was NOT modified"
|
||||||
|
echo " - Server is running with test keys"
|
||||||
206
tests/admin_event_test.sh
Executable file
206
tests/admin_event_test.sh
Executable 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
|
||||||
@@ -14,9 +14,9 @@ TESTS_PASSED=0
|
|||||||
TESTS_FAILED=0
|
TESTS_FAILED=0
|
||||||
TOTAL_TESTS=0
|
TOTAL_TESTS=0
|
||||||
|
|
||||||
# Test keys for different scenarios
|
# Test keys for different scenarios - Using WSB's keys for TEST_USER1
|
||||||
TEST_USER1_PRIVKEY="5c0c523f52a5b6fad39ed2403092df8cebc36318b39383bca6c00808626fab3a"
|
TEST_USER1_PRIVKEY="22cc83aa57928a2800234c939240c9a6f0f44a33ea3838a860ed38930b195afd"
|
||||||
TEST_USER1_PUBKEY="87d3561f19b74adbe8bf840682992466068830a9d8c36b4a0c99d36f826cb6cb"
|
TEST_USER1_PUBKEY="8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e"
|
||||||
|
|
||||||
TEST_USER2_PRIVKEY="182c3a5e3b7a1b7e4f5c6b7c8b4a5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
|
TEST_USER2_PRIVKEY="182c3a5e3b7a1b7e4f5c6b7c8b4a5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
|
||||||
TEST_USER2_PUBKEY="c95195e5e7de1ad8c4d3c0ac4e8b5c0c4e0c4d3c1e5c8d4c2e7e9f4a5b6c7d8e"
|
TEST_USER2_PUBKEY="c95195e5e7de1ad8c4d3c0ac4e8b5c0c4e0c4d3c1e5c8d4c2e7e9f4a5b6c7d8e"
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
e3ba927d32ca105a8a4cafa2e013b97945a165c38e9ce573446a2332dc312fdb
|
299c28eeb15df327c30c9afd952d4e35c3777443d2094b2caab2fc94599ce607
|
||||||
|
|||||||
@@ -5,12 +5,14 @@
|
|||||||
|
|
||||||
set -e # Exit on any error
|
set -e # Exit on any error
|
||||||
|
|
||||||
# Configuration
|
# Configuration - Using WSB's keys
|
||||||
# SERVER_URL="http://localhost:9001"
|
# SERVER_URL="http://localhost:9001"
|
||||||
SERVER_URL="https://localhost:9443"
|
SERVER_URL="https://localhost:9443"
|
||||||
UPLOAD_ENDPOINT="${SERVER_URL}/upload"
|
UPLOAD_ENDPOINT="${SERVER_URL}/upload"
|
||||||
TEST_FILE="test_blob_$(date +%s).txt"
|
TEST_FILE="test_blob_$(date +%s).txt"
|
||||||
CLEANUP_FILES=()
|
CLEANUP_FILES=()
|
||||||
|
NOSTR_PRIVKEY="22cc83aa57928a2800234c939240c9a6f0f44a33ea3838a860ed38930b195afd"
|
||||||
|
NOSTR_PUBKEY="8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e"
|
||||||
|
|
||||||
# Colors for output
|
# Colors for output
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
@@ -128,13 +130,14 @@ calculate_hash() {
|
|||||||
|
|
||||||
# Generate nostr event
|
# Generate nostr event
|
||||||
generate_nostr_event() {
|
generate_nostr_event() {
|
||||||
log_info "Generating kind 24242 nostr event with nak..."
|
log_info "Generating kind 24242 nostr event with nak using Alice's private key..."
|
||||||
|
|
||||||
# Calculate expiration time (1 hour from now)
|
# Calculate expiration time (1 hour from now)
|
||||||
EXPIRATION=$(date -d '+1 hour' +%s)
|
EXPIRATION=$(date -d '+1 hour' +%s)
|
||||||
|
|
||||||
# Generate the event using nak
|
# Generate the event using nak with Alice's private key
|
||||||
EVENT_JSON=$(nak event -k 24242 -c "" \
|
EVENT_JSON=$(nak event -k 24242 -c "" \
|
||||||
|
--sec "$NOSTR_PRIVKEY" \
|
||||||
-t "t=upload" \
|
-t "t=upload" \
|
||||||
-t "x=${HASH}" \
|
-t "x=${HASH}" \
|
||||||
-t "expiration=${EXPIRATION}")
|
-t "expiration=${EXPIRATION}")
|
||||||
|
|||||||
@@ -126,13 +126,14 @@ calculate_hash() {
|
|||||||
|
|
||||||
# Generate nostr event
|
# Generate nostr event
|
||||||
generate_nostr_event() {
|
generate_nostr_event() {
|
||||||
log_info "Generating kind 24242 nostr event with nak..."
|
log_info "Generating kind 24242 nostr event with nak using WSB's private key..."
|
||||||
|
|
||||||
# Calculate expiration time (1 hour from now)
|
# Calculate expiration time (1 hour from now)
|
||||||
EXPIRATION=$(date -d '+1 hour' +%s)
|
EXPIRATION=$(date -d '+1 hour' +%s)
|
||||||
|
|
||||||
# Generate the event using nak
|
# Generate the event using nak with WSB's private key
|
||||||
EVENT_JSON=$(nak event -k 24242 -c "" \
|
EVENT_JSON=$(nak event -k 24242 -c "" \
|
||||||
|
--sec "22cc83aa57928a2800234c939240c9a6f0f44a33ea3838a860ed38930b195afd" \
|
||||||
-t "t=upload" \
|
-t "t=upload" \
|
||||||
-t "x=${HASH}" \
|
-t "x=${HASH}" \
|
||||||
-t "expiration=${EXPIRATION}")
|
-t "expiration=${EXPIRATION}")
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
# This script tests the blob listing functionality
|
# This script tests the blob listing functionality
|
||||||
|
|
||||||
BASE_URL="http://localhost:9001"
|
BASE_URL="http://localhost:9001"
|
||||||
NOSTR_PRIVKEY="0000000000000000000000000000000000000000000000000000000000000001"
|
NOSTR_PRIVKEY="22cc83aa57928a2800234c939240c9a6f0f44a33ea3838a860ed38930b195afd"
|
||||||
NOSTR_PUBKEY="79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
|
NOSTR_PUBKEY="8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e"
|
||||||
|
|
||||||
# Colors for output
|
# Colors for output
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
@@ -19,128 +19,117 @@ echo
|
|||||||
# Function to generate a Nostr event for list authorization
|
# Function to generate a Nostr event for list authorization
|
||||||
generate_list_auth() {
|
generate_list_auth() {
|
||||||
local content="$1"
|
local content="$1"
|
||||||
local created_at=$(date +%s)
|
|
||||||
local expiration=$((created_at + 3600)) # 1 hour from now
|
|
||||||
|
|
||||||
# Note: This is a placeholder - in real implementation, you'd use nostr tools
|
# Use nak to generate properly signed events with Alice's private key
|
||||||
# to generate properly signed events. For now, we'll create the structure.
|
nak event -k 24242 -c "$content" \
|
||||||
cat << EOF
|
--sec "$NOSTR_PRIVKEY" \
|
||||||
{
|
-t "t=list" \
|
||||||
"id": "placeholder_id",
|
-t "expiration=$(( $(date +%s) + 3600 ))"
|
||||||
"pubkey": "$NOSTR_PUBKEY",
|
|
||||||
"kind": 24242,
|
|
||||||
"content": "$content",
|
|
||||||
"created_at": $created_at,
|
|
||||||
"tags": [
|
|
||||||
["t", "list"],
|
|
||||||
["expiration", "$expiration"]
|
|
||||||
],
|
|
||||||
"sig": "placeholder_signature"
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Test 1: List blobs without authorization (should work if optional auth)
|
# Test 1: List blobs without authorization (should work if optional auth)
|
||||||
echo -e "${YELLOW}Test 1: GET /list/<pubkey> without authorization${NC}"
|
echo -e "${YELLOW}Test 1: GET /list/<pubkey> without authorization${NC}"
|
||||||
|
|
||||||
RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" "$BASE_URL/list/$NOSTR_PUBKEY")
|
RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" "$BASE_URL/list/$NOSTR_PUBKEY")
|
||||||
HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
|
HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
|
||||||
BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
|
BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
|
||||||
|
|
||||||
|
echo "Using pubkey: $NOSTR_PUBKEY"
|
||||||
|
echo "HTTP Status: $HTTP_STATUS"
|
||||||
|
echo "Response: $BODY"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Test 2: List blobs with authorization
|
||||||
|
echo -e "${YELLOW}Test 2: GET /list/<pubkey> with authorization${NC}"
|
||||||
|
LIST_AUTH=$(generate_list_auth "List Blobs")
|
||||||
|
AUTH_B64=$(echo "$LIST_AUTH" | base64 -w 0)
|
||||||
|
RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \
|
||||||
|
-H "Authorization: Nostr $AUTH_B64" \
|
||||||
|
"$BASE_URL/list/$NOSTR_PUBKEY")
|
||||||
|
HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
|
||||||
|
BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
|
||||||
|
|
||||||
echo "HTTP Status: $HTTP_STATUS"
|
echo "HTTP Status: $HTTP_STATUS"
|
||||||
echo "Response: $BODY"
|
echo "Response: $BODY"
|
||||||
echo
|
echo
|
||||||
|
|
||||||
# # Test 2: List blobs with authorization
|
# Test 3: List blobs with since parameter
|
||||||
# echo -e "${YELLOW}Test 2: GET /list/<pubkey> with authorization${NC}"
|
echo -e "${YELLOW}Test 3: GET /list/<pubkey> with since parameter${NC}"
|
||||||
# LIST_AUTH=$(generate_list_auth "List Blobs")
|
SINCE_TIMESTAMP=$(($(date +%s) - 86400)) # 24 hours ago
|
||||||
# AUTH_B64=$(echo "$LIST_AUTH" | base64 -w 0)
|
RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \
|
||||||
# RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \
|
"$BASE_URL/list/$NOSTR_PUBKEY?since=$SINCE_TIMESTAMP")
|
||||||
# -H "Authorization: Nostr $AUTH_B64" \
|
HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
|
||||||
# "$BASE_URL/list/$NOSTR_PUBKEY")
|
BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
|
||||||
# HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
|
|
||||||
# BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
|
|
||||||
|
|
||||||
# echo "HTTP Status: $HTTP_STATUS"
|
echo "HTTP Status: $HTTP_STATUS"
|
||||||
# echo "Response: $BODY"
|
echo "Response: $BODY"
|
||||||
# echo
|
echo
|
||||||
|
|
||||||
# # Test 3: List blobs with since parameter
|
# Test 4: List blobs with until parameter
|
||||||
# echo -e "${YELLOW}Test 3: GET /list/<pubkey> with since parameter${NC}"
|
echo -e "${YELLOW}Test 4: GET /list/<pubkey> with until parameter${NC}"
|
||||||
# SINCE_TIMESTAMP=$(($(date +%s) - 86400)) # 24 hours ago
|
UNTIL_TIMESTAMP=$(date +%s) # now
|
||||||
# RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \
|
RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \
|
||||||
# "$BASE_URL/list/$NOSTR_PUBKEY?since=$SINCE_TIMESTAMP")
|
"$BASE_URL/list/$NOSTR_PUBKEY?until=$UNTIL_TIMESTAMP")
|
||||||
# HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
|
HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
|
||||||
# BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
|
BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
|
||||||
|
|
||||||
# echo "HTTP Status: $HTTP_STATUS"
|
echo "HTTP Status: $HTTP_STATUS"
|
||||||
# echo "Response: $BODY"
|
echo "Response: $BODY"
|
||||||
# echo
|
echo
|
||||||
|
|
||||||
# # Test 4: List blobs with until parameter
|
# Test 5: List blobs with both since and until parameters
|
||||||
# echo -e "${YELLOW}Test 4: GET /list/<pubkey> with until parameter${NC}"
|
echo -e "${YELLOW}Test 5: GET /list/<pubkey> with since and until parameters${NC}"
|
||||||
# UNTIL_TIMESTAMP=$(date +%s) # now
|
SINCE_TIMESTAMP=$(($(date +%s) - 86400)) # 24 hours ago
|
||||||
# RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \
|
UNTIL_TIMESTAMP=$(date +%s) # now
|
||||||
# "$BASE_URL/list/$NOSTR_PUBKEY?until=$UNTIL_TIMESTAMP")
|
RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \
|
||||||
# HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
|
"$BASE_URL/list/$NOSTR_PUBKEY?since=$SINCE_TIMESTAMP&until=$UNTIL_TIMESTAMP")
|
||||||
# BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
|
HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
|
||||||
|
BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
|
||||||
|
|
||||||
# echo "HTTP Status: $HTTP_STATUS"
|
echo "HTTP Status: $HTTP_STATUS"
|
||||||
# echo "Response: $BODY"
|
echo "Response: $BODY"
|
||||||
# echo
|
echo
|
||||||
|
|
||||||
# # Test 5: List blobs with both since and until parameters
|
# Test 6: List blobs for non-existent pubkey
|
||||||
# echo -e "${YELLOW}Test 5: GET /list/<pubkey> with since and until parameters${NC}"
|
echo -e "${YELLOW}Test 6: GET /list/<nonexistent_pubkey>${NC}"
|
||||||
# SINCE_TIMESTAMP=$(($(date +%s) - 86400)) # 24 hours ago
|
FAKE_PUBKEY="1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
|
||||||
# UNTIL_TIMESTAMP=$(date +%s) # now
|
RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" "$BASE_URL/list/$FAKE_PUBKEY")
|
||||||
# RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \
|
HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
|
||||||
# "$BASE_URL/list/$NOSTR_PUBKEY?since=$SINCE_TIMESTAMP&until=$UNTIL_TIMESTAMP")
|
BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
|
||||||
# HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
|
|
||||||
# BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
|
|
||||||
|
|
||||||
# echo "HTTP Status: $HTTP_STATUS"
|
if [ "$HTTP_STATUS" = "200" ]; then
|
||||||
# echo "Response: $BODY"
|
echo -e "${GREEN}✓ Correctly returned 200 with empty array${NC}"
|
||||||
# echo
|
else
|
||||||
|
echo "HTTP Status: $HTTP_STATUS"
|
||||||
|
fi
|
||||||
|
echo "Response: $BODY"
|
||||||
|
echo
|
||||||
|
|
||||||
# # Test 6: List blobs for non-existent pubkey
|
# Test 7: List blobs with invalid pubkey format
|
||||||
# echo -e "${YELLOW}Test 6: GET /list/<nonexistent_pubkey>${NC}"
|
echo -e "${YELLOW}Test 7: GET /list/<invalid_pubkey_format>${NC}"
|
||||||
# FAKE_PUBKEY="1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
|
INVALID_PUBKEY="invalid_pubkey"
|
||||||
# RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" "$BASE_URL/list/$FAKE_PUBKEY")
|
RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" "$BASE_URL/list/$INVALID_PUBKEY")
|
||||||
# HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
|
HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
|
||||||
# BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
|
BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
|
||||||
|
|
||||||
# if [ "$HTTP_STATUS" = "200" ]; then
|
if [ "$HTTP_STATUS" = "400" ]; then
|
||||||
# echo -e "${GREEN}✓ Correctly returned 200 with empty array${NC}"
|
echo -e "${GREEN}✓ Correctly returned 400 for invalid pubkey format${NC}"
|
||||||
# else
|
else
|
||||||
# echo "HTTP Status: $HTTP_STATUS"
|
echo "HTTP Status: $HTTP_STATUS"
|
||||||
# fi
|
fi
|
||||||
# echo "Response: $BODY"
|
echo "Response: $BODY"
|
||||||
# echo
|
echo
|
||||||
|
|
||||||
# # Test 7: List blobs with invalid pubkey format
|
# Test 8: List blobs with invalid since/until parameters
|
||||||
# echo -e "${YELLOW}Test 7: GET /list/<invalid_pubkey_format>${NC}"
|
echo -e "${YELLOW}Test 8: GET /list/<pubkey> with invalid timestamp parameters${NC}"
|
||||||
# INVALID_PUBKEY="invalid_pubkey"
|
RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \
|
||||||
# RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" "$BASE_URL/list/$INVALID_PUBKEY")
|
"$BASE_URL/list/$NOSTR_PUBKEY?since=invalid&until=invalid")
|
||||||
# HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
|
HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
|
||||||
# BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
|
BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
|
||||||
|
|
||||||
# if [ "$HTTP_STATUS" = "400" ]; then
|
echo "HTTP Status: $HTTP_STATUS"
|
||||||
# echo -e "${GREEN}✓ Correctly returned 400 for invalid pubkey format${NC}"
|
echo "Response: $BODY"
|
||||||
# else
|
echo
|
||||||
# echo "HTTP Status: $HTTP_STATUS"
|
|
||||||
# fi
|
|
||||||
# echo "Response: $BODY"
|
|
||||||
# echo
|
|
||||||
|
|
||||||
# # Test 8: List blobs with invalid since/until parameters
|
|
||||||
# echo -e "${YELLOW}Test 8: GET /list/<pubkey> with invalid timestamp parameters${NC}"
|
|
||||||
# RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \
|
|
||||||
# "$BASE_URL/list/$NOSTR_PUBKEY?since=invalid&until=invalid")
|
|
||||||
# HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
|
|
||||||
# BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
|
|
||||||
|
|
||||||
# echo "HTTP Status: $HTTP_STATUS"
|
|
||||||
# echo "Response: $BODY"
|
|
||||||
# echo
|
|
||||||
|
|
||||||
# echo "=== List Tests Complete ==="
|
# echo "=== List Tests Complete ==="
|
||||||
# echo
|
# echo
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ TESTS_PASSED=0
|
|||||||
TESTS_FAILED=0
|
TESTS_FAILED=0
|
||||||
TOTAL_TESTS=0
|
TOTAL_TESTS=0
|
||||||
|
|
||||||
# Test keys for different scenarios
|
# Test keys for different scenarios - Using WSB's keys for TEST_USER1
|
||||||
# Generated using: nak key public <privkey>
|
# Generated using: nak key public <privkey>
|
||||||
TEST_USER1_PRIVKEY="5c0c523f52a5b6fad39ed2403092df8cebc36318b39383bca6c00808626fab3a"
|
TEST_USER1_PRIVKEY="22cc83aa57928a2800234c939240c9a6f0f44a33ea3838a860ed38930b195afd"
|
||||||
TEST_USER1_PUBKEY="87d3561f19b74adbe8bf840682992466068830a9d8c36b4a0c99d36f826cb6cb"
|
TEST_USER1_PUBKEY="8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e"
|
||||||
|
|
||||||
TEST_USER2_PRIVKEY="182c3a5e3b7a1b7e4f5c6b7c8b4a5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
|
TEST_USER2_PRIVKEY="182c3a5e3b7a1b7e4f5c6b7c8b4a5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
|
||||||
TEST_USER2_PUBKEY="0396b426090284a28294078dce53fe73791ab623c3fc46ab4409fea05109a6db"
|
TEST_USER2_PUBKEY="0396b426090284a28294078dce53fe73791ab623c3fc46ab4409fea05109a6db"
|
||||||
@@ -132,7 +132,6 @@ test_upload() {
|
|||||||
# Clean up any existing rules from previous tests
|
# Clean up any existing rules from previous tests
|
||||||
echo "Cleaning up existing auth rules..."
|
echo "Cleaning up existing auth rules..."
|
||||||
sqlite3 "$DB_PATH" "DELETE FROM auth_rules;" 2>/dev/null
|
sqlite3 "$DB_PATH" "DELETE FROM auth_rules;" 2>/dev/null
|
||||||
sqlite3 "$DB_PATH" "DELETE FROM auth_rules_cache;" 2>/dev/null
|
|
||||||
|
|
||||||
# Enable authentication rules
|
# Enable authentication rules
|
||||||
echo "Enabling authentication rules..."
|
echo "Enabling authentication rules..."
|
||||||
@@ -180,7 +179,6 @@ echo
|
|||||||
|
|
||||||
# Clean rules
|
# Clean rules
|
||||||
sqlite3 "$DB_PATH" "DELETE FROM auth_rules;"
|
sqlite3 "$DB_PATH" "DELETE FROM auth_rules;"
|
||||||
sqlite3 "$DB_PATH" "DELETE FROM auth_rules_cache;"
|
|
||||||
|
|
||||||
# Test 3: Create a file and blacklist its hash
|
# Test 3: Create a file and blacklist its hash
|
||||||
test_file5=$(create_test_file "hash_blacklist_test.txt" "This specific file is blacklisted")
|
test_file5=$(create_test_file "hash_blacklist_test.txt" "This specific file is blacklisted")
|
||||||
@@ -299,39 +297,11 @@ test_upload "Test 9a: Disabled Rule Not Enforced" "$TEST_USER1_PRIVKEY" "$test_f
|
|||||||
# Test 9b: Enable the rule
|
# Test 9b: Enable the rule
|
||||||
echo "Enabling the blacklist rule..."
|
echo "Enabling the blacklist rule..."
|
||||||
sqlite3 "$DB_PATH" "UPDATE auth_rules SET enabled = 1 WHERE rule_target = '$TEST_USER1_PUBKEY';"
|
sqlite3 "$DB_PATH" "UPDATE auth_rules SET enabled = 1 WHERE rule_target = '$TEST_USER1_PUBKEY';"
|
||||||
sqlite3 "$DB_PATH" "DELETE FROM auth_rules_cache;" # Clear cache
|
|
||||||
|
|
||||||
# Test 9c: Upload should now be denied
|
# Test 9c: Upload should now be denied
|
||||||
test_file13=$(create_test_file "enabled_rule_test.txt" "Testing enabled rule")
|
test_file13=$(create_test_file "enabled_rule_test.txt" "Testing enabled rule")
|
||||||
test_upload "Test 9c: Enabled Rule Enforced" "$TEST_USER1_PRIVKEY" "$test_file13" "403"
|
test_upload "Test 9c: Enabled Rule Enforced" "$TEST_USER1_PRIVKEY" "$test_file13" "403"
|
||||||
|
|
||||||
echo
|
|
||||||
echo "=== SECTION 10: CACHE FUNCTIONALITY ==="
|
|
||||||
echo
|
|
||||||
|
|
||||||
# Clean rules
|
|
||||||
sqlite3 "$DB_PATH" "DELETE FROM auth_rules;"
|
|
||||||
sqlite3 "$DB_PATH" "DELETE FROM auth_rules_cache;"
|
|
||||||
|
|
||||||
# Test 10: Add a blacklist rule and verify cache is populated
|
|
||||||
echo "Adding blacklist rule to test caching..."
|
|
||||||
sqlite3 "$DB_PATH" "INSERT INTO auth_rules (rule_type, rule_target, operation, priority, description) VALUES ('pubkey_blacklist', '$TEST_USER2_PUBKEY', 'upload', 10, 'Cache test');"
|
|
||||||
|
|
||||||
# Test 10a: First request (cache miss)
|
|
||||||
test_file14=$(create_test_file "cache_test1.txt" "First request - cache miss")
|
|
||||||
test_upload "Test 10a: First Request (Cache Miss)" "$TEST_USER2_PRIVKEY" "$test_file14" "403"
|
|
||||||
|
|
||||||
# Test 10b: Second request (should hit cache)
|
|
||||||
test_file15=$(create_test_file "cache_test2.txt" "Second request - cache hit")
|
|
||||||
test_upload "Test 10b: Second Request (Cache Hit)" "$TEST_USER2_PRIVKEY" "$test_file15" "403"
|
|
||||||
|
|
||||||
# Test 10c: Verify cache entry exists
|
|
||||||
CACHE_COUNT=$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM auth_rules_cache WHERE pubkey = '$TEST_USER2_PUBKEY';" 2>/dev/null)
|
|
||||||
if [[ "$CACHE_COUNT" -gt 0 ]]; then
|
|
||||||
record_test_result "Test 10c: Cache Entry Created" "1" "1"
|
|
||||||
else
|
|
||||||
record_test_result "Test 10c: Cache Entry Created" "1" "0"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "=== SECTION 11: CLEANUP AND RESET ==="
|
echo "=== SECTION 11: CLEANUP AND RESET ==="
|
||||||
@@ -340,26 +310,18 @@ echo
|
|||||||
# Clean up all test rules
|
# Clean up all test rules
|
||||||
echo "Cleaning up test rules..."
|
echo "Cleaning up test rules..."
|
||||||
sqlite3 "$DB_PATH" "DELETE FROM auth_rules;"
|
sqlite3 "$DB_PATH" "DELETE FROM auth_rules;"
|
||||||
sqlite3 "$DB_PATH" "DELETE FROM auth_rules_cache;"
|
|
||||||
|
|
||||||
# Verify cleanup
|
# Verify cleanup
|
||||||
RULE_COUNT=$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM auth_rules;" 2>/dev/null)
|
RULE_COUNT=$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM auth_rules;" 2>/dev/null)
|
||||||
if [[ "$RULE_COUNT" -eq 0 ]]; then
|
if [[ "$RULE_COUNT" -eq 0 ]]; then
|
||||||
record_test_result "Test 11a: Rules Cleanup" "0" "0"
|
record_test_result "Test 10a: Rules Cleanup" "0" "0"
|
||||||
else
|
else
|
||||||
record_test_result "Test 11a: Rules Cleanup" "0" "$RULE_COUNT"
|
record_test_result "Test 10a: Rules Cleanup" "0" "$RULE_COUNT"
|
||||||
fi
|
|
||||||
|
|
||||||
CACHE_COUNT=$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM auth_rules_cache;" 2>/dev/null)
|
|
||||||
if [[ "$CACHE_COUNT" -eq 0 ]]; then
|
|
||||||
record_test_result "Test 11b: Cache Cleanup" "0" "0"
|
|
||||||
else
|
|
||||||
record_test_result "Test 11b: Cache Cleanup" "0" "$CACHE_COUNT"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test that uploads work again after cleanup
|
# Test that uploads work again after cleanup
|
||||||
test_file16=$(create_test_file "cleanup_test.txt" "Testing after cleanup")
|
test_file16=$(create_test_file "cleanup_test.txt" "Testing after cleanup")
|
||||||
test_upload "Test 11c: Upload After Cleanup" "$TEST_USER1_PRIVKEY" "$test_file16" "200"
|
test_upload "Test 10b: Upload After Cleanup" "$TEST_USER1_PRIVKEY" "$test_file16" "200"
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "=========================================="
|
echo "=========================================="
|
||||||
@@ -382,7 +344,6 @@ if [[ $TESTS_FAILED -eq 0 ]]; then
|
|||||||
echo "- Operation-specific rules: Working"
|
echo "- Operation-specific rules: Working"
|
||||||
echo "- Wildcard operations: Working"
|
echo "- Wildcard operations: Working"
|
||||||
echo "- Enable/disable rules: Working"
|
echo "- Enable/disable rules: Working"
|
||||||
echo "- Cache functionality: Working"
|
|
||||||
else
|
else
|
||||||
echo "⚠️ Some tests failed. Check output above for details."
|
echo "⚠️ Some tests failed. Check output above for details."
|
||||||
echo "Success rate: $(( (TESTS_PASSED * 100) / TOTAL_TESTS ))%"
|
echo "Success rate: $(( (TESTS_PASSED * 100) / TOTAL_TESTS ))%"
|
||||||
|
|||||||
54
update_remote_nginx_conf.sh
Executable file
54
update_remote_nginx_conf.sh
Executable file
@@ -0,0 +1,54 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# update_remote_nginx_conf.sh
|
||||||
|
# Updates the remote nginx configuration on laantungir.net
|
||||||
|
# Copies contents of ./remote.nginx.config to /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=== Updating Remote Nginx Configuration ==="
|
||||||
|
echo "Server: laantungir.net"
|
||||||
|
echo "User: ubuntu"
|
||||||
|
echo "Local config: ./remote.nginx.config"
|
||||||
|
echo "Remote config: /etc/nginx/conf.d/default.conf"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Check if local config exists
|
||||||
|
if [[ ! -f "./remote.nginx.config" ]]; then
|
||||||
|
echo "ERROR: ./remote.nginx.config not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Copying remote.nginx.config to laantungir.net:/etc/nginx/conf.d/default.conf..."
|
||||||
|
|
||||||
|
# Copy the config file to the remote server (using user's home directory)
|
||||||
|
scp ./remote.nginx.config ubuntu@laantungir.net:~/remote.nginx.config
|
||||||
|
|
||||||
|
# Move to final location and backup old config
|
||||||
|
ssh ubuntu@laantungir.net << 'EOF'
|
||||||
|
echo "Creating backup of current config..."
|
||||||
|
sudo cp /etc/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf.backup.$(date +%Y%m%d_%H%M%S)
|
||||||
|
|
||||||
|
echo "Installing new config..."
|
||||||
|
sudo cp ~/remote.nginx.config /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
echo "Testing nginx configuration..."
|
||||||
|
if sudo nginx -t; then
|
||||||
|
echo "✅ Nginx config test passed"
|
||||||
|
echo "Reloading nginx..."
|
||||||
|
sudo nginx -s reload
|
||||||
|
echo "✅ Nginx reloaded successfully"
|
||||||
|
else
|
||||||
|
echo "❌ Nginx config test failed"
|
||||||
|
echo "Restoring backup..."
|
||||||
|
sudo cp /etc/nginx/conf.d/default.conf.backup.* /etc/nginx/conf.d/default.conf 2>/dev/null || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Cleaning up temporary file..."
|
||||||
|
rm ~/remote.nginx.config
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "=== Update Complete ==="
|
||||||
|
echo "The remote nginx configuration has been updated."
|
||||||
Reference in New Issue
Block a user