diff --git a/.gitignore b/.gitignore index 9c4b0e5..4255469 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ nostr_core_lib/ nips/ build/ +relay.log +Trash/ diff --git a/.roo/rules-code/rules.md b/.roo/rules-code/rules.md new file mode 100644 index 0000000..ea83dcc --- /dev/null +++ b/.roo/rules-code/rules.md @@ -0,0 +1 @@ +Use ./make_and_restart_relay.sh instead of make to build the project. \ No newline at end of file diff --git a/Makefile b/Makefile index 20be12f..d0d5b05 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ LIBS = -lsqlite3 -lwebsockets -lz -ldl -lpthread -lm -L/usr/local/lib -lsecp256k BUILD_DIR = build # Source files -MAIN_SRC = src/main.c +MAIN_SRC = src/main.c src/config.c NOSTR_CORE_LIB = nostr_core_lib/libnostr_core_x64.a # Architecture detection diff --git a/admin_spec.md b/admin_spec.md deleted file mode 100644 index b774c0c..0000000 --- a/admin_spec.md +++ /dev/null @@ -1,387 +0,0 @@ -# Ginxsom Admin System - Comprehensive Specification - -## Overview - -The Ginxsom admin system provides both programmatic (API-based) and interactive (web-based) administration capabilities for the Ginxsom Blossom server. The system is designed around Nostr-based authentication and supports multiple administration workflows including first-run setup, ongoing configuration management, and operational monitoring. - -## Architecture Components - -### 1. Configuration System -- **File-based configuration**: Signed Nostr events stored as JSON files following XDG Base Directory specification -- **Database configuration**: Key-value pairs stored in SQLite for runtime configuration -- **Interactive setup**: Command-line wizard for initial server configuration -- **Manual setup**: Scripts for generating signed configuration events - -### 2. Authentication & Authorization -- **Nostr-based auth**: All admin operations require valid Nostr event signatures -- **Admin pubkey verification**: Only configured admin public keys can perform admin operations -- **Event validation**: Full cryptographic verification of Nostr events including structure, signature, and expiration -- **Method-specific authorization**: Different event types for different operations (upload, admin, delete, etc.) - -### 3. API System -- **RESTful endpoints**: `/api/*` routes for programmatic administration -- **Command-line testing**: Complete test suite using `nak` and `curl` -- **JSON responses**: Structured data for all admin operations -- **CORS support**: Cross-origin requests for web admin interface - -### 4. Web Interface (Future) -- **Single-page application**: Self-contained HTML file with inline CSS/JS -- **Real-time monitoring**: Statistics and system health dashboards -- **Configuration management**: GUI for server settings -- **File management**: Browse and manage uploaded blobs - -## Configuration System Architecture - -### File-based Configuration (Priority 1) - -**Location**: Follows XDG Base Directory Specification -- `$XDG_CONFIG_HOME/ginxsom/ginxsom_config_event.json` -- Falls back to `$HOME/.config/ginxsom/ginxsom_config_event.json` - -**Format**: Signed Nostr event containing server configuration -```json -{ - "kind": 33333, - "created_at": 1704067200, - "tags": [ - ["server_privkey", "server_private_key_hex"], - ["cdn_origin", "https://cdn.example.com"], - ["max_file_size", "104857600"], - ["nip94_enabled", "true"] - ], - "content": "Ginxsom server configuration", - "pubkey": "admin_public_key_hex", - "id": "event_id_hash", - "sig": "event_signature" -} -``` - -**Loading Process**: -1. Check for file-based config at XDG location -2. Validate Nostr event structure and signature -3. Extract configuration from event tags -4. Apply settings to server (database storage) -5. Fall back to database-only config if file missing/invalid - -### Database Configuration (Priority 2) - -**Table**: `server_config` -```sql -CREATE TABLE server_config ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL, - description TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); -``` - -**Key Configuration Items**: -- `admin_pubkey`: Authorized admin public key -- `admin_enabled`: Enable/disable admin interface -- `cdn_origin`: Base URL for blob access -- `max_file_size`: Maximum upload size in bytes -- `nip94_enabled`: Enable NIP-94 metadata emission -- `auth_rules_enabled`: Enable authentication rules system - -### Setup Workflows - -#### Interactive Setup (Command Line) -```bash -# First-run detection -if [[ ! -f "$XDG_CONFIG_HOME/ginxsom/ginxsom_config_event.json" ]]; then - echo "=== Ginxsom First-Time Setup Required ===" - echo "1. Run interactive setup wizard" - echo "2. Exit and create config manually" - read -p "Choice (1/2): " choice - - if [[ "$choice" == "1" ]]; then - ./scripts/setup.sh - else - echo "Manual setup: Run ./scripts/generate_config.sh" - exit 1 - fi -fi -``` - -#### Manual Setup (Script-based) -```bash -# Generate configuration event -./scripts/generate_config.sh --admin-key \ - --server-key \ - --cdn-origin "https://cdn.example.com" \ - --output "$XDG_CONFIG_HOME/ginxsom/ginxsom_config_event.json" -``` - -### C Implementation Functions - -#### Configuration Loading -```c -// Get XDG-compliant config file path -int get_config_file_path(char* path, size_t path_size); - -// Load and validate config event from file -int load_server_config(const char* config_path); - -// Extract config from validated event and apply to server -int apply_config_from_event(cJSON* event); - -// Interactive setup runner for first-run -int run_interactive_setup(const char* config_path); -``` - -#### Security Features -- Server private key stored only in memory (never in database) -- Config file must be signed Nostr event -- Full cryptographic validation of config events -- Admin pubkey verification for all operations - -## Admin API Specification - -### Authentication Model - -All admin API endpoints (except `/api/health`) require Nostr authentication: - -**Authorization Header Format**: -``` -Authorization: Nostr -``` - -**Required Event Structure**: -```json -{ - "kind": 24242, - "created_at": 1704067200, - "tags": [ - ["t", "GET"], - ["expiration", "1704070800"] - ], - "content": "admin_request", - "pubkey": "admin_public_key", - "id": "event_id", - "sig": "event_signature" -} -``` - -### API Endpoints - -#### GET /api/health -**Purpose**: System health check (no authentication required) -**Response**: -```json -{ - "status": "success", - "data": { - "database": "connected", - "blob_directory": "accessible", - "server_time": 1704067200, - "uptime": 3600, - "disk_usage": { - "total_bytes": 1073741824, - "used_bytes": 536870912, - "available_bytes": 536870912, - "usage_percent": 50.0 - } - } -} -``` - -#### GET /api/stats -**Purpose**: Server statistics and metrics -**Authentication**: Required (admin pubkey) -**Response**: -```json -{ - "status": "success", - "data": { - "total_files": 1234, - "total_bytes": 104857600, - "total_size_mb": 100.0, - "unique_uploaders": 56, - "first_upload": 1693929600, - "last_upload": 1704067200, - "avg_file_size": 85049, - "file_types": { - "image/png": 45, - "image/jpeg": 32, - "application/pdf": 12, - "other": 8 - } - } -} -``` - -#### GET /api/config -**Purpose**: Retrieve current server configuration -**Authentication**: Required (admin pubkey) -**Response**: -```json -{ - "status": "success", - "data": { - "cdn_origin": "http://localhost:9001", - "max_file_size": "104857600", - "nip94_enabled": "true", - "auth_rules_enabled": "false", - "auth_cache_ttl": "300" - } -} -``` - -#### PUT /api/config -**Purpose**: Update server configuration -**Authentication**: Required (admin pubkey) -**Request Body**: -```json -{ - "max_file_size": "209715200", - "nip94_enabled": "true", - "cdn_origin": "https://cdn.example.com" -} -``` -**Response**: -```json -{ - "status": "success", - "message": "Configuration updated successfully", - "updated_keys": ["max_file_size", "cdn_origin"] -} -``` - -#### GET /api/files -**Purpose**: List recent files with pagination -**Authentication**: Required (admin pubkey) -**Parameters**: -- `limit` (default: 50): Number of files to return -- `offset` (default: 0): Pagination offset -**Response**: -```json -{ - "status": "success", - "data": { - "files": [ - { - "sha256": "b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553", - "size": 184292, - "type": "application/pdf", - "uploaded_at": 1725105921, - "uploader_pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", - "filename": "document.pdf", - "url": "http://localhost:9001/b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf" - } - ], - "total": 1234, - "limit": 50, - "offset": 0 - } -} -``` - -## Implementation Status - -### ✅ Completed Components -1. **Database-based configuration loading** - Implemented in main.c -2. **Admin API authentication system** - Implemented in admin_api.c -3. **Nostr event validation** - Full cryptographic verification -4. **Admin pubkey verification** - Database-backed authorization -5. **Basic API endpoints** - Health, stats, config, files - -### ✅ Recently Completed Components -1. **File-based configuration system** - Fully implemented in main.c with XDG compliance -2. **Interactive setup wizard** - Complete shell script with guided setup process (`scripts/setup.sh`) -3. **Manual config generation** - Full-featured command-line config generator (`scripts/generate_config.sh`) -4. **Testing infrastructure** - Comprehensive admin API test suite (`scripts/test_admin.sh`) -5. **Documentation system** - Complete setup and usage documentation (`scripts/README.md`) - -### 📋 Planned Components -1. **Web admin interface** - Single-page HTML application -2. **Enhanced monitoring** - Real-time statistics dashboard -3. **Bulk operations** - Multi-file management APIs -4. **Configuration validation** - Advanced config checking -5. **Audit logging** - Admin action tracking - -## Setup Instructions - -### 1. Enable Admin Interface -```bash -# Configure admin pubkey and enable interface -sqlite3 db/ginxsom.db << EOF -INSERT OR REPLACE INTO server_config (key, value, description) VALUES - ('admin_pubkey', 'your_admin_public_key_here', 'Authorized admin public key'), - ('admin_enabled', 'true', 'Enable admin interface'); -EOF -``` - -### 2. Test API Access -```bash -# Generate admin authentication event -ADMIN_PRIVKEY="your_admin_private_key" -EVENT=$(nak event -k 24242 -c "admin_request" \ - --tag t="GET" \ - --tag expiration="$(date -d '+1 hour' +%s)" \ - --sec "$ADMIN_PRIVKEY") - -# Test admin API -AUTH_HEADER="Nostr $(echo "$EVENT" | base64 -w 0)" -curl -H "Authorization: $AUTH_HEADER" http://localhost:9001/api/stats -``` - -### 3. Configure File-based Setup (Future) -```bash -# Create XDG config directory -mkdir -p "$XDG_CONFIG_HOME/ginxsom" - -# Generate signed config event -./scripts/generate_config.sh \ - --admin-key "your_admin_pubkey" \ - --server-key "generated_server_privkey" \ - --output "$XDG_CONFIG_HOME/ginxsom/ginxsom_config_event.json" -``` - -## Security Considerations - -### Authentication Security -- **Event expiration**: All admin events must include expiration timestamps -- **Signature validation**: Full secp256k1 cryptographic verification -- **Replay protection**: Event IDs tracked to prevent reuse -- **Admin key rotation**: Support for updating admin pubkeys - -### Configuration Security -- **File permissions**: Config files should be readable only by server user -- **Private key handling**: Server private keys never stored in database -- **Config validation**: All configuration changes validated before application -- **Backup verification**: Config events cryptographically verifiable - -### Operational Security -- **Access logging**: All admin operations logged with timestamps -- **Rate limiting**: API endpoints protected against abuse -- **Input validation**: All user input sanitized and validated -- **Database security**: Prepared statements prevent SQL injection - -## Future Enhancements - -### 1. Web Admin Interface -- Self-contained HTML file with inline CSS/JavaScript -- Real-time monitoring dashboards -- Visual configuration management -- File upload/management interface - -### 2. Advanced Monitoring -- Performance metrics collection -- Alert system for critical events -- Historical data trending -- Resource usage tracking - -### 3. Multi-admin Support -- Multiple authorized admin pubkeys -- Role-based permissions (read-only vs full admin) -- Admin action audit trails -- Delegation capabilities - -### 4. Integration Features -- Nostr relay integration for admin events -- Webhook notifications for admin actions -- External authentication providers -- API key management for programmatic access - -This specification represents the current understanding and planned development of the Ginxsom admin system, focusing on security, usability, and maintainability. diff --git a/build_output.log b/build_output.log new file mode 100644 index 0000000..66e0a59 --- /dev/null +++ b/build_output.log @@ -0,0 +1,28 @@ +=== C Nostr Relay Build and Restart Script === +Removing old configuration file to trigger regeneration... +✓ Configuration file removed - will be regenerated with latest database values +Building project... +rm -rf build +Clean complete +mkdir -p build +Compiling C-Relay for architecture: x86_64 +gcc -Wall -Wextra -std=c99 -g -O2 -I. -Inostr_core_lib -Inostr_core_lib/nostr_core -Inostr_core_lib/cjson -Inostr_core_lib/nostr_websocket src/main.c src/config.c -o build/c_relay_x86 nostr_core_lib/libnostr_core_x64.a -lsqlite3 -lwebsockets -lz -ldl -lpthread -lm -L/usr/local/lib -lsecp256k1 -lssl -lcrypto -L/usr/local/lib -lcurl +Build complete: build/c_relay_x86 +Build successful. Proceeding with relay restart... +Stopping any existing relay servers... +No existing relay found +Starting relay server... +Debug: Current processes: None +Started with PID: 786684 +Relay started successfully! +PID: 786684 +WebSocket endpoint: ws://127.0.0.1:8888 +Log file: relay.log + +=== Relay server running in background === +To kill relay: pkill -f 'c_relay_' +To check status: ps aux | grep c_relay_ +To view logs: tail -f relay.log +Binary: ./build/c_relay_x86 +Ready for Nostr client connections! + diff --git a/db/c_nostr_relay.db b/db/c_nostr_relay.db index a79b8e8..0f9f3fa 100644 Binary files a/db/c_nostr_relay.db and b/db/c_nostr_relay.db differ diff --git a/db/c_nostr_relay.db-shm b/db/c_nostr_relay.db-shm index 0b1a672..f3c0e56 100644 Binary files a/db/c_nostr_relay.db-shm and b/db/c_nostr_relay.db-shm differ diff --git a/db/c_nostr_relay.db-wal b/db/c_nostr_relay.db-wal index 02a34c8..310a42b 100644 Binary files a/db/c_nostr_relay.db-wal and b/db/c_nostr_relay.db-wal differ diff --git a/db/c_nostr_relay.db.backup.20250905_152104 b/db/c_nostr_relay.db.backup.20250905_152104 new file mode 100644 index 0000000..a79b8e8 Binary files /dev/null and b/db/c_nostr_relay.db.backup.20250905_152104 differ diff --git a/db/init.sh b/db/init.sh index 0932567..d85816c 100755 --- a/db/init.sh +++ b/db/init.sh @@ -114,12 +114,12 @@ verify_database() { local schema_version=$(sqlite3 "$DB_PATH" "PRAGMA user_version;") log_info "Database schema version: $schema_version" - # Check that main tables exist - local table_count=$(sqlite3 "$DB_PATH" "SELECT count(*) FROM sqlite_master WHERE type='table' AND name IN ('events', 'schema_info');") - if [ "$table_count" -eq 2 ]; then - log_success "Core tables created successfully" + # Check that main tables exist (including configuration tables) + local table_count=$(sqlite3 "$DB_PATH" "SELECT count(*) FROM sqlite_master WHERE type='table' AND name IN ('events', 'schema_info', 'server_config');") + if [ "$table_count" -eq 3 ]; then + log_success "Core tables created successfully (including configuration tables)" else - log_error "Missing core tables (expected 2, found $table_count)" + log_error "Missing core tables (expected 3, found $table_count)" exit 1 fi } diff --git a/db/schema.sql b/db/schema.sql index 4333821..87f3d7a 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -2,7 +2,7 @@ -- SQLite schema for storing Nostr events with JSON tags support -- Schema version tracking -PRAGMA user_version = 2; +PRAGMA user_version = 3; -- Enable foreign key support PRAGMA foreign_keys = ON; @@ -44,9 +44,9 @@ CREATE TABLE schema_info ( ); -- Insert schema metadata -INSERT INTO schema_info (key, value) VALUES - ('version', '2'), - ('description', 'Hybrid single-table Nostr relay schema with JSON tags'), +INSERT INTO schema_info (key, value) VALUES + ('version', '3'), + ('description', 'Hybrid single-table Nostr relay schema with JSON tags and configuration management'), ('created_at', strftime('%s', 'now')); -- Helper views for common queries @@ -178,4 +178,122 @@ WHERE event_type = 'created' AND subscription_id NOT IN ( SELECT subscription_id FROM subscription_events WHERE event_type IN ('closed', 'expired', 'disconnected') -); \ No newline at end of file +); + +-- ================================ +-- CONFIGURATION MANAGEMENT TABLES +-- ================================ + +-- Core server configuration table +CREATE TABLE server_config ( + key TEXT PRIMARY KEY, -- Configuration key (unique identifier) + value TEXT NOT NULL, -- Configuration value (stored as string) + description TEXT, -- Human-readable description + config_type TEXT DEFAULT 'user' CHECK (config_type IN ('system', 'user', 'runtime')), + data_type TEXT DEFAULT 'string' CHECK (data_type IN ('string', 'integer', 'boolean', 'json')), + validation_rules TEXT, -- JSON validation rules (optional) + is_sensitive INTEGER DEFAULT 0, -- 1 if value should be masked in logs + requires_restart INTEGER DEFAULT 0, -- 1 if change requires server restart + created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), + updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) +); + +-- Configuration change history table +CREATE TABLE config_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + config_key TEXT NOT NULL, -- Key that was changed + old_value TEXT, -- Previous value (NULL for new keys) + new_value TEXT NOT NULL, -- New value + changed_by TEXT DEFAULT 'system', -- Who made the change (system/admin/user) + change_reason TEXT, -- Optional reason for change + changed_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), + FOREIGN KEY (config_key) REFERENCES server_config(key) +); + +-- Configuration validation errors log +CREATE TABLE config_validation_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + config_key TEXT NOT NULL, + attempted_value TEXT, + validation_error TEXT NOT NULL, + error_source TEXT DEFAULT 'validation', -- validation/parsing/constraint + attempted_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) +); + +-- Cache for file-based configuration events +CREATE TABLE config_file_cache ( + file_path TEXT PRIMARY KEY, -- Full path to config file + file_hash TEXT NOT NULL, -- SHA256 hash of file content + event_id TEXT, -- Nostr event ID from file + event_pubkey TEXT, -- Admin pubkey that signed event + loaded_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), + validation_status TEXT CHECK (validation_status IN ('valid', 'invalid', 'unverified')), + validation_error TEXT -- Error details if invalid +); + +-- Performance indexes for configuration tables +CREATE INDEX idx_server_config_type ON server_config(config_type); +CREATE INDEX idx_server_config_updated ON server_config(updated_at DESC); +CREATE INDEX idx_config_history_key ON config_history(config_key); +CREATE INDEX idx_config_history_time ON config_history(changed_at DESC); +CREATE INDEX idx_config_validation_key ON config_validation_log(config_key); +CREATE INDEX idx_config_validation_time ON config_validation_log(attempted_at DESC); + +-- Trigger to update timestamp on configuration changes +CREATE TRIGGER update_config_timestamp + AFTER UPDATE ON server_config +BEGIN + UPDATE server_config SET updated_at = strftime('%s', 'now') WHERE key = NEW.key; +END; + +-- Trigger to log configuration changes to history +CREATE TRIGGER log_config_changes + AFTER UPDATE ON server_config + WHEN OLD.value != NEW.value +BEGIN + INSERT INTO config_history (config_key, old_value, new_value, changed_by, change_reason) + VALUES (NEW.key, OLD.value, NEW.value, 'system', 'configuration update'); +END; + +-- Active Configuration View +CREATE VIEW active_config AS +SELECT + key, + value, + description, + config_type, + data_type, + requires_restart, + updated_at +FROM server_config +WHERE config_type IN ('system', 'user') +ORDER BY config_type, key; + +-- Runtime Statistics View +CREATE VIEW runtime_stats AS +SELECT + key, + value, + description, + updated_at +FROM server_config +WHERE config_type = 'runtime' +ORDER BY key; + +-- Configuration Change Summary +CREATE VIEW recent_config_changes AS +SELECT + ch.config_key, + sc.description, + ch.old_value, + ch.new_value, + ch.changed_by, + ch.change_reason, + ch.changed_at +FROM config_history ch +JOIN server_config sc ON ch.config_key = sc.key +ORDER BY ch.changed_at DESC +LIMIT 50; + +-- Runtime Statistics (initialized by server on startup) +-- These will be populated when configuration system initializes \ No newline at end of file diff --git a/docs/config_schema_design.md b/docs/config_schema_design.md new file mode 100644 index 0000000..2c80f49 --- /dev/null +++ b/docs/config_schema_design.md @@ -0,0 +1,280 @@ +# Database Configuration Schema Design + +## Overview +This document outlines the database configuration schema additions for the C Nostr Relay startup config file system. The design follows the Ginxsom admin system approach with signed Nostr events and database storage. + +## Schema Version Update +- Current Version: 2 +- Target Version: 3 +- Update: Add server configuration management tables + +## Core Configuration Tables + +### 1. `server_config` Table + +```sql +-- Server configuration table - core configuration storage +CREATE TABLE server_config ( + key TEXT PRIMARY KEY, -- Configuration key (unique identifier) + value TEXT NOT NULL, -- Configuration value (stored as string) + description TEXT, -- Human-readable description + config_type TEXT DEFAULT 'user' CHECK (config_type IN ('system', 'user', 'runtime')), + data_type TEXT DEFAULT 'string' CHECK (data_type IN ('string', 'integer', 'boolean', 'json')), + validation_rules TEXT, -- JSON validation rules (optional) + is_sensitive INTEGER DEFAULT 0, -- 1 if value should be masked in logs + requires_restart INTEGER DEFAULT 0, -- 1 if change requires server restart + created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), + updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) +); +``` + +**Configuration Types:** +- `system`: Core system settings (admin keys, security) +- `user`: User-configurable settings (relay info, features) +- `runtime`: Dynamic runtime values (statistics, cache) + +**Data Types:** +- `string`: Text values +- `integer`: Numeric values +- `boolean`: True/false values (stored as "true"/"false") +- `json`: JSON object/array values + +### 2. `config_history` Table + +```sql +-- Configuration change history table +CREATE TABLE config_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + config_key TEXT NOT NULL, -- Key that was changed + old_value TEXT, -- Previous value (NULL for new keys) + new_value TEXT NOT NULL, -- New value + changed_by TEXT DEFAULT 'system', -- Who made the change (system/admin/user) + change_reason TEXT, -- Optional reason for change + changed_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), + FOREIGN KEY (config_key) REFERENCES server_config(key) +); +``` + +### 3. `config_validation_log` Table + +```sql +-- Configuration validation errors log +CREATE TABLE config_validation_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + config_key TEXT NOT NULL, + attempted_value TEXT, + validation_error TEXT NOT NULL, + error_source TEXT DEFAULT 'validation', -- validation/parsing/constraint + attempted_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) +); +``` + +### 4. Configuration File Cache Table + +```sql +-- Cache for file-based configuration events +CREATE TABLE config_file_cache ( + file_path TEXT PRIMARY KEY, -- Full path to config file + file_hash TEXT NOT NULL, -- SHA256 hash of file content + event_id TEXT, -- Nostr event ID from file + event_pubkey TEXT, -- Admin pubkey that signed event + loaded_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), + validation_status TEXT CHECK (validation_status IN ('valid', 'invalid', 'unverified')), + validation_error TEXT -- Error details if invalid +); +``` + +## Indexes and Performance + +```sql +-- Performance indexes for configuration tables +CREATE INDEX idx_server_config_type ON server_config(config_type); +CREATE INDEX idx_server_config_updated ON server_config(updated_at DESC); +CREATE INDEX idx_config_history_key ON config_history(config_key); +CREATE INDEX idx_config_history_time ON config_history(changed_at DESC); +CREATE INDEX idx_config_validation_key ON config_validation_log(config_key); +CREATE INDEX idx_config_validation_time ON config_validation_log(attempted_at DESC); +``` + +## Triggers + +### Update Timestamp Trigger + +```sql +-- Trigger to update timestamp on configuration changes +CREATE TRIGGER update_config_timestamp + AFTER UPDATE ON server_config +BEGIN + UPDATE server_config SET updated_at = strftime('%s', 'now') WHERE key = NEW.key; +END; +``` + +### Configuration History Trigger + +```sql +-- Trigger to log configuration changes to history +CREATE TRIGGER log_config_changes + AFTER UPDATE ON server_config + WHEN OLD.value != NEW.value +BEGIN + INSERT INTO config_history (config_key, old_value, new_value, changed_by, change_reason) + VALUES (NEW.key, OLD.value, NEW.value, 'system', 'configuration update'); +END; +``` + +## Default Configuration Values + +### Core System Settings + +```sql +INSERT OR IGNORE INTO server_config (key, value, description, config_type, data_type, requires_restart) VALUES +-- Administrative settings +('admin_pubkey', '', 'Authorized admin public key (hex)', 'system', 'string', 1), +('admin_enabled', 'false', 'Enable admin interface', 'system', 'boolean', 1), + +-- Server core settings +('relay_port', '8888', 'WebSocket server port', 'user', 'integer', 1), +('database_path', 'db/c_nostr_relay.db', 'SQLite database file path', 'user', 'string', 1), +('max_connections', '100', 'Maximum concurrent connections', 'user', 'integer', 1), + +-- NIP-11 Relay Information +('relay_name', 'C Nostr Relay', 'Relay name for NIP-11', 'user', 'string', 0), +('relay_description', 'High-performance C Nostr relay with SQLite storage', 'Relay description', 'user', 'string', 0), +('relay_contact', '', 'Contact information', 'user', 'string', 0), +('relay_pubkey', '', 'Relay public key', 'user', 'string', 0), +('relay_software', 'https://github.com/laantungir/c-relay', 'Software URL', 'user', 'string', 0), +('relay_version', '0.2.0', 'Software version', 'user', 'string', 0), + +-- NIP-13 Proof of Work +('pow_enabled', 'true', 'Enable NIP-13 Proof of Work validation', 'user', 'boolean', 0), +('pow_min_difficulty', '0', 'Minimum PoW difficulty required', 'user', 'integer', 0), +('pow_mode', 'basic', 'PoW validation mode (basic/full/strict)', 'user', 'string', 0), + +-- NIP-40 Expiration Timestamp +('expiration_enabled', 'true', 'Enable NIP-40 expiration handling', 'user', 'boolean', 0), +('expiration_strict', 'true', 'Reject expired events on submission', 'user', 'boolean', 0), +('expiration_filter', 'true', 'Filter expired events from responses', 'user', 'boolean', 0), +('expiration_grace_period', '300', 'Grace period for clock skew (seconds)', 'user', 'integer', 0), + +-- Subscription limits +('max_subscriptions_per_client', '20', 'Max subscriptions per client', 'user', 'integer', 0), +('max_total_subscriptions', '5000', 'Max total concurrent subscriptions', 'user', 'integer', 0), +('subscription_id_max_length', '64', 'Maximum subscription ID length', 'user', 'integer', 0), + +-- Event processing limits +('max_event_tags', '100', 'Maximum tags per event', 'user', 'integer', 0), +('max_content_length', '8196', 'Maximum content length', 'user', 'integer', 0), +('max_message_length', '16384', 'Maximum message length', 'user', 'integer', 0), + +-- Performance settings +('default_limit', '500', 'Default query limit', 'user', 'integer', 0), +('max_limit', '5000', 'Maximum query limit', 'user', 'integer', 0); +``` + +### Runtime Statistics + +```sql +INSERT OR IGNORE INTO server_config (key, value, description, config_type, data_type) VALUES +-- Runtime statistics (updated by server) +('server_start_time', '0', 'Server startup timestamp', 'runtime', 'integer'), +('total_events_processed', '0', 'Total events processed', 'runtime', 'integer'), +('total_subscriptions_created', '0', 'Total subscriptions created', 'runtime', 'integer'), +('current_connections', '0', 'Current active connections', 'runtime', 'integer'), +('database_size_bytes', '0', 'Database file size in bytes', 'runtime', 'integer'); +``` + +## Configuration Views + +### Active Configuration View + +```sql +CREATE VIEW active_config AS +SELECT + key, + value, + description, + config_type, + data_type, + requires_restart, + updated_at +FROM server_config +WHERE config_type IN ('system', 'user') +ORDER BY config_type, key; +``` + +### Runtime Statistics View + +```sql +CREATE VIEW runtime_stats AS +SELECT + key, + value, + description, + updated_at +FROM server_config +WHERE config_type = 'runtime' +ORDER BY key; +``` + +### Configuration Change Summary + +```sql +CREATE VIEW recent_config_changes AS +SELECT + ch.config_key, + sc.description, + ch.old_value, + ch.new_value, + ch.changed_by, + ch.change_reason, + ch.changed_at +FROM config_history ch +JOIN server_config sc ON ch.config_key = sc.key +ORDER BY ch.changed_at DESC +LIMIT 50; +``` + +## Validation Rules Format + +Configuration validation rules are stored as JSON strings in the `validation_rules` column: + +```json +{ + "type": "integer", + "min": 1, + "max": 65535, + "required": true +} +``` + +```json +{ + "type": "string", + "pattern": "^[0-9a-fA-F]{64}$", + "required": false, + "description": "64-character hex string" +} +``` + +```json +{ + "type": "boolean", + "required": true +} +``` + +## Migration Strategy + +1. **Phase 1**: Add configuration tables to existing schema +2. **Phase 2**: Populate with current hardcoded values +3. **Phase 3**: Update application code to read from database +4. **Phase 4**: Add file-based configuration loading +5. **Phase 5**: Remove hardcoded defaults and environment variable fallbacks + +## Integration Points + +- **Startup**: Load configuration from file → database → apply to application +- **Runtime**: Read configuration values from database cache +- **Updates**: Write changes to database → optionally update file +- **Validation**: Validate all configuration changes before applying +- **History**: Track all configuration changes for audit purposes \ No newline at end of file diff --git a/docs/file_config_design.md b/docs/file_config_design.md new file mode 100644 index 0000000..099d4e8 --- /dev/null +++ b/docs/file_config_design.md @@ -0,0 +1,493 @@ +# File-Based Configuration Architecture Design + +## Overview +This document outlines the XDG-compliant file-based configuration system for the C Nostr Relay, following the Ginxsom admin system approach using signed Nostr events. + +## XDG Base Directory Specification Compliance + +### File Location Strategy + +**Primary Location:** +``` +$XDG_CONFIG_HOME/c-relay/c_relay_config_event.json +``` + +**Fallback Location:** +``` +$HOME/.config/c-relay/c_relay_config_event.json +``` + +**System-wide Fallback:** +``` +/etc/c-relay/c_relay_config_event.json +``` + +### Directory Structure +``` +$XDG_CONFIG_HOME/c-relay/ +├── c_relay_config_event.json # Main configuration file +├── backup/ # Configuration backups +│ ├── c_relay_config_event.json.bak +│ └── c_relay_config_event.20241205.json +└── validation/ # Validation logs + └── config_validation.log +``` + +## Configuration File Format + +### Signed Nostr Event Structure + +The configuration file contains a signed Nostr event (kind 33334) with relay configuration: + +```json +{ + "kind": 33334, + "created_at": 1704067200, + "tags": [ + ["relay_name", "C Nostr Relay"], + ["relay_description", "High-performance C Nostr relay with SQLite storage"], + ["relay_port", "8888"], + ["database_path", "db/c_nostr_relay.db"], + ["admin_pubkey", ""], + ["admin_enabled", "false"], + + ["pow_enabled", "true"], + ["pow_min_difficulty", "0"], + ["pow_mode", "basic"], + + ["expiration_enabled", "true"], + ["expiration_strict", "true"], + ["expiration_filter", "true"], + ["expiration_grace_period", "300"], + + ["max_subscriptions_per_client", "20"], + ["max_total_subscriptions", "5000"], + ["max_connections", "100"], + + ["relay_contact", ""], + ["relay_pubkey", ""], + ["relay_software", "https://github.com/laantungir/c-relay"], + ["relay_version", "0.2.0"], + + ["max_event_tags", "100"], + ["max_content_length", "8196"], + ["max_message_length", "16384"], + ["default_limit", "500"], + ["max_limit", "5000"] + ], + "content": "C Nostr Relay configuration event", + "pubkey": "admin_public_key_hex_64_chars", + "id": "computed_event_id_hex_64_chars", + "sig": "computed_signature_hex_128_chars" +} +``` + +### Event Kind Definition + +**Kind 33334**: C Nostr Relay Configuration Event +- Parameterized replaceable event +- Must be signed by authorized admin pubkey +- Contains relay configuration as tags +- Validation required on load + +## Configuration Loading Architecture + +### Loading Priority Chain + +1. **Command Line Arguments** (highest priority) +2. **File-based Configuration** (signed Nostr event) +3. **Database Configuration** (persistent storage) +4. **Environment Variables** (compatibility mode) +5. **Hardcoded Defaults** (fallback) + +### Loading Process Flow + +```mermaid +flowchart TD + A[Server Startup] --> B[Get Config File Path] + B --> C{File Exists?} + C -->|No| D[Check Database Config] + C -->|Yes| E[Load & Parse JSON] + E --> F[Validate Event Structure] + F --> G{Valid Event?} + G -->|No| H[Log Error & Use Database] + G -->|Yes| I[Verify Event Signature] + I --> J{Signature Valid?} + J -->|No| K[Log Error & Use Database] + J -->|Yes| L[Extract Configuration Tags] + L --> M[Apply to Database] + M --> N[Apply to Application] + D --> O[Load from Database] + H --> O + K --> O + O --> P[Apply Environment Variable Overrides] + P --> Q[Apply Command Line Overrides] + Q --> N + N --> R[Server Ready] +``` + +## C Implementation Architecture + +### Core Data Structures + +```c +// Configuration file management +typedef struct { + char file_path[512]; + char file_hash[65]; // SHA256 hash + time_t last_modified; + time_t last_loaded; + int validation_status; // 0=valid, 1=invalid, 2=unverified + char validation_error[256]; +} config_file_info_t; + +// Configuration event structure +typedef struct { + char event_id[65]; + char pubkey[65]; + char signature[129]; + long created_at; + int kind; + cJSON* tags; + char* content; +} config_event_t; + +// Configuration management context +typedef struct { + config_file_info_t file_info; + config_event_t event; + int loaded_from_file; + int loaded_from_database; + char admin_pubkey[65]; + time_t load_timestamp; +} config_context_t; +``` + +### Core Function Signatures + +```c +// XDG path resolution +int get_config_file_path(char* path, size_t path_size); +int create_config_directories(const char* config_path); + +// File operations +int load_config_from_file(const char* config_path, config_context_t* ctx); +int save_config_to_file(const char* config_path, const config_event_t* event); +int backup_config_file(const char* config_path); + +// Event validation +int validate_config_event_structure(const cJSON* event); +int verify_config_event_signature(const config_event_t* event, const char* admin_pubkey); +int validate_config_tag_values(const cJSON* tags); + +// Configuration extraction and application +int extract_config_from_tags(const cJSON* tags, config_context_t* ctx); +int apply_config_to_database(const config_context_t* ctx); +int apply_config_to_globals(const config_context_t* ctx); + +// File monitoring and updates +int monitor_config_file_changes(const char* config_path); +int reload_config_on_change(config_context_t* ctx); + +// Error handling and logging +int log_config_validation_error(const char* config_key, const char* error); +int log_config_load_event(const config_context_t* ctx, const char* source); +``` + +## Configuration Validation Rules + +### Event Structure Validation + +1. **Required Fields**: `kind`, `created_at`, `tags`, `content`, `pubkey`, `id`, `sig` +2. **Kind Validation**: Must be exactly 33334 +3. **Timestamp Validation**: Must be reasonable (not too old, not future) +4. **Tags Format**: Array of string arrays `[["key", "value"], ...]` +5. **Signature Verification**: Must be signed by authorized admin pubkey + +### Configuration Value Validation + +```c +typedef struct { + char* key; + char* data_type; // "string", "integer", "boolean", "json" + char* validation_rule; // JSON validation rule + int required; + char* default_value; +} config_validation_rule_t; + +static config_validation_rule_t validation_rules[] = { + {"relay_port", "integer", "{\"min\": 1, \"max\": 65535}", 1, "8888"}, + {"pow_min_difficulty", "integer", "{\"min\": 0, \"max\": 64}", 1, "0"}, + {"expiration_grace_period", "integer", "{\"min\": 0, \"max\": 86400}", 1, "300"}, + {"admin_pubkey", "string", "{\"pattern\": \"^[0-9a-fA-F]{64}$\"}", 0, ""}, + {"pow_enabled", "boolean", "{}", 1, "true"}, + // ... more rules +}; +``` + +### Security Validation + +1. **Admin Pubkey Verification**: Only configured admin pubkeys can create config events +2. **Event ID Verification**: Event ID must match computed hash +3. **Signature Verification**: Signature must be valid for the event and pubkey +4. **Timestamp Validation**: Prevent replay attacks with old events +5. **File Permission Checks**: Config files should have appropriate permissions + +## File Management Features + +### Configuration File Operations + +**File Creation:** +- Generate initial configuration file with default values +- Sign with admin private key +- Set appropriate file permissions (600 - owner read/write only) + +**File Updates:** +- Create backup of existing file +- Validate new configuration +- Atomic file replacement (write to temp, then rename) +- Update file metadata cache + +**File Monitoring:** +- Watch for file system changes using inotify (Linux) +- Reload configuration automatically when file changes +- Validate changes before applying +- Log all configuration reload events + +### Backup and Recovery + +**Automatic Backups:** +``` +$XDG_CONFIG_HOME/c-relay/backup/ +├── c_relay_config_event.json.bak # Last working config +├── c_relay_config_event.20241205-143022.json # Timestamped backups +└── c_relay_config_event.20241204-091530.json +``` + +**Recovery Process:** +1. Detect corrupted or invalid config file +2. Attempt to load from `.bak` backup +3. If backup fails, generate default configuration +4. Log recovery actions for audit + +## Integration with Database Schema + +### File-Database Synchronization + +**On File Load:** +1. Parse and validate file-based configuration +2. Extract configuration values from event tags +3. Update database `server_config` table +4. Record file metadata in `config_file_cache` table +5. Log configuration changes in `config_history` table + +**Configuration Priority Resolution:** +```c +char* get_config_value(const char* key, const char* default_value) { + // Priority: CLI args > File config > DB config > Env vars > Default + char* value = NULL; + + // 1. Check command line overrides (if implemented) + value = get_cli_override(key); + if (value) return value; + + // 2. Check database (updated from file) + value = get_database_config(key); + if (value) return value; + + // 3. Check environment variables (compatibility) + value = get_env_config(key); + if (value) return value; + + // 4. Return default + return strdup(default_value); +} +``` + +## Error Handling and Recovery + +### Validation Error Handling + +```c +typedef enum { + CONFIG_ERROR_NONE = 0, + CONFIG_ERROR_FILE_NOT_FOUND = 1, + CONFIG_ERROR_PARSE_FAILED = 2, + CONFIG_ERROR_INVALID_STRUCTURE = 3, + CONFIG_ERROR_SIGNATURE_INVALID = 4, + CONFIG_ERROR_UNAUTHORIZED = 5, + CONFIG_ERROR_VALUE_INVALID = 6, + CONFIG_ERROR_IO_ERROR = 7 +} config_error_t; + +typedef struct { + config_error_t error_code; + char error_message[256]; + char config_key[64]; + char invalid_value[128]; + time_t error_timestamp; +} config_error_info_t; +``` + +### Graceful Degradation + +**File Load Failure:** +1. Log detailed error information +2. Fall back to database configuration +3. Continue operation with last known good config +4. Set service status to "degraded" mode + +**Validation Failure:** +1. Log validation errors with specific details +2. Skip invalid configuration items +3. Use default values for failed items +4. Continue with partial configuration + +**Permission Errors:** +1. Log permission issues +2. Attempt to use fallback locations +3. Generate temporary config if needed +4. Alert administrator via logs + +## Configuration Update Process + +### Safe Configuration Updates + +**Atomic Update Process:** +1. Create backup of current configuration +2. Write new configuration to temporary file +3. Validate new configuration completely +4. If valid, rename temporary file to active config +5. Update database with new values +6. Apply changes to running server +7. Log successful update + +**Rollback Process:** +1. Detect invalid configuration at startup +2. Restore from backup file +3. Log rollback event +4. Continue with previous working configuration + +### Hot Reload Support + +**File Change Detection:** +```c +int monitor_config_file_changes(const char* config_path) { + // Use inotify on Linux to watch file changes + int inotify_fd = inotify_init(); + int watch_fd = inotify_add_watch(inotify_fd, config_path, IN_MODIFY | IN_MOVED_TO); + + // Monitor in separate thread + // On change: validate -> apply -> log + return 0; +} +``` + +**Runtime Configuration Updates:** +- Reload configuration on file change +- Apply non-restart-required changes immediately +- Queue restart-required changes for next restart +- Notify operators of configuration changes + +## Security Considerations + +### Access Control + +**File Permissions:** +- Config files: 600 (owner read/write only) +- Directories: 700 (owner access only) +- Backup files: 600 (owner read/write only) + +**Admin Key Management:** +- Admin private keys never stored in config files +- Only admin pubkeys stored for verification +- Support for multiple admin pubkeys +- Key rotation support + +### Signature Validation + +**Event Signature Verification:** +```c +int verify_config_event_signature(const config_event_t* event, const char* admin_pubkey) { + // 1. Reconstruct event for signing (without id and sig) + // 2. Compute event ID and verify against stored ID + // 3. Verify signature using admin pubkey + // 4. Check admin pubkey authorization + return NOSTR_SUCCESS; +} +``` + +**Anti-Replay Protection:** +- Configuration events must be newer than current +- Event timestamps validated against reasonable bounds +- Configuration history prevents replay attacks + +## Implementation Phases + +### Phase 1: Basic File Support +- XDG path resolution +- File loading and parsing +- Basic validation +- Database integration + +### Phase 2: Security Features +- Event signature verification +- Admin pubkey management +- File permission checks +- Error handling + +### Phase 3: Advanced Features +- Hot reload support +- Automatic backups +- Configuration utilities +- Interactive setup + +### Phase 4: Monitoring & Management +- Configuration change monitoring +- Advanced validation rules +- Configuration audit logging +- Management utilities + +## Configuration Generation Utilities + +### Interactive Setup Script + +```bash +#!/bin/bash +# scripts/setup_config.sh - Interactive configuration setup + +create_initial_config() { + echo "=== C Nostr Relay Initial Configuration ===" + + # Collect basic information + read -p "Relay name [C Nostr Relay]: " relay_name + read -p "Admin public key (hex): " admin_pubkey + read -p "Server port [8888]: " server_port + + # Generate signed configuration event + ./scripts/generate_config.sh \ + --admin-key "$admin_pubkey" \ + --relay-name "${relay_name:-C Nostr Relay}" \ + --port "${server_port:-8888}" \ + --output "$XDG_CONFIG_HOME/c-relay/c_relay_config_event.json" +} +``` + +### Configuration Validation Utility + +```bash +#!/bin/bash +# scripts/validate_config.sh - Validate configuration file + +validate_config_file() { + local config_file="$1" + + # Check file exists and is readable + # Validate JSON structure + # Verify event signature + # Check configuration values + # Report validation results +} +``` + +This comprehensive file-based configuration design provides a robust, secure, and maintainable system that follows industry standards while integrating seamlessly with the existing C Nostr Relay architecture. \ No newline at end of file diff --git a/make_and_restart_relay.sh b/make_and_restart_relay.sh index c86aa09..69708db 100755 --- a/make_and_restart_relay.sh +++ b/make_and_restart_relay.sh @@ -5,6 +5,53 @@ echo "=== C Nostr Relay Build and Restart Script ===" +# Parse command line arguments +PRESERVE_CONFIG=false +HELP=false + +while [[ $# -gt 0 ]]; do + case $1 in + --preserve-config|-p) + PRESERVE_CONFIG=true + shift + ;; + --help|-h) + HELP=true + shift + ;; + *) + echo "Unknown option: $1" + HELP=true + shift + ;; + esac +done + +# Show help +if [ "$HELP" = true ]; then + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " --preserve-config Keep existing configuration file (don't regenerate)" + echo " --help, -h Show this help message" + echo "" + echo "Default behavior: Automatically regenerates configuration file on each build" + echo " for development purposes" + exit 0 +fi + +# Handle configuration file regeneration +CONFIG_FILE="$HOME/.config/c-relay/c_relay_config_event.json" +if [ "$PRESERVE_CONFIG" = false ] && [ -f "$CONFIG_FILE" ]; then + echo "Removing old configuration file to trigger regeneration..." + rm -f "$CONFIG_FILE" + echo "✓ Configuration file removed - will be regenerated with latest database values" +elif [ "$PRESERVE_CONFIG" = true ] && [ -f "$CONFIG_FILE" ]; then + echo "Preserving existing configuration file as requested" +elif [ ! -f "$CONFIG_FILE" ]; then + echo "No existing configuration file found - will generate new one" +fi + # Build the project first echo "Building project..." make clean all @@ -90,6 +137,19 @@ if ps -p "$RELAY_PID" >/dev/null 2>&1; then # Save PID for debugging echo $RELAY_PID > relay.pid + # Check if a new private key was generated and display it + sleep 1 # Give relay time to write initial logs + if grep -q "GENERATED RELAY ADMIN PRIVATE KEY" relay.log 2>/dev/null; then + echo "=== IMPORTANT: NEW ADMIN PRIVATE KEY GENERATED ===" + echo "" + # Extract and display the private key section from the log + grep -A 8 -B 2 "GENERATED RELAY ADMIN PRIVATE KEY" relay.log | head -n 12 + echo "" + echo "⚠️ SAVE THIS PRIVATE KEY SECURELY - IT CONTROLS YOUR RELAY!" + echo "⚠️ This key is also logged in relay.log for reference" + echo "" + fi + echo "=== Relay server running in background ===" echo "To kill relay: pkill -f 'c_relay_'" echo "To check status: ps aux | grep c_relay_" diff --git a/relay.log b/relay.log index 6168b99..58431ac 100644 --- a/relay.log +++ b/relay.log @@ -1,83 +1,50 @@ === C Nostr Relay Server === [SUCCESS] Database connection established +[INFO] Initializing configuration system... +[INFO] Configuration directory: %s + /home/teknari/.config/c-relay +[INFO] Configuration file: %s + /home/teknari/.config/c-relay/c_relay_config_event.json +[INFO] Initializing configuration database statements... +[SUCCESS] Configuration database statements initialized +[INFO] Generating missing configuration file... +[INFO] Using private key from environment variable +[INFO] Creating configuration Nostr event... +[SUCCESS] Configuration Nostr event created successfully + Event ID: 03021d58b91941a3bb9284ee704e069c50c9ac09a20eb049d8de676757dde83a + Public Key: 8d8fbfb027872f13ed09e9e61f1d09473f3bec24bcfa9183e76cc1ceb789eb5e +[INFO] Stored admin public key in configuration database + Admin Public Key: 8d8fbfb027872f13ed09e9e61f1d09473f3bec24bcfa9183e76cc1ceb789eb5e +[SUCCESS] Configuration file written successfully + File: /home/teknari/.config/c-relay/c_relay_config_event.json +[SUCCESS] Configuration file generated successfully +[INFO] Loading configuration from all sources... +[INFO] Configuration file found, attempting to load... +[INFO] Loading configuration from file... +[INFO] Validating configuration event... +[INFO] Configuration event structure validation passed +[INFO] Configuration tags validation passed (%d tags) + Found 27 configuration tags +[WARNING] Signature verification not yet implemented - accepting event +[SUCCESS] Applied configuration from file + Applied 27 configuration values +[SUCCESS] Configuration event validation and application completed +[SUCCESS] Configuration loaded from file successfully +[SUCCESS] File configuration loaded successfully +[INFO] Loading configuration from database... +[SUCCESS] Database configuration validated (%d entries) + Found 27 configuration entries +[SUCCESS] Database configuration loaded +[INFO] Applying configuration to global variables... +[SUCCESS] Configuration applied to global variables +[SUCCESS] Configuration system initialized successfully [SUCCESS] Relay information initialized with default values [INFO] Initializing NIP-13 Proof of Work configuration +[INFO] PoW configured in basic validation mode [INFO] PoW Configuration: enabled=true, min_difficulty=0, validation_flags=0x1, mode=full [INFO] Initializing NIP-40 Expiration Timestamp configuration [INFO] Expiration Configuration: enabled=true, strict_mode=true, filter_responses=true, grace_period=300 seconds +[INFO] Subscription limits: max_per_client=25, max_total=5000 [INFO] Starting relay server... [INFO] Starting libwebsockets-based Nostr relay server... [SUCCESS] WebSocket relay started on ws://127.0.0.1:8888 -[INFO] HTTP request received -[INFO] Handling NIP-11 relay information request -[SUCCESS] NIP-11 relay information served successfully -[INFO] HTTP request received -[INFO] Handling NIP-11 relay information request -[SUCCESS] NIP-11 relay information served successfully -[INFO] WebSocket connection established -[INFO] Received WebSocket message -[INFO] Handling EVENT message with full NIP-01 validation -[SUCCESS] Event stored in database -[SUCCESS] Event validated and stored successfully -[INFO] WebSocket connection closed -[INFO] WebSocket connection established -[INFO] Received WebSocket message -[INFO] Handling EVENT message with full NIP-01 validation -[SUCCESS] Event stored in database -[SUCCESS] Event validated and stored successfully -[INFO] WebSocket connection closed -[INFO] WebSocket connection established -[INFO] Received WebSocket message -[INFO] Handling EVENT message with full NIP-01 validation -[WARNING] Event rejected: expired timestamp -[INFO] WebSocket connection closed -[INFO] WebSocket connection established -[INFO] Received WebSocket message -[INFO] Handling EVENT message with full NIP-01 validation -[SUCCESS] Event stored in database -[SUCCESS] Event validated and stored successfully -[INFO] WebSocket connection closed -[INFO] WebSocket connection established -[INFO] Received WebSocket message -[INFO] Handling EVENT message with full NIP-01 validation -[SUCCESS] Event stored in database -[SUCCESS] Event validated and stored successfully -[INFO] WebSocket connection closed -[INFO] WebSocket connection established -[INFO] Received WebSocket message -[INFO] Handling EVENT message with full NIP-01 validation -[SUCCESS] Event stored in database -[SUCCESS] Event validated and stored successfully -[INFO] WebSocket connection closed -[INFO] WebSocket connection established -[INFO] Received WebSocket message -[INFO] Handling EVENT message with full NIP-01 validation -[WARNING] Event rejected: expired timestamp -[INFO] WebSocket connection closed -[INFO] WebSocket connection established -[INFO] Received WebSocket message -[INFO] Handling REQ message for persistent subscription -[INFO] Added subscription 'filter_test' (total: 1) -[INFO] Executing SQL: SELECT id, pubkey, created_at, kind, content, sig, tags FROM events WHERE 1=1 AND kind IN (1) ORDER BY created_at DESC LIMIT 10 -[INFO] Query returned 10 rows -[INFO] Total events sent: 10 -[INFO] Received WebSocket message -[INFO] Removed subscription 'filter_test' (total: 0) -[INFO] Closed subscription: filter_test -[INFO] WebSocket connection closed -[WARNING] Subscription 'a' not found for removal -[INFO] WebSocket connection established -[INFO] Received WebSocket message -[INFO] Handling EVENT message with full NIP-01 validation -[SUCCESS] Event stored in database -[SUCCESS] Event validated and stored successfully -[INFO] WebSocket connection closed -[INFO] WebSocket connection established -[INFO] Received WebSocket message -[INFO] Handling EVENT message with full NIP-01 validation -[SUCCESS] Event stored in database -[SUCCESS] Event validated and stored successfully -[INFO] WebSocket connection closed -[INFO] HTTP request received -[INFO] Handling NIP-11 relay information request -[SUCCESS] NIP-11 relay information served successfully diff --git a/relay.pid b/relay.pid index 62dfa5e..42a4827 100644 --- a/relay.pid +++ b/relay.pid @@ -1 +1 @@ -743964 +793733 diff --git a/src/config.c b/src/config.c new file mode 100644 index 0000000..ae9793b --- /dev/null +++ b/src/config.c @@ -0,0 +1,1000 @@ +#include "config.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include "../nostr_core_lib/nostr_core/nostr_core.h" + +// External database connection (from main.c) +extern sqlite3* g_db; + +// Global configuration manager instance +config_manager_t g_config_manager = {0}; + +// Logging functions (defined in main.c) +extern void log_info(const char* message); +extern void log_success(const char* message); +extern void log_warning(const char* message); +extern void log_error(const char* message); + +// ================================ +// CORE CONFIGURATION FUNCTIONS +// ================================ + +int init_configuration_system(void) { + log_info("Initializing configuration system..."); + + // Clear configuration manager state + memset(&g_config_manager, 0, sizeof(config_manager_t)); + g_config_manager.db = g_db; + + // Get XDG configuration directory + if (get_xdg_config_dir(g_config_manager.config_dir_path, sizeof(g_config_manager.config_dir_path)) != 0) { + log_error("Failed to determine XDG configuration directory"); + return -1; + } + + // Build configuration file path + snprintf(g_config_manager.config_file_path, sizeof(g_config_manager.config_file_path), + "%s/%s", g_config_manager.config_dir_path, CONFIG_FILE_NAME); + + log_info("Configuration directory: %s"); + printf(" %s\n", g_config_manager.config_dir_path); + log_info("Configuration file: %s"); + printf(" %s\n", g_config_manager.config_file_path); + + // Initialize database prepared statements + if (init_config_database_statements() != 0) { + log_error("Failed to initialize configuration database statements"); + return -1; + } + + // Generate configuration file if missing + if (generate_config_file_if_missing() != 0) { + log_warning("Failed to generate configuration file, continuing with database configuration"); + } + + // Load configuration from all sources + if (load_configuration() != 0) { + log_error("Failed to load configuration"); + return -1; + } + + // Apply configuration to global variables + if (apply_configuration_to_globals() != 0) { + log_error("Failed to apply configuration to global variables"); + return -1; + } + + log_success("Configuration system initialized successfully"); + return 0; +} + +void cleanup_configuration_system(void) { + log_info("Cleaning up configuration system..."); + + // Finalize prepared statements + if (g_config_manager.get_config_stmt) { + sqlite3_finalize(g_config_manager.get_config_stmt); + g_config_manager.get_config_stmt = NULL; + } + if (g_config_manager.set_config_stmt) { + sqlite3_finalize(g_config_manager.set_config_stmt); + g_config_manager.set_config_stmt = NULL; + } + if (g_config_manager.log_change_stmt) { + sqlite3_finalize(g_config_manager.log_change_stmt); + g_config_manager.log_change_stmt = NULL; + } + + // Clear manager state + memset(&g_config_manager, 0, sizeof(config_manager_t)); + + log_success("Configuration system cleaned up"); +} + +int load_configuration(void) { + log_info("Loading configuration from all sources..."); + + // Try to load configuration from file first + if (config_file_exists()) { + log_info("Configuration file found, attempting to load..."); + if (load_config_from_file() == 0) { + g_config_manager.file_config_loaded = 1; + log_success("File configuration loaded successfully"); + } else { + log_warning("Failed to load file configuration, falling back to database"); + } + } else { + log_info("No configuration file found, checking database"); + } + + // Load configuration from database (either as primary or fallback) + if (load_config_from_database() == 0) { + g_config_manager.database_config_loaded = 1; + log_success("Database configuration loaded"); + } else { + log_error("Failed to load database configuration"); + return -1; + } + + g_config_manager.last_reload = time(NULL); + return 0; +} + +int apply_configuration_to_globals(void) { + log_info("Applying configuration to global variables..."); + + // Apply configuration values to existing global variables + // This would update the existing hardcoded values with database values + + // For now, this is a placeholder - in Phase 4 we'll implement + // the actual mapping to existing global variables + + log_success("Configuration applied to global variables"); + return 0; +} + +// ================================ +// DATABASE CONFIGURATION FUNCTIONS +// ================================ + +int init_config_database_statements(void) { + if (!g_db) { + log_error("Database connection not available for configuration"); + return -1; + } + + log_info("Initializing configuration database statements..."); + + // Prepare statement for getting configuration values + const char* get_sql = "SELECT value FROM server_config WHERE key = ?"; + int rc = sqlite3_prepare_v2(g_db, get_sql, -1, &g_config_manager.get_config_stmt, NULL); + if (rc != SQLITE_OK) { + log_error("Failed to prepare get_config statement"); + return -1; + } + + // Prepare statement for setting configuration values + const char* set_sql = "INSERT OR REPLACE INTO server_config (key, value, updated_at) VALUES (?, ?, strftime('%s', 'now'))"; + rc = sqlite3_prepare_v2(g_db, set_sql, -1, &g_config_manager.set_config_stmt, NULL); + if (rc != SQLITE_OK) { + log_error("Failed to prepare set_config statement"); + return -1; + } + + // Prepare statement for logging configuration changes + const char* log_sql = "INSERT INTO config_history (config_key, old_value, new_value, changed_by) VALUES (?, ?, ?, ?)"; + rc = sqlite3_prepare_v2(g_db, log_sql, -1, &g_config_manager.log_change_stmt, NULL); + if (rc != SQLITE_OK) { + log_error("Failed to prepare log_change statement"); + return -1; + } + + log_success("Configuration database statements initialized"); + return 0; +} + +int get_database_config(const char* key, char* value, size_t value_size) { + if (!key || !value || !g_config_manager.get_config_stmt) { + return -1; + } + + // Reset and bind parameters + sqlite3_reset(g_config_manager.get_config_stmt); + sqlite3_bind_text(g_config_manager.get_config_stmt, 1, key, -1, SQLITE_STATIC); + + int result = -1; + if (sqlite3_step(g_config_manager.get_config_stmt) == SQLITE_ROW) { + const char* db_value = (const char*)sqlite3_column_text(g_config_manager.get_config_stmt, 0); + if (db_value) { + strncpy(value, db_value, value_size - 1); + value[value_size - 1] = '\0'; + result = 0; + } + } + + return result; +} + +int set_database_config(const char* key, const char* new_value, const char* changed_by) { + if (!key || !new_value || !g_config_manager.set_config_stmt) { + return -1; + } + + // Get old value for logging + char old_value[CONFIG_VALUE_MAX_LENGTH] = {0}; + get_database_config(key, old_value, sizeof(old_value)); + + // Set new value + sqlite3_reset(g_config_manager.set_config_stmt); + sqlite3_bind_text(g_config_manager.set_config_stmt, 1, key, -1, SQLITE_STATIC); + sqlite3_bind_text(g_config_manager.set_config_stmt, 2, new_value, -1, SQLITE_STATIC); + + int result = 0; + if (sqlite3_step(g_config_manager.set_config_stmt) != SQLITE_DONE) { + log_error("Failed to set configuration value"); + result = -1; + } else { + // Log the change + if (g_config_manager.log_change_stmt) { + sqlite3_reset(g_config_manager.log_change_stmt); + sqlite3_bind_text(g_config_manager.log_change_stmt, 1, key, -1, SQLITE_STATIC); + sqlite3_bind_text(g_config_manager.log_change_stmt, 2, strlen(old_value) > 0 ? old_value : NULL, -1, SQLITE_STATIC); + sqlite3_bind_text(g_config_manager.log_change_stmt, 3, new_value, -1, SQLITE_STATIC); + sqlite3_bind_text(g_config_manager.log_change_stmt, 4, changed_by ? changed_by : "system", -1, SQLITE_STATIC); + sqlite3_step(g_config_manager.log_change_stmt); + } + } + + return result; +} + +int load_config_from_database(void) { + log_info("Loading configuration from database..."); + + // Database configuration is already populated by schema defaults + // This function validates that the configuration tables exist and are accessible + + const char* test_sql = "SELECT COUNT(*) FROM server_config WHERE config_type IN ('system', 'user')"; + sqlite3_stmt* test_stmt; + + int rc = sqlite3_prepare_v2(g_db, test_sql, -1, &test_stmt, NULL); + if (rc != SQLITE_OK) { + log_error("Failed to prepare database configuration test query"); + return -1; + } + + int config_count = 0; + if (sqlite3_step(test_stmt) == SQLITE_ROW) { + config_count = sqlite3_column_int(test_stmt, 0); + } + + sqlite3_finalize(test_stmt); + + if (config_count > 0) { + log_success("Database configuration validated (%d entries)"); + printf(" Found %d configuration entries\n", config_count); + return 0; + } else { + log_error("No configuration entries found in database"); + return -1; + } +} + +// ================================ +// FILE CONFIGURATION FUNCTIONS +// ================================ + +int get_xdg_config_dir(char* path, size_t path_size) { + const char* xdg_config_home = getenv("XDG_CONFIG_HOME"); + + if (xdg_config_home && strlen(xdg_config_home) > 0) { + // Use XDG_CONFIG_HOME if set + snprintf(path, path_size, "%s/%s", xdg_config_home, CONFIG_XDG_DIR_NAME); + } else { + // Fall back to ~/.config + const char* home = getenv("HOME"); + if (!home) { + log_error("Neither XDG_CONFIG_HOME nor HOME environment variable is set"); + return -1; + } + snprintf(path, path_size, "%s/.config/%s", home, CONFIG_XDG_DIR_NAME); + } + + return 0; +} + +int config_file_exists(void) { + struct stat st; + return (stat(g_config_manager.config_file_path, &st) == 0); +} + +int load_config_from_file(void) { + log_info("Loading configuration from file..."); + + FILE* file = fopen(g_config_manager.config_file_path, "r"); + if (!file) { + log_error("Failed to open configuration file"); + return -1; + } + + // Read file contents + fseek(file, 0, SEEK_END); + long file_size = ftell(file); + fseek(file, 0, SEEK_SET); + + char* file_content = malloc(file_size + 1); + if (!file_content) { + log_error("Failed to allocate memory for configuration file"); + fclose(file); + return -1; + } + + size_t read_size = fread(file_content, 1, file_size, file); + file_content[read_size] = '\0'; + fclose(file); + + // Parse JSON + cJSON* json = cJSON_Parse(file_content); + free(file_content); + + if (!json) { + log_error("Failed to parse configuration file as JSON"); + return -1; + } + + // Validate Nostr event structure + int result = validate_and_apply_config_event(json); + cJSON_Delete(json); + + if (result == 0) { + log_success("Configuration loaded from file successfully"); + } else { + log_error("Configuration file validation failed"); + } + + return result; +} + +// ================================ +// NOSTR EVENT VALIDATION FUNCTIONS +// ================================ + +int validate_nostr_event_structure(const cJSON* event) { + if (!event || !cJSON_IsObject(event)) { + log_error("Configuration event is not a valid JSON object"); + return -1; + } + + // Check required fields + cJSON* kind = cJSON_GetObjectItem(event, "kind"); + cJSON* created_at = cJSON_GetObjectItem(event, "created_at"); + cJSON* tags = cJSON_GetObjectItem(event, "tags"); + cJSON* content = cJSON_GetObjectItem(event, "content"); + cJSON* pubkey = cJSON_GetObjectItem(event, "pubkey"); + cJSON* id = cJSON_GetObjectItem(event, "id"); + cJSON* sig = cJSON_GetObjectItem(event, "sig"); + + if (!kind || !cJSON_IsNumber(kind)) { + log_error("Configuration event missing or invalid 'kind' field"); + return -1; + } + + if (cJSON_GetNumberValue(kind) != 33334) { + log_error("Configuration event has wrong kind (expected 33334)"); + return -1; + } + + if (!created_at || !cJSON_IsNumber(created_at)) { + log_error("Configuration event missing or invalid 'created_at' field"); + return -1; + } + + if (!tags || !cJSON_IsArray(tags)) { + log_error("Configuration event missing or invalid 'tags' field"); + return -1; + } + + if (!content || !cJSON_IsString(content)) { + log_error("Configuration event missing or invalid 'content' field"); + return -1; + } + + if (!pubkey || !cJSON_IsString(pubkey)) { + log_error("Configuration event missing or invalid 'pubkey' field"); + return -1; + } + + if (!id || !cJSON_IsString(id)) { + log_error("Configuration event missing or invalid 'id' field"); + return -1; + } + + if (!sig || !cJSON_IsString(sig)) { + log_error("Configuration event missing or invalid 'sig' field"); + return -1; + } + + // Validate pubkey format (64 hex characters) + const char* pubkey_str = cJSON_GetStringValue(pubkey); + if (strlen(pubkey_str) != 64) { + log_error("Configuration event pubkey has invalid length"); + return -1; + } + + // Validate id format (64 hex characters) + const char* id_str = cJSON_GetStringValue(id); + if (strlen(id_str) != 64) { + log_error("Configuration event id has invalid length"); + return -1; + } + + // Validate signature format (128 hex characters) + const char* sig_str = cJSON_GetStringValue(sig); + if (strlen(sig_str) != 128) { + log_error("Configuration event signature has invalid length"); + return -1; + } + + log_info("Configuration event structure validation passed"); + return 0; +} + +int validate_config_tags(const cJSON* tags) { + if (!tags || !cJSON_IsArray(tags)) { + return -1; + } + + int tag_count = 0; + const cJSON* tag = NULL; + + cJSON_ArrayForEach(tag, tags) { + if (!cJSON_IsArray(tag)) { + log_error("Configuration tag is not an array"); + return -1; + } + + int tag_size = cJSON_GetArraySize(tag); + if (tag_size < 2) { + log_error("Configuration tag has insufficient elements"); + return -1; + } + + cJSON* key = cJSON_GetArrayItem(tag, 0); + cJSON* value = cJSON_GetArrayItem(tag, 1); + + if (!key || !cJSON_IsString(key) || !value || !cJSON_IsString(value)) { + log_error("Configuration tag key or value is not a string"); + return -1; + } + + tag_count++; + } + + log_info("Configuration tags validation passed (%d tags)"); + printf(" Found %d configuration tags\n", tag_count); + return 0; +} + +int extract_and_apply_config_tags(const cJSON* tags) { + if (!tags || !cJSON_IsArray(tags)) { + return -1; + } + + int applied_count = 0; + const cJSON* tag = NULL; + + cJSON_ArrayForEach(tag, tags) { + cJSON* key = cJSON_GetArrayItem(tag, 0); + cJSON* value = cJSON_GetArrayItem(tag, 1); + + if (!key || !value) continue; + + const char* key_str = cJSON_GetStringValue(key); + const char* value_str = cJSON_GetStringValue(value); + + if (!key_str || !value_str) continue; + + // Validate configuration value + config_validation_result_t validation = validate_config_value(key_str, value_str); + if (validation != CONFIG_VALID) { + log_config_validation_error(key_str, value_str, "Value failed validation"); + continue; + } + + // Apply configuration to database + if (set_database_config(key_str, value_str, "file") == 0) { + applied_count++; + } else { + log_error("Failed to apply configuration"); + printf(" Key: %s, Value: %s\n", key_str, value_str); + } + } + + if (applied_count > 0) { + log_success("Applied configuration from file"); + printf(" Applied %d configuration values\n", applied_count); + return 0; + } else { + log_warning("No valid configuration values found in file"); + return -1; + } +} + +int validate_and_apply_config_event(const cJSON* event) { + log_info("Validating configuration event..."); + + // Step 1: Validate event structure + if (validate_nostr_event_structure(event) != 0) { + return -1; + } + + // Step 2: Extract and validate tags + cJSON* tags = cJSON_GetObjectItem(event, "tags"); + if (validate_config_tags(tags) != 0) { + return -1; + } + + // Step 3: For now, skip signature verification (would require Nostr crypto library) + // In production, this would verify the event signature against admin pubkeys + log_warning("Signature verification not yet implemented - accepting event"); + + // Step 4: Extract and apply configuration + if (extract_and_apply_config_tags(tags) != 0) { + return -1; + } + + log_success("Configuration event validation and application completed"); + return 0; +} + +// ================================ +// CONFIGURATION ACCESS FUNCTIONS +// ================================ + +const char* get_config_value(const char* key) { + static char buffer[CONFIG_VALUE_MAX_LENGTH]; + + if (!key) { + return NULL; + } + + // Priority 1: Database configuration (updated from file) + if (get_database_config(key, buffer, sizeof(buffer)) == 0) { + return buffer; + } + + // Priority 2: Environment variables (fallback) + const char* env_value = getenv(key); + if (env_value) { + return env_value; + } + + // No value found + return NULL; +} + +int get_config_int(const char* key, int default_value) { + const char* str_value = get_config_value(key); + if (!str_value) { + return default_value; + } + + char* endptr; + long val = strtol(str_value, &endptr, 10); + + if (endptr == str_value || *endptr != '\0') { + // Invalid integer format + return default_value; + } + + return (int)val; +} + +int get_config_bool(const char* key, int default_value) { + const char* str_value = get_config_value(key); + if (!str_value) { + return default_value; + } + + // Check for boolean values + if (strcasecmp(str_value, "true") == 0 || + strcasecmp(str_value, "yes") == 0 || + strcasecmp(str_value, "1") == 0) { + return 1; + } else if (strcasecmp(str_value, "false") == 0 || + strcasecmp(str_value, "no") == 0 || + strcasecmp(str_value, "0") == 0) { + return 0; + } + + return default_value; +} + +int set_config_value(const char* key, const char* value) { + if (!key || !value) { + return -1; + } + + return set_database_config(key, value, "api"); +} + +// ================================ +// CONFIGURATION VALIDATION +// ================================ + +config_validation_result_t validate_config_value(const char* key, const char* value) { + // Placeholder for validation logic + // Will implement full validation in Phase 3 + + if (!key || !value) { + return CONFIG_MISSING_REQUIRED; + } + + // Basic validation - all values are valid for now + return CONFIG_VALID; +} + +void log_config_validation_error(const char* key, const char* value, const char* error) { + log_error("Configuration validation error"); + printf(" Key: %s\n", key ? key : "NULL"); + printf(" Value: %s\n", value ? value : "NULL"); + printf(" Error: %s\n", error ? error : "Unknown error"); +} + +// ================================ +// UTILITY FUNCTIONS +// ================================ + +const char* config_type_to_string(config_type_t type) { + switch (type) { + case CONFIG_TYPE_SYSTEM: return "system"; + case CONFIG_TYPE_USER: return "user"; + case CONFIG_TYPE_RUNTIME: return "runtime"; + default: return "unknown"; + } +} + +const char* config_data_type_to_string(config_data_type_t type) { + switch (type) { + case CONFIG_DATA_STRING: return "string"; + case CONFIG_DATA_INTEGER: return "integer"; + case CONFIG_DATA_BOOLEAN: return "boolean"; + case CONFIG_DATA_JSON: return "json"; + default: return "unknown"; + } +} + +config_type_t string_to_config_type(const char* str) { + if (!str) return CONFIG_TYPE_USER; + + if (strcmp(str, "system") == 0) return CONFIG_TYPE_SYSTEM; + if (strcmp(str, "user") == 0) return CONFIG_TYPE_USER; + if (strcmp(str, "runtime") == 0) return CONFIG_TYPE_RUNTIME; + + return CONFIG_TYPE_USER; +} + +config_data_type_t string_to_config_data_type(const char* str) { + if (!str) return CONFIG_DATA_STRING; + + if (strcmp(str, "string") == 0) return CONFIG_DATA_STRING; + if (strcmp(str, "integer") == 0) return CONFIG_DATA_INTEGER; + if (strcmp(str, "boolean") == 0) return CONFIG_DATA_BOOLEAN; + if (strcmp(str, "json") == 0) return CONFIG_DATA_JSON; + + return CONFIG_DATA_STRING; +} + +int config_requires_restart(const char* key) { + if (!key) return 0; + + // Check database for requires_restart flag + const char* sql = "SELECT requires_restart FROM server_config WHERE key = ?"; + sqlite3_stmt* stmt; + + int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return 0; + } + + sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC); + + int requires_restart = 0; + if (sqlite3_step(stmt) == SQLITE_ROW) { + requires_restart = sqlite3_column_int(stmt, 0); + } + + sqlite3_finalize(stmt); + return requires_restart; +} + +// ================================ +// NOSTR EVENT GENERATION FUNCTIONS +// ================================ + +#include + +cJSON* create_config_nostr_event(const char* privkey_hex) { + log_info("Creating configuration Nostr event..."); + + // Convert hex private key to bytes + unsigned char privkey_bytes[32]; + if (nostr_hex_to_bytes(privkey_hex, privkey_bytes, 32) != 0) { + log_error("Failed to convert private key from hex"); + return NULL; + } + + // Create tags array with default configuration values + cJSON* tags = cJSON_CreateArray(); + + // Default configuration values (moved from schema.sql) + typedef struct { + const char* key; + const char* value; + } default_config_t; + + static const default_config_t defaults[] = { + // Administrative settings + {"admin_pubkey", ""}, + {"admin_enabled", "false"}, + + // Server core settings + {"relay_port", "8888"}, + {"database_path", "db/c_nostr_relay.db"}, + {"max_connections", "100"}, + + // NIP-11 Relay Information + {"relay_name", "C Nostr Relay"}, + {"relay_description", "High-performance C Nostr relay with SQLite storage"}, + {"relay_contact", ""}, + {"relay_pubkey", ""}, + {"relay_software", "https://github.com/teknari/c-relay"}, + {"relay_version", "0.2.0"}, + + // NIP-13 Proof of Work + {"pow_enabled", "true"}, + {"pow_min_difficulty", "0"}, + {"pow_mode", "basic"}, + + // NIP-40 Expiration Timestamp + {"expiration_enabled", "true"}, + {"expiration_strict", "true"}, + {"expiration_filter", "true"}, + {"expiration_grace_period", "300"}, + + // Subscription limits + {"max_subscriptions_per_client", "25"}, + {"max_total_subscriptions", "5000"}, + {"max_filters_per_subscription", "10"}, + + // Event processing limits + {"max_event_tags", "100"}, + {"max_content_length", "8196"}, + {"max_message_length", "16384"}, + + // Performance settings + {"default_limit", "500"}, + {"max_limit", "5000"} + }; + + int defaults_count = sizeof(defaults) / sizeof(defaults[0]); + + // First try to load from database, fall back to defaults + const char* sql = "SELECT key, value FROM server_config WHERE config_type IN ('system', 'user') ORDER BY key"; + sqlite3_stmt* stmt; + + int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); + if (rc == SQLITE_OK) { + // Load existing values from database + 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* tag = cJSON_CreateArray(); + cJSON_AddItemToArray(tag, cJSON_CreateString(key)); + cJSON_AddItemToArray(tag, cJSON_CreateString(value)); + cJSON_AddItemToArray(tags, tag); + } + } + sqlite3_finalize(stmt); + } + + // If database is empty, use defaults + if (cJSON_GetArraySize(tags) == 0) { + log_info("Database empty, using default configuration values"); + for (int i = 0; i < defaults_count; i++) { + cJSON* tag = cJSON_CreateArray(); + cJSON_AddItemToArray(tag, cJSON_CreateString(defaults[i].key)); + cJSON_AddItemToArray(tag, cJSON_CreateString(defaults[i].value)); + cJSON_AddItemToArray(tags, tag); + } + } + + // Create and sign event using nostr_core_lib + cJSON* event = nostr_create_and_sign_event( + 33334, // kind + "C Nostr Relay configuration event", // content + tags, // tags + privkey_bytes, // private key bytes for signing + time(NULL) // created_at timestamp + ); + + cJSON_Delete(tags); // Clean up tags as they were duplicated in nostr_create_and_sign_event + + if (!event) { + log_error("Failed to create and sign configuration event"); + return NULL; + } + + // Log success information + cJSON* id_obj = cJSON_GetObjectItem(event, "id"); + cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey"); + + if (id_obj && pubkey_obj) { + log_success("Configuration Nostr event created successfully"); + printf(" Event ID: %s\n", cJSON_GetStringValue(id_obj)); + printf(" Public Key: %s\n", cJSON_GetStringValue(pubkey_obj)); + } + + return event; +} + +int write_config_event_to_file(const cJSON* event) { + if (!event) { + return -1; + } + + // Ensure config directory exists + struct stat st = {0}; + if (stat(g_config_manager.config_dir_path, &st) == -1) { + if (mkdir(g_config_manager.config_dir_path, 0700) != 0) { + log_error("Failed to create configuration directory"); + return -1; + } + log_info("Created configuration directory: %s"); + printf(" %s\n", g_config_manager.config_dir_path); + } + + // Write to file with custom formatting for better readability + FILE* file = fopen(g_config_manager.config_file_path, "w"); + if (!file) { + log_error("Failed to open configuration file for writing"); + return -1; + } + + // Custom formatting: tags first, then other fields + fprintf(file, "{\n"); + + // First, write tags array with each tag on its own line + cJSON* tags = cJSON_GetObjectItem(event, "tags"); + if (tags && cJSON_IsArray(tags)) { + fprintf(file, " \"tags\": [\n"); + int tag_count = cJSON_GetArraySize(tags); + for (int i = 0; i < tag_count; i++) { + cJSON* tag = cJSON_GetArrayItem(tags, i); + if (tag && cJSON_IsArray(tag)) { + char* tag_str = cJSON_Print(tag); + if (tag_str) { + fprintf(file, " %s%s\n", tag_str, (i < tag_count - 1) ? "," : ""); + free(tag_str); + } + } + } + fprintf(file, " ],\n"); + } + + // Then write other fields in order + const char* field_order[] = {"id", "pubkey", "created_at", "kind", "content", "sig"}; + int field_count = sizeof(field_order) / sizeof(field_order[0]); + + for (int i = 0; i < field_count; i++) { + cJSON* field = cJSON_GetObjectItem(event, field_order[i]); + if (field) { + fprintf(file, " \"%s\": ", field_order[i]); + if (cJSON_IsString(field)) { + fprintf(file, "\"%s\"", cJSON_GetStringValue(field)); + } else if (cJSON_IsNumber(field)) { + fprintf(file, "%ld", (long)cJSON_GetNumberValue(field)); + } + if (i < field_count - 1) { + fprintf(file, ","); + } + fprintf(file, "\n"); + } + } + + fprintf(file, "}\n"); + fclose(file); + + log_success("Configuration file written successfully"); + printf(" File: %s\n", g_config_manager.config_file_path); + + return 0; +} + +int generate_config_file_if_missing(void) { + // Check if config file already exists + if (config_file_exists()) { + log_info("Configuration file already exists, skipping generation"); + return 0; + } + + log_info("Generating missing configuration file..."); + + // Get private key from environment variable or generate random one + char privkey_hex[65]; + const char* env_privkey = getenv(CONFIG_PRIVKEY_ENV); + + if (env_privkey && strlen(env_privkey) == 64) { + // Use provided private key + strncpy(privkey_hex, env_privkey, sizeof(privkey_hex) - 1); + privkey_hex[sizeof(privkey_hex) - 1] = '\0'; + log_info("Using private key from environment variable"); + } else { + // Generate random private key manually (nostr_core_lib doesn't have a key generation function) + FILE* urandom = fopen("/dev/urandom", "rb"); + if (!urandom) { + log_error("Failed to open /dev/urandom for key generation"); + return -1; + } + + unsigned char privkey_bytes[32]; + if (fread(privkey_bytes, 1, 32, urandom) != 32) { + log_error("Failed to read random bytes for private key"); + fclose(urandom); + return -1; + } + fclose(urandom); + + // Convert to hex + nostr_bytes_to_hex(privkey_bytes, 32, privkey_hex); + + // Generate corresponding public key + char pubkey_hex[65]; + unsigned char pubkey_bytes[32]; + if (nostr_ec_public_key_from_private_key(privkey_bytes, pubkey_bytes) == 0) { + nostr_bytes_to_hex(pubkey_bytes, 32, pubkey_hex); + } else { + log_error("Failed to derive public key from private key"); + return -1; + } + + log_info("Generated random private key for configuration signing"); + + // Print the generated private key prominently for the administrator + printf("\n"); + printf("=================================================================\n"); + printf("IMPORTANT: GENERATED RELAY ADMIN PRIVATE KEY\n"); + printf("=================================================================\n"); + printf("Private Key: %s\n", privkey_hex); + printf("Public Key: %s\n", pubkey_hex); + printf("\nSAVE THIS PRIVATE KEY SECURELY - IT CONTROLS YOUR RELAY!\n"); + printf("\nTo use this key in future sessions:\n"); + printf(" export %s=%s\n", CONFIG_PRIVKEY_ENV, privkey_hex); + printf("=================================================================\n"); + printf("\n"); + + char warning_msg[256]; + snprintf(warning_msg, sizeof(warning_msg), + "To use a specific private key, set the %s environment variable", CONFIG_PRIVKEY_ENV); + log_warning(warning_msg); + } + + // Create Nostr event + cJSON* event = create_config_nostr_event(privkey_hex); + if (!event) { + log_error("Failed to create configuration event"); + return -1; + } + + // Extract and store the admin public key in database configuration + cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey"); + if (pubkey_obj && cJSON_IsString(pubkey_obj)) { + const char* admin_pubkey = cJSON_GetStringValue(pubkey_obj); + if (set_database_config("relay_admin_pubkey", admin_pubkey, "system") == 0) { + log_info("Stored admin public key in configuration database"); + printf(" Admin Public Key: %s\n", admin_pubkey); + } else { + log_warning("Failed to store admin public key in database"); + } + } + + // Write to file + int result = write_config_event_to_file(event); + cJSON_Delete(event); + + if (result == 0) { + log_success("Configuration file generated successfully"); + } else { + log_error("Failed to write configuration file"); + } + + return result; +} \ No newline at end of file diff --git a/src/config.h b/src/config.h new file mode 100644 index 0000000..61e788a --- /dev/null +++ b/src/config.h @@ -0,0 +1,223 @@ +#ifndef CONFIG_H +#define CONFIG_H + +#include +#include +#include +#include + +// Configuration system constants +#define CONFIG_KEY_MAX_LENGTH 64 +#define CONFIG_VALUE_MAX_LENGTH 512 +#define CONFIG_DESCRIPTION_MAX_LENGTH 256 +#define CONFIG_XDG_DIR_NAME "c-relay" +#define CONFIG_FILE_NAME "c_relay_config_event.json" +#define CONFIG_PRIVKEY_ENV "C_RELAY_CONFIG_PRIVKEY" +#define NOSTR_PUBKEY_HEX_LENGTH 64 +#define NOSTR_PRIVKEY_HEX_LENGTH 64 +#define NOSTR_EVENT_ID_HEX_LENGTH 64 +#define NOSTR_SIGNATURE_HEX_LENGTH 128 + +// Protocol and implementation constants (hardcoded - should NOT be configurable) +#define SUBSCRIPTION_ID_MAX_LENGTH 64 +#define CLIENT_IP_MAX_LENGTH 64 +#define RELAY_NAME_MAX_LENGTH 128 +#define RELAY_DESCRIPTION_MAX_LENGTH 1024 +#define RELAY_URL_MAX_LENGTH 256 +#define RELAY_CONTACT_MAX_LENGTH 128 +#define RELAY_PUBKEY_MAX_LENGTH 65 +#define DATABASE_PATH "db/c_nostr_relay.db" + +// Default configuration values (used as fallbacks if database config fails) +#define DEFAULT_PORT 8888 +#define DEFAULT_HOST "127.0.0.1" +#define MAX_CLIENTS 100 +#define MAX_SUBSCRIPTIONS_PER_CLIENT 20 +#define MAX_TOTAL_SUBSCRIPTIONS 5000 +#define MAX_FILTERS_PER_SUBSCRIPTION 10 + +// Configuration types +typedef enum { + CONFIG_TYPE_SYSTEM = 0, + CONFIG_TYPE_USER = 1, + CONFIG_TYPE_RUNTIME = 2 +} config_type_t; + +// Configuration data types +typedef enum { + CONFIG_DATA_STRING = 0, + CONFIG_DATA_INTEGER = 1, + CONFIG_DATA_BOOLEAN = 2, + CONFIG_DATA_JSON = 3 +} config_data_type_t; + +// Configuration validation result +typedef enum { + CONFIG_VALID = 0, + CONFIG_INVALID_TYPE = 1, + CONFIG_INVALID_RANGE = 2, + CONFIG_INVALID_FORMAT = 3, + CONFIG_MISSING_REQUIRED = 4 +} config_validation_result_t; + +// Configuration entry structure +typedef struct { + char key[CONFIG_KEY_MAX_LENGTH]; + char value[CONFIG_VALUE_MAX_LENGTH]; + char description[CONFIG_DESCRIPTION_MAX_LENGTH]; + config_type_t config_type; + config_data_type_t data_type; + int is_sensitive; + int requires_restart; + time_t created_at; + time_t updated_at; +} config_entry_t; + +// Configuration manager state +typedef struct { + sqlite3* db; + sqlite3_stmt* get_config_stmt; + sqlite3_stmt* set_config_stmt; + sqlite3_stmt* log_change_stmt; + + // Configuration loading status + int file_config_loaded; + int database_config_loaded; + time_t last_reload; + + // XDG configuration directory + char config_dir_path[512]; + char config_file_path[600]; +} config_manager_t; + +// Global configuration manager instance +extern config_manager_t g_config_manager; + +// ================================ +// CORE CONFIGURATION FUNCTIONS +// ================================ + +// Initialize configuration system +int init_configuration_system(void); + +// Cleanup configuration system +void cleanup_configuration_system(void); + +// Load configuration from all sources (file -> database -> defaults) +int load_configuration(void); + +// Apply loaded configuration to global variables +int apply_configuration_to_globals(void); + +// ================================ +// DATABASE CONFIGURATION FUNCTIONS +// ================================ + +// Initialize database prepared statements +int init_config_database_statements(void); + +// Get configuration value from database +int get_database_config(const char* key, char* value, size_t value_size); + +// Set configuration value in database +int set_database_config(const char* key, const char* new_value, const char* changed_by); + +// Load all configuration from database +int load_config_from_database(void); + +// ================================ +// FILE CONFIGURATION FUNCTIONS +// ================================ + +// Get XDG configuration directory path +int get_xdg_config_dir(char* path, size_t path_size); + +// Check if configuration file exists +int config_file_exists(void); + +// Load configuration from file +int load_config_from_file(void); + +// Validate and apply Nostr configuration event +int validate_and_apply_config_event(const cJSON* event); + +// Validate Nostr event structure +int validate_nostr_event_structure(const cJSON* event); + +// Validate configuration tags array +int validate_config_tags(const cJSON* tags); + +// Extract and apply configuration tags to database +int extract_and_apply_config_tags(const cJSON* tags); + +// ================================ +// CONFIGURATION ACCESS FUNCTIONS +// ================================ + +// Get configuration value (checks all sources: file -> database -> environment -> defaults) +const char* get_config_value(const char* key); + +// Get configuration value as integer +int get_config_int(const char* key, int default_value); + +// Get configuration value as boolean +int get_config_bool(const char* key, int default_value); + +// Set configuration value (updates database) +int set_config_value(const char* key, const char* value); + +// ================================ +// CONFIGURATION VALIDATION +// ================================ + +// Validate configuration value +config_validation_result_t validate_config_value(const char* key, const char* value); + +// Log validation error +void log_config_validation_error(const char* key, const char* value, const char* error); + +// ================================ +// UTILITY FUNCTIONS +// ================================ + +// Convert config type enum to string +const char* config_type_to_string(config_type_t type); + +// Convert config data type enum to string +const char* config_data_type_to_string(config_data_type_t type); + +// Convert string to config type enum +config_type_t string_to_config_type(const char* str); + +// Convert string to config data type enum +config_data_type_t string_to_config_data_type(const char* str); + +// Check if configuration key requires restart +int config_requires_restart(const char* key); + +// ================================ +// NOSTR EVENT GENERATION FUNCTIONS +// ================================ + +// Generate configuration file with valid Nostr event if it doesn't exist +int generate_config_file_if_missing(void); + +// Create a valid Nostr configuration event from database values +cJSON* create_config_nostr_event(const char* privkey_hex); + +// Generate a random private key (32 bytes as hex string) +int generate_random_privkey(char* privkey_hex, size_t buffer_size); + +// Derive public key from private key (using secp256k1) +int derive_pubkey_from_privkey(const char* privkey_hex, char* pubkey_hex, size_t buffer_size); + +// Create Nostr event ID (SHA256 of serialized event data) +int create_nostr_event_id(const cJSON* event, char* event_id_hex, size_t buffer_size); + +// Sign Nostr event (using secp256k1 Schnorr signature) +int sign_nostr_event(const cJSON* event, const char* privkey_hex, char* signature_hex, size_t buffer_size); + +// Write configuration event to file +int write_config_event_to_file(const cJSON* event); + +#endif // CONFIG_H \ No newline at end of file diff --git a/src/main.c b/src/main.c index 758215a..c82c292 100644 --- a/src/main.c +++ b/src/main.c @@ -15,26 +15,7 @@ #include "../nostr_core_lib/cjson/cJSON.h" #include "../nostr_core_lib/nostr_core/nostr_core.h" #include "../nostr_core_lib/nostr_core/nip013.h" // NIP-13: Proof of Work - -// Server Configuration -#define DEFAULT_PORT 8888 -#define DEFAULT_HOST "127.0.0.1" -#define DATABASE_PATH "db/c_nostr_relay.db" -#define MAX_CLIENTS 100 - -// Persistent subscription system configuration -#define MAX_SUBSCRIPTIONS_PER_CLIENT 20 -#define MAX_TOTAL_SUBSCRIPTIONS 5000 -#define MAX_FILTERS_PER_SUBSCRIPTION 10 -#define SUBSCRIPTION_ID_MAX_LENGTH 64 -#define CLIENT_IP_MAX_LENGTH 64 - -// NIP-11 relay information configuration -#define RELAY_NAME_MAX_LENGTH 128 -#define RELAY_DESCRIPTION_MAX_LENGTH 1024 -#define RELAY_URL_MAX_LENGTH 256 -#define RELAY_CONTACT_MAX_LENGTH 128 -#define RELAY_PUBKEY_MAX_LENGTH 65 // 64 hex chars + null terminator +#include "config.h" // Configuration management system // Color constants for logging #define RED "\033[31m" @@ -45,7 +26,7 @@ #define RESET "\033[0m" // Global state -static sqlite3* g_db = NULL; +sqlite3* g_db = NULL; // Non-static so config.c can access it static int g_server_running = 1; static struct lws_context *ws_context = NULL; @@ -188,8 +169,8 @@ static subscription_manager_t g_subscription_manager = { .active_subscriptions = NULL, .subscriptions_lock = PTHREAD_MUTEX_INITIALIZER, .total_subscriptions = 0, - .max_subscriptions_per_client = MAX_SUBSCRIPTIONS_PER_CLIENT, - .max_total_subscriptions = MAX_TOTAL_SUBSCRIPTIONS, + .max_subscriptions_per_client = MAX_SUBSCRIPTIONS_PER_CLIENT, // Will be updated from config + .max_total_subscriptions = MAX_TOTAL_SUBSCRIPTIONS, // Will be updated from config .total_created = 0, .total_events_broadcast = 0 }; @@ -200,6 +181,9 @@ void log_success(const char* message); void log_error(const char* message); void log_warning(const char* message); +// Forward declaration for subscription manager configuration +void update_subscription_manager_config(void); + // Forward declarations for subscription database logging void log_subscription_created(const subscription_t* sub); void log_subscription_closed(const char* sub_id, const char* client_ip, const char* reason); @@ -955,6 +939,19 @@ void log_warning(const char* message) { fflush(stdout); } +// Update subscription manager configuration from config system +void update_subscription_manager_config(void) { + g_subscription_manager.max_subscriptions_per_client = get_config_int("max_subscriptions_per_client", MAX_SUBSCRIPTIONS_PER_CLIENT); + g_subscription_manager.max_total_subscriptions = get_config_int("max_total_subscriptions", MAX_TOTAL_SUBSCRIPTIONS); + + char config_msg[256]; + snprintf(config_msg, sizeof(config_msg), + "Subscription limits: max_per_client=%d, max_total=%d", + g_subscription_manager.max_subscriptions_per_client, + g_subscription_manager.max_total_subscriptions); + log_info(config_msg); +} + // Signal handler for graceful shutdown void signal_handler(int sig) { if (sig == SIGINT || sig == SIGTERM) { @@ -1288,13 +1285,47 @@ int mark_event_as_deleted(const char* event_id, const char* deletion_event_id, c ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// -// Initialize relay information with default values +// Initialize relay information using configuration system void init_relay_info() { - // Set default relay information - strncpy(g_relay_info.name, "C Nostr Relay", sizeof(g_relay_info.name) - 1); - strncpy(g_relay_info.description, "A high-performance Nostr relay implemented in C with SQLite storage", sizeof(g_relay_info.description) - 1); - strncpy(g_relay_info.software, "https://github.com/teknari/c-relay", sizeof(g_relay_info.software) - 1); - strncpy(g_relay_info.version, "0.1.0", sizeof(g_relay_info.version) - 1); + // Load relay information from configuration system + const char* relay_name = get_config_value("relay_name"); + if (relay_name) { + strncpy(g_relay_info.name, relay_name, sizeof(g_relay_info.name) - 1); + } else { + strncpy(g_relay_info.name, "C Nostr Relay", sizeof(g_relay_info.name) - 1); + } + + const char* relay_description = get_config_value("relay_description"); + if (relay_description) { + strncpy(g_relay_info.description, relay_description, sizeof(g_relay_info.description) - 1); + } else { + strncpy(g_relay_info.description, "A high-performance Nostr relay implemented in C with SQLite storage", sizeof(g_relay_info.description) - 1); + } + + const char* relay_software = get_config_value("relay_software"); + if (relay_software) { + strncpy(g_relay_info.software, relay_software, sizeof(g_relay_info.software) - 1); + } else { + strncpy(g_relay_info.software, "https://github.com/laantungir/c-relay", sizeof(g_relay_info.software) - 1); + } + + const char* relay_version = get_config_value("relay_version"); + if (relay_version) { + strncpy(g_relay_info.version, relay_version, sizeof(g_relay_info.version) - 1); + } else { + strncpy(g_relay_info.version, "0.2.0", sizeof(g_relay_info.version) - 1); + } + + // Load optional fields + const char* relay_contact = get_config_value("relay_contact"); + if (relay_contact) { + strncpy(g_relay_info.contact, relay_contact, sizeof(g_relay_info.contact) - 1); + } + + const char* relay_pubkey = get_config_value("relay_pubkey"); + if (relay_pubkey) { + strncpy(g_relay_info.pubkey, relay_pubkey, sizeof(g_relay_info.pubkey) - 1); + } // Initialize supported NIPs array g_relay_info.supported_nips = cJSON_CreateArray(); @@ -1308,22 +1339,22 @@ void init_relay_info() { cJSON_AddItemToArray(g_relay_info.supported_nips, cJSON_CreateNumber(40)); // NIP-40: Expiration Timestamp } - // Initialize server limitations + // Initialize server limitations using configuration g_relay_info.limitation = cJSON_CreateObject(); if (g_relay_info.limitation) { - cJSON_AddNumberToObject(g_relay_info.limitation, "max_message_length", 16384); - cJSON_AddNumberToObject(g_relay_info.limitation, "max_subscriptions", MAX_SUBSCRIPTIONS_PER_CLIENT); - cJSON_AddNumberToObject(g_relay_info.limitation, "max_limit", 5000); + cJSON_AddNumberToObject(g_relay_info.limitation, "max_message_length", get_config_int("max_message_length", 16384)); + cJSON_AddNumberToObject(g_relay_info.limitation, "max_subscriptions", get_config_int("max_subscriptions_per_client", 20)); + cJSON_AddNumberToObject(g_relay_info.limitation, "max_limit", get_config_int("max_limit", 5000)); cJSON_AddNumberToObject(g_relay_info.limitation, "max_subid_length", SUBSCRIPTION_ID_MAX_LENGTH); - cJSON_AddNumberToObject(g_relay_info.limitation, "max_event_tags", 100); - cJSON_AddNumberToObject(g_relay_info.limitation, "max_content_length", 8196); + cJSON_AddNumberToObject(g_relay_info.limitation, "max_event_tags", get_config_int("max_event_tags", 100)); + cJSON_AddNumberToObject(g_relay_info.limitation, "max_content_length", get_config_int("max_content_length", 8196)); cJSON_AddNumberToObject(g_relay_info.limitation, "min_pow_difficulty", g_pow_config.min_pow_difficulty); - cJSON_AddBoolToObject(g_relay_info.limitation, "auth_required", cJSON_False); + cJSON_AddBoolToObject(g_relay_info.limitation, "auth_required", get_config_bool("admin_enabled", 0) ? cJSON_True : cJSON_False); cJSON_AddBoolToObject(g_relay_info.limitation, "payment_required", cJSON_False); cJSON_AddBoolToObject(g_relay_info.limitation, "restricted_writes", cJSON_False); cJSON_AddNumberToObject(g_relay_info.limitation, "created_at_lower_limit", 0); cJSON_AddNumberToObject(g_relay_info.limitation, "created_at_upper_limit", 2147483647); - cJSON_AddNumberToObject(g_relay_info.limitation, "default_limit", 500); + cJSON_AddNumberToObject(g_relay_info.limitation, "default_limit", get_config_int("default_limit", 500)); } // Initialize empty retention policies (can be configured later) @@ -1636,48 +1667,39 @@ int handle_nip11_http_request(struct lws* wsi, const char* accept_header) { ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// -// Initialize PoW configuration with environment variables and defaults +// Initialize PoW configuration using configuration system void init_pow_config() { log_info("Initializing NIP-13 Proof of Work configuration"); - // Initialize with defaults (already set in struct initialization) + // Load PoW settings from configuration system + g_pow_config.enabled = get_config_bool("pow_enabled", 1); + g_pow_config.min_pow_difficulty = get_config_int("pow_min_difficulty", 0); - // Check environment variables for configuration - const char* pow_enabled_env = getenv("RELAY_POW_ENABLED"); - if (pow_enabled_env) { - g_pow_config.enabled = (strcmp(pow_enabled_env, "1") == 0 || - strcmp(pow_enabled_env, "true") == 0 || - strcmp(pow_enabled_env, "yes") == 0); - } - - const char* min_diff_env = getenv("RELAY_MIN_POW_DIFFICULTY"); - if (min_diff_env) { - int min_diff = atoi(min_diff_env); - if (min_diff >= 0 && min_diff <= 64) { // Reasonable bounds - g_pow_config.min_pow_difficulty = min_diff; - } - } - - const char* pow_mode_env = getenv("RELAY_POW_MODE"); - if (pow_mode_env) { - if (strcmp(pow_mode_env, "strict") == 0) { + // Get PoW mode from configuration + const char* pow_mode = get_config_value("pow_mode"); + if (pow_mode) { + if (strcmp(pow_mode, "strict") == 0) { g_pow_config.validation_flags = NOSTR_POW_VALIDATE_ANTI_SPAM | NOSTR_POW_STRICT_FORMAT; g_pow_config.require_nonce_tag = 1; g_pow_config.reject_lower_targets = 1; g_pow_config.strict_format = 1; g_pow_config.anti_spam_mode = 1; log_info("PoW configured in strict anti-spam mode"); - } else if (strcmp(pow_mode_env, "full") == 0) { + } else if (strcmp(pow_mode, "full") == 0) { g_pow_config.validation_flags = NOSTR_POW_VALIDATE_FULL; g_pow_config.require_nonce_tag = 1; log_info("PoW configured in full validation mode"); - } else if (strcmp(pow_mode_env, "basic") == 0) { + } else if (strcmp(pow_mode, "basic") == 0) { g_pow_config.validation_flags = NOSTR_POW_VALIDATE_BASIC; log_info("PoW configured in basic validation mode"); - } else if (strcmp(pow_mode_env, "disabled") == 0) { + } else if (strcmp(pow_mode, "disabled") == 0) { g_pow_config.enabled = 0; - log_info("PoW validation disabled via RELAY_POW_MODE"); + log_info("PoW validation disabled via configuration"); } + } else { + // Default to basic mode + g_pow_config.validation_flags = NOSTR_POW_VALIDATE_BASIC; + log_info("PoW configured in basic validation mode (default)"); } // Log final configuration @@ -1801,45 +1823,21 @@ int validate_event_pow(cJSON* event, char* error_message, size_t error_size) { ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// -// Initialize expiration configuration with environment variables and defaults +// Initialize expiration configuration using configuration system void init_expiration_config() { log_info("Initializing NIP-40 Expiration Timestamp configuration"); - // Check environment variables for configuration - const char* exp_enabled_env = getenv("RELAY_EXPIRATION_ENABLED"); - if (exp_enabled_env) { - g_expiration_config.enabled = (strcmp(exp_enabled_env, "1") == 0 || - strcmp(exp_enabled_env, "true") == 0 || - strcmp(exp_enabled_env, "yes") == 0); - } + // Load expiration settings from configuration system + g_expiration_config.enabled = get_config_bool("expiration_enabled", 1); + g_expiration_config.strict_mode = get_config_bool("expiration_strict", 1); + g_expiration_config.filter_responses = get_config_bool("expiration_filter", 1); + g_expiration_config.delete_expired = get_config_bool("expiration_delete", 0); + g_expiration_config.grace_period = get_config_int("expiration_grace_period", 300); - const char* exp_strict_env = getenv("RELAY_EXPIRATION_STRICT"); - if (exp_strict_env) { - g_expiration_config.strict_mode = (strcmp(exp_strict_env, "1") == 0 || - strcmp(exp_strict_env, "true") == 0 || - strcmp(exp_strict_env, "yes") == 0); - } - - const char* exp_filter_env = getenv("RELAY_EXPIRATION_FILTER"); - if (exp_filter_env) { - g_expiration_config.filter_responses = (strcmp(exp_filter_env, "1") == 0 || - strcmp(exp_filter_env, "true") == 0 || - strcmp(exp_filter_env, "yes") == 0); - } - - const char* exp_delete_env = getenv("RELAY_EXPIRATION_DELETE"); - if (exp_delete_env) { - g_expiration_config.delete_expired = (strcmp(exp_delete_env, "1") == 0 || - strcmp(exp_delete_env, "true") == 0 || - strcmp(exp_delete_env, "yes") == 0); - } - - const char* exp_grace_env = getenv("RELAY_EXPIRATION_GRACE_PERIOD"); - if (exp_grace_env) { - long grace_period = atol(exp_grace_env); - if (grace_period >= 0 && grace_period <= 86400) { // Max 24 hours - g_expiration_config.grace_period = grace_period; - } + // Validate grace period bounds + if (g_expiration_config.grace_period < 0 || g_expiration_config.grace_period > 86400) { + log_warning("Invalid grace period, using default of 300 seconds"); + g_expiration_config.grace_period = 300; } // Log final configuration @@ -2259,7 +2257,7 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru } // Check session subscription limits - if (pss && pss->subscription_count >= MAX_SUBSCRIPTIONS_PER_CLIENT) { + if (pss && pss->subscription_count >= g_subscription_manager.max_subscriptions_per_client) { log_error("Maximum subscriptions per client exceeded"); // Send CLOSED notice @@ -2883,7 +2881,7 @@ int start_websocket_relay() { log_info("Starting libwebsockets-based Nostr relay server..."); memset(&info, 0, sizeof(info)); - info.port = DEFAULT_PORT; + info.port = get_config_int("relay_port", DEFAULT_PORT); info.protocols = protocols; info.gid = -1; info.uid = -1; @@ -2909,7 +2907,9 @@ int start_websocket_relay() { return -1; } - log_success("WebSocket relay started on ws://127.0.0.1:8888"); + char startup_msg[256]; + snprintf(startup_msg, sizeof(startup_msg), "WebSocket relay started on ws://127.0.0.1:%d", info.port); + log_success(startup_msg); // Main event loop with proper signal handling while (g_server_running) { @@ -2965,6 +2965,12 @@ int main(int argc, char* argv[]) { log_error("Invalid port number"); return 1; } + // Store port in configuration system + char port_str[16]; + snprintf(port_str, sizeof(port_str), "%d", port); + set_database_config("relay_port", port_str, "command_line"); + // Re-apply configuration to make sure global variables are updated + apply_configuration_to_globals(); } else { log_error("Port argument requires a value"); return 1; @@ -2982,19 +2988,28 @@ int main(int argc, char* argv[]) { printf(BLUE BOLD "=== C Nostr Relay Server ===" RESET "\n"); - // Initialize database + // Initialize database FIRST (required for configuration system) if (init_database() != 0) { log_error("Failed to initialize database"); return 1; } - // Initialize nostr library + // Initialize nostr library BEFORE configuration system + // (required for Nostr event generation in config files) if (nostr_init() != 0) { log_error("Failed to initialize nostr library"); close_database(); return 1; } + // Initialize configuration system (loads file + database + applies to globals) + if (init_configuration_system() != 0) { + log_error("Failed to initialize configuration system"); + nostr_cleanup(); + close_database(); + return 1; + } + // Initialize NIP-11 relay information init_relay_info(); @@ -3004,6 +3019,9 @@ int main(int argc, char* argv[]) { // Initialize NIP-40 expiration configuration init_expiration_config(); + // Update subscription manager configuration + update_subscription_manager_config(); + log_info("Starting relay server..."); // Start WebSocket Nostr relay server @@ -3011,6 +3029,7 @@ int main(int argc, char* argv[]) { // Cleanup cleanup_relay_info(); + cleanup_configuration_system(); nostr_cleanup(); close_database();