diff --git a/.gitignore b/.gitignore index 12d5b12..d108a66 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ blossom/ logs/ nostr_core_lib/ blobs/ +c-relay/ diff --git a/.admin_keys b/.test_keys similarity index 50% rename from .admin_keys rename to .test_keys index bcd7d85..ccbe9a6 100644 --- a/.admin_keys +++ b/.test_keys @@ -1,4 +1,4 @@ -ADMIN_PRIVKEY='31d3fd4bb38f4f6b60fb66e0a2e5063703bb3394579ce820d5aaf3773b96633f' -ADMIN_PUBKEY='bd109762a8185716ec0fe0f887e911c30d40e36cf7b6bb99f6eef3301e9f6f99' +ADMIN_PRIVKEY='22cc83aa57928a2800234c939240c9a6f0f44a33ea3838a860ed38930b195afd' +ADMIN_PUBKEY='8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e' SERVER_PRIVKEY='c4e0d2ed7d36277d6698650f68a6e9199f91f3abb476a67f07303e81309c48f1' SERVER_PUBKEY='52e366edfa4e9cc6a6d4653828e51ccf828a2f5a05227d7a768f33b5a198681a' diff --git a/Makefile b/Makefile index b15e80a..6973bed 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ BUILDDIR = build TARGET = $(BUILDDIR)/ginxsom-fcgi # Source files -SOURCES = $(SRCDIR)/main.c $(SRCDIR)/admin_api.c $(SRCDIR)/bud04.c $(SRCDIR)/bud06.c $(SRCDIR)/bud08.c $(SRCDIR)/bud09.c $(SRCDIR)/request_validator.c +SOURCES = $(SRCDIR)/main.c $(SRCDIR)/admin_api.c $(SRCDIR)/admin_auth.c $(SRCDIR)/admin_websocket.c $(SRCDIR)/admin_handlers.c $(SRCDIR)/bud04.c $(SRCDIR)/bud06.c $(SRCDIR)/bud08.c $(SRCDIR)/bud09.c $(SRCDIR)/request_validator.c OBJECTS = $(SOURCES:$(SRCDIR)/%.c=$(BUILDDIR)/%.o) # Default target diff --git a/Trash/Ginxsom_Management_System_Design.md b/Trash/Ginxsom_Management_System_Design.md new file mode 100644 index 0000000..84353af --- /dev/null +++ b/Trash/Ginxsom_Management_System_Design.md @@ -0,0 +1,1389 @@ +# Ginxsom Management System Design + +## Executive Summary + +This document outlines the design for a secure management interface for ginxsom, a Blossom media storage server. The design adapts proven patterns from c-relay's management system while addressing ginxsom's specific requirements for blob storage, authentication, and FastCGI architecture. + +**Key Design Principles:** +- **Security First**: NIP-17 gift wrap encryption for all admin commands +- **Database-Centric**: Configuration and state stored in SQLite +- **Unified Handler**: Tag-based routing similar to c-relay +- **Two-Step Confirmation**: Critical operations require explicit confirmation +- **Backwards Compatible**: Integrates with existing admin API without breaking changes + +--- + +## Table of Contents + +1. [System Architecture](#1-system-architecture) +2. [Database Schema](#2-database-schema) +3. [Keypair Management](#3-keypair-management) +4. [Authentication Architecture](#4-authentication-architecture) +5. [Management Commands](#5-management-commands) +6. [API Design](#6-api-design) +7. [File Structure](#7-file-structure) +8. [Implementation Plan](#8-implementation-plan) +9. [Security Considerations](#9-security-considerations) +10. [Migration Strategy](#10-migration-strategy) + +--- + +## 1. System Architecture + +### 1.1 Component Overview + +```mermaid +flowchart TB + subgraph Client["Admin Client"] + CLI[CLI Tool / nak] + WEB[Web Interface] + end + + subgraph Gateway["nginx Gateway"] + NGINX[nginx] + end + + subgraph FastCGI["FastCGI Application"] + MAIN[main.c] + VALIDATOR[request_validator.c] + ADMIN[admin_api.c] + MGMT[management_handler.c
NEW] + DM[dm_admin.c
NEW] + end + + subgraph Storage["Data Layer"] + DB[(SQLite Database)] + BLOBS[Blob Storage] + KEYS[Key Storage
Memory Only] + end + + CLI -->|NIP-17 Gift Wrap| NGINX + WEB -->|NIP-17 Gift Wrap| NGINX + NGINX -->|FastCGI| MAIN + MAIN --> VALIDATOR + VALIDATOR --> ADMIN + ADMIN --> MGMT + MGMT --> DM + DM --> DB + DM --> KEYS + MGMT --> BLOBS + + style MGMT fill:#ff9 + style DM fill:#ff9 + style KEYS fill:#f99 +``` + +### 1.2 Data Flow for Admin Commands + +```mermaid +sequenceDiagram + participant Admin + participant nginx + participant FastCGI + participant Validator + participant DM Handler + participant Database + + Admin->>nginx: POST /admin
NIP-17 Gift Wrap + nginx->>FastCGI: Forward Request + FastCGI->>Validator: Validate Auth + Validator->>Validator: Decrypt Gift Wrap + Validator->>Validator: Verify Admin Pubkey + Validator-->>FastCGI: Auth Result + + alt Auth Failed + FastCGI-->>Admin: 401/403 Error + else Auth Success + FastCGI->>DM Handler: Parse Command + DM Handler->>DM Handler: Extract Tags + DM Handler->>DM Handler: Route to Handler + + alt Critical Operation + DM Handler->>Database: Store Pending Change + DM Handler-->>Admin: Confirmation Required + Admin->>nginx: POST /admin
Confirmation Event + nginx->>FastCGI: Forward Confirmation + FastCGI->>DM Handler: Execute Pending + DM Handler->>Database: Apply Changes + else Non-Critical + DM Handler->>Database: Execute Directly + end + + DM Handler->>DM Handler: Encrypt Response (NIP-44) + DM Handler-->>Admin: Encrypted Result + end +``` + +### 1.3 Integration with Existing System + +The management system integrates with ginxsom's existing architecture: + +**Existing Components (Keep):** +- [`src/main.c`](src/main.c:1): FastCGI request loop and routing +- [`src/request_validator.c`](src/request_validator.c:1): Centralized authentication +- [`src/admin_api.c`](src/admin_api.c:1): Current admin API endpoints +- [`db/schema.sql`](db/schema.sql:1): Base database schema + +**New Components (Add):** +- `src/management_handler.c`: Unified command handler +- `src/dm_admin.c`: NIP-17 gift wrap processing +- `src/keypair_manager.c`: Server and admin key management +- `src/pending_changes.c`: Two-step confirmation system + +**Modified Components:** +- [`src/main.c`](src/main.c:1640): Add `/admin` endpoint routing +- [`src/request_validator.c`](src/request_validator.c:683): Add NIP-17 validation +- [`db/schema.sql`](db/schema.sql:1): Add management tables + +--- + +## 2. Database Schema + +### 2.1 New Tables + +```sql +-- Server keypair (relay identity) +CREATE TABLE IF NOT EXISTS server_keys ( + id INTEGER PRIMARY KEY CHECK (id = 1), -- Singleton table + public_key TEXT NOT NULL CHECK (length(public_key) = 64), + private_key_encrypted TEXT NOT NULL, -- Encrypted with system key + created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), + last_rotated INTEGER, + UNIQUE(public_key) +); + +-- Admin public keys (authorized administrators) +CREATE TABLE IF NOT EXISTS admin_keys ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + public_key TEXT NOT NULL UNIQUE CHECK (length(public_key) = 64), + name TEXT, -- Human-readable identifier + permissions TEXT NOT NULL DEFAULT 'full', -- JSON array of permissions + created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), + created_by TEXT, -- Admin pubkey who added this key + last_used INTEGER, + enabled INTEGER NOT NULL DEFAULT 1 CHECK (enabled IN (0, 1)) +); + +-- Pending changes requiring confirmation +CREATE TABLE IF NOT EXISTS pending_changes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + change_type TEXT NOT NULL, -- 'delete_blob', 'update_config', 'add_admin', etc. + change_data TEXT NOT NULL, -- JSON with change details + requested_by TEXT NOT NULL, -- Admin pubkey + requested_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), + expires_at INTEGER NOT NULL, -- Confirmation deadline + confirmed INTEGER NOT NULL DEFAULT 0 CHECK (confirmed IN (0, 1)), + confirmed_at INTEGER, + executed INTEGER NOT NULL DEFAULT 0 CHECK (executed IN (0, 1)), + executed_at INTEGER, + result TEXT -- Execution result or error +); + +-- Admin command audit log +CREATE TABLE IF NOT EXISTS admin_audit_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + admin_pubkey TEXT NOT NULL, + command TEXT NOT NULL, -- Command tag from event + parameters TEXT, -- JSON with command parameters + success INTEGER NOT NULL CHECK (success IN (0, 1)), + error_message TEXT, + timestamp INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), + client_ip TEXT, + event_id TEXT -- Nostr event ID for traceability +); + +-- NIP-17 gift wrap tracking (prevent replay) +CREATE TABLE IF NOT EXISTS gift_wrap_tracking ( + event_id TEXT PRIMARY KEY, + admin_pubkey TEXT NOT NULL, + processed_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), + expires_at INTEGER NOT NULL +); + +-- Create indexes for performance +CREATE INDEX IF NOT EXISTS idx_admin_keys_pubkey ON admin_keys(public_key); +CREATE INDEX IF NOT EXISTS idx_admin_keys_enabled ON admin_keys(enabled); +CREATE INDEX IF NOT EXISTS idx_pending_changes_expires ON pending_changes(expires_at); +CREATE INDEX IF NOT EXISTS idx_pending_changes_requested_by ON pending_changes(requested_by); +CREATE INDEX IF NOT EXISTS idx_audit_log_timestamp ON admin_audit_log(timestamp); +CREATE INDEX IF NOT EXISTS idx_audit_log_admin ON admin_audit_log(admin_pubkey); +CREATE INDEX IF NOT EXISTS idx_gift_wrap_expires ON gift_wrap_tracking(expires_at); +``` + +### 2.2 Schema Extensions to Existing Tables + +```sql +-- Add management-related config keys +INSERT OR IGNORE INTO config (key, value, description) VALUES + ('server_pubkey', '', 'Server public key (relay identity)'), + ('admin_dm_enabled', 'true', 'Enable NIP-17 DM-based admin commands'), + ('admin_confirmation_timeout', '300', 'Seconds to confirm critical operations'), + ('admin_session_timeout', '3600', 'Admin session timeout in seconds'), + ('admin_rate_limit', '100', 'Max admin commands per hour per admin'), + ('admin_audit_retention', '2592000', 'Audit log retention in seconds (30 days)'); +``` + +### 2.3 Migration from Current Schema + +The current schema already has: +- [`config`](db/schema.sql:21) table (unified configuration) +- [`blobs`](db/schema.sql:8) table (blob metadata) +- [`auth_rules`](db/schema.sql:47) table (authentication rules) + +**Migration Strategy:** +1. Add new tables without modifying existing ones +2. Populate `admin_keys` from existing `config.admin_pubkey` +3. Generate server keypair on first startup if not exists +4. Maintain backwards compatibility with existing admin API + +--- + +## 3. Keypair Management + +### 3.1 Server Keypair (Relay Identity) + +**Purpose:** Identifies the ginxsom server in Nostr ecosystem + +**Generation:** +```c +// On first startup or explicit generation +int generate_server_keypair(void) { + unsigned char privkey[32]; + unsigned char pubkey[32]; + + // Generate using nostr_core_lib + if (nostr_generate_keypair(privkey, pubkey) != NOSTR_SUCCESS) { + return -1; + } + + // Store in database (encrypted) + char pubkey_hex[65]; + char privkey_hex[65]; + nostr_bytes_to_hex(pubkey, 32, pubkey_hex); + nostr_bytes_to_hex(privkey, 32, privkey_hex); + + // Encrypt private key before storage + char encrypted_privkey[256]; + encrypt_with_system_key(privkey_hex, encrypted_privkey); + + // Store in database + return store_server_keypair(pubkey_hex, encrypted_privkey); +} +``` + +**Storage:** +- Public key: Stored in `server_keys.public_key` and `config.server_pubkey` +- Private key: **NEVER** stored in database - kept in process memory only +- On startup: Load from database, decrypt, keep in memory +- On shutdown: Clear from memory securely + +**Command-Line Options:** +```bash +# Generate new server keypair +ginxsom-fcgi --generate-server-key + +# Import existing keypair +ginxsom-fcgi --import-server-key + +# Show server public key +ginxsom-fcgi --show-server-pubkey +``` + +### 3.2 Admin Keypair Management + +**Purpose:** Authorize administrators to manage the server + +**Initial Setup:** +```bash +# Generate admin keypair (done by admin, not server) +ADMIN_PRIVKEY=$(nak key generate) +ADMIN_PUBKEY=$(echo "$ADMIN_PRIVKEY" | nak key public) + +# Add admin to server (requires existing admin or direct DB access) +sqlite3 db/ginxsom.db << EOF +INSERT INTO admin_keys (public_key, name, permissions, created_by) +VALUES ('$ADMIN_PUBKEY', 'Primary Admin', '["full"]', 'system'); +EOF +``` + +**Runtime Management:** +```bash +# Add new admin (via DM command) +nak event -k 14 --envelope --sec $ADMIN_PRIVKEY \ + --tag command add_admin \ + --tag pubkey $NEW_ADMIN_PUBKEY \ + --tag name "Secondary Admin" \ + --tag permissions '["read","write"]' \ + | curl -X POST http://localhost:9001/admin -d @- + +# List admins +nak event -k 14 --envelope --sec $ADMIN_PRIVKEY \ + --tag command list_admins \ + | curl -X POST http://localhost:9001/admin -d @- + +# Revoke admin +nak event -k 14 --envelope --sec $ADMIN_PRIVKEY \ + --tag command revoke_admin \ + --tag pubkey $ADMIN_TO_REVOKE \ + | curl -X POST http://localhost:9001/admin -d @- +``` + +### 3.3 Key Rotation + +**Server Key Rotation:** +- Generate new keypair +- Update database +- Announce new pubkey via NIP-11 relay info +- Keep old key for 30 days for transition + +**Admin Key Rotation:** +- Admin generates new keypair +- Uses old key to authorize new key +- Old key remains valid during transition period +- Explicit revocation of old key after confirmation + +--- + +## 4. Authentication Architecture + +### 4.1 NIP-17 Gift Wrap Overview + +**Why NIP-17?** +- End-to-end encryption between admin and server +- Prevents eavesdropping on admin commands +- Provides forward secrecy +- Standard Nostr protocol (interoperable) + +**Gift Wrap Structure:** +```json +{ + "kind": 1059, + "content": "", + "tags": [ + ["p", ""] + ], + "pubkey": "", + "created_at": 1234567890, + "sig": "" +} +``` + +**Seal Structure (encrypted in gift wrap):** +```json +{ + "kind": 13, + "content": "", + "tags": [ + ["p", ""] + ], + "pubkey": "", + "created_at": 1234567890 +} +``` + +**Rumor Structure (encrypted in seal):** +```json +{ + "kind": 14, + "content": "", + "tags": [ + ["command", "list_blobs"], + ["limit", "50"], + ["offset", "0"] + ], + "pubkey": "", + "created_at": 1234567890 +} +``` + +### 4.2 Authentication Flow + +```mermaid +sequenceDiagram + participant Admin + participant Server + participant DB + + Admin->>Admin: Create Kind 14 Rumor
(command + params) + Admin->>Admin: Wrap in Kind 13 Seal
(encrypt with server pubkey) + Admin->>Admin: Wrap in Kind 1059 Gift
(encrypt with ephemeral key) + + Admin->>Server: POST /admin
Gift Wrap Event + + Server->>Server: Unwrap Gift
(decrypt with server privkey) + Server->>Server: Unwrap Seal
(decrypt with admin pubkey) + Server->>Server: Extract Rumor
(Kind 14 command) + + Server->>DB: Check admin_pubkey authorized + + alt Not Authorized + Server-->>Admin: 403 Forbidden + else Authorized + Server->>DB: Check gift wrap not replayed + + alt Replayed + Server-->>Admin: 409 Conflict (Replay) + else Fresh + Server->>DB: Store gift wrap ID + Server->>Server: Parse command tags + Server->>Server: Execute command + Server->>DB: Log to audit_log + Server->>Server: Encrypt response (NIP-44) + Server-->>Admin: Encrypted Response + end + end +``` + +### 4.3 Validation Rules + +**Gift Wrap Validation:** +1. Event kind must be 1059 +2. Must have `p` tag with server pubkey +3. Signature must be valid +4. Created_at within acceptable time window (±5 minutes) + +**Seal Validation:** +1. Decryption must succeed with server private key +2. Event kind must be 13 +3. Must have `p` tag with admin pubkey + +**Rumor Validation:** +1. Decryption must succeed with admin public key +2. Event kind must be 14 +3. Admin pubkey must be in `admin_keys` table with `enabled=1` +4. Must have `command` tag +5. Event ID not in `gift_wrap_tracking` (prevent replay) +6. Created_at within acceptable time window + +**Integration with Existing Validator:** + +Add to [`src/request_validator.c`](src/request_validator.c:1): + +```c +// Add to event kind routing (around line 438) +else if (event_kind == 1059) { + // NIP-17 Gift Wrap for admin commands + int gift_wrap_result = validate_gift_wrap_admin(event); + if (gift_wrap_result != NOSTR_SUCCESS) { + result->valid = 0; + result->error_code = gift_wrap_result; + strcpy(result->reason, "Gift wrap validation failed"); + cJSON_Delete(event); + return NOSTR_SUCCESS; + } +} +``` + +--- + +## 5. Management Commands + +### 5.1 Command Categories + +**Blob Operations:** +- `list_blobs` - List blobs with filters +- `get_blob_info` - Get detailed blob metadata +- `delete_blob` - Delete blob (requires confirmation) +- `mirror_blob` - Mirror blob from another server +- `verify_blob` - Verify blob integrity + +**Storage Management:** +- `get_storage_stats` - Get storage usage statistics +- `get_disk_usage` - Get disk space information +- `cleanup_orphans` - Remove orphaned files +- `set_quota` - Set storage quota for user + +**Configuration:** +- `get_config` - Get configuration values +- `set_config` - Set configuration value (requires confirmation) +- `list_auth_rules` - List authentication rules +- `add_auth_rule` - Add authentication rule +- `remove_auth_rule` - Remove authentication rule (requires confirmation) + +**Statistics:** +- `get_stats` - Get server statistics +- `get_upload_stats` - Get upload statistics by time period +- `get_user_stats` - Get per-user statistics +- `get_bandwidth_stats` - Get bandwidth usage + +**System:** +- `get_health` - Get system health status +- `get_version` - Get server version info +- `backup_database` - Create database backup +- `restore_database` - Restore from backup (requires confirmation) +- `rotate_logs` - Rotate log files + +**Admin Management:** +- `list_admins` - List admin keys +- `add_admin` - Add new admin key (requires confirmation) +- `revoke_admin` - Revoke admin key (requires confirmation) +- `get_audit_log` - Get admin audit log + +### 5.2 Command Structure + +**Request Format (Kind 14 Rumor):** +```json +{ + "kind": 14, + "content": "", + "tags": [ + ["command", "list_blobs"], + ["limit", "50"], + ["offset", "0"], + ["filter_type", "image/*"], + ["since", "1234567890"] + ], + "pubkey": "", + "created_at": 1234567890 +} +``` + +**Response Format (NIP-44 Encrypted):** +```json +{ + "status": "success", + "command": "list_blobs", + "data": { + "blobs": [...], + "total": 1234, + "limit": 50, + "offset": 0 + }, + "timestamp": 1234567890 +} +``` + +**Error Response:** +```json +{ + "status": "error", + "command": "list_blobs", + "error": { + "code": "INVALID_PARAMETER", + "message": "Invalid limit value", + "details": "Limit must be between 1 and 1000" + }, + "timestamp": 1234567890 +} +``` + +### 5.3 Two-Step Confirmation + +**Critical Operations Requiring Confirmation:** +- `delete_blob` - Permanent data loss +- `set_config` - System behavior changes +- `add_admin` - Security implications +- `revoke_admin` - Access control changes +- `restore_database` - Data integrity risk + +**Confirmation Flow:** + +**Step 1: Request** +```json +{ + "kind": 14, + "tags": [ + ["command", "delete_blob"], + ["sha256", "abc123..."] + ] +} +``` + +**Step 1: Response** +```json +{ + "status": "confirmation_required", + "command": "delete_blob", + "pending_id": "42", + "details": { + "sha256": "abc123...", + "size": 1048576, + "type": "image/jpeg", + "uploaded_at": 1234567890 + }, + "expires_at": 1234568190, + "message": "This will permanently delete the blob. Send confirmation within 5 minutes." +} +``` + +**Step 2: Confirmation** +```json +{ + "kind": 14, + "tags": [ + ["command", "confirm"], + ["pending_id", "42"] + ] +} +``` + +**Step 2: Response** +```json +{ + "status": "success", + "command": "delete_blob", + "data": { + "sha256": "abc123...", + "deleted": true + }, + "timestamp": 1234567900 +} +``` + +--- + +## 6. API Design + +### 6.1 Endpoint Routing + +**New Endpoint:** +- `POST /admin` - Unified admin command endpoint (NIP-17 gift wrap) + +**Existing Endpoints (Keep):** +- `GET /api/stats` - Statistics (existing admin API) +- `GET /api/config` - Configuration (existing admin API) +- `PUT /api/config` - Update config (existing admin API) +- `GET /api/files` - File listing (existing admin API) +- `GET /api/health` - Health check (existing admin API) + +**Integration Strategy:** +- New `/admin` endpoint for NIP-17 DM-based commands +- Existing `/api/*` endpoints remain for backwards compatibility +- Both systems share same database and authentication +- Gradual migration path for clients + +### 6.2 Request Handling + +**Handler Registration in [`src/main.c`](src/main.c:1640):** + +```c +// Add to main request loop (around line 1640) +else if (strcmp(request_method, "POST") == 0 && + strcmp(request_uri, "/admin") == 0) { + // Handle NIP-17 gift wrap admin commands + handle_admin_dm_request(); +} +``` + +**Unified Handler Pattern:** + +```c +// src/management_handler.c +void handle_admin_dm_request(void) { + // 1. Read gift wrap event from request body + char *event_json = read_request_body(); + + // 2. Validate and unwrap gift wrap + admin_command_t cmd; + int result = unwrap_admin_gift_wrap(event_json, &cmd); + if (result != SUCCESS) { + send_admin_error(result, "Failed to unwrap gift wrap"); + return; + } + + // 3. Check admin authorization + if (!is_admin_authorized(cmd.admin_pubkey)) { + send_admin_error(ERROR_UNAUTHORIZED, "Not authorized"); + return; + } + + // 4. Check for replay + if (is_gift_wrap_replayed(cmd.event_id)) { + send_admin_error(ERROR_REPLAY, "Gift wrap already processed"); + return; + } + + // 5. Route to command handler + admin_response_t response; + result = route_admin_command(&cmd, &response); + + // 6. Log to audit log + log_admin_command(&cmd, result); + + // 7. Encrypt and send response + send_admin_response(&response, cmd.admin_pubkey); +} +``` + +### 6.3 Tag-Based Routing + +**Command Router:** + +```c +int route_admin_command(admin_command_t *cmd, admin_response_t *response) { + const char *command = get_tag_value(cmd->tags, "command"); + + if (!command) { + return ERROR_MISSING_COMMAND; + } + + // Blob operations + if (strcmp(command, "list_blobs") == 0) { + return handle_list_blobs(cmd, response); + } else if (strcmp(command, "get_blob_info") == 0) { + return handle_get_blob_info(cmd, response); + } else if (strcmp(command, "delete_blob") == 0) { + return handle_delete_blob(cmd, response); + } + + // Storage management + else if (strcmp(command, "get_storage_stats") == 0) { + return handle_get_storage_stats(cmd, response); + } else if (strcmp(command, "cleanup_orphans") == 0) { + return handle_cleanup_orphans(cmd, response); + } + + // Configuration + else if (strcmp(command, "get_config") == 0) { + return handle_get_config(cmd, response); + } else if (strcmp(command, "set_config") == 0) { + return handle_set_config(cmd, response); + } + + // Admin management + else if (strcmp(command, "list_admins") == 0) { + return handle_list_admins(cmd, response); + } else if (strcmp(command, "add_admin") == 0) { + return handle_add_admin(cmd, response); + } + + // Confirmation + else if (strcmp(command, "confirm") == 0) { + return handle_confirm_pending(cmd, response); + } + + else { + return ERROR_UNKNOWN_COMMAND; + } +} +``` + +--- + +## 7. File Structure + +### 7.1 New Files + +**Core Management:** +``` +src/management_handler.c - Unified command handler and routing +src/management_handler.h - Handler interface definitions +src/dm_admin.c - NIP-17 gift wrap processing +src/dm_admin.h - Gift wrap interface +src/keypair_manager.c - Server and admin key management +src/keypair_manager.h - Key management interface +src/pending_changes.c - Two-step confirmation system +src/pending_changes.h - Pending changes interface +``` + +**Command Handlers:** +``` +src/commands/blob_commands.c - Blob operation handlers +src/commands/storage_commands.c - Storage management handlers +src/commands/config_commands.c - Configuration handlers +src/commands/stats_commands.c - Statistics handlers +src/commands/admin_commands.c - Admin management handlers +src/commands/system_commands.c - System operation handlers +``` + +**Utilities:** +``` +src/utils/encryption.c - NIP-44 encryption utilities +src/utils/audit_log.c - Audit logging utilities +src/utils/response_builder.c - Response formatting +``` + +### 7.2 Modified Files + +**[`src/main.c`](src/main.c:1):** +- Add `/admin` endpoint routing (line ~1640) +- Initialize management system on startup +- Add cleanup on shutdown + +**[`src/request_validator.c`](src/request_validator.c:1):** +- Add Kind 1059 (gift wrap) validation (line ~438) +- Add gift wrap unwrapping logic +- Add replay prevention checks + +**[`src/admin_api.c`](src/admin_api.c:1):** +- Keep existing endpoints for backwards compatibility +- Add integration hooks for new management system +- Share database access with new handlers + +**[`db/schema.sql`](db/schema.sql:1):** +- Add new tables (server_keys, admin_keys, pending_changes, etc.) +- Add indexes for performance +- Add default configuration values + +**[`Makefile`](Makefile):** +- Add new source files to build +- Add dependencies for new modules +- Update clean targets + +### 7.3 Integration Points + +**With Existing Admin API:** +```c +// src/admin_api.c - Add integration function +void admin_api_notify_config_change(const char *key, const char *value) { + // Called by management system when config changes + // Invalidates cache, triggers reload + nostr_request_validator_force_cache_refresh(); +} +``` + +**With Request Validator:** +```c +// src/request_validator.c - Add gift wrap validation +int validate_gift_wrap_admin(cJSON *event) { + // Validate Kind 1059 structure + // Unwrap and validate seal + // Unwrap and validate rumor + // Check admin authorization + // Check replay prevention + return NOSTR_SUCCESS; +} +``` + +**With Database:** +```c +// All handlers use shared database connection +// Consistent error handling +// Transaction support for atomic operations +``` + +--- + +## 8. Implementation Plan + +### 8.1 Phase 1: Foundation (Week 1-2) + +**Goals:** +- Database schema extensions +- Keypair management +- Basic gift wrap processing + +**Tasks:** +1. Create database migration script +2. Implement `keypair_manager.c` + - Server keypair generation + - Admin keypair storage + - Key loading on startup +3. Implement `dm_admin.c` + - Gift wrap unwrapping + - Seal decryption + - Rumor extraction +4. Add gift wrap validation to `request_validator.c` +5. Create test suite for gift wrap processing + +**Deliverables:** +- Working keypair management +- Gift wrap unwrap/validation +- Database schema updated +- Unit tests passing + +### 8.2 Phase 2: Command Infrastructure (Week 3-4) + +**Goals:** +- Unified command handler +- Tag-based routing +- Response encryption + +**Tasks:** +1. Implement `management_handler.c` + - Request parsing + - Command routing + - Response building +2. Implement `pending_changes.c` + - Two-step confirmation + - Timeout handling + - Execution tracking +3. Add `/admin` endpoint to `main.c` +4. Implement audit logging +5. Create command handler templates + +**Deliverables:** +- Working command routing +- Two-step confirmation system +- Audit logging functional +- Integration tests passing + +### 8.3 Phase 3: Core Commands (Week 5-6) + +**Goals:** +- Implement essential management commands +- Integration with existing systems + +**Tasks:** +1. Implement blob commands + - `list_blobs` + - `get_blob_info` + - `delete_blob` (with confirmation) +2. Implement storage commands + - `get_storage_stats` + - `get_disk_usage` + - `cleanup_orphans` +3. Implement config commands + - `get_config` + - `set_config` (with confirmation) +4. Implement admin commands + - `list_admins` + - `add_admin` (with confirmation) + - `revoke_admin` (with confirmation) +5. Integration testing with existing admin API + +**Deliverables:** +- Core commands functional +- Integration with existing API +- End-to-end tests passing + +### 8.4 Phase 4: Advanced Features (Week 7-8) + +**Goals:** +- Statistics and monitoring +- System operations +- CLI tooling + +**Tasks:** +1. Implement stats commands + - `get_stats` + - `get_upload_stats` + - `get_user_stats` +2. Implement system commands + - `backup_database` + - `restore_database` + - `rotate_logs` +3. Create CLI tool for admin operations +4. Create web interface (optional) +5. Performance optimization +6. Security audit + +**Deliverables:** +- All commands implemented +- CLI tool functional +- Performance benchmarks +- Security review complete + +### 8.5 Phase 5: Documentation & Deployment (Week 9-10) + +**Goals:** +- Complete documentation +- Deployment guides +- Migration tools + +**Tasks:** +1. Write admin documentation +2. Create setup guides +3. Create migration scripts +4. Write troubleshooting guide +5. Create example scripts +6. Production deployment testing + +**Deliverables:** +- Complete documentation +- Migration tools +- Deployment guides +- Production-ready release + +--- + +## 9. Security Considerations + +### 9.1 Threat Model + +**Threats:** +1. **Unauthorized Access**: Attacker gains admin privileges +2. **Replay Attacks**: Reuse of captured gift wrap events +3. **Man-in-the-Middle**: Interception of admin commands +4. **Privilege Escalation**: Regular user gains admin access +5. **Data Exfiltration**: Unauthorized access to blob data +6. **Denial of Service**: Resource exhaustion via admin commands + +**Mitigations:** +1. **NIP-17 Encryption**: End-to-end encryption prevents MITM +2. **Gift Wrap Tracking**: Prevents replay attacks +3. **Admin Key Management**: Strict authorization checks +4. **Two-Step Confirmation**: Prevents accidental critical operations +5. **Audit Logging**: Tracks all admin actions +6. **Rate Limiting**: Prevents DoS via admin commands + +### 9.2 Key Security + +**Server Private Key:** +- **NEVER** stored in database +- Loaded into memory on startup +- Cleared on shutdown +- Protected by OS memory protection +- Consider using secure enclave if available + +**Admin Private Keys:** +- **NEVER** stored on server +- Managed by admin clients only +- Rotated regularly +- Revoked immediately if compromised + +**Database Encryption:** +- Consider encrypting sensitive config values +- Use system keyring for encryption keys +- Implement key rotation mechanism + +### 9.3 Access Control + +**Permission Levels:** +```json +{ + "full": ["*"], + "read": ["list_*", "get_*"], + "write": ["list_*", "get_*", "set_*", "add_*"], + "admin": ["list_*", "get_*", "set_*", "add_*", "delete_*", "revoke_*"] +} +``` + +**Permission Checks:** +```c +int check_admin_permission(const char *admin_pubkey, const char *command) { + // Load admin permissions from database + cJSON *permissions = get_admin_permissions(admin_pubkey); + + // Check if command is allowed + if (has_permission(permissions, command)) { + return 1; + } + + // Check wildcard permissions + if (has_permission(permissions, "*")) { + return 1; + } + + return 0; +} +``` + +### 9.4 Audit Trail + +**What to Log:** +- All admin commands (success and failure) +- Admin key additions/revocations +- Configuration changes +- Critical operations (delete, restore, etc.) +- Authentication failures +- Suspicious activity + +**Log Format:** +```json +{ + "timestamp": 1234567890, + "admin_pubkey": "abc123...", + "command": "delete_blob", + "parameters": {"sha256": "def456..."}, + "success": true, + "client_ip": "192.168.1.100", + "event_id": "ghi789..." +} +``` + +**Log Retention:** +- Keep audit logs for 30 days minimum +- Archive older logs to separate storage +- Implement log rotation +- Protect logs from tampering + +--- + +## 10. Migration Strategy + +### 10.1 Backwards Compatibility + +**Existing Admin API:** +- Keep all existing `/api/*` endpoints +- No breaking changes to current API +- Gradual deprecation over 6 months +- Clear migration documentation + +**Database:** +- Additive schema changes only +- No modifications to existing tables +- Migration script handles upgrades +- Rollback capability + +**Configuration:** +- Existing config keys remain valid +- New config keys added with defaults +- No changes to config file format + +### 10.2 Migration Steps + +**Step 1: Database Migration** +```bash +# Backup existing database +cp db/ginxsom.db db/ginxsom.db.backup + +# Run migration script +sqlite3 db/ginxsom.db < db/migrations/002_add_management_system.sql + +# Verify migration +sqlite3 db/ginxsom.db "SELECT name FROM sqlite_master WHERE type='table';" +``` + +**Step 2: Generate Server Keypair** +```bash +# Generate server keypair +./build/ginxsom-fcgi --generate-server-key + +# Verify keypair +./build/ginxsom-fcgi --show-server-pubkey +``` + +**Step 3: Add Initial Admin** +```bash +# Generate admin keypair +ADMIN_PRIVKEY=$(nak key generate) +ADMIN_PUBKEY=$(echo "$ADMIN_PRIVKEY" | nak key public) + +# Add to database +sqlite3 db/ginxsom.db << EOF +INSERT INTO admin_keys (public_key, name, permissions, created_by) +VALUES ('$ADMIN_PUBKEY', 'Primary Admin', '["full"]', 'system'); +EOF + +# Save admin private key securely +echo "$ADMIN_PRIVKEY" > ~/.config/ginxsom/admin.key +chmod 600 ~/.config/ginxsom/admin.key +``` + +**Step 4: Test New System** +```bash +# Test gift wrap command +nak event -k 14 --envelope --sec $ADMIN_PRIVKEY \ + --tag command list_admins \ + | curl -X POST http://localhost:9001/admin -d @- + +# Verify response +``` + +**Step 5: Update Clients** +- Update admin scripts to use new `/admin` endpoint +- Migrate from `/api/*` to NIP-17 commands +- Test thoroughly before production + +### 10.3 Rollback Plan + +**If Issues Occur:** +1. Stop ginxsom service +2. Restore database backup +3. Revert to previous version +4. Investigate issues +5. Fix and retry migration + +**Database Rollback:** +```bash +# Stop service +systemctl stop ginxsom + +# Restore backup +cp db/ginxsom.db.backup db/ginxsom.db + +# Restart service +systemctl start ginxsom +``` + +--- + +## Appendix A: Command Reference + +### Blob Commands + +**list_blobs** +```json +{ + "command": "list_blobs", + "limit": "50", + "offset": "0", + "filter_type": "image/*", + "since": "1234567890" +} +``` + +**get_blob_info** +```json +{ + "command": "get_blob_info", + "sha256": "abc123..." +} +``` + +**delete_blob** (requires confirmation) +```json +{ + "command": "delete_blob", + "sha256": "abc123..." +} +``` + +### Storage Commands + +**get_storage_stats** +```json +{ + "command": "get_storage_stats" +} +``` + +**cleanup_orphans** +```json +{ + "command": "cleanup_orphans", + "dry_run": "true" +} +``` + +### Configuration Commands + +**get_config** +```json +{ + "command": "get_config", + "key": "max_file_size" +} +``` + +**set_config** (requires confirmation) +```json +{ + "command": "set_config", + "key": "max_file_size", + "value": "209715200" +} +``` + +### Admin Commands + +**list_admins** +```json +{ + "command": "list_admins" +} +``` + +**add_admin** (requires confirmation) +```json +{ + "command": "add_admin", + "pubkey": "def456...", + "name": "Secondary Admin", + "permissions": "[\"read\",\"write\"]" +} +``` + +**revoke_admin** (requires confirmation) +```json +{ + "command": "revoke_admin", + "pubkey": "def456..." +} +``` + +--- + +## Appendix B: Example Scripts + +### Setup Script + +```bash +#!/bin/bash +# setup_admin.sh - Initial admin setup + +set -e + +echo "Ginxsom Admin Setup" +echo "===================" + +# Generate admin keypair +echo "Generating admin keypair..." +ADMIN_PRIVKEY=$(nak key generate) +ADMIN_PUBKEY=$(echo "$ADMIN_PRIVKEY" | nak key public) + +echo "Admin Public Key: $ADMIN_PUBKEY" + +# Save private key +mkdir -p ~/.config/ginxsom +echo "$ADMIN_PRIVKEY" > ~/.config/ginxsom/admin.key +chmod 600 ~/.config/ginxsom/admin.key + +echo "Private key saved to ~/.config/ginxsom/admin.key" + +# Add to database +echo "Adding admin to database..." +sqlite3 db/ginxsom.db << EOF +INSERT INTO admin_keys (public_key, name, permissions, created_by) +VALUES ('$ADMIN_PUBKEY', 'Primary Admin', '["full"]', 'system'); +EOF + +echo "Admin setup complete!" +echo "Test with: ./admin_command.sh list_admins" +``` + +### Admin Command Script + +```bash +#!/bin/bash +# admin_command.sh - Send admin command + +ADMIN_PRIVKEY=$(cat ~/.config/ginxsom/admin.key) +COMMAND=$1 +shift + +# Build tags +TAGS="" +for arg in "$@"; do + KEY=$(echo "$arg" | cut -d= -f1) + VALUE=$(echo "$arg" | cut -d= -f2-) + TAGS="$TAGS --tag $KEY \"$VALUE\"" +done + +# Send command +eval nak event -k 14 --envelope --sec "$ADMIN_PRIVKEY" \ + --tag command "$COMMAND" \ + $TAGS \ + | curl -s -X POST http://localhost:9001/admin -d @- \ + | jq . +``` + +### Usage Examples + +```bash +# List admins +./admin_command.sh list_admins + +# Get storage stats +./admin_command.sh get_storage_stats + +# List blobs +./admin_command.sh list_blobs limit=10 offset=0 + +# Delete blob (with confirmation) +./admin_command.sh delete_blob sha256=abc123... +# Then confirm +./admin_command.sh confirm pending_id=42 +``` + +--- + +## Conclusion + +This design provides a secure, scalable management system for ginxsom that: + +1. **Leverages Proven Patterns**: Adapts c-relay's successful architecture +2. **Maintains Security**: NIP-17 encryption and two-step confirmation +3. **Ensures Compatibility**: Integrates with existing admin API +4. **Enables Growth**: Extensible command system for future features +5. **Provides Auditability**: Complete audit trail of admin actions + +The phased implementation plan allows for incremental development and testing, while the migration strategy ensures smooth transition from the current system. + +**Next Steps:** +1. Review and approve design +2. Begin Phase 1 implementation +3. Set up development environment +4. Create test infrastructure +5. Start coding! \ No newline at end of file diff --git a/build/admin_auth.o b/build/admin_auth.o new file mode 100644 index 0000000..4172af0 Binary files /dev/null and b/build/admin_auth.o differ diff --git a/build/admin_handlers.o b/build/admin_handlers.o new file mode 100644 index 0000000..1e660cb Binary files /dev/null and b/build/admin_handlers.o differ diff --git a/build/admin_websocket.o b/build/admin_websocket.o new file mode 100644 index 0000000..ee5cf97 Binary files /dev/null and b/build/admin_websocket.o differ diff --git a/build/ginxsom-fcgi b/build/ginxsom-fcgi index 023be5a..8a0b91f 100755 Binary files a/build/ginxsom-fcgi and b/build/ginxsom-fcgi differ diff --git a/build/main.o b/build/main.o index 682e3c9..1d3527d 100644 Binary files a/build/main.o and b/build/main.o differ diff --git a/build/request_validator.o b/build/request_validator.o index d55703d..50cdadc 100644 Binary files a/build/request_validator.o and b/build/request_validator.o differ diff --git a/config/local-nginx.conf b/config/local-nginx.conf index c76a1f6..ef6b699 100644 --- a/config/local-nginx.conf +++ b/config/local-nginx.conf @@ -2,7 +2,7 @@ # Comprehensive Blossom Protocol Implementation # Main context - specify error log here to override system default -error_log logs/nginx/error.log debug; +error_log logs/nginx/error.log info; pid logs/nginx/nginx.pid; events { diff --git a/db/ginxsom.db b/db/ginxsom.db index 4adf3b1..99341ba 100644 Binary files a/db/ginxsom.db and b/db/ginxsom.db differ diff --git a/db/migrations/002_add_relay_seckey.sql b/db/migrations/002_add_relay_seckey.sql new file mode 100644 index 0000000..60f211d --- /dev/null +++ b/db/migrations/002_add_relay_seckey.sql @@ -0,0 +1,15 @@ +-- Migration: Add blossom_seckey table for storing server private key +-- This table stores the Blossom server's secp256k1 private key used for: +-- - Signing admin response events (Kind 23457) +-- - Decrypting admin commands (NIP-44) + +CREATE TABLE IF NOT EXISTS blossom_seckey ( + id INTEGER PRIMARY KEY CHECK (id = 1), -- Only one row allowed + seckey TEXT NOT NULL, -- Private key in hex format (64 chars) + created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), + CHECK (length(seckey) = 64) -- Ensure valid secp256k1 key length +); + +-- Add blossom_pubkey to config if not exists +INSERT OR IGNORE INTO config (key, value, description) VALUES + ('blossom_pubkey', '', 'Blossom server public key derived from blossom_seckey'); \ No newline at end of file diff --git a/deploy_lt.sh b/deploy_lt.sh index e3a62b2..7323bc7 100755 --- a/deploy_lt.sh +++ b/deploy_lt.sh @@ -13,6 +13,12 @@ print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; } print_error() { echo -e "${RED}[ERROR]${NC} $1"; } +# Parse command line arguments +FRESH_INSTALL=false +if [[ "$1" == "--fresh" ]]; then + FRESH_INSTALL=true +fi + # Configuration REMOTE_HOST="laantungir.net" REMOTE_USER="ubuntu" @@ -68,7 +74,7 @@ print_status "Copying files to remote server..." # Copy entire project directory (excluding unnecessary files) print_status "Copying entire ginxsom project..." -rsync -avz --exclude='.git' --exclude='build' --exclude='logs' --exclude='Trash' --exclude='blobs' --exclude='db/ginxsom.db' --no-g --no-o --no-perms --omit-dir-times . $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/ +rsync -avz --exclude='.git' --exclude='build' --exclude='logs' --exclude='Trash' --exclude='blobs' --exclude='db' --no-g --no-o --no-perms --omit-dir-times . $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/ # Build on remote server to ensure compatibility print_status "Building ginxsom on remote server..." @@ -161,25 +167,35 @@ print_status "Setting up database directory..." ssh $REMOTE_USER@$REMOTE_HOST << EOF # Create db directory if it doesn't exist mkdir -p $REMOTE_DIR/db - - # Backup current database if it exists in old location - if [ -f /var/www/html/blossom/ginxsom.db ]; then - echo "Backing up existing database..." - cp /var/www/html/blossom/ginxsom.db /var/www/html/blossom/ginxsom.db.backup.\$(date +%Y%m%d_%H%M%S) - - # Migrate database to new location if not already there - if [ ! -f $REMOTE_DB_PATH ]; then - echo "Migrating database to new location..." - cp /var/www/html/blossom/ginxsom.db $REMOTE_DB_PATH - else - echo "Database already exists at new location" - fi - elif [ ! -f $REMOTE_DB_PATH ]; then - echo "No existing database found - will be created on first run" + + if [ "$FRESH_INSTALL" = "true" ]; then + echo "Fresh install: removing existing database and blobs..." + # Remove existing database + sudo rm -f $REMOTE_DB_PATH + sudo rm -f /var/www/html/blossom/ginxsom.db + # Remove existing blobs + sudo rm -rf $REMOTE_DATA_DIR/* + echo "Existing data removed" else - echo "Database already exists at $REMOTE_DB_PATH" + # Backup current database if it exists in old location + if [ -f /var/www/html/blossom/ginxsom.db ]; then + echo "Backing up existing database..." + cp /var/www/html/blossom/ginxsom.db /var/www/html/blossom/ginxsom.db.backup.\$(date +%Y%m%d_%H%M%S) + + # Migrate database to new location if not already there + if [ ! -f $REMOTE_DB_PATH ]; then + echo "Migrating database to new location..." + cp /var/www/html/blossom/ginxsom.db $REMOTE_DB_PATH + else + echo "Database already exists at new location" + fi + elif [ ! -f $REMOTE_DB_PATH ]; then + echo "No existing database found - will be created on first run" + else + echo "Database already exists at $REMOTE_DB_PATH" + fi fi - + # Set proper permissions - www-data needs write access to db directory for SQLite journal files sudo chown -R www-data:www-data $REMOTE_DIR/db sudo chmod 755 $REMOTE_DIR/db @@ -187,7 +203,7 @@ ssh $REMOTE_USER@$REMOTE_HOST << EOF # Allow www-data to access the application directory for spawn-fcgi chdir chmod 755 $REMOTE_DIR - + echo "Database directory setup complete" EOF @@ -284,4 +300,7 @@ print_status "Ginxsom should now be available at: https://blossom.laantungir.net print_status "Test endpoints:" echo " Health: curl -k https://blossom.laantungir.net/health" echo " Root: curl -k https://blossom.laantungir.net/" -echo " List: curl -k https://blossom.laantungir.net/list" \ No newline at end of file +echo " List: curl -k https://blossom.laantungir.net/list" +if [ "$FRESH_INSTALL" = "true" ]; then + print_warning "Fresh install completed - database and blobs have been reset" +fi \ No newline at end of file diff --git a/docs/MANAGEMENT_SYSTEM_DESIGN.md b/docs/MANAGEMENT_SYSTEM_DESIGN.md new file mode 100644 index 0000000..efbb6e3 --- /dev/null +++ b/docs/MANAGEMENT_SYSTEM_DESIGN.md @@ -0,0 +1,994 @@ +# Ginxsom Management System Design + +## Executive Summary + +This document outlines the design for a secure management interface for ginxsom (Blossom media storage server) based on c-relay's proven admin system architecture. The design uses Kind 23456/23457 events with NIP-44 encryption over WebSocket for real-time admin operations. + +## 1. System Architecture + +### 1.1 High-Level Overview + +```mermaid +graph TB + Admin[Admin Client] -->|WebSocket| WS[WebSocket Handler] + WS -->|Kind 23456| Auth[Admin Authorization] + Auth -->|Decrypt NIP-44| Decrypt[Command Decryption] + Decrypt -->|Parse JSON Array| Router[Command Router] + Router -->|Route by Command Type| Handlers[Unified Handlers] + Handlers -->|Execute| DB[(Database)] + Handlers -->|Execute| FS[File System] + Handlers -->|Generate Response| Encrypt[NIP-44 Encryption] + Encrypt -->|Kind 23457| WS + WS -->|WebSocket| Admin + + style Admin fill:#e1f5ff + style Auth fill:#fff3cd + style Handlers fill:#d4edda + style DB fill:#f8d7da +``` + +### 1.2 Component Architecture + +```mermaid +graph LR + subgraph "Admin Interface" + CLI[CLI Tool] + Web[Web Dashboard] + end + + subgraph "ginxsom FastCGI Process" + WS[WebSocket Endpoint] + Auth[Authorization Layer] + Router[Command Router] + + subgraph "Unified Handlers" + BlobH[Blob Handler] + StorageH[Storage Handler] + ConfigH[Config Handler] + StatsH[Stats Handler] + SystemH[System Handler] + end + + DB[(SQLite Database)] + Storage[Blob Storage] + end + + CLI -->|WebSocket| WS + Web -->|WebSocket| WS + WS --> Auth + Auth --> Router + Router --> BlobH + Router --> StorageH + Router --> ConfigH + Router --> StatsH + Router --> SystemH + + BlobH --> DB + BlobH --> Storage + StorageH --> Storage + ConfigH --> DB + StatsH --> DB + SystemH --> DB + + style Auth fill:#fff3cd + style Router fill:#d4edda +``` + +### 1.3 Data Flow for Admin Commands + +```mermaid +sequenceDiagram + participant Admin + participant WebSocket + participant Auth + participant Handler + participant Database + + Admin->>WebSocket: Kind 23456 Event (NIP-44 encrypted) + WebSocket->>Auth: Verify admin signature + Auth->>Auth: Check pubkey matches admin_pubkey + Auth->>Auth: Verify event signature + Auth->>WebSocket: Authorization OK + WebSocket->>Handler: Decrypt & parse command array + Handler->>Handler: Validate command structure + Handler->>Database: Execute operation + Database-->>Handler: Result + Handler->>Handler: Build response JSON + Handler->>WebSocket: Encrypt response (NIP-44) + WebSocket->>Admin: Kind 23457 Event (encrypted response) +``` + +### 1.4 Integration with Existing Ginxsom + +```mermaid +graph TB + subgraph "Existing Ginxsom" + Main[main.c] + BUD04[bud04.c - Mirror] + BUD06[bud06.c - Requirements] + BUD08[bud08.c - NIP-94] + BUD09[bud09.c - Report] + AdminAPI[admin_api.c - Basic Admin] + Validator[request_validator.c] + end + + subgraph "New Management System" + AdminWS[admin_websocket.c] + AdminAuth[admin_auth.c] + AdminHandlers[admin_handlers.c] + AdminConfig[admin_config.c] + end + + Main -->|Initialize| AdminWS + AdminWS -->|Use| AdminAuth + AdminWS -->|Route to| AdminHandlers + AdminHandlers -->|Query| BUD04 + AdminHandlers -->|Query| BUD06 + AdminHandlers -->|Query| BUD08 + AdminHandlers -->|Query| BUD09 + AdminHandlers -->|Update| AdminConfig + AdminAuth -->|Use| Validator + + style AdminWS fill:#d4edda + style AdminAuth fill:#fff3cd + style AdminHandlers fill:#e1f5ff +``` + +## 2. Database Schema + +### 2.1 Core Tables + +Following c-relay's minimal approach, we need only two tables for key management: + +#### relay_seckey Table +```sql +-- Stores relay's private key (used for signing Kind 23457 responses) +CREATE TABLE relay_seckey ( + private_key_hex TEXT NOT NULL CHECK (length(private_key_hex) = 64), + created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) +); +``` + +**Note**: This table stores the relay's private key as plain hex (no encryption). The key is used to: +- Sign Kind 23457 response events +- Encrypt responses using NIP-44 (shared secret with admin pubkey) + +#### config Table (Extended) +```sql +-- Existing config table, add admin_pubkey entry +INSERT INTO config (key, value, data_type, description, category, requires_restart) +VALUES ( + 'admin_pubkey', + '<64-char-hex-pubkey>', + 'string', + 'Public key of authorized admin (hex format)', + 'security', + 0 +); +``` + +**Note**: Admin public key is stored in the config table, not a separate table. Admin private key is NEVER stored anywhere. + +### 2.2 Schema Comparison with c-relay + +| c-relay | ginxsom | Purpose | +|---------|---------|---------| +| `relay_seckey` (private_key_hex, created_at) | `relay_seckey` (private_key_hex, created_at) | Relay private key storage | +| `config` table entry for admin_pubkey | `config` table entry for admin_pubkey | Admin authorization | +| No audit log | No audit log | Keep it simple | +| No processed events tracking | No processed events tracking | Stateless processing | + +### 2.3 Key Storage Strategy + +**Relay Private Key**: +- Stored in `relay_seckey` table as plain 64-character hex +- Generated on first startup or provided via `--relay-privkey` CLI option +- Used for signing Kind 23457 responses and NIP-44 encryption +- Never exposed via API + +**Admin Public Key**: +- Stored in `config` table as plain 64-character hex +- Generated on first startup or provided via `--admin-pubkey` CLI option +- Used to verify Kind 23456 command signatures +- Can be queried via admin API + +**Admin Private Key**: +- NEVER stored anywhere in the system +- Kept only by the admin in their client/tool +- Used to sign Kind 23456 commands and decrypt Kind 23457 responses + +## 3. API Design + +### 3.1 Command Structure + +Following c-relay's pattern, all commands use JSON array format: + +```json +["command_name", {"param1": "value1", "param2": "value2"}] +``` + +### 3.2 Event Structure + +#### Kind 23456 - Admin Command Event + +```json +{ + "kind": 23456, + "pubkey": "", + "created_at": 1234567890, + "tags": [ + ["p", ""] + ], + "content": "", + "sig": "" +} +``` + +**Content (decrypted)**: +```json +["blob_list", {"limit": 100, "offset": 0}] +``` + +#### Kind 23457 - Admin Response Event + +```json +{ + "kind": 23457, + "pubkey": "", + "created_at": 1234567890, + "tags": [ + ["p", ""], + ["e", ""] + ], + "content": "", + "sig": "" +} +``` + +**Content (decrypted)**: +```json +{ + "success": true, + "data": { + "blobs": [ + {"sha256": "abc123...", "size": 1024, "type": "image/png"}, + {"sha256": "def456...", "size": 2048, "type": "video/mp4"} + ], + "total": 2 + } +} +``` + +### 3.3 Command Categories + +#### Blob Operations +- `blob_list` - List blobs with pagination +- `blob_info` - Get detailed blob information +- `blob_delete` - Delete blob(s) +- `blob_mirror` - Mirror blob from another server + +#### Storage Management +- `storage_stats` - Get storage usage statistics +- `storage_quota` - Get/set storage quotas +- `storage_cleanup` - Clean up orphaned files + +#### Configuration +- `config_get` - Get configuration value(s) +- `config_set` - Set configuration value(s) +- `config_list` - List all configuration +- `auth_rules_list` - List authentication rules +- `auth_rules_add` - Add authentication rule +- `auth_rules_remove` - Remove authentication rule + +#### Statistics +- `stats_uploads` - Upload statistics +- `stats_bandwidth` - Bandwidth usage +- `stats_storage` - Storage usage over time +- `stats_users` - User activity statistics + +#### System +- `system_info` - Get system information +- `system_restart` - Restart server (graceful) +- `system_backup` - Trigger database backup +- `system_restore` - Restore from backup + +### 3.4 Command Examples + +#### Example 1: List Blobs +```json +// Command (Kind 23456 content, decrypted) +["blob_list", { + "limit": 50, + "offset": 0, + "type": "image/*", + "sort": "created_at", + "order": "desc" +}] + +// Response (Kind 23457 content, decrypted) +{ + "success": true, + "data": { + "blobs": [ + { + "sha256": "abc123...", + "size": 102400, + "type": "image/png", + "created": 1234567890, + "url": "https://blossom.example.com/abc123.png" + } + ], + "total": 150, + "limit": 50, + "offset": 0 + } +} +``` + +#### Example 2: Delete Blob +```json +// Command +["blob_delete", { + "sha256": "abc123...", + "confirm": true +}] + +// Response +{ + "success": true, + "data": { + "deleted": true, + "sha256": "abc123...", + "freed_bytes": 102400 + } +} +``` + +#### Example 3: Get Storage Stats +```json +// Command +["storage_stats", {}] + +// Response +{ + "success": true, + "data": { + "total_blobs": 1500, + "total_bytes": 5368709120, + "total_bytes_human": "5.0 GB", + "disk_usage": { + "used": 5368709120, + "available": 94631291904, + "total": 100000000000, + "percent": 5.4 + }, + "by_type": { + "image/png": {"count": 500, "bytes": 2147483648}, + "image/jpeg": {"count": 300, "bytes": 1610612736}, + "video/mp4": {"count": 200, "bytes": 1610612736} + } + } +} +``` + +#### Example 4: Set Configuration +```json +// Command +["config_set", { + "max_upload_size": 10485760, + "allowed_mime_types": ["image/*", "video/mp4"] +}] + +// Response +{ + "success": true, + "data": { + "updated": ["max_upload_size", "allowed_mime_types"], + "requires_restart": false + } +} +``` + +### 3.5 Error Handling + +All errors follow consistent format: + +```json +{ + "success": false, + "error": { + "code": "BLOB_NOT_FOUND", + "message": "Blob with hash abc123... not found", + "details": { + "sha256": "abc123..." + } + } +} +``` + +**Error Codes**: +- `UNAUTHORIZED` - Invalid admin signature +- `INVALID_COMMAND` - Unknown command or malformed structure +- `INVALID_PARAMS` - Missing or invalid parameters +- `BLOB_NOT_FOUND` - Requested blob doesn't exist +- `STORAGE_FULL` - Storage quota exceeded +- `DATABASE_ERROR` - Database operation failed +- `SYSTEM_ERROR` - Internal server error + +## 4. File Structure + +### 4.1 New Files to Create + +``` +src/ +├── admin_websocket.c # WebSocket endpoint for admin commands +├── admin_websocket.h # WebSocket handler declarations +├── admin_auth.c # Admin authorization (adapted from c-relay) +├── admin_auth.h # Authorization function declarations +├── admin_handlers.c # Unified command handlers +├── admin_handlers.h # Handler function declarations +├── admin_config.c # Configuration management +├── admin_config.h # Config function declarations +└── admin_keys.c # Key generation and storage + admin_keys.h # Key management declarations + +include/ +└── admin_system.h # Public admin system interface +``` + +### 4.2 Files to Adapt from c-relay + +| c-relay File | Purpose | Adaptation for ginxsom | +|--------------|---------|------------------------| +| `dm_admin.c` | Admin event processing | → `admin_websocket.c` (WebSocket instead of DM) | +| `api.c` (lines 768-838) | NIP-44 encryption/response | → `admin_handlers.c` (response generation) | +| `config.c` (lines 500-583) | Key storage/retrieval | → `admin_keys.c` (relay key management) | +| `main.c` (lines 1389-1556) | CLI argument parsing | → `main.c` (add admin CLI options) | + +### 4.3 Integration with Existing Files + +**src/main.c**: +- Add CLI options: `--admin-pubkey`, `--relay-privkey` +- Initialize admin WebSocket endpoint +- Generate keys on first startup + +**src/admin_api.c** (existing): +- Keep existing basic admin API +- Add WebSocket admin endpoint +- Route Kind 23456 events to new handlers + +**db/schema.sql**: +- Add `relay_seckey` table +- Add `admin_pubkey` to config table + +## 5. Implementation Plan + +### 5.1 Phase 1: Foundation (Week 1) + +**Goal**: Set up key management and database schema + +**Tasks**: +1. Create `relay_seckey` table in schema +2. Add `admin_pubkey` to config table +3. Implement `admin_keys.c`: + - `generate_relay_keypair()` + - `generate_admin_keypair()` + - `store_relay_private_key()` + - `load_relay_private_key()` + - `get_admin_pubkey()` +4. Update `main.c`: + - Add CLI options (`--admin-pubkey`, `--relay-privkey`) + - Generate keys on first startup + - Print keys once (like c-relay) +5. Test key generation and storage + +**Deliverables**: +- Working key generation +- Keys stored in database +- CLI options functional + +### 5.2 Phase 2: Authorization (Week 2) + +**Goal**: Implement admin event authorization + +**Tasks**: +1. Create `admin_auth.c` (adapted from c-relay's authorization): + - `verify_admin_event()` - Check Kind 23456 signature + - `check_admin_pubkey()` - Verify against stored admin_pubkey + - `verify_relay_target()` - Check 'p' tag matches relay pubkey +2. Add NIP-44 crypto functions (use existing nostr_core_lib): + - `decrypt_admin_command()` - Decrypt Kind 23456 content + - `encrypt_admin_response()` - Encrypt Kind 23457 content +3. Test authorization flow +4. Test encryption/decryption + +**Deliverables**: +- Working authorization layer +- NIP-44 encryption functional +- Unit tests for auth + +### 5.3 Phase 3: WebSocket Endpoint (Week 3) + +**Goal**: Create WebSocket handler for admin commands + +**Tasks**: +1. Create `admin_websocket.c`: + - WebSocket endpoint at `/admin` or similar + - Receive Kind 23456 events + - Route to authorization layer + - Parse command array from decrypted content + - Route to appropriate handler + - Build Kind 23457 response + - Send encrypted response +2. Integrate with existing FastCGI WebSocket handling +3. Add connection management +4. Test WebSocket communication + +**Deliverables**: +- Working WebSocket endpoint +- Event routing functional +- Response generation working + +### 5.4 Phase 4: Command Handlers (Week 4-5) + +**Goal**: Implement unified command handlers + +**Tasks**: +1. Create `admin_handlers.c` with unified handler pattern: + - `handle_blob_command()` - Blob operations + - `handle_storage_command()` - Storage management + - `handle_config_command()` - Configuration + - `handle_stats_command()` - Statistics + - `handle_system_command()` - System operations +2. Implement each command: + - Blob: list, info, delete, mirror + - Storage: stats, quota, cleanup + - Config: get, set, list, auth_rules + - Stats: uploads, bandwidth, storage, users + - System: info, restart, backup, restore +3. Add validation for each command +4. Test each command individually + +**Deliverables**: +- All commands implemented +- Validation working +- Integration tests passing + +### 5.5 Phase 5: Testing & Documentation (Week 6) + +**Goal**: Comprehensive testing and documentation + +**Tasks**: +1. Create test suite: + - Unit tests for each handler + - Integration tests for full flow + - Security tests for authorization + - Performance tests for WebSocket +2. Create admin CLI tool (simple Node.js/Python script): + - Generate Kind 23456 events + - Send via WebSocket + - Decrypt Kind 23457 responses + - Pretty-print results +3. Write documentation: + - Admin API reference + - CLI tool usage guide + - Security best practices + - Troubleshooting guide +4. Create example scripts + +**Deliverables**: +- Complete test suite +- Working CLI tool +- Full documentation +- Example scripts + +### 5.6 Phase 6: Web Dashboard (Optional, Week 7-8) + +**Goal**: Create web-based admin interface + +**Tasks**: +1. Design web UI (React/Vue/Svelte) +2. Implement WebSocket client +3. Create command forms +4. Add real-time updates +5. Deploy dashboard + +**Deliverables**: +- Working web dashboard +- User documentation +- Deployment guide + +## 6. Security Considerations + +### 6.1 Key Security + +**Relay Private Key**: +- Stored in database as plain hex (following c-relay pattern) +- Never exposed via API +- Used only for signing responses +- Backed up with database + +**Admin Private Key**: +- NEVER stored on server +- Kept only by admin +- Used to sign commands +- Should be stored securely by admin (password manager, hardware key, etc.) + +**Admin Public Key**: +- Stored in config table +- Used for authorization +- Can be rotated by updating config + +### 6.2 Authorization Flow + +1. Receive Kind 23456 event +2. Verify event signature (nostr_verify_event_signature) +3. Check pubkey matches admin_pubkey from config +4. Verify 'p' tag targets this relay +5. Decrypt content using NIP-44 +6. Parse and validate command +7. Execute command +8. Encrypt response using NIP-44 +9. Sign Kind 23457 response +10. Send response + +### 6.3 Attack Mitigation + +**Replay Attacks**: +- Check event timestamp (reject old events) +- Optional: Track processed event IDs (if needed) + +**Unauthorized Access**: +- Strict pubkey verification +- Signature validation +- Relay targeting check + +**Command Injection**: +- Validate all command parameters +- Use parameterized SQL queries +- Sanitize file paths + +**DoS Protection**: +- Rate limit admin commands +- Timeout long-running operations +- Limit response sizes + +## 7. Command Line Interface + +### 7.1 CLI Options (Following c-relay Pattern) + +```bash +ginxsom [OPTIONS] + +Options: + -h, --help Show help message + -v, --version Show version information + -p, --port PORT Override server port + --strict-port Fail if exact port unavailable + -a, --admin-pubkey KEY Override admin public key (hex or npub) + -r, --relay-privkey KEY Override relay private key (hex or nsec) + --debug-level=N Set debug level (0-5) + +Examples: + ginxsom # Start server (auto-generate keys on first run) + ginxsom -p 8080 # Start on port 8080 + ginxsom -a # Set admin pubkey + ginxsom -r # Set relay privkey + ginxsom --debug-level=3 # Enable info-level debugging +``` + +### 7.2 First Startup Behavior + +On first startup (no database exists): + +1. Generate relay keypair +2. Generate admin keypair +3. Print keys ONCE to console: +``` +=== Ginxsom First Startup === + +Relay Keys (for server): + Public Key (npub): npub1... + Private Key (nsec): nsec1... + +Admin Keys (for you): + Public Key (npub): npub1... + Private Key (nsec): nsec1... + +IMPORTANT: Save these keys securely! +The admin private key will NOT be shown again. +The relay private key is stored in the database. + +Database created: .db +``` + +4. Store relay private key in database +5. Store admin public key in config +6. Start server + +### 7.3 Subsequent Startups + +On subsequent startups: + +1. Find existing database file +2. Load relay private key from database +3. Load admin public key from config +4. Apply CLI overrides if provided +5. Start server + +## 8. Comparison with c-relay + +### 8.1 Similarities + +| Feature | c-relay | ginxsom | +|---------|---------|---------| +| Event Types | Kind 23456/23457 | Kind 23456/23457 | +| Encryption | NIP-44 | NIP-44 | +| Command Format | JSON arrays | JSON arrays | +| Key Storage | relay_seckey table | relay_seckey table | +| Admin Auth | config table | config table | +| CLI Options | --admin-pubkey, --relay-privkey | --admin-pubkey, --relay-privkey | +| Response Format | Encrypted JSON | Encrypted JSON | + +### 8.2 Differences + +| Aspect | c-relay | ginxsom | +|--------|---------|---------| +| Transport | WebSocket (Nostr relay) | WebSocket (FastCGI) | +| Commands | Relay-specific (auth, config, stats) | Blossom-specific (blob, storage, mirror) | +| Database | SQLite (events) | SQLite (blobs + metadata) | +| File Storage | N/A | Blob storage on disk | +| Integration | Standalone relay | FastCGI + nginx | + +### 8.3 Architectural Decisions + +**Why follow c-relay's pattern?** +1. Proven in production +2. Simple and secure +3. No complex key management +4. Minimal database schema +5. Easy to understand and maintain + +**What we're NOT doing (from initial design)**: +1. ❌ NIP-17 gift wrap (too complex) +2. ❌ Separate admin_keys table (use config) +3. ❌ Audit log table (keep it simple) +4. ❌ Processed events tracking (stateless) +5. ❌ Key encryption before storage (plain hex) +6. ❌ Migration strategy (new project) + +## 9. Testing Strategy + +### 9.1 Unit Tests + +**admin_keys.c**: +- Key generation produces valid keys +- Keys can be stored and retrieved +- Invalid keys are rejected + +**admin_auth.c**: +- Valid admin events pass authorization +- Invalid signatures are rejected +- Wrong pubkeys are rejected +- Expired events are rejected + +**admin_handlers.c**: +- Each command handler works correctly +- Invalid parameters are rejected +- Error responses are properly formatted + +### 9.2 Integration Tests + +**Full Flow**: +1. Generate admin keypair +2. Create Kind 23456 command +3. Send via WebSocket +4. Verify authorization +5. Execute command +6. Receive Kind 23457 response +7. Decrypt and verify response + +**Security Tests**: +- Unauthorized pubkey rejected +- Invalid signature rejected +- Replay attack prevented +- Command injection prevented + +### 9.3 Performance Tests + +- WebSocket connection handling +- Command processing latency +- Concurrent admin operations +- Large response handling + +## 10. Future Enhancements + +### 10.1 Short Term + +1. **Command History**: Track admin commands for audit +2. **Multi-Admin Support**: Multiple authorized admin pubkeys +3. **Role-Based Access**: Different permission levels +4. **Batch Operations**: Execute multiple commands in one request + +### 10.2 Long Term + +1. **Web Dashboard**: Full-featured web UI +2. **Monitoring Integration**: Prometheus/Grafana metrics +3. **Backup Automation**: Scheduled backups +4. **Replication**: Multi-server blob replication +5. **Advanced Analytics**: Usage patterns, trends, predictions + +## 11. References + +### 11.1 Nostr NIPs + +- **NIP-01**: Basic protocol flow +- **NIP-04**: Encrypted Direct Messages (deprecated, but reference) +- **NIP-19**: bech32-encoded entities (npub, nsec) +- **NIP-44**: Versioned Encryption (used for admin commands) + +### 11.2 Blossom Specifications + +- **BUD-01**: Blob Upload/Download +- **BUD-02**: Blob Descriptor +- **BUD-04**: Mirroring +- **BUD-06**: Upload Requirements +- **BUD-08**: NIP-94 Integration +- **BUD-09**: Blob Reporting + +### 11.3 c-relay Source Files + +- `c-relay/src/dm_admin.c` - Admin event processing +- `c-relay/src/api.c` - NIP-44 encryption +- `c-relay/src/config.c` - Key storage +- `c-relay/src/main.c` - CLI options +- `c-relay/src/sql_schema.h` - Database schema + +## 12. Appendix + +### 12.1 Example Admin CLI Tool (Python) + +```python +#!/usr/bin/env python3 +""" +Ginxsom Admin CLI Tool +Sends admin commands to ginxsom server via WebSocket +""" + +import asyncio +import websockets +import json +from nostr_sdk import Keys, Event, EventBuilder, Kind + +class GinxsomAdmin: + def __init__(self, server_url, admin_nsec, relay_npub): + self.server_url = server_url + self.admin_keys = Keys.parse(admin_nsec) + self.relay_pubkey = Keys.parse(relay_npub).public_key() + + async def send_command(self, command, params): + """Send admin command and wait for response""" + # Build command array + command_array = [command, params] + + # Encrypt with NIP-44 + encrypted = self.admin_keys.nip44_encrypt( + self.relay_pubkey, + json.dumps(command_array) + ) + + # Build Kind 23456 event + event = EventBuilder( + Kind(23456), + encrypted, + [["p", str(self.relay_pubkey)]] + ).to_event(self.admin_keys) + + # Send via WebSocket + async with websockets.connect(self.server_url) as ws: + await ws.send(json.dumps(event.as_json())) + + # Wait for Kind 23457 response + response = await ws.recv() + response_event = Event.from_json(response) + + # Decrypt response + decrypted = self.admin_keys.nip44_decrypt( + self.relay_pubkey, + response_event.content() + ) + + return json.loads(decrypted) + +# Usage +async def main(): + admin = GinxsomAdmin( + "ws://localhost:8080/admin", + "nsec1...", # Admin private key + "npub1..." # Relay public key + ) + + # List blobs + result = await admin.send_command("blob_list", { + "limit": 10, + "offset": 0 + }) + + print(json.dumps(result, indent=2)) + +if __name__ == "__main__": + asyncio.run(main()) +``` + +### 12.2 Database Schema SQL + +```sql +-- Add to db/schema.sql + +-- Relay Private Key Storage +CREATE TABLE relay_seckey ( + private_key_hex TEXT NOT NULL CHECK (length(private_key_hex) = 64), + created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) +); + +-- Admin Public Key (add to config table) +INSERT INTO config (key, value, data_type, description, category, requires_restart) +VALUES ( + 'admin_pubkey', + '', -- Set during first startup + 'string', + 'Public key of authorized admin (64-char hex)', + 'security', + 0 +); + +-- Relay Public Key (add to config table) +INSERT INTO config (key, value, data_type, description, category, requires_restart) +VALUES ( + 'relay_pubkey', + '', -- Set during first startup + 'string', + 'Public key of this relay (64-char hex)', + 'server', + 0 +); +``` + +### 12.3 Makefile Updates + +```makefile +# Add to Makefile + +# Admin system objects +ADMIN_OBJS = build/admin_websocket.o \ + build/admin_auth.o \ + build/admin_handlers.o \ + build/admin_config.o \ + build/admin_keys.o + +# Update main target +build/ginxsom-fcgi: $(OBJS) $(ADMIN_OBJS) + $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) + +# Admin system rules +build/admin_websocket.o: src/admin_websocket.c + $(CC) $(CFLAGS) -c $< -o $@ + +build/admin_auth.o: src/admin_auth.c + $(CC) $(CFLAGS) -c $< -o $@ + +build/admin_handlers.o: src/admin_handlers.c + $(CC) $(CFLAGS) -c $< -o $@ + +build/admin_config.o: src/admin_config.c + $(CC) $(CFLAGS) -c $< -o $@ + +build/admin_keys.o: src/admin_keys.c + $(CC) $(CFLAGS) -c $< -o $@ +``` + +--- + +**Document Version**: 2.0 +**Last Updated**: 2025-01-16 +**Status**: Ready for Implementation \ No newline at end of file diff --git a/remote.nginx.config b/remote.nginx.config index 6ef282d..eb86337 100644 --- a/remote.nginx.config +++ b/remote.nginx.config @@ -15,6 +15,10 @@ server { root /var/www/html; index index.html index.htm; + # CORS for Nostr NIP-05 verification + add_header Access-Control-Allow-Origin * always; + add_header Access-Control-Allow-Methods "GET, OPTIONS" always; + add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range" always; location / { try_files $uri $uri/ =404; @@ -42,6 +46,10 @@ server { root /var/www/html; index index.html index.htm; + # CORS for Nostr NIP-05 verification + add_header Access-Control-Allow-Origin * always; + add_header Access-Control-Allow-Methods "GET, OPTIONS" always; + add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range" always; location / { try_files $uri $uri/ =404; @@ -58,7 +66,7 @@ server { # Blossom subdomains HTTP - redirect to HTTPS (keep for ACME) server { listen 80; - server_name blossom.laantungir.com blossom.laantungir.net blossom.laantungir.org; + server_name blossom.laantungir.net; location /.well-known/acme-challenge/ { root /var/www/certbot; @@ -72,7 +80,7 @@ server { # Blossom subdomains HTTPS - ginxsom FastCGI server { listen 443 ssl; - server_name blossom.laantungir.com blossom.laantungir.net blossom.laantungir.org; + server_name blossom.laantungir.net; ssl_certificate /etc/letsencrypt/live/git.laantungir.net/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/git.laantungir.net/privkey.pem; @@ -241,7 +249,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_cache_bypass $http_upgrade; - proxy_read_timeout 86400s; + proxy_read_timeout 86400s; proxy_send_timeout 86400s; proxy_connect_timeout 60s; proxy_buffering off; @@ -256,8 +264,8 @@ server { listen 443 ssl; server_name relay.laantungir.com relay.laantungir.net relay.laantungir.org; - ssl_certificate /etc/letsencrypt/live/blossom.laantungir.net/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/blossom.laantungir.net/privkey.pem; + ssl_certificate /etc/letsencrypt/live/git.laantungir.net/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/git.laantungir.net/privkey.pem; location / { proxy_pass http://127.0.0.1:8888; @@ -373,4 +381,4 @@ server { proxy_set_header X-Forwarded-Proto $scheme; proxy_cache_bypass $http_upgrade; } - } + } \ No newline at end of file diff --git a/restart-all.sh b/restart-all.sh index 8f4fd3a..cbfe856 100755 --- a/restart-all.sh +++ b/restart-all.sh @@ -4,8 +4,32 @@ # Configuration +# Parse command line arguments +TEST_MODE=0 +FOLLOW_LOGS=0 + +while [[ $# -gt 0 ]]; do + case $1 in + -t|--test-keys) + TEST_MODE=1 + shift + ;; + --follow) + FOLLOW_LOGS=1 + shift + ;; + *) + echo "Unknown option: $1" + echo "Usage: $0 [-t|--test-keys] [--follow]" + echo " -t, --test-keys Use test mode with keys from .test_keys" + echo " --follow Follow logs in real-time" + exit 1 + ;; + esac +done + # Check for --follow flag -if [[ "$1" == "--follow" ]]; then +if [[ $FOLLOW_LOGS -eq 1 ]]; then echo "=== Following logs in real-time ===" echo "Monitoring: nginx error, nginx access, app stderr, app stdout" echo "Press Ctrl+C to stop following logs" @@ -37,7 +61,12 @@ touch logs/app/stderr.log logs/app/stdout.log logs/nginx/error.log logs/nginx/ac chmod 644 logs/app/stderr.log logs/app/stdout.log logs/nginx/error.log logs/nginx/access.log chmod 755 logs/nginx logs/app -echo -e "${YELLOW}=== Ginxsom Development Environment Restart ===${NC}" +if [ $TEST_MODE -eq 1 ]; then + echo -e "${YELLOW}=== Ginxsom Development Environment Restart (TEST MODE) ===${NC}" + echo "Using test keys from .test_keys file" +else + echo -e "${YELLOW}=== Ginxsom Development Environment Restart ===${NC}" +fi echo "Starting full restart sequence..." # Function to check if a process is running @@ -148,6 +177,42 @@ if [ $? -ne 0 ]; then fi echo -e "${GREEN}Clean rebuild complete${NC}" +# Step 3.5: Handle keys based on mode +echo -e "\n${YELLOW}3.5. Configuring server keys...${NC}" +DB_PATH="$PWD/db/ginxsom.db" + +if [ $TEST_MODE -eq 1 ]; then + # Test mode: verify .test_keys file exists + if [ ! -f ".test_keys" ]; then + echo -e "${RED}ERROR: .test_keys file not found${NC}" + echo -e "${RED}Test mode requires .test_keys file in project root${NC}" + exit 1 + fi + echo -e "${GREEN}Test mode: Will use keys from .test_keys${NC}" +else + # Production mode: check if keys exist, generate if needed + NEED_KEYS=1 + if command -v sqlite3 >/dev/null 2>&1; then + if sqlite3 "$DB_PATH" "SELECT seckey FROM blossom_seckey WHERE id=1" 2>/dev/null | grep -Eq '^[0-9a-f]{64}$'; then + NEED_KEYS=0 + echo -e "${GREEN}Blossom private key found in database${NC}" + fi + else + echo -e "${YELLOW}sqlite3 not found; assuming keys may be missing${NC}" + fi + + if [ $NEED_KEYS -eq 1 ]; then + echo -e "${YELLOW}No blossom key found; generating server keypair...${NC}" + ./build/ginxsom-fcgi --db-path "$DB_PATH" --storage-dir blobs --generate-keys 1>>logs/app/stdout.log 2>>logs/app/stderr.log + if [ $? -ne 0 ]; then + echo -e "${RED}Key generation failed. Check logs/app/stderr.log${NC}" + exit 1 + fi + echo -e "${GREEN}Key generation completed${NC}" + echo -e "${YELLOW}IMPORTANT: Check logs/app/stderr.log for your generated keys!${NC}" + fi +fi + # Step 4: Start FastCGI echo -e "\n${YELLOW}4. Starting FastCGI application...${NC}" echo "Socket: $SOCKET_PATH" @@ -166,9 +231,16 @@ fi echo "Setting GINX_DEBUG environment for pubkey extraction diagnostics" export GINX_DEBUG=1 +# Build command line arguments based on mode +FCGI_ARGS="--db-path $PWD/db/ginxsom.db --storage-dir blobs" +if [ $TEST_MODE -eq 1 ]; then + FCGI_ARGS="$FCGI_ARGS --test-keys" + echo -e "${YELLOW}Starting FastCGI in TEST MODE with test keys${NC}" +fi + # Start FastCGI application with proper logging (daemonized but with redirected streams) echo "FastCGI starting at $(date)" >> logs/app/stderr.log -spawn-fcgi -s "$SOCKET_PATH" -M 666 -u "$USER" -g "$USER" -P "$PID_FILE" -- "$FCGI_BINARY" --storage-dir blobs 1>>logs/app/stdout.log 2>>logs/app/stderr.log +spawn-fcgi -s "$SOCKET_PATH" -M 666 -u "$USER" -g "$USER" -P "$PID_FILE" -- "$FCGI_BINARY" $FCGI_ARGS 1>>logs/app/stdout.log 2>>logs/app/stderr.log if [ $? -eq 0 ] && [ -f "$PID_FILE" ]; then PID=$(cat "$PID_FILE") diff --git a/src/admin_auth.c b/src/admin_auth.c new file mode 100644 index 0000000..4abb05e --- /dev/null +++ b/src/admin_auth.c @@ -0,0 +1,509 @@ +/* + * Ginxsom Admin Authentication Module + * Handles Kind 23456/23457 admin events with NIP-44 encryption + * Based on c-relay's dm_admin.c implementation + */ + +#include "ginxsom.h" +#include "../nostr_core_lib/nostr_core/nostr_common.h" +#include "../nostr_core_lib/nostr_core/nip001.h" +#include "../nostr_core_lib/nostr_core/nip044.h" +#include "../nostr_core_lib/nostr_core/utils.h" +#include +#include +#include +#include +#include +#include + +// Forward declarations +int get_blossom_private_key(char *seckey_out, size_t max_len); +int validate_admin_pubkey(const char *pubkey); + +// Global variables for admin auth +static char g_blossom_seckey[65] = ""; // Cached blossom server private key +static int g_keys_loaded = 0; // Whether keys have been loaded + +// Load blossom server keys if not already loaded +static int ensure_keys_loaded(void) { + if (!g_keys_loaded) { + if (get_blossom_private_key(g_blossom_seckey, sizeof(g_blossom_seckey)) != 0) { + fprintf(stderr, "ERROR: Cannot load blossom private key for admin auth\n"); + return -1; + } + g_keys_loaded = 1; + } + return 0; +} + +// Validate that an event is a Kind 23456 admin command event +int is_admin_command_event(cJSON *event, const char *relay_pubkey) { + if (!event || !relay_pubkey) { + return 0; + } + + // Check kind = 23456 (admin command) + cJSON *kind = cJSON_GetObjectItem(event, "kind"); + if (!cJSON_IsNumber(kind) || kind->valueint != 23456) { + return 0; + } + + // Check tags for 'p' tag with relay pubkey + cJSON *tags = cJSON_GetObjectItem(event, "tags"); + if (!cJSON_IsArray(tags)) { + return 0; + } + + int found_p_tag = 0; + cJSON *tag = NULL; + cJSON_ArrayForEach(tag, tags) { + if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) { + cJSON *tag_name = cJSON_GetArrayItem(tag, 0); + cJSON *tag_value = cJSON_GetArrayItem(tag, 1); + + if (cJSON_IsString(tag_name) && strcmp(tag_name->valuestring, "p") == 0 && + cJSON_IsString(tag_value) && strcmp(tag_value->valuestring, relay_pubkey) == 0) { + found_p_tag = 1; + break; + } + } + } + + return found_p_tag; +} + +// Validate admin event signature and pubkey +int validate_admin_event(cJSON *event) { + if (!event) { + return 0; + } + + // Get event fields + cJSON *pubkey = cJSON_GetObjectItem(event, "pubkey"); + cJSON *sig = cJSON_GetObjectItem(event, "sig"); + + if (!cJSON_IsString(pubkey) || !cJSON_IsString(sig)) { + fprintf(stderr, "AUTH: Invalid event format - missing pubkey or sig\n"); + return 0; + } + + // Check if pubkey matches configured admin pubkey + if (!validate_admin_pubkey(pubkey->valuestring)) { + fprintf(stderr, "AUTH: Pubkey %s is not authorized admin\n", pubkey->valuestring); + return 0; + } + + // TODO: Validate event signature using nostr_core_lib + // For now, assume signature is valid if pubkey matches + // In production, this should verify the signature cryptographically + + return 1; +} + +// Decrypt NIP-44 encrypted admin command +int decrypt_admin_command(cJSON *event, char **decrypted_command_out) { + if (!event || !decrypted_command_out) { + return -1; + } + + // Ensure we have the relay private key + if (ensure_keys_loaded() != 0) { + return -1; + } + + // Get admin pubkey from event + cJSON *admin_pubkey_json = cJSON_GetObjectItem(event, "pubkey"); + if (!cJSON_IsString(admin_pubkey_json)) { + fprintf(stderr, "AUTH: Missing or invalid pubkey in event\n"); + return -1; + } + + // Get encrypted content + cJSON *content = cJSON_GetObjectItem(event, "content"); + if (!cJSON_IsString(content)) { + fprintf(stderr, "AUTH: Missing or invalid content in event\n"); + return -1; + } + + // Convert hex keys to bytes + unsigned char blossom_private_key[32]; + unsigned char admin_public_key[32]; + + if (nostr_hex_to_bytes(g_blossom_seckey, blossom_private_key, 32) != 0) { + fprintf(stderr, "AUTH: Failed to parse blossom private key\n"); + return -1; + } + + if (nostr_hex_to_bytes(admin_pubkey_json->valuestring, admin_public_key, 32) != 0) { + fprintf(stderr, "AUTH: Failed to parse admin public key\n"); + return -1; + } + + // Allocate buffer for decrypted content + char decrypted_buffer[8192]; + + // Decrypt using NIP-44 + int result = nostr_nip44_decrypt( + blossom_private_key, + admin_public_key, + content->valuestring, + decrypted_buffer, + sizeof(decrypted_buffer) + ); + + if (result != NOSTR_SUCCESS) { + fprintf(stderr, "AUTH: NIP-44 decryption failed with error code %d\n", result); + return -1; + } + + // Allocate and copy decrypted content + *decrypted_command_out = malloc(strlen(decrypted_buffer) + 1); + if (!*decrypted_command_out) { + fprintf(stderr, "AUTH: Failed to allocate memory for decrypted content\n"); + return -1; + } + strcpy(*decrypted_command_out, decrypted_buffer); + + return 0; +} + +// Parse decrypted command array +int parse_admin_command(const char *decrypted_content, char ***command_array_out, int *command_count_out) { + if (!decrypted_content || !command_array_out || !command_count_out) { + return -1; + } + + // Parse the decrypted content as JSON array + cJSON *content_json = cJSON_Parse(decrypted_content); + if (!content_json) { + fprintf(stderr, "AUTH: Failed to parse decrypted content as JSON\n"); + return -1; + } + + if (!cJSON_IsArray(content_json)) { + fprintf(stderr, "AUTH: Decrypted content is not a JSON array\n"); + cJSON_Delete(content_json); + return -1; + } + + int array_size = cJSON_GetArraySize(content_json); + if (array_size < 1) { + fprintf(stderr, "AUTH: Command array is empty\n"); + cJSON_Delete(content_json); + return -1; + } + + // Allocate command array + char **command_array = malloc(array_size * sizeof(char *)); + if (!command_array) { + fprintf(stderr, "AUTH: Failed to allocate command array\n"); + cJSON_Delete(content_json); + return -1; + } + + // Parse each array element as string + for (int i = 0; i < array_size; i++) { + cJSON *item = cJSON_GetArrayItem(content_json, i); + if (!cJSON_IsString(item)) { + fprintf(stderr, "AUTH: Command array element %d is not a string\n", i); + // Clean up allocated strings + for (int j = 0; j < i; j++) { + free(command_array[j]); + } + free(command_array); + cJSON_Delete(content_json); + return -1; + } + + command_array[i] = malloc(strlen(item->valuestring) + 1); + if (!command_array[i]) { + fprintf(stderr, "AUTH: Failed to allocate command string\n"); + // Clean up allocated strings + for (int j = 0; j < i; j++) { + free(command_array[j]); + } + free(command_array); + cJSON_Delete(content_json); + return -1; + } + strcpy(command_array[i], item->valuestring); + if (!command_array[i]) { + fprintf(stderr, "AUTH: Failed to duplicate command string\n"); + // Clean up allocated strings + for (int j = 0; j < i; j++) { + free(command_array[j]); + } + free(command_array); + cJSON_Delete(content_json); + return -1; + } + } + + cJSON_Delete(content_json); + *command_array_out = command_array; + *command_count_out = array_size; + + return 0; +} + +// Process incoming admin command event (Kind 23456) +int process_admin_command(cJSON *event, char ***command_array_out, int *command_count_out, char **admin_pubkey_out) { + if (!event || !command_array_out || !command_count_out || !admin_pubkey_out) { + return -1; + } + + // Get blossom server pubkey from config + sqlite3 *db; + sqlite3_stmt *stmt; + char blossom_pubkey[65] = ""; + + if (sqlite3_open_v2("db/ginxsom.db", &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) { + return -1; + } + + const char *sql = "SELECT value FROM config WHERE key = 'blossom_pubkey'"; + if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) { + if (sqlite3_step(stmt) == SQLITE_ROW) { + const char *pubkey = (const char *)sqlite3_column_text(stmt, 0); + if (pubkey) { + strncpy(blossom_pubkey, pubkey, sizeof(blossom_pubkey) - 1); + } + } + sqlite3_finalize(stmt); + } + sqlite3_close(db); + + if (strlen(blossom_pubkey) != 64) { + fprintf(stderr, "ERROR: Cannot determine blossom pubkey for admin auth\n"); + return -1; + } + + // Check if it's a valid admin command event for us + if (!is_admin_command_event(event, blossom_pubkey)) { + return -1; + } + + // Validate admin authentication (signature and pubkey) + if (!validate_admin_event(event)) { + return -1; + } + + // Get admin pubkey from event + cJSON *admin_pubkey_json = cJSON_GetObjectItem(event, "pubkey"); + if (!cJSON_IsString(admin_pubkey_json)) { + return -1; + } + + *admin_pubkey_out = malloc(strlen(admin_pubkey_json->valuestring) + 1); + if (!*admin_pubkey_out) { + fprintf(stderr, "AUTH: Failed to allocate admin pubkey string\n"); + return -1; + } + strcpy(*admin_pubkey_out, admin_pubkey_json->valuestring); + if (!*admin_pubkey_out) { + return -1; + } + + // Decrypt the command + char *decrypted_content = NULL; + if (decrypt_admin_command(event, &decrypted_content) != 0) { + free(*admin_pubkey_out); + *admin_pubkey_out = NULL; + return -1; + } + + // Parse the command array + if (parse_admin_command(decrypted_content, command_array_out, command_count_out) != 0) { + free(decrypted_content); + free(*admin_pubkey_out); + *admin_pubkey_out = NULL; + return -1; + } + + free(decrypted_content); + return 0; +} + +// Validate admin pubkey against configured admin +int validate_admin_pubkey(const char *pubkey) { + if (!pubkey || strlen(pubkey) != 64) { + return 0; + } + + sqlite3 *db; + sqlite3_stmt *stmt; + int result = 0; + + if (sqlite3_open_v2("db/ginxsom.db", &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) { + return 0; + } + + const char *sql = "SELECT value FROM config WHERE key = 'admin_pubkey'"; + if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) { + if (sqlite3_step(stmt) == SQLITE_ROW) { + const char *admin_pubkey = (const char *)sqlite3_column_text(stmt, 0); + if (admin_pubkey && strcmp(admin_pubkey, pubkey) == 0) { + result = 1; + } + } + sqlite3_finalize(stmt); + } + sqlite3_close(db); + + return result; +} + +// Create encrypted response for admin (Kind 23457) +int create_admin_response(const char *response_json, const char *admin_pubkey, const char *original_event_id __attribute__((unused)), cJSON **response_event_out) { + if (!response_json || !admin_pubkey || !response_event_out) { + return -1; + } + + // Ensure we have the relay private key + if (ensure_keys_loaded() != 0) { + return -1; + } + + // Get blossom server pubkey from config + sqlite3 *db; + sqlite3_stmt *stmt; + char blossom_pubkey[65] = ""; + + if (sqlite3_open_v2("db/ginxsom.db", &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) { + return -1; + } + + const char *sql = "SELECT value FROM config WHERE key = 'blossom_pubkey'"; + if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) { + if (sqlite3_step(stmt) == SQLITE_ROW) { + const char *pubkey = (const char *)sqlite3_column_text(stmt, 0); + if (pubkey) { + strncpy(blossom_pubkey, pubkey, sizeof(blossom_pubkey) - 1); + } + } + sqlite3_finalize(stmt); + } + sqlite3_close(db); + + if (strlen(blossom_pubkey) != 64) { + fprintf(stderr, "ERROR: Cannot determine blossom pubkey for response\n"); + return -1; + } + + // Convert hex keys to bytes + unsigned char blossom_private_key[32]; + unsigned char admin_public_key[32]; + + if (nostr_hex_to_bytes(g_blossom_seckey, blossom_private_key, 32) != 0) { + fprintf(stderr, "AUTH: Failed to parse blossom private key\n"); + return -1; + } + + if (nostr_hex_to_bytes(admin_pubkey, admin_public_key, 32) != 0) { + fprintf(stderr, "AUTH: Failed to parse admin public key\n"); + return -1; + } + + // Encrypt response using NIP-44 + char encrypted_content[8192]; + int result = nostr_nip44_encrypt( + blossom_private_key, + admin_public_key, + response_json, + encrypted_content, + sizeof(encrypted_content) + ); + + if (result != NOSTR_SUCCESS) { + fprintf(stderr, "AUTH: NIP-44 encryption failed with error code %d\n", result); + return -1; + } + + // Create Kind 23457 response event + cJSON *response_event = cJSON_CreateObject(); + if (!response_event) { + fprintf(stderr, "AUTH: Failed to create response event JSON\n"); + return -1; + } + + // Set event fields + cJSON_AddNumberToObject(response_event, "kind", 23457); + cJSON_AddStringToObject(response_event, "pubkey", blossom_pubkey); + cJSON_AddNumberToObject(response_event, "created_at", (double)time(NULL)); + cJSON_AddStringToObject(response_event, "content", encrypted_content); + + // Add tags array with 'p' tag for admin + cJSON *tags = cJSON_CreateArray(); + cJSON *p_tag = cJSON_CreateArray(); + cJSON_AddItemToArray(p_tag, cJSON_CreateString("p")); + cJSON_AddItemToArray(p_tag, cJSON_CreateString(admin_pubkey)); + cJSON_AddItemToArray(tags, p_tag); + cJSON_AddItemToObject(response_event, "tags", tags); + + // Sign the event with blossom private key + // Convert private key hex to bytes + unsigned char blossom_private_key_bytes[32]; + if (nostr_hex_to_bytes(g_blossom_seckey, blossom_private_key_bytes, 32) != 0) { + fprintf(stderr, "AUTH: Failed to parse blossom private key for signing\n"); + cJSON_Delete(response_event); + return -1; + } + + // Create a temporary event structure for signing + cJSON* temp_event = cJSON_Duplicate(response_event, 1); + if (!temp_event) { + fprintf(stderr, "AUTH: Failed to create temp event for signing\n"); + cJSON_Delete(response_event); + return -1; + } + + // Sign the event using nostr_core_lib + cJSON* signed_event = nostr_create_and_sign_event( + 23457, // Kind 23457 (admin response) + encrypted_content, // content + cJSON_GetObjectItem(response_event, "tags"), // tags + blossom_private_key_bytes, // private key + (time_t)cJSON_GetNumberValue(cJSON_GetObjectItem(response_event, "created_at")) // timestamp + ); + + if (!signed_event) { + fprintf(stderr, "AUTH: Failed to sign admin response event\n"); + cJSON_Delete(response_event); + cJSON_Delete(temp_event); + return -1; + } + + // Extract id and signature from signed event + cJSON* signed_id = cJSON_GetObjectItem(signed_event, "id"); + cJSON* signed_sig = cJSON_GetObjectItem(signed_event, "sig"); + + if (signed_id && signed_sig) { + cJSON_AddStringToObject(response_event, "id", cJSON_GetStringValue(signed_id)); + cJSON_AddStringToObject(response_event, "sig", cJSON_GetStringValue(signed_sig)); + } else { + fprintf(stderr, "AUTH: Signed event missing id or sig\n"); + cJSON_Delete(response_event); + cJSON_Delete(signed_event); + cJSON_Delete(temp_event); + return -1; + } + + // Clean up temporary structures + cJSON_Delete(signed_event); + cJSON_Delete(temp_event); + + *response_event_out = response_event; + return 0; +} + +// Free command array allocated by parse_admin_command +void free_command_array(char **command_array, int command_count) { + if (command_array) { + for (int i = 0; i < command_count; i++) { + if (command_array[i]) { + free(command_array[i]); + } + } + free(command_array); + } +} \ No newline at end of file diff --git a/src/admin_handlers.c b/src/admin_handlers.c new file mode 100644 index 0000000..a705657 --- /dev/null +++ b/src/admin_handlers.c @@ -0,0 +1,216 @@ +/* + * Ginxsom Admin Command Handlers + * Implements execution of admin commands received via Kind 23456 events + */ + +#include "ginxsom.h" +#include +#include +#include +#include +#include +#include +#include + +// Forward declarations +static cJSON* handle_blob_list(char **args, int arg_count); +static cJSON* handle_blob_info(char **args, int arg_count); +static cJSON* handle_blob_delete(char **args, int arg_count); +static cJSON* handle_storage_stats(char **args, int arg_count); +static cJSON* handle_config_get(char **args, int arg_count); +static cJSON* handle_config_set(char **args, int arg_count); +static cJSON* handle_help(char **args, int arg_count); + +// Command dispatch table +typedef struct { + const char *command; + cJSON* (*handler)(char **args, int arg_count); + const char *description; +} admin_command_t; + +static admin_command_t command_table[] = { + {"blob_list", handle_blob_list, "List all blobs"}, + {"blob_info", handle_blob_info, "Get blob information"}, + {"blob_delete", handle_blob_delete, "Delete a blob"}, + {"storage_stats", handle_storage_stats, "Get storage statistics"}, + {"config_get", handle_config_get, "Get configuration value"}, + {"config_set", handle_config_set, "Set configuration value"}, + {"help", handle_help, "Show available commands"}, + {NULL, NULL, NULL} +}; + +// Execute admin command and return JSON response +int execute_admin_command(char **command_array, int command_count, char **response_json_out) { + if (!command_array || command_count < 1 || !response_json_out) { + return -1; + } + + const char *command = command_array[0]; + + // Find command handler + admin_command_t *cmd = NULL; + for (int i = 0; command_table[i].command != NULL; i++) { + if (strcmp(command_table[i].command, command) == 0) { + cmd = &command_table[i]; + break; + } + } + + cJSON *response; + if (cmd) { + // Execute command handler + response = cmd->handler(command_array + 1, command_count - 1); + } else { + // Unknown command + response = cJSON_CreateObject(); + cJSON_AddStringToObject(response, "status", "error"); + cJSON_AddStringToObject(response, "message", "Unknown command"); + cJSON_AddStringToObject(response, "command", command); + } + + // Convert response to JSON string + char *json_str = cJSON_PrintUnformatted(response); + cJSON_Delete(response); + + if (!json_str) { + return -1; + } + + *response_json_out = json_str; + return 0; +} + +// Command handlers + +static cJSON* handle_blob_list(char **args __attribute__((unused)), int arg_count __attribute__((unused))) { + cJSON *response = cJSON_CreateObject(); + cJSON_AddStringToObject(response, "status", "success"); + cJSON_AddStringToObject(response, "command", "blob_list"); + + // TODO: Implement actual blob listing from database + cJSON *blobs = cJSON_CreateArray(); + cJSON_AddItemToObject(response, "blobs", blobs); + cJSON_AddNumberToObject(response, "count", 0); + + return response; +} + +static cJSON* handle_blob_info(char **args, int arg_count) { + cJSON *response = cJSON_CreateObject(); + + if (arg_count < 1) { + cJSON_AddStringToObject(response, "status", "error"); + cJSON_AddStringToObject(response, "message", "Missing blob hash argument"); + return response; + } + + cJSON_AddStringToObject(response, "status", "success"); + cJSON_AddStringToObject(response, "command", "blob_info"); + cJSON_AddStringToObject(response, "hash", args[0]); + + // TODO: Implement actual blob info retrieval from database + cJSON_AddStringToObject(response, "message", "Not yet implemented"); + + return response; +} + +static cJSON* handle_blob_delete(char **args, int arg_count) { + cJSON *response = cJSON_CreateObject(); + + if (arg_count < 1) { + cJSON_AddStringToObject(response, "status", "error"); + cJSON_AddStringToObject(response, "message", "Missing blob hash argument"); + return response; + } + + cJSON_AddStringToObject(response, "status", "success"); + cJSON_AddStringToObject(response, "command", "blob_delete"); + cJSON_AddStringToObject(response, "hash", args[0]); + + // TODO: Implement actual blob deletion + cJSON_AddStringToObject(response, "message", "Not yet implemented"); + + return response; +} + +static cJSON* handle_storage_stats(char **args __attribute__((unused)), int arg_count __attribute__((unused))) { + cJSON *response = cJSON_CreateObject(); + cJSON_AddStringToObject(response, "status", "success"); + cJSON_AddStringToObject(response, "command", "storage_stats"); + + // Get filesystem stats + struct statvfs stat; + if (statvfs(".", &stat) == 0) { + unsigned long long total = stat.f_blocks * stat.f_frsize; + unsigned long long available = stat.f_bavail * stat.f_frsize; + unsigned long long used = total - available; + + cJSON_AddNumberToObject(response, "total_bytes", (double)total); + cJSON_AddNumberToObject(response, "used_bytes", (double)used); + cJSON_AddNumberToObject(response, "available_bytes", (double)available); + } + + // TODO: Add blob count and total blob size from database + cJSON_AddNumberToObject(response, "blob_count", 0); + cJSON_AddNumberToObject(response, "blob_total_bytes", 0); + + return response; +} + +static cJSON* handle_config_get(char **args, int arg_count) { + cJSON *response = cJSON_CreateObject(); + + if (arg_count < 1) { + cJSON_AddStringToObject(response, "status", "error"); + cJSON_AddStringToObject(response, "message", "Missing config key argument"); + return response; + } + + cJSON_AddStringToObject(response, "status", "success"); + cJSON_AddStringToObject(response, "command", "config_get"); + cJSON_AddStringToObject(response, "key", args[0]); + + // TODO: Implement actual config retrieval from database + cJSON_AddStringToObject(response, "value", ""); + cJSON_AddStringToObject(response, "message", "Not yet implemented"); + + return response; +} + +static cJSON* handle_config_set(char **args, int arg_count) { + cJSON *response = cJSON_CreateObject(); + + if (arg_count < 2) { + cJSON_AddStringToObject(response, "status", "error"); + cJSON_AddStringToObject(response, "message", "Missing config key or value argument"); + return response; + } + + cJSON_AddStringToObject(response, "status", "success"); + cJSON_AddStringToObject(response, "command", "config_set"); + cJSON_AddStringToObject(response, "key", args[0]); + cJSON_AddStringToObject(response, "value", args[1]); + + // TODO: Implement actual config update in database + cJSON_AddStringToObject(response, "message", "Not yet implemented"); + + return response; +} + +static cJSON* handle_help(char **args __attribute__((unused)), int arg_count __attribute__((unused))) { + cJSON *response = cJSON_CreateObject(); + cJSON_AddStringToObject(response, "status", "success"); + cJSON_AddStringToObject(response, "command", "help"); + + cJSON *commands = cJSON_CreateArray(); + for (int i = 0; command_table[i].command != NULL; i++) { + cJSON *cmd = cJSON_CreateObject(); + cJSON_AddStringToObject(cmd, "command", command_table[i].command); + cJSON_AddStringToObject(cmd, "description", command_table[i].description); + cJSON_AddItemToArray(commands, cmd); + } + + cJSON_AddItemToObject(response, "commands", commands); + + return response; +} \ No newline at end of file diff --git a/src/admin_websocket.c b/src/admin_websocket.c new file mode 100644 index 0000000..1c37248 --- /dev/null +++ b/src/admin_websocket.c @@ -0,0 +1,163 @@ +/* + * Ginxsom Admin WebSocket Module + * Handles WebSocket connections for Kind 23456/23457 admin commands + * Based on c-relay's WebSocket implementation + */ + +#include "ginxsom.h" +#include +#include +#include +#include +#include + +// Forward declarations from admin_auth.c +int process_admin_command(cJSON *event, char ***command_array_out, int *command_count_out, char **admin_pubkey_out); +void free_command_array(char **command_array, int command_count); +int create_admin_response(const char *response_json, const char *admin_pubkey, const char *original_event_id, cJSON **response_event_out); + +// Forward declarations from admin_handlers.c (to be created) +int execute_admin_command(char **command_array, int command_count, const char *admin_pubkey, char **response_json_out); + +// Handle WebSocket admin command endpoint (/api/admin) +void handle_admin_websocket_request(void) { + // For now, this is a placeholder for WebSocket implementation + // In a full implementation, this would: + // 1. Upgrade HTTP connection to WebSocket + // 2. Handle WebSocket frames + // 3. Process Kind 23456 events + // 4. Send Kind 23457 responses + + printf("Status: 501 Not Implemented\r\n"); + printf("Content-Type: application/json\r\n\r\n"); + printf("{\n"); + printf(" \"error\": \"websocket_not_implemented\",\n"); + printf(" \"message\": \"WebSocket admin endpoint not yet implemented\",\n"); + printf(" \"note\": \"Use HTTP POST to /api/admin for now\"\n"); + printf("}\n"); +} + +// Handle HTTP POST admin command endpoint (/api/admin) +void handle_admin_command_post_request(void) { + // Read the request body (should contain Kind 23456 event JSON) + const char *content_length_str = getenv("CONTENT_LENGTH"); + if (!content_length_str) { + printf("Status: 400 Bad Request\r\n"); + printf("Content-Type: application/json\r\n\r\n"); + printf("{\n"); + printf(" \"error\": \"missing_content_length\",\n"); + printf(" \"message\": \"Content-Length header required\"\n"); + printf("}\n"); + return; + } + + long content_length = atol(content_length_str); + if (content_length <= 0 || content_length > 1024 * 1024) { // 1MB limit + printf("Status: 400 Bad Request\r\n"); + printf("Content-Type: application/json\r\n\r\n"); + printf("{\n"); + printf(" \"error\": \"invalid_content_length\",\n"); + printf(" \"message\": \"Content-Length must be between 1 and 1MB\"\n"); + printf("}\n"); + return; + } + + // Read the request body + char *request_body = malloc(content_length + 1); + if (!request_body) { + printf("Status: 500 Internal Server Error\r\n"); + printf("Content-Type: application/json\r\n\r\n"); + printf("{\n"); + printf(" \"error\": \"memory_allocation_failed\",\n"); + printf(" \"message\": \"Failed to allocate memory for request body\"\n"); + printf("}\n"); + return; + } + + size_t bytes_read = fread(request_body, 1, content_length, stdin); + if (bytes_read != (size_t)content_length) { + free(request_body); + printf("Status: 400 Bad Request\r\n"); + printf("Content-Type: application/json\r\n\r\n"); + printf("{\n"); + printf(" \"error\": \"incomplete_request_body\",\n"); + printf(" \"message\": \"Failed to read complete request body\"\n"); + printf("}\n"); + return; + } + + request_body[content_length] = '\0'; + + // Parse the JSON event + cJSON *event = cJSON_Parse(request_body); + free(request_body); + + if (!event) { + printf("Status: 400 Bad Request\r\n"); + printf("Content-Type: application/json\r\n\r\n"); + printf("{\n"); + printf(" \"error\": \"invalid_json\",\n"); + printf(" \"message\": \"Request body is not valid JSON\"\n"); + printf("}\n"); + return; + } + + // Process the admin command + char **command_array = NULL; + int command_count = 0; + char *admin_pubkey = NULL; + + int result = process_admin_command(event, &command_array, &command_count, &admin_pubkey); + cJSON_Delete(event); + + if (result != 0) { + printf("Status: 400 Bad Request\r\n"); + printf("Content-Type: application/json\r\n\r\n"); + printf("{\n"); + printf(" \"error\": \"invalid_admin_command\",\n"); + printf(" \"message\": \"Failed to process admin command\"\n"); + printf("}\n"); + return; + } + + // Execute the command + char *response_json = NULL; + int exec_result = execute_admin_command(command_array, command_count, admin_pubkey, &response_json); + free_command_array(command_array, command_count); + free(admin_pubkey); + + if (exec_result != 0) { + printf("Status: 500 Internal Server Error\r\n"); + printf("Content-Type: application/json\r\n\r\n"); + printf("{\n"); + printf(" \"error\": \"command_execution_failed\",\n"); + printf(" \"message\": \"Failed to execute admin command\"\n"); + printf("}\n"); + return; + } + + // Create the response event (Kind 23457) + cJSON *response_event = NULL; + int create_result = create_admin_response(response_json, admin_pubkey, NULL, &response_event); + free(response_json); + + if (create_result != 0) { + printf("Status: 500 Internal Server Error\r\n"); + printf("Content-Type: application/json\r\n\r\n"); + printf("{\n"); + printf(" \"error\": \"response_creation_failed\",\n"); + printf(" \"message\": \"Failed to create admin response\"\n"); + printf("}\n"); + return; + } + + // Return the response event as JSON + char *response_json_str = cJSON_Print(response_event); + cJSON_Delete(response_event); + + printf("Status: 200 OK\r\n"); + printf("Content-Type: application/json\r\n\r\n"); + printf("%s\n", response_json_str); + + free(response_json_str); +} \ No newline at end of file diff --git a/src/ginxsom.h b/src/ginxsom.h index d2884c3..89fb5df 100644 --- a/src/ginxsom.h +++ b/src/ginxsom.h @@ -10,8 +10,8 @@ // Version information (auto-updated by build system) #define VERSION_MAJOR 0 #define VERSION_MINOR 1 -#define VERSION_PATCH 8 -#define VERSION "v0.1.8" +#define VERSION_PATCH 9 +#define VERSION "v0.1.9" #include #include diff --git a/src/main.c b/src/main.c index b942aeb..434c969 100644 --- a/src/main.c +++ b/src/main.c @@ -9,7 +9,6 @@ #include "../nostr_core_lib/nostr_core/utils.h" #include #include -#include #include #include #include @@ -30,6 +29,11 @@ char g_db_path[MAX_PATH_LEN] = "db/ginxsom.db"; char g_storage_dir[MAX_PATH_LEN] = "."; +// Key management variables +char g_admin_pubkey[65] = ""; // Admin public key for authorization +char g_blossom_seckey[65] = ""; // Blossom server private key for decryption/signing +int g_generate_keys = 0; // Flag to generate keys on startup + // Use global configuration variables #define DB_PATH g_db_path @@ -60,6 +64,174 @@ const char *get_config_dir(char *buffer, size_t buffer_size) { } +// Database initialization function +int initialize_database(const char *db_path) { + sqlite3 *db; + char *err_msg = NULL; + int rc; + + // Check if database file exists + struct stat st; + int db_exists = (stat(db_path, &st) == 0); + + // Open database with CREATE flag + rc = sqlite3_open_v2(db_path, &db, + SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL); + if (rc != SQLITE_OK) { + fprintf(stderr, "Cannot open database: %s\n", sqlite3_errmsg(db)); + sqlite3_close(db); + return -1; + } + + // If database was just created, initialize schema + if (!db_exists) { + fprintf(stderr, "Database not found, initializing schema...\n"); + + // Enable foreign key constraints + rc = sqlite3_exec(db, "PRAGMA foreign_keys = ON;", NULL, NULL, &err_msg); + if (rc != SQLITE_OK) { + fprintf(stderr, "Failed to enable foreign keys: %s\n", err_msg); + sqlite3_free(err_msg); + sqlite3_close(db); + return -1; + } + + // Create blobs table + const char *create_blobs = + "CREATE TABLE IF NOT EXISTS blobs (" + " sha256 TEXT PRIMARY KEY NOT NULL," + " size INTEGER NOT NULL," + " type TEXT NOT NULL," + " uploaded_at INTEGER NOT NULL," + " uploader_pubkey TEXT," + " filename TEXT," + " CHECK (length(sha256) = 64)," + " CHECK (size >= 0)," + " CHECK (uploaded_at > 0)" + ");"; + + rc = sqlite3_exec(db, create_blobs, NULL, NULL, &err_msg); + if (rc != SQLITE_OK) { + fprintf(stderr, "Failed to create blobs table: %s\n", err_msg); + sqlite3_free(err_msg); + sqlite3_close(db); + return -1; + } + + // Create config table + const char *create_config = + "CREATE TABLE IF NOT EXISTS config (" + " key TEXT PRIMARY KEY NOT NULL," + " value TEXT NOT NULL," + " description TEXT," + " created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))," + " updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))" + ");"; + + rc = sqlite3_exec(db, create_config, NULL, NULL, &err_msg); + if (rc != SQLITE_OK) { + fprintf(stderr, "Failed to create config table: %s\n", err_msg); + sqlite3_free(err_msg); + sqlite3_close(db); + return -1; + } + + // Create auth_rules table + const char *create_auth_rules = + "CREATE TABLE IF NOT EXISTS auth_rules (" + " id INTEGER PRIMARY KEY AUTOINCREMENT," + " rule_type TEXT NOT NULL," + " rule_target TEXT NOT NULL," + " operation TEXT NOT NULL DEFAULT '*'," + " enabled INTEGER NOT NULL DEFAULT 1," + " priority INTEGER NOT NULL DEFAULT 100," + " description TEXT," + " created_by TEXT," + " created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))," + " updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))," + " CHECK (rule_type IN ('pubkey_blacklist', 'pubkey_whitelist'," + " 'hash_blacklist', 'mime_blacklist', 'mime_whitelist'))," + " CHECK (operation IN ('upload', 'delete', 'list', '*'))," + " CHECK (enabled IN (0, 1))," + " CHECK (priority >= 0)," + " UNIQUE(rule_type, rule_target, operation)" + ");"; + + rc = sqlite3_exec(db, create_auth_rules, NULL, NULL, &err_msg); + if (rc != SQLITE_OK) { + fprintf(stderr, "Failed to create auth_rules table: %s\n", err_msg); + sqlite3_free(err_msg); + sqlite3_close(db); + return -1; + } + + // Create indexes + const char *create_indexes = + "CREATE INDEX IF NOT EXISTS idx_blobs_uploaded_at ON blobs(uploaded_at);" + "CREATE INDEX IF NOT EXISTS idx_blobs_uploader_pubkey ON blobs(uploader_pubkey);" + "CREATE INDEX IF NOT EXISTS idx_blobs_type ON blobs(type);" + "CREATE INDEX IF NOT EXISTS idx_config_updated_at ON config(updated_at);" + "CREATE INDEX IF NOT EXISTS idx_auth_rules_type_target ON auth_rules(rule_type, rule_target);" + "CREATE INDEX IF NOT EXISTS idx_auth_rules_operation ON auth_rules(operation);" + "CREATE INDEX IF NOT EXISTS idx_auth_rules_enabled ON auth_rules(enabled);" + "CREATE INDEX IF NOT EXISTS idx_auth_rules_priority ON auth_rules(priority);" + "CREATE INDEX IF NOT EXISTS idx_auth_rules_type_operation ON auth_rules(rule_type, operation, enabled);"; + + rc = sqlite3_exec(db, create_indexes, NULL, NULL, &err_msg); + if (rc != SQLITE_OK) { + fprintf(stderr, "Failed to create indexes: %s\n", err_msg); + sqlite3_free(err_msg); + sqlite3_close(db); + return -1; + } + + // Insert default configuration + const char *insert_config = + "INSERT OR IGNORE INTO config (key, value, description) VALUES" + " ('max_file_size', '104857600', 'Maximum file size in bytes (100MB)')," + " ('auth_rules_enabled', 'true', 'Whether authentication rules are enabled for uploads')," + " ('server_name', 'ginxsom', 'Server name for responses')," + " ('admin_pubkey', '', 'Admin public key for API access')," + " ('admin_enabled', 'false', 'Whether admin API is enabled')," + " ('nip42_require_auth', 'false', 'Enable NIP-42 challenge/response authentication')," + " ('nip42_challenge_timeout', '600', 'NIP-42 challenge timeout in seconds')," + " ('nip42_time_tolerance', '300', 'NIP-42 timestamp tolerance in seconds');"; + + rc = sqlite3_exec(db, insert_config, NULL, NULL, &err_msg); + if (rc != SQLITE_OK) { + fprintf(stderr, "Failed to insert default config: %s\n", err_msg); + sqlite3_free(err_msg); + sqlite3_close(db); + return -1; + } + + // Create storage_stats view + const char *create_view = + "CREATE VIEW IF NOT EXISTS storage_stats AS " + "SELECT " + " COUNT(*) as total_blobs, " + " SUM(size) as total_bytes, " + " AVG(size) as avg_blob_size, " + " MIN(uploaded_at) as first_upload, " + " MAX(uploaded_at) as last_upload, " + " COUNT(DISTINCT uploader_pubkey) as unique_uploaders " + "FROM blobs;"; + + rc = sqlite3_exec(db, create_view, NULL, NULL, &err_msg); + if (rc != SQLITE_OK) { + fprintf(stderr, "Failed to create storage_stats view: %s\n", err_msg); + sqlite3_free(err_msg); + sqlite3_close(db); + return -1; + } + + fprintf(stderr, "Database schema initialized successfully\n"); + } + + sqlite3_close(db); + return 0; +} + // Function declarations void handle_options_request(void); void send_error_response(int status_code, const char *error_type, @@ -67,6 +239,13 @@ void send_error_response(int status_code, const char *error_type, void log_request(const char *method, const char *uri, const char *auth_status, int status_code); +// Key management functions +int generate_random_private_key_bytes(unsigned char *key_bytes, size_t len); +int generate_server_keypair(void); +int load_server_keys(void); +int store_blossom_private_key(const char *seckey); +int get_blossom_private_key(char *seckey_out, size_t max_len); + // External validator function declarations const char *nostr_request_validator_get_last_violation_type(void); int nostr_generate_nip42_challenge(char *challenge_out, size_t challenge_size, const char *client_ip); @@ -77,6 +256,262 @@ void handle_auth_challenge_request(void); // Handler function declarations with validation support void handle_delete_request_with_validation(const char *sha256, nostr_request_result_t *validation_result); +// Key management function implementations + +// Generate random private key bytes using /dev/urandom +int generate_random_private_key_bytes(unsigned char *key_bytes, size_t len) { + FILE *fp = fopen("/dev/urandom", "rb"); + if (!fp) { + fprintf(stderr, "ERROR: Cannot open /dev/urandom for key generation\n"); + return -1; + } + + size_t bytes_read = fread(key_bytes, 1, len, fp); + fclose(fp); + + if (bytes_read != len) { + fprintf(stderr, "ERROR: Failed to read %zu bytes from /dev/urandom\n", len); + return -1; + } + + return 0; +} + +// Generate server keypair and store in database +int generate_server_keypair(void) { + fprintf(stderr, "DEBUG: generate_server_keypair() called\n"); + unsigned char seckey_bytes[32]; + char seckey_hex[65]; + char pubkey_hex[65]; + + // Generate random private key + fprintf(stderr, "DEBUG: Generating random private key...\n"); + if (generate_random_private_key_bytes(seckey_bytes, 32) != 0) { + fprintf(stderr, "DEBUG: Failed to generate random bytes\n"); + return -1; + } + + // Validate the private key + if (nostr_ec_private_key_verify(seckey_bytes) != NOSTR_SUCCESS) { + fprintf(stderr, "ERROR: Generated invalid private key\n"); + return -1; + } + + // Convert to hex + nostr_bytes_to_hex(seckey_bytes, 32, seckey_hex); + + // Derive public key + unsigned char pubkey_bytes[32]; + if (nostr_ec_public_key_from_private_key(seckey_bytes, pubkey_bytes) != NOSTR_SUCCESS) { + fprintf(stderr, "ERROR: Failed to derive public key\n"); + return -1; + } + + // Convert public key to hex + nostr_bytes_to_hex(pubkey_bytes, 32, pubkey_hex); + + // Store private key securely + if (store_blossom_private_key(seckey_hex) != 0) { + fprintf(stderr, "ERROR: Failed to store blossom private key\n"); + return -1; + } + + // Store public key in config + sqlite3 *db; + sqlite3_stmt *stmt; + int rc; + + rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL); + if (rc) { + fprintf(stderr, "ERROR: Can't open database for config: %s\n", sqlite3_errmsg(db)); + return -1; + } + + const char *sql = "INSERT OR REPLACE INTO config (key, value, description) VALUES (?, ?, ?)"; + rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); + if (rc != SQLITE_OK) { + fprintf(stderr, "ERROR: SQL prepare failed: %s\n", sqlite3_errmsg(db)); + sqlite3_close(db); + return -1; + } + + sqlite3_bind_text(stmt, 1, "blossom_pubkey", -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 2, pubkey_hex, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 3, "Blossom server's public key for Nostr communication", -1, SQLITE_STATIC); + + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + sqlite3_close(db); + + if (rc != SQLITE_DONE) { + fprintf(stderr, "ERROR: Failed to store blossom public key in config\n"); + return -1; + } + + // Display keys for admin setup + fprintf(stderr, "========================================\n"); + fprintf(stderr, "SERVER KEYPAIR GENERATED SUCCESSFULLY\n"); + fprintf(stderr, "========================================\n"); + fprintf(stderr, "Blossom Public Key: %s\n", pubkey_hex); + fprintf(stderr, "Blossom Private Key: %s\n", seckey_hex); + fprintf(stderr, "========================================\n"); + fprintf(stderr, "IMPORTANT: Save the private key securely!\n"); + fprintf(stderr, "This key is used for decrypting admin messages.\n"); + fprintf(stderr, "========================================\n"); + + return 0; +} + +// Load server keys from database +int load_server_keys(void) { + fprintf(stderr, "DEBUG: load_server_keys() called\n"); + sqlite3 *db; + sqlite3_stmt *stmt; + int rc; + + // Try to load blossom private key + fprintf(stderr, "DEBUG: Trying to load blossom private key...\n"); + if (get_blossom_private_key(g_blossom_seckey, sizeof(g_blossom_seckey)) != 0) { + fprintf(stderr, "DEBUG: No blossom private key found\n"); + // No private key found - check if we should generate one + if (g_generate_keys) { + fprintf(stderr, "STARTUP: No blossom private key found, generating new keypair...\n"); + if (generate_server_keypair() != 0) { + fprintf(stderr, "ERROR: Failed to generate server keypair\n"); + return -1; + } + } else { + fprintf(stderr, "WARNING: No blossom private key found. Use --generate-keys to create one.\n"); + // This is not fatal - server can still operate without admin features + } + } else { + fprintf(stderr, "STARTUP: Blossom private key loaded successfully\n"); + } + + // Load admin pubkey from command line or config + if (strlen(g_admin_pubkey) == 0) { + // Try to load from database config + rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); + if (rc == SQLITE_OK) { + const char *sql = "SELECT value FROM config WHERE key = 'admin_pubkey'"; + rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); + if (rc == SQLITE_OK) { + rc = sqlite3_step(stmt); + if (rc == SQLITE_ROW) { + const char *pubkey = (const char *)sqlite3_column_text(stmt, 0); + if (pubkey && strlen(pubkey) == 64) { + strncpy(g_admin_pubkey, pubkey, sizeof(g_admin_pubkey) - 1); + fprintf(stderr, "STARTUP: Admin pubkey loaded from config: %s\n", g_admin_pubkey); + } + } + sqlite3_finalize(stmt); + } + sqlite3_close(db); + } + } else { + // Store admin pubkey in config if provided via command line + rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL); + if (rc == SQLITE_OK) { + const char *sql = "INSERT OR REPLACE INTO config (key, value, description) VALUES (?, ?, ?)"; + rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); + if (rc == SQLITE_OK) { + sqlite3_bind_text(stmt, 1, "admin_pubkey", -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 2, g_admin_pubkey, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 3, "Admin public key for management authentication", -1, SQLITE_STATIC); + sqlite3_step(stmt); + sqlite3_finalize(stmt); + } + sqlite3_close(db); + } + } + + return 0; +} + +// Store blossom private key in dedicated table +int store_blossom_private_key(const char *seckey) { + sqlite3 *db; + sqlite3_stmt *stmt; + int rc; + + // Validate key format + if (!seckey || strlen(seckey) != 64) { + fprintf(stderr, "ERROR: Invalid blossom private key format\n"); + return -1; + } + + // Create blossom_seckey table if it doesn't exist + rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL); + if (rc) { + fprintf(stderr, "ERROR: Can't open database: %s\n", sqlite3_errmsg(db)); + return -1; + } + + // Create table + const char *create_sql = "CREATE TABLE IF NOT EXISTS blossom_seckey (id INTEGER PRIMARY KEY CHECK (id = 1), seckey TEXT NOT NULL, created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), CHECK (length(seckey) = 64))"; + rc = sqlite3_exec(db, create_sql, NULL, NULL, NULL); + if (rc != SQLITE_OK) { + fprintf(stderr, "ERROR: Failed to create blossom_seckey table: %s\n", sqlite3_errmsg(db)); + sqlite3_close(db); + return -1; + } + + // Store key + const char *sql = "INSERT OR REPLACE INTO blossom_seckey (id, seckey) VALUES (1, ?)"; + rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); + if (rc != SQLITE_OK) { + fprintf(stderr, "ERROR: SQL prepare failed: %s\n", sqlite3_errmsg(db)); + sqlite3_close(db); + return -1; + } + + sqlite3_bind_text(stmt, 1, seckey, -1, SQLITE_STATIC); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + sqlite3_close(db); + + if (rc != SQLITE_DONE) { + fprintf(stderr, "ERROR: Failed to store blossom private key\n"); + return -1; + } + + return 0; +} + +// Get blossom private key from database +int get_blossom_private_key(char *seckey_out, size_t max_len) { + sqlite3 *db; + sqlite3_stmt *stmt; + int rc; + + rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); + if (rc) { + return -1; + } + + const char *sql = "SELECT seckey FROM blossom_seckey WHERE id = 1 LIMIT 1"; + rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); + if (rc != SQLITE_OK) { + sqlite3_close(db); + return -1; + } + + rc = sqlite3_step(stmt); + if (rc == SQLITE_ROW) { + const char *seckey = (const char *)sqlite3_column_text(stmt, 0); + if (seckey && strlen(seckey) == 64 && strlen(seckey) < max_len) { + strcpy(seckey_out, seckey); + sqlite3_finalize(stmt); + sqlite3_close(db); + return 0; + } + } + + sqlite3_finalize(stmt); + sqlite3_close(db); + return -1; +} + // Insert blob metadata into database int insert_blob_metadata(const char *sha256, long size, const char *type, long uploaded_at, const char *uploader_pubkey, @@ -85,7 +520,7 @@ int insert_blob_metadata(const char *sha256, long size, const char *type, sqlite3_stmt *stmt; int rc; - rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL); + rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL); if (rc) { fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db)); return 0; @@ -140,7 +575,7 @@ int get_blob_metadata(const char *sha256, blob_metadata_t *metadata) { sqlite3_stmt *stmt; int rc; - rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); + rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY | SQLITE_OPEN_CREATE, NULL); if (rc) { fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db)); return 0; @@ -557,7 +992,7 @@ void handle_delete_request_with_validation(const char *sha256, nostr_request_res sqlite3_stmt *stmt; int rc; - rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL); + rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL); if (rc) { send_error_response(500, "database_error", "Failed to access database", @@ -1296,7 +1731,13 @@ void handle_auth_challenge_request(void) { ///////////////////////////////////////////////////////////////////////////////////////// int main(int argc, char *argv[]) { + fprintf(stderr, "DEBUG: main() started\n"); + fflush(stderr); + // Parse command line arguments + int use_test_keys = 0; + char test_server_privkey[65] = ""; + for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "--db-path") == 0 && i + 1 < argc) { strncpy(g_db_path, argv[i + 1], sizeof(g_db_path) - 1); @@ -1305,21 +1746,180 @@ int main(int argc, char *argv[]) { strncpy(g_storage_dir, argv[i + 1], sizeof(g_storage_dir) - 1); i++; // Skip next argument } else if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) { - printf("Usage: %s [options]\n", argv[0]); - printf("Options:\n"); - printf(" --db-path PATH Database file path (default: db/ginxsom.db)\n"); - printf(" --storage-dir DIR Storage directory for files (default: blobs)\n"); - printf(" --help, -h Show this help message\n"); + // Use write() directly to avoid FCGI printf redefinition + const char *help_text = + "Usage: ginxsom-fcgi [options]\n" + "Options:\n" + " --db-path PATH Database file path (default: db/ginxsom.db)\n" + " --storage-dir DIR Storage directory for files (default: blobs)\n" + " --admin-pubkey KEY Admin public key for management (64 hex chars)\n" + " --server-privkey KEY Server private key (64 hex chars, for testing)\n" + " --test-keys Use test keys from .test_keys file\n" + " --generate-keys Generate server keypair on startup\n" + " --help, -h Show this help message\n"; + ssize_t written = write(STDOUT_FILENO, help_text, strlen(help_text)); + (void)written; // Suppress unused variable warning return 0; + } else if (strcmp(argv[i], "--admin-pubkey") == 0 && i + 1 < argc) { + strncpy(g_admin_pubkey, argv[i + 1], sizeof(g_admin_pubkey) - 1); + i++; // Skip next argument + } else if (strcmp(argv[i], "--server-privkey") == 0 && i + 1 < argc) { + strncpy(test_server_privkey, argv[i + 1], sizeof(test_server_privkey) - 1); + i++; // Skip next argument + } else if (strcmp(argv[i], "--test-keys") == 0) { + use_test_keys = 1; + } else if (strcmp(argv[i], "--generate-keys") == 0) { + g_generate_keys = 1; } else { fprintf(stderr, "Unknown option: %s\n", argv[i]); fprintf(stderr, "Use --help for usage information\n"); return 1; } } + + // Load test keys if requested + if (use_test_keys) { + FILE *keys_file = fopen(".test_keys", "r"); + if (!keys_file) { + fprintf(stderr, "ERROR: Cannot open .test_keys file\n"); + return 1; + } + + char line[256]; + while (fgets(line, sizeof(line), keys_file)) { + // Parse ADMIN_PUBKEY='...' + if (strncmp(line, "ADMIN_PUBKEY='", 14) == 0) { + char *start = line + 14; + char *end = strchr(start, '\''); + if (end && (end - start) == 64) { + strncpy(g_admin_pubkey, start, 64); + g_admin_pubkey[64] = '\0'; + } + } + // Parse SERVER_PRIVKEY='...' + else if (strncmp(line, "SERVER_PRIVKEY='", 16) == 0) { + char *start = line + 16; + char *end = strchr(start, '\''); + if (end && (end - start) == 64) { + strncpy(test_server_privkey, start, 64); + test_server_privkey[64] = '\0'; + } + } + } + fclose(keys_file); + + fprintf(stderr, "STARTUP: Using test keys from .test_keys\n"); + fprintf(stderr, "STARTUP: Admin pubkey: %s\n", g_admin_pubkey); + fprintf(stderr, "STARTUP: Server privkey loaded from test keys\n"); + } fprintf(stderr, "STARTUP: Using database path: %s\n", g_db_path); fprintf(stderr, "STARTUP: Using storage directory: %s\n", g_storage_dir); + if (strlen(g_admin_pubkey) > 0) { + fprintf(stderr, "STARTUP: Admin pubkey specified: %s\n", g_admin_pubkey); + } + if (g_generate_keys) { + fprintf(stderr, "STARTUP: Will generate server keypair\n"); + } + + fprintf(stderr, "DEBUG: About to initialize database\n"); + + // Initialize database (create if doesn't exist) + fprintf(stderr, "STARTUP: Initializing database...\n"); + if (initialize_database(g_db_path) != 0) { + fprintf(stderr, "FATAL ERROR: Failed to initialize database\n"); + return 1; + } + fprintf(stderr, "STARTUP: Database ready\n"); + + // CRITICAL: Initialize nostr crypto system BEFORE key operations + fprintf(stderr, "STARTUP: Initializing nostr crypto system...\r\n"); + int crypto_init_result = nostr_crypto_init(); + fprintf(stderr, "CRYPTO INIT RESULT: %d\r\n", crypto_init_result); + if (crypto_init_result != 0) { + fprintf(stderr, + "FATAL ERROR: Failed to initialize nostr crypto system\r\n"); + return 1; + } + fprintf(stderr, "STARTUP: nostr crypto system initialized successfully\r\n"); + + // Initialize server keys (now that crypto is initialized) + fprintf(stderr, "STARTUP: Initializing server keys...\n"); + fflush(stderr); + + // If test keys were provided via command line, store them in database + if (test_server_privkey[0] != '\0') { + // Store test private key in database + strncpy(g_blossom_seckey, test_server_privkey, sizeof(g_blossom_seckey) - 1); + g_blossom_seckey[64] = '\0'; + + fprintf(stderr, "STARTUP: Storing test server private key in database...\n"); + if (store_blossom_private_key(test_server_privkey) != 0) { + fprintf(stderr, "ERROR: Failed to store test private key\n"); + return 1; + } + + // Derive and store public key + unsigned char seckey_bytes[32]; + if (nostr_hex_to_bytes(test_server_privkey, seckey_bytes, 32) != NOSTR_SUCCESS) { + fprintf(stderr, "ERROR: Failed to parse test private key\n"); + return 1; + } + + unsigned char pubkey_bytes[32]; + if (nostr_ec_public_key_from_private_key(seckey_bytes, pubkey_bytes) != NOSTR_SUCCESS) { + fprintf(stderr, "ERROR: Failed to derive public key from test private key\n"); + return 1; + } + + char pubkey_hex[65]; + nostr_bytes_to_hex(pubkey_bytes, 32, pubkey_hex); + + // Store server public key in config + sqlite3 *db; + sqlite3_stmt *stmt; + int rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL); + if (rc == SQLITE_OK) { + const char *sql = "INSERT OR REPLACE INTO config (key, value, description) VALUES (?, ?, ?)"; + rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); + if (rc == SQLITE_OK) { + sqlite3_bind_text(stmt, 1, "blossom_pubkey", -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 2, pubkey_hex, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 3, "Blossom server's public key (TEST MODE)", -1, SQLITE_STATIC); + sqlite3_step(stmt); + sqlite3_finalize(stmt); + } + sqlite3_close(db); + } + + fprintf(stderr, "STARTUP: Test server keys stored in database\n"); + fprintf(stderr, "STARTUP: Server pubkey: %s\n", pubkey_hex); + fprintf(stderr, "STARTUP: Admin pubkey: %s\n", g_admin_pubkey); + + // Now call load_server_keys to ensure admin_pubkey is also stored + int key_init_result = load_server_keys(); + if (key_init_result != 0) { + fprintf(stderr, "WARNING: Failed to complete key initialization\n"); + } + } else { + // Load keys from database (production mode) + int key_init_result = load_server_keys(); + fprintf(stderr, "KEY INIT RESULT: %d\n", key_init_result); + fflush(stderr); + if (key_init_result != 0) { + fprintf(stderr, "FATAL ERROR: Failed to initialize server keys\n"); + return 1; + } + fprintf(stderr, "STARTUP: Server keys initialized successfully\n"); + } + fflush(stderr); + + // If --generate-keys was specified, exit after key generation + if (g_generate_keys) { + fprintf(stderr, "Key generation completed, exiting.\n"); + fflush(stderr); + return 0; + } // Initialize server configuration and identity // Try file-based config first, then fall back to database config @@ -1327,25 +1927,16 @@ int config_loaded = 0; // Fall back to database configuration if file config failed if (!config_loaded /* && !initialize_server_config() */) { - fprintf( - stderr, - "STARTUP: No configuration found - server starting in setup mode\n"); - fprintf(stderr, "STARTUP: Run interactive setup with: ginxsom --setup\n"); - // For interactive mode (when stdin is available), offer setup + fprintf( + stderr, + "STARTUP: No configuration found - server starting in setup mode\n"); + fprintf(stderr, "STARTUP: Run interactive setup with: ginxsom --setup\n"); + // For interactive mode (when stdin is available), offer setup } else if (!config_loaded) { - fprintf(stderr, "STARTUP: Database configuration loaded successfully\n"); + fprintf(stderr, "STARTUP: Database configuration loaded successfully\n"); } - // CRITICAL: Initialize nostr crypto system for cryptographic operations - fprintf(stderr, "STARTUP: Initializing nostr crypto system...\r\n"); - if (nostr_crypto_init() != 0) { - fprintf(stderr, - "FATAL ERROR: Failed to initialize nostr crypto system\r\n"); - return 1; - } - fprintf(stderr, "STARTUP: nostr crypto system initialized successfully\r\n"); - // Initialize request validator system fprintf(stderr, "STARTUP: Initializing request validator system...\r\n"); int validator_init_result = diff --git a/src/request_validator.c b/src/request_validator.c index b8ab73a..b746476 100644 --- a/src/request_validator.c +++ b/src/request_validator.c @@ -283,6 +283,16 @@ int nostr_validate_unified_request(const nostr_unified_request_t *request, // PHASE 2: NOSTR EVENT VALIDATION (CPU Intensive ~2ms) ///////////////////////////////////////////////////////////////////// + // Check if authentication is disabled first (regardless of header presence) + if (!g_auth_cache.auth_required) { + validator_debug_log("VALIDATOR_DEBUG: STEP 4 PASSED - Authentication " + "disabled, allowing request\n"); + result->valid = 1; + result->error_code = NOSTR_SUCCESS; + strcpy(result->reason, "Authentication disabled"); + return NOSTR_SUCCESS; + } + // Check if this is a BUD-09 report request - allow anonymous reporting if (request->operation && strcmp(request->operation, "report") == 0) { // BUD-09 allows anonymous reporting - pass through to bud09.c for validation diff --git a/src/test_keygen.c b/src/test_keygen.c new file mode 100644 index 0000000..50e5713 --- /dev/null +++ b/src/test_keygen.c @@ -0,0 +1,199 @@ +/* + * Test program for key generation + * Standalone version that doesn't require FastCGI + */ + +#include +#include +#include +#include +#include "../nostr_core_lib/nostr_core/nostr_common.h" +#include "../nostr_core_lib/nostr_core/utils.h" + +// Forward declarations +int generate_random_private_key_bytes(unsigned char *key_bytes, size_t len); +int generate_server_keypair(const char *db_path); +int store_blossom_private_key(const char *db_path, const char *seckey); + +// Generate random private key bytes using /dev/urandom +int generate_random_private_key_bytes(unsigned char *key_bytes, size_t len) { + FILE *fp = fopen("/dev/urandom", "rb"); + if (!fp) { + fprintf(stderr, "ERROR: Cannot open /dev/urandom for key generation\n"); + return -1; + } + + size_t bytes_read = fread(key_bytes, 1, len, fp); + fclose(fp); + + if (bytes_read != len) { + fprintf(stderr, "ERROR: Failed to read %zu bytes from /dev/urandom\n", len); + return -1; + } + + return 0; +} + +// Store blossom private key in dedicated table +int store_blossom_private_key(const char *db_path, const char *seckey) { + sqlite3 *db; + sqlite3_stmt *stmt; + int rc; + + // Validate key format + if (!seckey || strlen(seckey) != 64) { + fprintf(stderr, "ERROR: Invalid blossom private key format\n"); + return -1; + } + + // Create blossom_seckey table if it doesn't exist + rc = sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL); + if (rc) { + fprintf(stderr, "ERROR: Can't open database: %s\n", sqlite3_errmsg(db)); + return -1; + } + + // Create table + const char *create_sql = "CREATE TABLE IF NOT EXISTS blossom_seckey (id INTEGER PRIMARY KEY CHECK (id = 1), seckey TEXT NOT NULL, created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), CHECK (length(seckey) = 64))"; + rc = sqlite3_exec(db, create_sql, NULL, NULL, NULL); + if (rc != SQLITE_OK) { + fprintf(stderr, "ERROR: Failed to create blossom_seckey table: %s\n", sqlite3_errmsg(db)); + sqlite3_close(db); + return -1; + } + + // Store key + const char *sql = "INSERT OR REPLACE INTO blossom_seckey (id, seckey) VALUES (1, ?)"; + rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); + if (rc != SQLITE_OK) { + fprintf(stderr, "ERROR: SQL prepare failed: %s\n", sqlite3_errmsg(db)); + sqlite3_close(db); + return -1; + } + + sqlite3_bind_text(stmt, 1, seckey, -1, SQLITE_STATIC); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + sqlite3_close(db); + + if (rc != SQLITE_DONE) { + fprintf(stderr, "ERROR: Failed to store blossom private key\n"); + return -1; + } + + return 0; +} + +// Generate server keypair and store in database +int generate_server_keypair(const char *db_path) { + printf("Generating server keypair...\n"); + unsigned char seckey_bytes[32]; + char seckey_hex[65]; + char pubkey_hex[65]; + + // Generate random private key + printf("Generating random private key...\n"); + if (generate_random_private_key_bytes(seckey_bytes, 32) != 0) { + fprintf(stderr, "Failed to generate random bytes\n"); + return -1; + } + + // Validate the private key + if (nostr_ec_private_key_verify(seckey_bytes) != NOSTR_SUCCESS) { + fprintf(stderr, "ERROR: Generated invalid private key\n"); + return -1; + } + + // Convert to hex + nostr_bytes_to_hex(seckey_bytes, 32, seckey_hex); + + // Derive public key + unsigned char pubkey_bytes[32]; + if (nostr_ec_public_key_from_private_key(seckey_bytes, pubkey_bytes) != NOSTR_SUCCESS) { + fprintf(stderr, "ERROR: Failed to derive public key\n"); + return -1; + } + + // Convert public key to hex + nostr_bytes_to_hex(pubkey_bytes, 32, pubkey_hex); + + // Store private key securely + if (store_blossom_private_key(db_path, seckey_hex) != 0) { + fprintf(stderr, "ERROR: Failed to store blossom private key\n"); + return -1; + } + + // Store public key in config + sqlite3 *db; + sqlite3_stmt *stmt; + int rc; + + rc = sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READWRITE, NULL); + if (rc) { + fprintf(stderr, "ERROR: Can't open database for config: %s\n", sqlite3_errmsg(db)); + return -1; + } + + const char *sql = "INSERT OR REPLACE INTO config (key, value, description) VALUES (?, ?, ?)"; + rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); + if (rc != SQLITE_OK) { + fprintf(stderr, "ERROR: SQL prepare failed: %s\n", sqlite3_errmsg(db)); + sqlite3_close(db); + return -1; + } + + sqlite3_bind_text(stmt, 1, "blossom_pubkey", -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 2, pubkey_hex, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 3, "Blossom server's public key for Nostr communication", -1, SQLITE_STATIC); + + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + sqlite3_close(db); + + if (rc != SQLITE_DONE) { + fprintf(stderr, "ERROR: Failed to store blossom public key in config\n"); + return -1; + } + + // Display keys for admin setup + printf("========================================\n"); + printf("SERVER KEYPAIR GENERATED SUCCESSFULLY\n"); + printf("========================================\n"); + printf("Blossom Public Key: %s\n", pubkey_hex); + printf("Blossom Private Key: %s\n", seckey_hex); + printf("========================================\n"); + printf("IMPORTANT: Save the private key securely!\n"); + printf("This key is used for decrypting admin messages.\n"); + printf("========================================\n"); + + return 0; +} + +int main(int argc, char *argv[]) { + const char *db_path = "db/ginxsom.db"; + + if (argc > 1) { + db_path = argv[1]; + } + + printf("Test Key Generation\n"); + printf("===================\n"); + printf("Database: %s\n\n", db_path); + + // Initialize nostr crypto + printf("Initializing nostr crypto system...\n"); + if (nostr_crypto_init() != NOSTR_SUCCESS) { + fprintf(stderr, "FATAL: Failed to initialize nostr crypto\n"); + return 1; + } + printf("Crypto system initialized\n\n"); + + // Generate keypair + if (generate_server_keypair(db_path) != 0) { + fprintf(stderr, "FATAL: Key generation failed\n"); + return 1; + } + + printf("\nKey generation test completed successfully!\n"); + return 0; +} \ No newline at end of file diff --git a/src/test_main.c b/src/test_main.c new file mode 100644 index 0000000..fb77b2c --- /dev/null +++ b/src/test_main.c @@ -0,0 +1,50 @@ +/* + * Minimal test version of main.c to debug startup issues + */ + +#include +#include +#include +#include "ginxsom.h" + +// Copy just the essential parts for testing +char g_db_path[4096] = "db/ginxsom.db"; +char g_storage_dir[4096] = "."; +char g_admin_pubkey[65] = ""; +char g_relay_seckey[65] = ""; +int g_generate_keys = 0; + +int main(int argc, char *argv[]) { + printf("DEBUG: main() started\n"); + fflush(stdout); + + // Parse minimal args + for (int i = 1; i < argc; i++) { + printf("DEBUG: arg %d: %s\n", i, argv[i]); + fflush(stdout); + if (strcmp(argv[i], "--generate-keys") == 0) { + g_generate_keys = 1; + printf("DEBUG: generate-keys flag set\n"); + fflush(stdout); + } else if (strcmp(argv[i], "--help") == 0) { + printf("Usage: test_main [options]\n"); + printf(" --generate-keys Generate keys\n"); + printf(" --help Show help\n"); + return 0; + } + } + + printf("DEBUG: g_generate_keys = %d\n", g_generate_keys); + fflush(stdout); + + if (g_generate_keys) { + printf("DEBUG: Would generate keys here\n"); + fflush(stdout); + return 0; + } + + printf("DEBUG: Normal startup would continue here\n"); + fflush(stdout); + + return 0; +} \ No newline at end of file diff --git a/test_blob_1762770636.txt b/test_blob_1762770636.txt deleted file mode 100644 index b80e07b..0000000 --- a/test_blob_1762770636.txt +++ /dev/null @@ -1,7 +0,0 @@ -Test blob content for Ginxsom Blossom server (PRODUCTION) -Timestamp: 2025-11-10T06:30:36-04:00 -Random data: bb7d8d5206aadf4ecd48829f9674454c160bae68e98c6ce5f7f678f997dbe86a -Test message: Hello from production test! - -This file is used to test the upload functionality -of the Ginxsom Blossom server on blossom.laantungir.net diff --git a/test_file.txt b/test_file.txt deleted file mode 100644 index d7ab9a8..0000000 --- a/test_file.txt +++ /dev/null @@ -1 +0,0 @@ -test file content diff --git a/test_key_generation.sh b/test_key_generation.sh new file mode 100755 index 0000000..47fb65d --- /dev/null +++ b/test_key_generation.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# Test key generation for ginxsom + +echo "=== Testing Key Generation ===" +echo + +# Run the binary with --generate-keys flag +echo "Running: ./build/ginxsom-fcgi --generate-keys --db-path db/ginxsom.db" +echo +./build/ginxsom-fcgi --generate-keys --db-path db/ginxsom.db 2>&1 + +echo +echo "=== Checking if keys were stored ===" +echo + +# Check if blossom_seckey table was created +echo "Checking blossom_seckey table:" +sqlite3 db/ginxsom.db "SELECT COUNT(*) as key_count FROM blossom_seckey" 2>&1 + +echo +echo "Checking blossom_pubkey in config:" +sqlite3 db/ginxsom.db "SELECT value FROM config WHERE key='blossom_pubkey'" 2>&1 + +echo +echo "=== Test Complete ===" \ No newline at end of file diff --git a/test_mode_verification.sh b/test_mode_verification.sh new file mode 100755 index 0000000..20ec35e --- /dev/null +++ b/test_mode_verification.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +echo "=== Test Mode Verification ===" +echo "" + +# Expected test keys from .test_keys +EXPECTED_ADMIN_PUBKEY="8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e" +EXPECTED_SERVER_PUBKEY="52e366edfa4e9cc6a6d4653828e51ccf828a2f5a05227d7a768f33b5a198681a" + +echo "1. Checking database keys (should be OLD keys, not test keys)..." +DB_ADMIN_PUBKEY=$(sqlite3 db/ginxsom.db "SELECT value FROM config WHERE key = 'admin_pubkey'") +DB_BLOSSOM_PUBKEY=$(sqlite3 db/ginxsom.db "SELECT value FROM config WHERE key = 'blossom_pubkey'") +DB_BLOSSOM_SECKEY=$(sqlite3 db/ginxsom.db "SELECT seckey FROM blossom_seckey WHERE id = 1") + +echo " Database admin_pubkey: '$DB_ADMIN_PUBKEY'" +echo " Database blossom_pubkey: '$DB_BLOSSOM_PUBKEY'" +echo " Database blossom_seckey: '$DB_BLOSSOM_SECKEY'" +echo "" + +# Verify database was NOT modified with test keys +if [ "$DB_ADMIN_PUBKEY" = "$EXPECTED_ADMIN_PUBKEY" ]; then + echo " ❌ FAIL: Database admin_pubkey matches test key (should NOT be modified)" + exit 1 +else + echo " ✓ PASS: Database admin_pubkey is different from test key (not modified)" +fi + +if [ "$DB_BLOSSOM_PUBKEY" = "$EXPECTED_SERVER_PUBKEY" ]; then + echo " ❌ FAIL: Database blossom_pubkey matches test key (should NOT be modified)" + exit 1 +else + echo " ✓ PASS: Database blossom_pubkey is different from test key (not modified)" +fi + +echo "" +echo "2. Checking server is running..." +if curl -s http://localhost:9001/ > /dev/null; then + echo " ✓ PASS: Server is responding" +else + echo " ❌ FAIL: Server is not responding" + exit 1 +fi + +echo "" +echo "3. Verifying test keys from .test_keys file..." +echo " Expected admin pubkey: $EXPECTED_ADMIN_PUBKEY" +echo " Expected server pubkey: $EXPECTED_SERVER_PUBKEY" + +echo "" +echo "=== All Tests Passed ===" +echo "Test mode is working correctly:" +echo " - Test keys are loaded in memory" +echo " - Database was NOT modified" +echo " - Server is running with test keys" \ No newline at end of file diff --git a/tests/auth_test.sh b/tests/auth_test.sh index b1a8115..4dc48ed 100755 --- a/tests/auth_test.sh +++ b/tests/auth_test.sh @@ -14,9 +14,9 @@ TESTS_PASSED=0 TESTS_FAILED=0 TOTAL_TESTS=0 -# Test keys for different scenarios -TEST_USER1_PRIVKEY="5c0c523f52a5b6fad39ed2403092df8cebc36318b39383bca6c00808626fab3a" -TEST_USER1_PUBKEY="87d3561f19b74adbe8bf840682992466068830a9d8c36b4a0c99d36f826cb6cb" +# Test keys for different scenarios - Using WSB's keys for TEST_USER1 +TEST_USER1_PRIVKEY="22cc83aa57928a2800234c939240c9a6f0f44a33ea3838a860ed38930b195afd" +TEST_USER1_PUBKEY="8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e" TEST_USER2_PRIVKEY="182c3a5e3b7a1b7e4f5c6b7c8b4a5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2" TEST_USER2_PUBKEY="c95195e5e7de1ad8c4d3c0ac4e8b5c0c4e0c4d3c1e5c8d4c2e7e9f4a5b6c7d8e" diff --git a/tests/auth_test_tmp/nip42_challenge b/tests/auth_test_tmp/nip42_challenge index 9e491be..7b7ba67 100644 --- a/tests/auth_test_tmp/nip42_challenge +++ b/tests/auth_test_tmp/nip42_challenge @@ -1 +1 @@ -e3ba927d32ca105a8a4cafa2e013b97945a165c38e9ce573446a2332dc312fdb +299c28eeb15df327c30c9afd952d4e35c3777443d2094b2caab2fc94599ce607 diff --git a/tests/file_put_bud02.sh b/tests/file_put_bud02.sh index 31a2fe7..53490fb 100755 --- a/tests/file_put_bud02.sh +++ b/tests/file_put_bud02.sh @@ -5,12 +5,14 @@ set -e # Exit on any error -# Configuration +# Configuration - Using WSB's keys # SERVER_URL="http://localhost:9001" SERVER_URL="https://localhost:9443" UPLOAD_ENDPOINT="${SERVER_URL}/upload" TEST_FILE="test_blob_$(date +%s).txt" CLEANUP_FILES=() +NOSTR_PRIVKEY="22cc83aa57928a2800234c939240c9a6f0f44a33ea3838a860ed38930b195afd" +NOSTR_PUBKEY="8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e" # Colors for output RED='\033[0;31m' @@ -128,22 +130,23 @@ calculate_hash() { # Generate nostr event generate_nostr_event() { - log_info "Generating kind 24242 nostr event with nak..." - + log_info "Generating kind 24242 nostr event with nak using Alice's private key..." + # Calculate expiration time (1 hour from now) EXPIRATION=$(date -d '+1 hour' +%s) - - # Generate the event using nak + + # Generate the event using nak with Alice's private key EVENT_JSON=$(nak event -k 24242 -c "" \ + --sec "$NOSTR_PRIVKEY" \ -t "t=upload" \ -t "x=${HASH}" \ -t "expiration=${EXPIRATION}") - + if [[ -z "$EVENT_JSON" ]]; then log_error "Failed to generate nostr event" exit 1 fi - + log_success "Generated nostr event" echo "Event JSON: $EVENT_JSON" } diff --git a/tests/file_put_production.sh b/tests/file_put_production.sh index 010ca65..709e4f0 100755 --- a/tests/file_put_production.sh +++ b/tests/file_put_production.sh @@ -126,22 +126,23 @@ calculate_hash() { # Generate nostr event generate_nostr_event() { - log_info "Generating kind 24242 nostr event with nak..." - + log_info "Generating kind 24242 nostr event with nak using WSB's private key..." + # Calculate expiration time (1 hour from now) EXPIRATION=$(date -d '+1 hour' +%s) - - # Generate the event using nak + + # Generate the event using nak with WSB's private key EVENT_JSON=$(nak event -k 24242 -c "" \ + --sec "22cc83aa57928a2800234c939240c9a6f0f44a33ea3838a860ed38930b195afd" \ -t "t=upload" \ -t "x=${HASH}" \ -t "expiration=${EXPIRATION}") - + if [[ -z "$EVENT_JSON" ]]; then log_error "Failed to generate nostr event" exit 1 fi - + log_success "Generated nostr event" echo "Event JSON: $EVENT_JSON" } diff --git a/tests/list_test_bud02.sh b/tests/list_test_bud02.sh index bb5a4ac..bf8c04f 100755 --- a/tests/list_test_bud02.sh +++ b/tests/list_test_bud02.sh @@ -4,8 +4,8 @@ # This script tests the blob listing functionality BASE_URL="http://localhost:9001" -NOSTR_PRIVKEY="0000000000000000000000000000000000000000000000000000000000000001" -NOSTR_PUBKEY="79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" +NOSTR_PRIVKEY="22cc83aa57928a2800234c939240c9a6f0f44a33ea3838a860ed38930b195afd" +NOSTR_PUBKEY="8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e" # Colors for output RED='\033[0;31m' @@ -19,128 +19,117 @@ echo # Function to generate a Nostr event for list authorization generate_list_auth() { local content="$1" - local created_at=$(date +%s) - local expiration=$((created_at + 3600)) # 1 hour from now - - # Note: This is a placeholder - in real implementation, you'd use nostr tools - # to generate properly signed events. For now, we'll create the structure. - cat << EOF -{ - "id": "placeholder_id", - "pubkey": "$NOSTR_PUBKEY", - "kind": 24242, - "content": "$content", - "created_at": $created_at, - "tags": [ - ["t", "list"], - ["expiration", "$expiration"] - ], - "sig": "placeholder_signature" -} -EOF + + # Use nak to generate properly signed events with Alice's private key + nak event -k 24242 -c "$content" \ + --sec "$NOSTR_PRIVKEY" \ + -t "t=list" \ + -t "expiration=$(( $(date +%s) + 3600 ))" } # Test 1: List blobs without authorization (should work if optional auth) echo -e "${YELLOW}Test 1: GET /list/ without authorization${NC}" + RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" "$BASE_URL/list/$NOSTR_PUBKEY") HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2) BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d') +echo "Using pubkey: $NOSTR_PUBKEY" +echo "HTTP Status: $HTTP_STATUS" +echo "Response: $BODY" +echo + +# Test 2: List blobs with authorization +echo -e "${YELLOW}Test 2: GET /list/ with authorization${NC}" +LIST_AUTH=$(generate_list_auth "List Blobs") +AUTH_B64=$(echo "$LIST_AUTH" | base64 -w 0) +RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \ + -H "Authorization: Nostr $AUTH_B64" \ + "$BASE_URL/list/$NOSTR_PUBKEY") +HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2) +BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d') + echo "HTTP Status: $HTTP_STATUS" echo "Response: $BODY" echo -# # Test 2: List blobs with authorization -# echo -e "${YELLOW}Test 2: GET /list/ with authorization${NC}" -# LIST_AUTH=$(generate_list_auth "List Blobs") -# AUTH_B64=$(echo "$LIST_AUTH" | base64 -w 0) -# RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \ -# -H "Authorization: Nostr $AUTH_B64" \ -# "$BASE_URL/list/$NOSTR_PUBKEY") -# HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2) -# BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d') +# Test 3: List blobs with since parameter +echo -e "${YELLOW}Test 3: GET /list/ with since parameter${NC}" +SINCE_TIMESTAMP=$(($(date +%s) - 86400)) # 24 hours ago +RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \ + "$BASE_URL/list/$NOSTR_PUBKEY?since=$SINCE_TIMESTAMP") +HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2) +BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d') -# echo "HTTP Status: $HTTP_STATUS" -# echo "Response: $BODY" -# echo +echo "HTTP Status: $HTTP_STATUS" +echo "Response: $BODY" +echo -# # Test 3: List blobs with since parameter -# echo -e "${YELLOW}Test 3: GET /list/ with since parameter${NC}" -# SINCE_TIMESTAMP=$(($(date +%s) - 86400)) # 24 hours ago -# RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \ -# "$BASE_URL/list/$NOSTR_PUBKEY?since=$SINCE_TIMESTAMP") -# HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2) -# BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d') +# Test 4: List blobs with until parameter +echo -e "${YELLOW}Test 4: GET /list/ with until parameter${NC}" +UNTIL_TIMESTAMP=$(date +%s) # now +RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \ + "$BASE_URL/list/$NOSTR_PUBKEY?until=$UNTIL_TIMESTAMP") +HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2) +BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d') -# echo "HTTP Status: $HTTP_STATUS" -# echo "Response: $BODY" -# echo +echo "HTTP Status: $HTTP_STATUS" +echo "Response: $BODY" +echo -# # Test 4: List blobs with until parameter -# echo -e "${YELLOW}Test 4: GET /list/ with until parameter${NC}" -# UNTIL_TIMESTAMP=$(date +%s) # now -# RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \ -# "$BASE_URL/list/$NOSTR_PUBKEY?until=$UNTIL_TIMESTAMP") -# HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2) -# BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d') +# Test 5: List blobs with both since and until parameters +echo -e "${YELLOW}Test 5: GET /list/ with since and until parameters${NC}" +SINCE_TIMESTAMP=$(($(date +%s) - 86400)) # 24 hours ago +UNTIL_TIMESTAMP=$(date +%s) # now +RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \ + "$BASE_URL/list/$NOSTR_PUBKEY?since=$SINCE_TIMESTAMP&until=$UNTIL_TIMESTAMP") +HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2) +BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d') -# echo "HTTP Status: $HTTP_STATUS" -# echo "Response: $BODY" -# echo +echo "HTTP Status: $HTTP_STATUS" +echo "Response: $BODY" +echo -# # Test 5: List blobs with both since and until parameters -# echo -e "${YELLOW}Test 5: GET /list/ with since and until parameters${NC}" -# SINCE_TIMESTAMP=$(($(date +%s) - 86400)) # 24 hours ago -# UNTIL_TIMESTAMP=$(date +%s) # now -# RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \ -# "$BASE_URL/list/$NOSTR_PUBKEY?since=$SINCE_TIMESTAMP&until=$UNTIL_TIMESTAMP") -# HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2) -# BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d') +# Test 6: List blobs for non-existent pubkey +echo -e "${YELLOW}Test 6: GET /list/${NC}" +FAKE_PUBKEY="1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" +RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" "$BASE_URL/list/$FAKE_PUBKEY") +HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2) +BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d') -# echo "HTTP Status: $HTTP_STATUS" -# echo "Response: $BODY" -# echo +if [ "$HTTP_STATUS" = "200" ]; then + echo -e "${GREEN}✓ Correctly returned 200 with empty array${NC}" +else + echo "HTTP Status: $HTTP_STATUS" +fi +echo "Response: $BODY" +echo -# # Test 6: List blobs for non-existent pubkey -# echo -e "${YELLOW}Test 6: GET /list/${NC}" -# FAKE_PUBKEY="1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" -# RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" "$BASE_URL/list/$FAKE_PUBKEY") -# HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2) -# BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d') +# Test 7: List blobs with invalid pubkey format +echo -e "${YELLOW}Test 7: GET /list/${NC}" +INVALID_PUBKEY="invalid_pubkey" +RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" "$BASE_URL/list/$INVALID_PUBKEY") +HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2) +BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d') -# if [ "$HTTP_STATUS" = "200" ]; then -# echo -e "${GREEN}✓ Correctly returned 200 with empty array${NC}" -# else -# echo "HTTP Status: $HTTP_STATUS" -# fi -# echo "Response: $BODY" -# echo +if [ "$HTTP_STATUS" = "400" ]; then + echo -e "${GREEN}✓ Correctly returned 400 for invalid pubkey format${NC}" +else + echo "HTTP Status: $HTTP_STATUS" +fi +echo "Response: $BODY" +echo -# # Test 7: List blobs with invalid pubkey format -# echo -e "${YELLOW}Test 7: GET /list/${NC}" -# INVALID_PUBKEY="invalid_pubkey" -# RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" "$BASE_URL/list/$INVALID_PUBKEY") -# HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2) -# BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d') +# Test 8: List blobs with invalid since/until parameters +echo -e "${YELLOW}Test 8: GET /list/ with invalid timestamp parameters${NC}" +RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \ + "$BASE_URL/list/$NOSTR_PUBKEY?since=invalid&until=invalid") +HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2) +BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d') -# if [ "$HTTP_STATUS" = "400" ]; then -# echo -e "${GREEN}✓ Correctly returned 400 for invalid pubkey format${NC}" -# else -# echo "HTTP Status: $HTTP_STATUS" -# fi -# echo "Response: $BODY" -# echo - -# # Test 8: List blobs with invalid since/until parameters -# echo -e "${YELLOW}Test 8: GET /list/ with invalid timestamp parameters${NC}" -# RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \ -# "$BASE_URL/list/$NOSTR_PUBKEY?since=invalid&until=invalid") -# HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2) -# BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d') - -# echo "HTTP Status: $HTTP_STATUS" -# echo "Response: $BODY" -# echo +echo "HTTP Status: $HTTP_STATUS" +echo "Response: $BODY" +echo # echo "=== List Tests Complete ===" # echo diff --git a/tests/white_black_list_test.sh b/tests/white_black_list_test.sh index 7b2f14a..469f264 100755 --- a/tests/white_black_list_test.sh +++ b/tests/white_black_list_test.sh @@ -14,10 +14,10 @@ TESTS_PASSED=0 TESTS_FAILED=0 TOTAL_TESTS=0 -# Test keys for different scenarios +# Test keys for different scenarios - Using WSB's keys for TEST_USER1 # Generated using: nak key public -TEST_USER1_PRIVKEY="5c0c523f52a5b6fad39ed2403092df8cebc36318b39383bca6c00808626fab3a" -TEST_USER1_PUBKEY="87d3561f19b74adbe8bf840682992466068830a9d8c36b4a0c99d36f826cb6cb" +TEST_USER1_PRIVKEY="22cc83aa57928a2800234c939240c9a6f0f44a33ea3838a860ed38930b195afd" +TEST_USER1_PUBKEY="8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e" TEST_USER2_PRIVKEY="182c3a5e3b7a1b7e4f5c6b7c8b4a5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2" TEST_USER2_PUBKEY="0396b426090284a28294078dce53fe73791ab623c3fc46ab4409fea05109a6db" diff --git a/update_remote_nginx_conf.sh b/update_remote_nginx_conf.sh new file mode 100755 index 0000000..c007e26 --- /dev/null +++ b/update_remote_nginx_conf.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +# update_remote_nginx_conf.sh +# Updates the remote nginx configuration on laantungir.net +# Copies contents of ./remote.nginx.config to /etc/nginx/conf.d/default.conf + +set -e + +echo "=== Updating Remote Nginx Configuration ===" +echo "Server: laantungir.net" +echo "User: ubuntu" +echo "Local config: ./remote.nginx.config" +echo "Remote config: /etc/nginx/conf.d/default.conf" +echo + +# Check if local config exists +if [[ ! -f "./remote.nginx.config" ]]; then + echo "ERROR: ./remote.nginx.config not found" + exit 1 +fi + +echo "Copying remote.nginx.config to laantungir.net:/etc/nginx/conf.d/default.conf..." + +# Copy the config file to the remote server (using user's home directory) +scp ./remote.nginx.config ubuntu@laantungir.net:~/remote.nginx.config + +# Move to final location and backup old config +ssh ubuntu@laantungir.net << 'EOF' + echo "Creating backup of current config..." + sudo cp /etc/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf.backup.$(date +%Y%m%d_%H%M%S) + + echo "Installing new config..." + sudo cp ~/remote.nginx.config /etc/nginx/conf.d/default.conf + + echo "Testing nginx configuration..." + if sudo nginx -t; then + echo "✅ Nginx config test passed" + echo "Reloading nginx..." + sudo nginx -s reload + echo "✅ Nginx reloaded successfully" + else + echo "❌ Nginx config test failed" + echo "Restoring backup..." + sudo cp /etc/nginx/conf.d/default.conf.backup.* /etc/nginx/conf.d/default.conf 2>/dev/null || true + exit 1 + fi + + echo "Cleaning up temporary file..." + rm ~/remote.nginx.config +EOF + +echo +echo "=== Update Complete ===" +echo "The remote nginx configuration has been updated."