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."