2 Commits

Author SHA1 Message Date
Your Name
deec021933 v0.1.11 - Last push before changing logging system 2025-12-07 12:50:12 -04:00
Your Name
db7621a293 v0.1.10 - In the middle of working on getting admin api working 2025-11-21 11:54:17 -04:00
29 changed files with 2469 additions and 419 deletions

View File

@@ -1,14 +1,14 @@
# Ginxsom Blossom Server Makefile
CC = gcc
CFLAGS = -Wall -Wextra -std=c99 -O2 -Inostr_core_lib/nostr_core -Inostr_core_lib/cjson
LIBS = -lfcgi -lsqlite3 nostr_core_lib/libnostr_core_x64.a -lz -ldl -lpthread -lm -L/usr/local/lib -lsecp256k1 -lssl -lcrypto -lcurl
CFLAGS = -Wall -Wextra -std=gnu99 -O2 -Inostr_core_lib/nostr_core -Inostr_core_lib/cjson $(shell pkg-config --cflags libwebsockets)
LIBS = -lfcgi -lsqlite3 nostr_core_lib/libnostr_core_x64.a -lz -ldl -lpthread -lm -L/usr/local/lib -lsecp256k1 -lssl -lcrypto -lcurl $(shell pkg-config --libs libwebsockets)
SRCDIR = src
BUILDDIR = build
TARGET = $(BUILDDIR)/ginxsom-fcgi
# Source files
SOURCES = $(SRCDIR)/main.c $(SRCDIR)/admin_api.c $(SRCDIR)/admin_auth.c $(SRCDIR)/admin_websocket.c $(SRCDIR)/admin_handlers.c $(SRCDIR)/bud04.c $(SRCDIR)/bud06.c $(SRCDIR)/bud08.c $(SRCDIR)/bud09.c $(SRCDIR)/request_validator.c
SOURCES = $(SRCDIR)/main.c $(SRCDIR)/admin_api.c $(SRCDIR)/admin_auth.c $(SRCDIR)/admin_event.c $(SRCDIR)/admin_websocket.c $(SRCDIR)/admin_handlers.c $(SRCDIR)/bud04.c $(SRCDIR)/bud06.c $(SRCDIR)/bud08.c $(SRCDIR)/bud09.c $(SRCDIR)/request_validator.c
OBJECTS = $(SOURCES:$(SRCDIR)/%.c=$(BUILDDIR)/%.o)
# Default target

View File

@@ -38,7 +38,7 @@ INSERT OR IGNORE INTO config (key, value, description) VALUES
('auth_rules_enabled', 'false', 'Whether authentication rules are enabled for uploads'),
('server_name', 'ginxsom', 'Server name for responses'),
('admin_pubkey', '', 'Admin public key for API access'),
('admin_enabled', 'false', 'Whether admin API is enabled'),
('admin_enabled', 'true', 'Whether admin API is enabled'),
('nip42_require_auth', 'false', 'Enable NIP-42 challenge/response authentication'),
('nip42_challenge_timeout', '600', 'NIP-42 challenge timeout in seconds'),
('nip42_time_tolerance', '300', 'NIP-42 timestamp tolerance in seconds');

Binary file not shown.

BIN
build/admin_event.o Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -219,9 +219,28 @@ http {
fastcgi_param HTTP_AUTHORIZATION $http_authorization;
}
# WebSocket Admin endpoint (/admin) - Nostr Kind 23456/23457 events
location /admin {
proxy_pass http://127.0.0.1:9442;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket timeouts
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
# Disable buffering for WebSocket
proxy_buffering off;
}
# Admin API endpoints (/api/*)
location /api/ {
if ($request_method !~ ^(GET|PUT)$) {
if ($request_method !~ ^(GET|PUT|POST)$) {
return 405;
}
fastcgi_pass fastcgi_backend;
@@ -570,9 +589,28 @@ http {
fastcgi_param HTTP_AUTHORIZATION $http_authorization;
}
# WebSocket Admin endpoint (/admin) - Nostr Kind 23456/23457 events
location /admin {
proxy_pass http://127.0.0.1:9442;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket timeouts
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
# Disable buffering for WebSocket
proxy_buffering off;
}
# Admin API endpoints (/api/*)
location /api/ {
if ($request_method !~ ^(GET|PUT)$) {
if ($request_method !~ ^(GET|PUT|POST)$) {
return 405;
}
fastcgi_pass fastcgi_backend;

View File

@@ -1,78 +0,0 @@
-- Migration: Add authentication rules tables
-- Purpose: Enable whitelist/blacklist functionality for Ginxsom
-- Date: 2025-01-12
-- Enable foreign key constraints
PRAGMA foreign_keys = ON;
-- Authentication rules table for whitelist/blacklist functionality
CREATE TABLE IF NOT EXISTS auth_rules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
rule_type TEXT NOT NULL, -- 'pubkey_blacklist', 'pubkey_whitelist',
-- 'hash_blacklist', 'mime_blacklist', 'mime_whitelist'
rule_target TEXT NOT NULL, -- The pubkey, hash, or MIME type to match
operation TEXT NOT NULL DEFAULT '*', -- 'upload', 'delete', 'list', or '*' for all
enabled INTEGER NOT NULL DEFAULT 1, -- 1 = enabled, 0 = disabled
priority INTEGER NOT NULL DEFAULT 100,-- Lower number = higher priority
description TEXT, -- Human-readable description
created_by TEXT, -- Admin pubkey who created the rule
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
-- Constraints
CHECK (rule_type IN ('pubkey_blacklist', 'pubkey_whitelist',
'hash_blacklist', 'mime_blacklist', 'mime_whitelist')),
CHECK (operation IN ('upload', 'delete', 'list', '*')),
CHECK (enabled IN (0, 1)),
CHECK (priority >= 0),
-- Unique constraint: one rule per type/target/operation combination
UNIQUE(rule_type, rule_target, operation)
);
-- Indexes for performance optimization
CREATE INDEX IF NOT EXISTS idx_auth_rules_type_target ON auth_rules(rule_type, rule_target);
CREATE INDEX IF NOT EXISTS idx_auth_rules_operation ON auth_rules(operation);
CREATE INDEX IF NOT EXISTS idx_auth_rules_enabled ON auth_rules(enabled);
CREATE INDEX IF NOT EXISTS idx_auth_rules_priority ON auth_rules(priority);
CREATE INDEX IF NOT EXISTS idx_auth_rules_type_operation ON auth_rules(rule_type, operation, enabled);
-- Cache table for authentication decisions (5-minute TTL)
CREATE TABLE IF NOT EXISTS auth_rules_cache (
cache_key TEXT PRIMARY KEY NOT NULL, -- SHA-256 hash of request parameters
decision INTEGER NOT NULL, -- 1 = allow, 0 = deny
reason TEXT, -- Reason for decision
pubkey TEXT, -- Public key from request
operation TEXT, -- Operation type
resource_hash TEXT, -- Resource hash (if applicable)
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
expires_at INTEGER NOT NULL, -- Expiration timestamp
CHECK (decision IN (0, 1))
);
-- Index for cache expiration cleanup
CREATE INDEX IF NOT EXISTS idx_auth_cache_expires ON auth_rules_cache(expires_at);
CREATE INDEX IF NOT EXISTS idx_auth_cache_pubkey ON auth_rules_cache(pubkey);
-- Insert example rules (commented out - uncomment to use)
-- Example: Blacklist a specific pubkey for uploads
-- INSERT INTO auth_rules (rule_type, rule_target, operation, priority, description, created_by) VALUES
-- ('pubkey_blacklist', '79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798', 'upload', 10, 'Example blacklisted user', 'admin_pubkey_here');
-- Example: Whitelist a specific pubkey for all operations
-- INSERT INTO auth_rules (rule_type, rule_target, operation, priority, description, created_by) VALUES
-- ('pubkey_whitelist', 'your_pubkey_here', '*', 300, 'Trusted user - all operations allowed', 'admin_pubkey_here');
-- Example: Blacklist executable MIME types
-- INSERT INTO auth_rules (rule_type, rule_target, operation, priority, description, created_by) VALUES
-- ('mime_blacklist', 'application/x-executable', 'upload', 200, 'Block executable files', 'admin_pubkey_here'),
-- ('mime_blacklist', 'application/x-msdos-program', 'upload', 200, 'Block DOS executables', 'admin_pubkey_here'),
-- ('mime_blacklist', 'application/x-msdownload', 'upload', 200, 'Block Windows executables', 'admin_pubkey_here');
-- Example: Whitelist common image types
-- INSERT INTO auth_rules (rule_type, rule_target, operation, priority, description, created_by) VALUES
-- ('mime_whitelist', 'image/jpeg', 'upload', 400, 'Allow JPEG images', 'admin_pubkey_here'),
-- ('mime_whitelist', 'image/png', 'upload', 400, 'Allow PNG images', 'admin_pubkey_here'),
-- ('mime_whitelist', 'image/gif', 'upload', 400, 'Allow GIF images', 'admin_pubkey_here'),
-- ('mime_whitelist', 'image/webp', 'upload', 400, 'Allow WebP images', 'admin_pubkey_here');

View File

@@ -1,15 +0,0 @@
-- Migration: Add blossom_seckey table for storing server private key
-- This table stores the Blossom server's secp256k1 private key used for:
-- - Signing admin response events (Kind 23457)
-- - Decrypting admin commands (NIP-44)
CREATE TABLE IF NOT EXISTS blossom_seckey (
id INTEGER PRIMARY KEY CHECK (id = 1), -- Only one row allowed
seckey TEXT NOT NULL, -- Private key in hex format (64 chars)
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
CHECK (length(seckey) = 64) -- Ensure valid secp256k1 key length
);
-- Add blossom_pubkey to config if not exists
INSERT OR IGNORE INTO config (key, value, description) VALUES
('blossom_pubkey', '', 'Blossom server public key derived from blossom_seckey');

View File

@@ -0,0 +1,300 @@
# Database Naming Design (c-relay Pattern)
## Overview
Following c-relay's architecture, ginxsom will use pubkey-based database naming to ensure database-key consistency and prevent mismatched configurations.
## Database Naming Convention
Database files are named after the blossom server's public key:
```
db/<blossom_pubkey>.db
```
Example:
```
db/52e366edfa4e9cc6a6d4653828e51ccf828a2f5a05227d7a768f33b5a198681a.db
```
## Startup Scenarios
### Scenario 1: No Arguments (Fresh Start)
```bash
./ginxsom-fcgi
```
**Behavior:**
1. Generate new server keypair
2. Create database file: `db/<new_pubkey>.db`
3. Store keys in the new database
4. Start server
**Result:** New instance with fresh keys and database
---
### Scenario 2: Database File Specified
```bash
./ginxsom-fcgi --db-path db/52e366ed...198681a.db
```
**Behavior:**
1. Open specified database
2. Load blossom_seckey from database
3. Verify pubkey matches database filename
4. Load admin_pubkey if present
5. Start server
**Validation:**
- Database MUST exist
- Database MUST contain blossom_seckey
- Derived pubkey MUST match filename
**Error Cases:**
- Database doesn't exist → Error: "Database file not found"
- Database missing blossom_seckey → Error: "Invalid database: missing server keys"
- Pubkey mismatch → Error: "Database pubkey mismatch: expected X, got Y"
---
### Scenario 3: Keys Specified (New Instance with Specific Keys)
```bash
./ginxsom-fcgi --server-privkey c4e0d2ed...309c48f1 --admin-pubkey 8ff74724...5eedde0e
```
**Behavior:**
1. Validate provided server private key
2. Derive server public key
3. Create database file: `db/<derived_pubkey>.db`
4. Store both keys in new database
5. Start server
**Validation:**
- server-privkey MUST be valid 64-char hex
- Derived database file MUST NOT already exist (prevents overwriting)
**Error Cases:**
- Invalid privkey format → Error: "Invalid server private key format"
- Database already exists → Error: "Database already exists for this pubkey"
---
### Scenario 4: Test Mode
```bash
./ginxsom-fcgi --test-keys
```
**Behavior:**
1. Load keys from `.test_keys` file
2. Derive server public key from SERVER_PRIVKEY
3. Create/overwrite database: `db/<test_pubkey>.db`
4. Store test keys in database
5. Start server
**Special Handling:**
- Test mode ALWAYS overwrites existing database (for clean testing)
- Database name derived from test SERVER_PRIVKEY
---
### Scenario 5: Database + Keys Specified (Validation Mode)
```bash
./ginxsom-fcgi --db-path db/52e366ed...198681a.db --server-privkey c4e0d2ed...309c48f1
```
**Behavior:**
1. Open specified database
2. Load blossom_seckey from database
3. Compare with provided --server-privkey
4. If match: continue normally
5. If mismatch: ERROR and exit
**Purpose:** Validation/verification that correct keys are being used
**Error Cases:**
- Key mismatch → Error: "Server private key doesn't match database"
---
## Command Line Options
### Updated Options
```
--db-path PATH Database file path (must match pubkey if keys exist)
--storage-dir DIR Storage directory for files (default: blobs)
--admin-pubkey KEY Admin public key (only used when creating new database)
--server-privkey KEY Server private key (creates new DB or validates existing)
--test-keys Use test keys from .test_keys file
--generate-keys Generate new keypair and create database (deprecated - default behavior)
--help, -h Show this help message
```
### Removed Options
- `--generate-keys` - No longer needed, this is default behavior when no args provided
---
## Database Directory Structure
```
db/
├── 52e366edfa4e9cc6a6d4653828e51ccf828a2f5a05227d7a768f33b5a198681a.db # Test instance
├── a1b2c3d4e5f6...xyz.db # Production instance 1
├── f9e8d7c6b5a4...abc.db # Production instance 2
└── schema.sql # Schema template
```
Each database is completely independent and tied to its keypair.
---
## Implementation Logic Flow
```
START
├─ Parse command line arguments
├─ Initialize crypto system
├─ Determine mode:
│ │
│ ├─ Test mode (--test-keys)?
│ │ ├─ Load keys from .test_keys
│ │ ├─ Derive pubkey
│ │ ├─ Set db_path = db/<pubkey>.db
│ │ └─ Create/overwrite database
│ │
│ ├─ Keys provided (--server-privkey)?
│ │ ├─ Validate privkey format
│ │ ├─ Derive pubkey
│ │ ├─ Set db_path = db/<pubkey>.db
│ │ │
│ │ ├─ Database specified (--db-path)?
│ │ │ ├─ YES: Validate keys match database
│ │ │ └─ NO: Create new database
│ │ │
│ │ └─ Store keys in database
│ │
│ ├─ Database specified (--db-path)?
│ │ ├─ Open database
│ │ ├─ Load blossom_seckey
│ │ ├─ Derive pubkey
│ │ ├─ Validate pubkey matches filename
│ │ └─ Load admin_pubkey
│ │
│ └─ No arguments (fresh start)?
│ ├─ Generate new keypair
│ ├─ Set db_path = db/<new_pubkey>.db
│ └─ Create new database with keys
├─ Initialize database schema (if new)
├─ Load/validate all keys
└─ Start FastCGI server
```
---
## Migration Path
### For Existing Installations
1. **Backup current database:**
```bash
cp db/ginxsom.db db/ginxsom.db.backup
```
2. **Extract current pubkey:**
```bash
PUBKEY=$(sqlite3 db/ginxsom.db "SELECT value FROM config WHERE key='blossom_pubkey'")
```
3. **Rename database:**
```bash
mv db/ginxsom.db db/${PUBKEY}.db
```
4. **Update restart-all.sh:**
- Remove hardcoded `db/ginxsom.db` references
- Let application determine database name from keys
---
## Benefits
1. **Database-Key Consistency:** Impossible to use wrong database with wrong keys
2. **Multiple Instances:** Can run multiple independent instances with different keys
3. **Clear Identity:** Database filename immediately identifies the server
4. **Test Isolation:** Test databases are clearly separate from production
5. **No Accidental Overwrites:** Each keypair has its own database
6. **Follows c-relay Pattern:** Proven architecture from production relay software
---
## Error Messages
### Clear, Actionable Errors
```
ERROR: Database file not found: db/52e366ed...198681a.db
→ Specify a different database or let the application create a new one
ERROR: Invalid database: missing server keys
→ Database is corrupted or not a valid ginxsom database
ERROR: Database pubkey mismatch
Expected: 52e366edfa4e9cc6a6d4653828e51ccf828a2f5a05227d7a768f33b5a198681a
Got: a1b2c3d4e5f6789...
→ Database filename doesn't match the keys stored inside
ERROR: Server private key doesn't match database
→ The --server-privkey you provided doesn't match the database keys
ERROR: Database already exists for this pubkey: db/52e366ed...198681a.db
→ Use --db-path to open existing database or use different keys
```
---
## Testing Strategy
### Test Cases
1. **Fresh start (no args)** → Creates new database with generated keys
2. **Specify database** → Opens and validates existing database
3. **Specify keys** → Creates new database with those keys
4. **Test mode** → Uses test keys and creates test database
5. **Database + matching keys** → Validates and continues
6. **Database + mismatched keys** → Errors appropriately
7. **Invalid database path** → Clear error message
8. **Corrupted database** → Detects and reports
### Test Script
```bash
#!/bin/bash
# Test database naming system
# Test 1: Fresh start
./ginxsom-fcgi --generate-keys
# Should create db/<new_pubkey>.db
# Test 2: Test mode
./ginxsom-fcgi --test-keys
# Should create db/52e366ed...198681a.db
# Test 3: Specify keys
./ginxsom-fcgi --server-privkey abc123...
# Should create db/<derived_pubkey>.db
# Test 4: Open existing
./ginxsom-fcgi --db-path db/52e366ed...198681a.db
# Should open and validate
# Test 5: Mismatch error
./ginxsom-fcgi --db-path db/52e366ed...198681a.db --server-privkey wrong_key
# Should error with clear message

8
ginxsom.code-workspace Normal file
View File

@@ -0,0 +1,8 @@
{
"folders": [
{
"path": "."
}
],
"settings": {}
}

View File

@@ -1,6 +1,7 @@
#!/bin/bash
# Restart Ginxsom Development Environment
# Combines nginx and FastCGI restart operations for debugging
# WARNING: This script DELETES all databases in db/ for fresh testing
# Configuration
@@ -177,9 +178,22 @@ if [ $? -ne 0 ]; then
fi
echo -e "${GREEN}Clean rebuild complete${NC}"
# Step 3.5: Handle keys based on mode
echo -e "\n${YELLOW}3.5. Configuring server keys...${NC}"
DB_PATH="$PWD/db/ginxsom.db"
# Step 3.5: Clean database directory for fresh testing
echo -e "\n${YELLOW}3.5. Cleaning database directory...${NC}"
echo "Removing all existing databases for fresh start..."
# Remove all .db files in db/ directory
if ls db/*.db 1> /dev/null 2>&1; then
echo "Found databases to remove:"
ls -lh db/*.db
rm -f db/*.db
echo -e "${GREEN}Database cleanup complete${NC}"
else
echo "No existing databases found"
fi
# Step 3.75: Handle keys based on mode
echo -e "\n${YELLOW}3.75. Configuring server keys...${NC}"
if [ $TEST_MODE -eq 1 ]; then
# Test mode: verify .test_keys file exists
@@ -188,29 +202,20 @@ if [ $TEST_MODE -eq 1 ]; then
echo -e "${RED}Test mode requires .test_keys file in project root${NC}"
exit 1
fi
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}"
# Extract test server pubkey to determine database name
TEST_PUBKEY=$(grep "^SERVER_PUBKEY=" .test_keys | cut -d"'" -f2)
if [ -z "$TEST_PUBKEY" ]; then
echo -e "${RED}ERROR: Could not extract SERVER_PUBKEY from .test_keys${NC}"
exit 1
fi
echo -e "${GREEN}Key generation completed${NC}"
echo -e "${YELLOW}IMPORTANT: Check logs/app/stderr.log for your generated keys!${NC}"
fi
echo -e "${GREEN}Test mode: Will use keys from .test_keys${NC}"
echo -e "${GREEN}Fresh test database will be created as: db/${TEST_PUBKEY}.db${NC}"
else
# Production mode: databases were cleaned, will generate new keypair
echo -e "${YELLOW}Production mode: Fresh start with new keypair${NC}"
echo -e "${YELLOW}New database will be created as db/<new_pubkey>.db${NC}"
fi
# Step 4: Start FastCGI
@@ -232,30 +237,46 @@ echo "Setting GINX_DEBUG environment for pubkey extraction diagnostics"
export GINX_DEBUG=1
# Build command line arguments based on mode
FCGI_ARGS="--db-path $PWD/db/ginxsom.db --storage-dir blobs"
FCGI_ARGS="--storage-dir blobs"
if [ $TEST_MODE -eq 1 ]; then
FCGI_ARGS="$FCGI_ARGS --test-keys"
echo -e "${YELLOW}Starting FastCGI in TEST MODE with test keys${NC}"
else
# Production mode: databases were cleaned, will generate new keys
echo -e "${YELLOW}Starting FastCGI in production mode - will generate new keys and create database${NC}"
fi
# Start FastCGI application with proper logging (daemonized but with redirected streams)
# Start FastCGI application with proper logging
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" $FCGI_ARGS 1>>logs/app/stdout.log 2>>logs/app/stderr.log
if [ $? -eq 0 ] && [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE")
# Use nohup with spawn-fcgi -n to keep process running with redirected output
# The key is: nohup prevents HUP signal, -n prevents daemonization (keeps stderr connected)
nohup spawn-fcgi -n -s "$SOCKET_PATH" -M 666 -u "$USER" -g "$USER" -- "$FCGI_BINARY" $FCGI_ARGS >>logs/app/stdout.log 2>>logs/app/stderr.log </dev/null &
SPAWN_PID=$!
# Wait for spawn-fcgi to spawn the child
sleep 1
# Get the actual FastCGI process PID (child of spawn-fcgi)
FCGI_PID=$(pgrep -f "ginxsom-fcgi.*--storage-dir" | head -1)
if [ -z "$FCGI_PID" ]; then
echo -e "${RED}Warning: Could not find FastCGI process${NC}"
FCGI_PID=$SPAWN_PID
fi
# Save PID
echo $FCGI_PID > "$PID_FILE"
# Give it a moment to start
sleep 1
if check_process "$FCGI_PID"; then
echo -e "${GREEN}FastCGI application started successfully${NC}"
echo "PID: $PID"
# Verify it's actually running
if check_process "$PID"; then
echo "PID: $FCGI_PID"
echo -e "${GREEN}Process confirmed running${NC}"
else
echo -e "${RED}Warning: Process may have crashed immediately${NC}"
exit 1
fi
else
echo -e "${RED}Failed to start FastCGI application${NC}"
echo -e "${RED}Process may have crashed immediately${NC}"
exit 1
fi
@@ -327,3 +348,7 @@ echo -e "${YELLOW}To monitor logs, check: logs/nginx/error.log, logs/nginx/acces
echo -e "\n${YELLOW}Server is available at:${NC}"
echo -e " ${GREEN}HTTP:${NC} http://localhost:9001"
echo -e " ${GREEN}HTTPS:${NC} https://localhost:9443"
echo -e "\n${YELLOW}Admin WebSocket endpoint:${NC}"
echo -e " ${GREEN}WSS:${NC} wss://localhost:9443/admin (via nginx proxy)"
echo -e " ${GREEN}WS:${NC} ws://localhost:9001/admin (via nginx proxy)"
echo -e " ${GREEN}Direct:${NC} ws://localhost:9442 (direct connection)"

View File

@@ -11,8 +11,8 @@
#include <unistd.h>
#include "ginxsom.h"
// Database path (consistent with main.c)
#define DB_PATH "db/ginxsom.db"
// Use global database path from main.c
extern char g_db_path[];
// Function declarations (moved from admin_api.h)
void handle_admin_api_request(const char* method, const char* uri, const char* validated_pubkey, int is_authenticated);
@@ -44,7 +44,7 @@ static int admin_nip94_get_origin(char* out, size_t out_size) {
sqlite3_stmt* stmt;
int rc;
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc) {
// Default on DB error
strncpy(out, "http://localhost:9001", out_size - 1);
@@ -130,8 +130,12 @@ void handle_admin_api_request(const char* method, const char* uri, const char* v
}
// Authentication now handled by centralized validation system
// Health endpoint is exempt from authentication requirement
if (strcmp(path, "/health") != 0) {
// Health endpoint and POST /admin (Kind 23456 events) are exempt from authentication requirement
// Kind 23456 events authenticate themselves via signed event validation
int skip_auth = (strcmp(path, "/health") == 0) ||
(strcmp(method, "POST") == 0 && strcmp(path, "/admin") == 0);
if (!skip_auth) {
if (!is_authenticated || !validated_pubkey) {
send_json_error(401, "admin_auth_required", "Valid admin authentication required");
return;
@@ -157,6 +161,13 @@ void handle_admin_api_request(const char* method, const char* uri, const char* v
} else {
send_json_error(404, "not_found", "API endpoint not found");
}
} else if (strcmp(method, "POST") == 0) {
if (strcmp(path, "/admin") == 0) {
// Handle Kind 23456/23457 admin event commands
handle_admin_event_request();
} else {
send_json_error(404, "not_found", "API endpoint not found");
}
} else if (strcmp(method, "PUT") == 0) {
if (strcmp(path, "/config") == 0) {
handle_config_put_api();
@@ -201,7 +212,7 @@ int verify_admin_pubkey(const char* event_pubkey) {
sqlite3_stmt* stmt;
int rc, is_admin = 0;
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc) {
return 0;
}
@@ -228,7 +239,7 @@ int is_admin_enabled(void) {
sqlite3_stmt* stmt;
int rc, enabled = 0;
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc) {
return 0; // Default disabled if can't access DB
}
@@ -254,7 +265,7 @@ void handle_stats_api(void) {
sqlite3_stmt* stmt;
int rc;
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc) {
send_json_error(500, "database_error", "Failed to open database");
return;
@@ -349,7 +360,7 @@ void handle_config_get_api(void) {
sqlite3_stmt* stmt;
int rc;
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc) {
send_json_error(500, "database_error", "Failed to open database");
return;
@@ -423,7 +434,7 @@ void handle_config_put_api(void) {
sqlite3_stmt* stmt;
int rc;
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL);
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE, NULL);
if (rc) {
free(json_body);
cJSON_Delete(config_data);
@@ -541,7 +552,7 @@ void handle_config_key_put_api(const char* key) {
sqlite3_stmt* stmt;
int rc;
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL);
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE, NULL);
if (rc) {
free(json_body);
cJSON_Delete(request_data);
@@ -621,7 +632,7 @@ void handle_files_api(void) {
sqlite3_stmt* stmt;
int rc;
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc) {
send_json_error(500, "database_error", "Failed to open database");
return;
@@ -715,7 +726,7 @@ void handle_health_api(void) {
// Check database connection
sqlite3* db;
int rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc == SQLITE_OK) {
cJSON_AddStringToObject(data, "database", "connected");
sqlite3_close(db);

471
src/admin_event.c Normal file
View File

@@ -0,0 +1,471 @@
// Admin event handler for Kind 23456/23457 admin commands
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "ginxsom.h"
// Forward declarations for nostr_core_lib functions
int nostr_hex_to_bytes(const char* hex, unsigned char* bytes, size_t bytes_len);
int nostr_nip44_decrypt(const unsigned char* recipient_private_key,
const unsigned char* sender_public_key,
const char* encrypted_data,
char* output,
size_t output_size);
int nostr_nip44_encrypt(const unsigned char* sender_private_key,
const unsigned char* recipient_public_key,
const char* plaintext,
char* output,
size_t output_size);
cJSON* nostr_create_and_sign_event(int kind, const char* content, cJSON* tags,
const unsigned char* private_key, time_t created_at);
// Use global database path from main.c
extern char g_db_path[];
// Forward declarations
static int get_server_privkey(unsigned char* privkey_bytes);
static int get_server_pubkey(char* pubkey_hex, size_t size);
static int handle_config_query_command(cJSON* response_data);
static int send_admin_response_event(const char* admin_pubkey, const char* request_id,
cJSON* response_data);
/**
* Handle Kind 23456 admin command event
* Expects POST to /api/admin with JSON body containing the event
*/
void handle_admin_event_request(void) {
// Read request body
const char* content_length_str = getenv("CONTENT_LENGTH");
if (!content_length_str) {
printf("Status: 411 Length Required\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Content-Length header required\"}\n");
return;
}
long content_length = atol(content_length_str);
if (content_length <= 0 || content_length > 65536) {
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Invalid content length\"}\n");
return;
}
char* json_body = malloc(content_length + 1);
if (!json_body) {
printf("Status: 500 Internal Server Error\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Memory allocation failed\"}\n");
return;
}
size_t bytes_read = fread(json_body, 1, content_length, stdin);
if (bytes_read != (size_t)content_length) {
free(json_body);
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Failed to read complete request body\"}\n");
return;
}
json_body[content_length] = '\0';
// Parse event JSON
cJSON* event = cJSON_Parse(json_body);
free(json_body);
if (!event) {
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Invalid JSON\"}\n");
return;
}
// Verify it's Kind 23456
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
if (!kind_obj || !cJSON_IsNumber(kind_obj) ||
(int)cJSON_GetNumberValue(kind_obj) != 23456) {
cJSON_Delete(event);
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Event must be Kind 23456\"}\n");
return;
}
// Get event ID for response correlation
cJSON* id_obj = cJSON_GetObjectItem(event, "id");
if (!id_obj || !cJSON_IsString(id_obj)) {
cJSON_Delete(event);
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Event missing id\"}\n");
return;
}
const char* request_id = cJSON_GetStringValue(id_obj);
// Get admin pubkey from event
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
if (!pubkey_obj || !cJSON_IsString(pubkey_obj)) {
cJSON_Delete(event);
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Event missing pubkey\"}\n");
return;
}
const char* admin_pubkey = cJSON_GetStringValue(pubkey_obj);
// Verify admin pubkey
sqlite3* db;
int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc != SQLITE_OK) {
cJSON_Delete(event);
printf("Status: 500 Internal Server Error\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Database error\"}\n");
return;
}
sqlite3_stmt* stmt;
const char* sql = "SELECT value FROM config WHERE key = 'admin_pubkey'";
int is_admin = 0;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
const char* db_admin_pubkey = (const char*)sqlite3_column_text(stmt, 0);
if (db_admin_pubkey && strcmp(admin_pubkey, db_admin_pubkey) == 0) {
is_admin = 1;
}
}
sqlite3_finalize(stmt);
}
sqlite3_close(db);
if (!is_admin) {
cJSON_Delete(event);
printf("Status: 403 Forbidden\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Not authorized as admin\"}\n");
return;
}
// Get encrypted content
cJSON* content_obj = cJSON_GetObjectItem(event, "content");
if (!content_obj || !cJSON_IsString(content_obj)) {
cJSON_Delete(event);
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Event missing content\"}\n");
return;
}
const char* encrypted_content = cJSON_GetStringValue(content_obj);
// Get server private key for decryption
unsigned char server_privkey[32];
if (get_server_privkey(server_privkey) != 0) {
cJSON_Delete(event);
printf("Status: 500 Internal Server Error\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Failed to get server private key\"}\n");
return;
}
// Convert admin pubkey to bytes
unsigned char admin_pubkey_bytes[32];
if (nostr_hex_to_bytes(admin_pubkey, admin_pubkey_bytes, 32) != 0) {
cJSON_Delete(event);
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Invalid admin pubkey format\"}\n");
return;
}
// Decrypt content using NIP-44 (or use plaintext for testing)
char decrypted_content[8192];
const char* content_to_parse = encrypted_content;
// Check if content is already plaintext JSON (starts with '[')
if (encrypted_content[0] != '[') {
// Content is encrypted, decrypt it
int decrypt_result = nostr_nip44_decrypt(
server_privkey,
admin_pubkey_bytes,
encrypted_content,
decrypted_content,
sizeof(decrypted_content)
);
if (decrypt_result != 0) {
cJSON_Delete(event);
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Failed to decrypt content\"}\n");
return;
}
content_to_parse = decrypted_content;
}
// Parse command array (either decrypted or plaintext)
cJSON* command_array = cJSON_Parse(content_to_parse);
if (!command_array || !cJSON_IsArray(command_array)) {
cJSON_Delete(event);
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Decrypted content is not a valid command array\"}\n");
return;
}
// Get command type
cJSON* command_type = cJSON_GetArrayItem(command_array, 0);
if (!command_type || !cJSON_IsString(command_type)) {
cJSON_Delete(command_array);
cJSON_Delete(event);
printf("Status: 400 Bad Request\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Invalid command format\"}\n");
return;
}
const char* cmd = cJSON_GetStringValue(command_type);
// Create response data object
cJSON* response_data = cJSON_CreateObject();
cJSON_AddStringToObject(response_data, "query_type", cmd);
cJSON_AddNumberToObject(response_data, "timestamp", (double)time(NULL));
// Handle command
int result = -1;
if (strcmp(cmd, "config_query") == 0) {
result = handle_config_query_command(response_data);
} else {
cJSON_AddStringToObject(response_data, "status", "error");
cJSON_AddStringToObject(response_data, "error", "Unknown command");
}
cJSON_Delete(command_array);
cJSON_Delete(event);
if (result == 0) {
// Send Kind 23457 response
send_admin_response_event(admin_pubkey, request_id, response_data);
} else {
cJSON_Delete(response_data);
printf("Status: 500 Internal Server Error\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Command processing failed\"}\n");
}
}
/**
* Get server private key from database (stored in blossom_seckey table)
*/
static int get_server_privkey(unsigned char* privkey_bytes) {
sqlite3* db;
int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc != SQLITE_OK) {
return -1;
}
sqlite3_stmt* stmt;
const char* sql = "SELECT seckey FROM blossom_seckey LIMIT 1";
int result = -1;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
const char* privkey_hex = (const char*)sqlite3_column_text(stmt, 0);
if (privkey_hex && nostr_hex_to_bytes(privkey_hex, privkey_bytes, 32) == 0) {
result = 0;
}
}
sqlite3_finalize(stmt);
}
sqlite3_close(db);
return result;
}
/**
* Get server public key from database (stored in config table as blossom_pubkey)
*/
static int get_server_pubkey(char* pubkey_hex, size_t size) {
sqlite3* db;
int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc != SQLITE_OK) {
return -1;
}
sqlite3_stmt* stmt;
const char* sql = "SELECT value FROM config WHERE key = 'blossom_pubkey'";
int result = -1;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
const char* pubkey = (const char*)sqlite3_column_text(stmt, 0);
if (pubkey) {
strncpy(pubkey_hex, pubkey, size - 1);
pubkey_hex[size - 1] = '\0';
result = 0;
}
}
sqlite3_finalize(stmt);
}
sqlite3_close(db);
return result;
}
/**
* Handle config_query command - returns all config values
*/
static int handle_config_query_command(cJSON* response_data) {
sqlite3* db;
int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc != SQLITE_OK) {
cJSON_AddStringToObject(response_data, "status", "error");
cJSON_AddStringToObject(response_data, "error", "Database error");
return -1;
}
cJSON_AddStringToObject(response_data, "status", "success");
cJSON* data = cJSON_CreateObject();
// Query all config settings
sqlite3_stmt* stmt;
const char* sql = "SELECT key, value FROM config ORDER BY key";
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
while (sqlite3_step(stmt) == SQLITE_ROW) {
const char* key = (const char*)sqlite3_column_text(stmt, 0);
const char* value = (const char*)sqlite3_column_text(stmt, 1);
if (key && value) {
cJSON_AddStringToObject(data, key, value);
}
}
sqlite3_finalize(stmt);
}
cJSON_AddItemToObject(response_data, "data", data);
sqlite3_close(db);
return 0;
}
/**
* Send Kind 23457 admin response event
*/
static int send_admin_response_event(const char* admin_pubkey, const char* request_id,
cJSON* response_data) {
// Get server keys
unsigned char server_privkey[32];
char server_pubkey[65];
if (get_server_privkey(server_privkey) != 0 ||
get_server_pubkey(server_pubkey, sizeof(server_pubkey)) != 0) {
cJSON_Delete(response_data);
printf("Status: 500 Internal Server Error\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Failed to get server keys\"}\n");
return -1;
}
// Convert response data to JSON string
char* response_json = cJSON_PrintUnformatted(response_data);
cJSON_Delete(response_data);
if (!response_json) {
printf("Status: 500 Internal Server Error\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Failed to serialize response\"}\n");
return -1;
}
// Convert admin pubkey to bytes for encryption
unsigned char admin_pubkey_bytes[32];
if (nostr_hex_to_bytes(admin_pubkey, admin_pubkey_bytes, 32) != 0) {
free(response_json);
printf("Status: 500 Internal Server Error\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Invalid admin pubkey\"}\n");
return -1;
}
// Encrypt response using NIP-44
char encrypted_response[131072];
int encrypt_result = nostr_nip44_encrypt(
server_privkey,
admin_pubkey_bytes,
response_json,
encrypted_response,
sizeof(encrypted_response)
);
free(response_json);
if (encrypt_result != 0) {
printf("Status: 500 Internal Server Error\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Failed to encrypt response\"}\n");
return -1;
}
// Create Kind 23457 response event
cJSON* response_event = cJSON_CreateObject();
cJSON_AddStringToObject(response_event, "pubkey", server_pubkey);
cJSON_AddNumberToObject(response_event, "created_at", (double)time(NULL));
cJSON_AddNumberToObject(response_event, "kind", 23457);
cJSON_AddStringToObject(response_event, "content", encrypted_response);
// Add tags
cJSON* tags = cJSON_CreateArray();
// p tag for admin
cJSON* p_tag = cJSON_CreateArray();
cJSON_AddItemToArray(p_tag, cJSON_CreateString("p"));
cJSON_AddItemToArray(p_tag, cJSON_CreateString(admin_pubkey));
cJSON_AddItemToArray(tags, p_tag);
// e tag for request correlation
cJSON* e_tag = cJSON_CreateArray();
cJSON_AddItemToArray(e_tag, cJSON_CreateString("e"));
cJSON_AddItemToArray(e_tag, cJSON_CreateString(request_id));
cJSON_AddItemToArray(tags, e_tag);
cJSON_AddItemToObject(response_event, "tags", tags);
// Sign the event
cJSON* signed_event = nostr_create_and_sign_event(
23457,
encrypted_response,
tags,
server_privkey,
time(NULL)
);
cJSON_Delete(response_event);
if (!signed_event) {
printf("Status: 500 Internal Server Error\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Failed to sign response event\"}\n");
return -1;
}
// Return the signed event as HTTP response
char* event_json = cJSON_PrintUnformatted(signed_event);
cJSON_Delete(signed_event);
if (!event_json) {
printf("Status: 500 Internal Server Error\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("{\"error\":\"Failed to serialize event\"}\n");
return -1;
}
printf("Status: 200 OK\r\n");
printf("Content-Type: application/json\r\n");
printf("Cache-Control: no-cache\r\n");
printf("\r\n");
printf("%s\n", event_json);
free(event_json);
return 0;
}

View File

@@ -1,163 +1,541 @@
/*
* Ginxsom Admin WebSocket Module
* Ginxsom Admin WebSocket Server
* Handles WebSocket connections for Kind 23456/23457 admin commands
* Based on c-relay's WebSocket implementation
* Based on c-relay's WebSocket implementation using libwebsockets
*/
#include "ginxsom.h"
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <cjson/cJSON.h>
#include <sqlite3.h>
#include <libwebsockets.h>
#include "ginxsom.h"
// Forward declarations from admin_auth.c
int process_admin_command(cJSON *event, char ***command_array_out, int *command_count_out, char **admin_pubkey_out);
void free_command_array(char **command_array, int command_count);
int create_admin_response(const char *response_json, const char *admin_pubkey, const char *original_event_id, cJSON **response_event_out);
// Forward declarations from admin_event.c
extern char g_db_path[];
extern int nostr_hex_to_bytes(const char* hex, unsigned char* bytes, size_t bytes_len);
extern int nostr_nip44_decrypt(const unsigned char* recipient_private_key,
const unsigned char* sender_public_key,
const char* encrypted_data,
char* output,
size_t output_size);
extern int nostr_nip44_encrypt(const unsigned char* sender_private_key,
const unsigned char* recipient_public_key,
const char* plaintext,
char* output,
size_t output_size);
extern cJSON* nostr_create_and_sign_event(int kind, const char* content, cJSON* tags,
const unsigned char* private_key, time_t created_at);
// 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);
// Per-session data for each WebSocket connection
struct per_session_data {
char admin_pubkey[65];
int authenticated;
unsigned char pending_response[LWS_PRE + 131072];
size_t pending_response_len;
};
// 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
// Global WebSocket context
static struct lws_context *ws_context = NULL;
static volatile int force_exit = 0;
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");
// Function prototypes
static int get_server_privkey(unsigned char* privkey_bytes);
static int get_server_pubkey(char* pubkey_hex, size_t size);
static int handle_config_query_command(cJSON* response_data);
static int process_admin_event(struct lws *wsi, struct per_session_data *pss, const char *json_str);
/**
* WebSocket protocol callback
*/
static int callback_admin_protocol(struct lws *wsi, enum lws_callback_reasons reason,
void *user, void *in, size_t len) {
struct per_session_data *pss = (struct per_session_data *)user;
switch (reason) {
case LWS_CALLBACK_ESTABLISHED:
fprintf(stderr, "[WebSocket] New connection established\n");
fflush(stderr);
memset(pss, 0, sizeof(*pss));
pss->authenticated = 0;
break;
case LWS_CALLBACK_RECEIVE:
fprintf(stderr, "[WebSocket] Received %zu bytes\n", len);
fflush(stderr);
// Null-terminate the received data
char *json_str = malloc(len + 1);
if (!json_str) {
fprintf(stderr, "[WebSocket] Memory allocation failed\n");
fflush(stderr);
return -1;
}
memcpy(json_str, in, len);
json_str[len] = '\0';
// Process the admin event
int result = process_admin_event(wsi, pss, json_str);
free(json_str);
if (result == 0 && pss->pending_response_len > 0) {
// Request callback to send response
lws_callback_on_writable(wsi);
}
break;
case LWS_CALLBACK_SERVER_WRITEABLE:
if (pss->pending_response_len > 0) {
fprintf(stderr, "[WebSocket] Sending %zu bytes\n", pss->pending_response_len - LWS_PRE);
fflush(stderr);
int written = lws_write(wsi,
&pss->pending_response[LWS_PRE],
pss->pending_response_len - LWS_PRE,
LWS_WRITE_TEXT);
if (written < 0) {
fprintf(stderr, "[WebSocket] Write failed\n");
fflush(stderr);
return -1;
}
// 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;
pss->pending_response_len = 0;
}
break;
case LWS_CALLBACK_CLOSED:
fprintf(stderr, "[WebSocket] Connection closed\n");
fflush(stderr);
break;
default:
break;
}
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;
return 0;
}
// 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);
/**
* WebSocket protocols
*/
static struct lws_protocols protocols[] = {
{
"nostr-admin",
callback_admin_protocol,
sizeof(struct per_session_data),
131072, // rx buffer size
0, NULL, 0
},
{ NULL, NULL, 0, 0, 0, NULL, 0 } // terminator
};
/**
* Process Kind 23456 admin event received via WebSocket
*/
static int process_admin_event(struct lws *wsi __attribute__((unused)), struct per_session_data *pss, const char *json_str) {
// Parse event JSON
cJSON *event = cJSON_Parse(json_str);
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;
fprintf(stderr, "[WebSocket] Invalid JSON\n");
fflush(stderr);
return -1;
}
// Process the admin command
char **command_array = NULL;
int command_count = 0;
char *admin_pubkey = NULL;
// Verify it's Kind 23456
cJSON *kind_obj = cJSON_GetObjectItem(event, "kind");
if (!kind_obj || !cJSON_IsNumber(kind_obj) ||
(int)cJSON_GetNumberValue(kind_obj) != 23456) {
fprintf(stderr, "[WebSocket] Not a Kind 23456 event\n");
fflush(stderr);
cJSON_Delete(event);
return -1;
}
int result = process_admin_command(event, &command_array, &command_count, &admin_pubkey);
// Get event ID for response correlation
cJSON *id_obj = cJSON_GetObjectItem(event, "id");
if (!id_obj || !cJSON_IsString(id_obj)) {
fprintf(stderr, "[WebSocket] Event missing id\n");
fflush(stderr);
cJSON_Delete(event);
return -1;
}
const char *request_id = cJSON_GetStringValue(id_obj);
// Get admin pubkey from event
cJSON *pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
if (!pubkey_obj || !cJSON_IsString(pubkey_obj)) {
fprintf(stderr, "[WebSocket] Event missing pubkey\n");
fflush(stderr);
cJSON_Delete(event);
return -1;
}
const char *admin_pubkey = cJSON_GetStringValue(pubkey_obj);
// Verify admin pubkey
if (!verify_admin_pubkey(admin_pubkey)) {
fprintf(stderr, "[WebSocket] Not authorized as admin: %s\n", admin_pubkey);
fflush(stderr);
cJSON_Delete(event);
return -1;
}
// Store admin pubkey in session
strncpy(pss->admin_pubkey, admin_pubkey, sizeof(pss->admin_pubkey) - 1);
pss->authenticated = 1;
// Get encrypted content
cJSON *content_obj = cJSON_GetObjectItem(event, "content");
if (!content_obj || !cJSON_IsString(content_obj)) {
fprintf(stderr, "[WebSocket] Event missing content\n");
fflush(stderr);
cJSON_Delete(event);
return -1;
}
const char *encrypted_content = cJSON_GetStringValue(content_obj);
// Get server private key for decryption
unsigned char server_privkey[32];
if (get_server_privkey(server_privkey) != 0) {
fprintf(stderr, "[WebSocket] Failed to get server private key\n");
fflush(stderr);
cJSON_Delete(event);
return -1;
}
// Convert admin pubkey to bytes
unsigned char admin_pubkey_bytes[32];
if (nostr_hex_to_bytes(admin_pubkey, admin_pubkey_bytes, 32) != 0) {
fprintf(stderr, "[WebSocket] Invalid admin pubkey format\n");
fflush(stderr);
cJSON_Delete(event);
return -1;
}
// Decrypt content using NIP-44
char decrypted_content[8192];
const char *content_to_parse = encrypted_content;
// Check if content is already plaintext JSON (starts with '[')
if (encrypted_content[0] != '[') {
int decrypt_result = nostr_nip44_decrypt(
server_privkey,
admin_pubkey_bytes,
encrypted_content,
decrypted_content,
sizeof(decrypted_content)
);
if (decrypt_result != 0) {
fprintf(stderr, "[WebSocket] Failed to decrypt content\n");
fflush(stderr);
cJSON_Delete(event);
return -1;
}
content_to_parse = decrypted_content;
}
// Parse command array
cJSON *command_array = cJSON_Parse(content_to_parse);
if (!command_array || !cJSON_IsArray(command_array)) {
fprintf(stderr, "[WebSocket] Decrypted content is not a valid command array\n");
fflush(stderr);
cJSON_Delete(event);
return -1;
}
// Get command type
cJSON *command_type = cJSON_GetArrayItem(command_array, 0);
if (!command_type || !cJSON_IsString(command_type)) {
fprintf(stderr, "[WebSocket] Invalid command format\n");
fflush(stderr);
cJSON_Delete(command_array);
cJSON_Delete(event);
return -1;
}
const char *cmd = cJSON_GetStringValue(command_type);
fprintf(stderr, "[WebSocket] Processing command: %s\n", cmd);
fflush(stderr);
// Create response data object
cJSON *response_data = cJSON_CreateObject();
cJSON_AddStringToObject(response_data, "query_type", cmd);
cJSON_AddNumberToObject(response_data, "timestamp", (double)time(NULL));
// Handle command
int result = -1;
if (strcmp(cmd, "config_query") == 0) {
result = handle_config_query_command(response_data);
} else {
cJSON_AddStringToObject(response_data, "status", "error");
cJSON_AddStringToObject(response_data, "error", "Unknown command");
}
cJSON_Delete(command_array);
cJSON_Delete(event);
if (result != 0) {
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;
if (result == 0) {
// Get server keys
char server_pubkey[65];
if (get_server_pubkey(server_pubkey, sizeof(server_pubkey)) != 0) {
fprintf(stderr, "[WebSocket] Failed to get server pubkey\n");
fflush(stderr);
cJSON_Delete(response_data);
return -1;
}
// 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);
// Convert response data to JSON string
char *response_json = cJSON_PrintUnformatted(response_data);
cJSON_Delete(response_data);
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;
if (!response_json) {
fprintf(stderr, "[WebSocket] Failed to serialize response\n");
fflush(stderr);
return -1;
}
// Create the response event (Kind 23457)
cJSON *response_event = NULL;
int create_result = create_admin_response(response_json, admin_pubkey, NULL, &response_event);
// Encrypt response using NIP-44
char encrypted_response[131072];
int encrypt_result = nostr_nip44_encrypt(
server_privkey,
admin_pubkey_bytes,
response_json,
encrypted_response,
sizeof(encrypted_response)
);
free(response_json);
if (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;
if (encrypt_result != 0) {
fprintf(stderr, "[WebSocket] Failed to encrypt response\n");
fflush(stderr);
return -1;
}
// Return the response event as JSON
char *response_json_str = cJSON_Print(response_event);
cJSON_Delete(response_event);
// Create Kind 23457 response event
cJSON *tags = cJSON_CreateArray();
printf("Status: 200 OK\r\n");
printf("Content-Type: application/json\r\n\r\n");
printf("%s\n", response_json_str);
// p tag for admin
cJSON *p_tag = cJSON_CreateArray();
cJSON_AddItemToArray(p_tag, cJSON_CreateString("p"));
cJSON_AddItemToArray(p_tag, cJSON_CreateString(admin_pubkey));
cJSON_AddItemToArray(tags, p_tag);
free(response_json_str);
// e tag for request correlation
cJSON *e_tag = cJSON_CreateArray();
cJSON_AddItemToArray(e_tag, cJSON_CreateString("e"));
cJSON_AddItemToArray(e_tag, cJSON_CreateString(request_id));
cJSON_AddItemToArray(tags, e_tag);
// Sign the event
cJSON *signed_event = nostr_create_and_sign_event(
23457,
encrypted_response,
tags,
server_privkey,
time(NULL)
);
if (!signed_event) {
fprintf(stderr, "[WebSocket] Failed to sign response event\n");
fflush(stderr);
return -1;
}
// Serialize event to JSON
char *event_json = cJSON_PrintUnformatted(signed_event);
cJSON_Delete(signed_event);
if (!event_json) {
fprintf(stderr, "[WebSocket] Failed to serialize event\n");
fflush(stderr);
return -1;
}
// Store response in session for sending
size_t json_len = strlen(event_json);
if (json_len + LWS_PRE < sizeof(pss->pending_response)) {
memcpy(&pss->pending_response[LWS_PRE], event_json, json_len);
pss->pending_response_len = LWS_PRE + json_len;
fprintf(stderr, "[WebSocket] Response prepared (%zu bytes)\n", json_len);
fflush(stderr);
} else {
fprintf(stderr, "[WebSocket] Response too large\n");
fflush(stderr);
}
free(event_json);
return 0;
} else {
cJSON_Delete(response_data);
return -1;
}
}
/**
* Get server private key from database
*/
static int get_server_privkey(unsigned char* privkey_bytes) {
sqlite3 *db;
int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc != SQLITE_OK) {
return -1;
}
sqlite3_stmt *stmt;
const char *sql = "SELECT seckey FROM blossom_seckey LIMIT 1";
int result = -1;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
const char *privkey_hex = (const char*)sqlite3_column_text(stmt, 0);
if (privkey_hex && nostr_hex_to_bytes(privkey_hex, privkey_bytes, 32) == 0) {
result = 0;
}
}
sqlite3_finalize(stmt);
}
sqlite3_close(db);
return result;
}
/**
* Get server public key from database
*/
static int get_server_pubkey(char* pubkey_hex, size_t size) {
sqlite3 *db;
int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc != SQLITE_OK) {
return -1;
}
sqlite3_stmt *stmt;
const char *sql = "SELECT value FROM config WHERE key = 'blossom_pubkey'";
int result = -1;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
const char *pubkey = (const char*)sqlite3_column_text(stmt, 0);
if (pubkey) {
strncpy(pubkey_hex, pubkey, size - 1);
pubkey_hex[size - 1] = '\0';
result = 0;
}
}
sqlite3_finalize(stmt);
}
sqlite3_close(db);
return result;
}
/**
* Handle config_query command
*/
static int handle_config_query_command(cJSON* response_data) {
sqlite3 *db;
int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc != SQLITE_OK) {
cJSON_AddStringToObject(response_data, "status", "error");
cJSON_AddStringToObject(response_data, "error", "Database error");
return -1;
}
cJSON_AddStringToObject(response_data, "status", "success");
cJSON *data = cJSON_CreateObject();
// Query all config settings
sqlite3_stmt *stmt;
const char *sql = "SELECT key, value FROM config ORDER BY key";
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
while (sqlite3_step(stmt) == SQLITE_ROW) {
const char *key = (const char*)sqlite3_column_text(stmt, 0);
const char *value = (const char*)sqlite3_column_text(stmt, 1);
if (key && value) {
cJSON_AddStringToObject(data, key, value);
}
}
sqlite3_finalize(stmt);
}
cJSON_AddItemToObject(response_data, "data", data);
sqlite3_close(db);
return 0;
}
/**
* WebSocket server thread
*/
void* admin_websocket_thread(void* arg) {
int port = *(int*)arg;
struct lws_context_creation_info info;
memset(&info, 0, sizeof(info));
info.port = port;
info.iface = "127.0.0.1"; // Force IPv4 binding for localhost compatibility
info.protocols = protocols;
info.gid = -1;
info.uid = -1;
info.options = LWS_SERVER_OPTION_VALIDATE_UTF8 | LWS_SERVER_OPTION_DISABLE_IPV6;
fprintf(stderr, "[WebSocket] Starting admin WebSocket server on 127.0.0.1:%d (IPv4 only)\n", port);
fflush(stderr);
ws_context = lws_create_context(&info);
if (!ws_context) {
fprintf(stderr, "[WebSocket] Failed to create context\n");
fflush(stderr);
return NULL;
}
fprintf(stderr, "[WebSocket] Server started successfully\n");
fflush(stderr);
// Service loop
while (!force_exit) {
lws_service(ws_context, 50);
}
lws_context_destroy(ws_context);
fprintf(stderr, "[WebSocket] Server stopped\n");
fflush(stderr);
return NULL;
}
/**
* Start admin WebSocket server
*/
int start_admin_websocket_server(int port) {
static int server_port;
server_port = port;
pthread_t thread;
int result = pthread_create(&thread, NULL, admin_websocket_thread, &server_port);
if (result != 0) {
fprintf(stderr, "[WebSocket] Failed to create thread: %d\n", result);
fflush(stderr);
return -1;
}
pthread_detach(thread);
fprintf(stderr, "[WebSocket] Thread started\n");
fflush(stderr);
return 0;
}
/**
* Stop admin WebSocket server
*/
void stop_admin_websocket_server(void) {
force_exit = 1;
}

View File

@@ -10,8 +10,8 @@
#include <stdint.h>
#include "ginxsom.h"
// Database path
#define DB_PATH "db/ginxsom.db"
// Use global database path from main.c
extern char g_db_path[];
// Check if NIP-94 metadata emission is enabled
int nip94_is_enabled(void) {
@@ -19,7 +19,7 @@ int nip94_is_enabled(void) {
sqlite3_stmt* stmt;
int rc, enabled = 1; // Default enabled
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc) {
return 1; // Default enabled on DB error
}
@@ -50,7 +50,7 @@ int nip94_get_origin(char* out, size_t out_size) {
sqlite3_stmt* stmt;
int rc;
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc == SQLITE_OK) {
const char* sql = "SELECT value FROM config WHERE key = 'cdn_origin'";
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);

View File

@@ -11,8 +11,8 @@
#include <time.h>
#include "ginxsom.h"
// Database path (should match main.c)
#define DB_PATH "db/ginxsom.db"
// Use global database path from main.c
extern char g_db_path[];
// Forward declarations for helper functions
void send_error_response(int status_code, const char* error_type, const char* message, const char* details);
@@ -154,7 +154,7 @@ int store_blob_report(const char* event_json, const char* reporter_pubkey) {
sqlite3_stmt* stmt;
int rc;
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL);
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE, NULL);
if (rc) {
return 0;
}

View File

@@ -10,8 +10,8 @@
// Version information (auto-updated by build system)
#define VERSION_MAJOR 0
#define VERSION_MINOR 1
#define VERSION_PATCH 9
#define VERSION "v0.1.9"
#define VERSION_PATCH 11
#define VERSION "v0.1.11"
#include <stddef.h>
#include <stdint.h>
@@ -262,6 +262,9 @@ int validate_sha256_format(const char* sha256);
// Admin API request handler
void handle_admin_api_request(const char* method, const char* uri, const char* validated_pubkey, int is_authenticated);
// Admin event handler (Kind 23456/23457)
void handle_admin_event_request(void);
// Individual endpoint handlers
void handle_stats_api(void);
void handle_config_get_api(void);
@@ -280,6 +283,10 @@ void send_json_response(int status, const char* json_content);
void send_json_error(int status, const char* error, const char* message);
int parse_query_params(const char* query_string, char params[][256], int max_params);
// Admin WebSocket server functions
int start_admin_websocket_server(int port);
void stop_admin_websocket_server(void);
#ifdef __cplusplus
}
#endif

View File

@@ -26,13 +26,14 @@
#define MAX_MIME_LEN 128
// Configuration variables - can be overridden via command line
char g_db_path[MAX_PATH_LEN] = "db/ginxsom.db";
char g_db_path[MAX_PATH_LEN] = ""; // Will be set based on pubkey
char g_storage_dir[MAX_PATH_LEN] = ".";
// Key management variables
char g_admin_pubkey[65] = ""; // Admin public key for authorization
char g_blossom_seckey[65] = ""; // Blossom server private key for decryption/signing
int g_generate_keys = 0; // Flag to generate keys on startup
char g_blossom_pubkey[65] = ""; // Blossom server public key (derived from seckey)
int g_generate_keys = 0; // Flag to generate keys on startup (deprecated)
// Use global configuration variables
#define DB_PATH g_db_path
@@ -192,7 +193,7 @@ int initialize_database(const char *db_path) {
" ('auth_rules_enabled', 'true', 'Whether authentication rules are enabled for uploads'),"
" ('server_name', 'ginxsom', 'Server name for responses'),"
" ('admin_pubkey', '', 'Admin public key for API access'),"
" ('admin_enabled', 'false', 'Whether admin API is enabled'),"
" ('admin_enabled', 'true', 'Whether admin API is enabled'),"
" ('nip42_require_auth', 'false', 'Enable NIP-42 challenge/response authentication'),"
" ('nip42_challenge_timeout', '600', 'NIP-42 challenge timeout in seconds'),"
" ('nip42_time_tolerance', '300', 'NIP-42 timestamp tolerance in seconds');";
@@ -245,6 +246,9 @@ int generate_server_keypair(void);
int load_server_keys(void);
int store_blossom_private_key(const char *seckey);
int get_blossom_private_key(char *seckey_out, size_t max_len);
int derive_pubkey_from_privkey(const char *privkey_hex, char *pubkey_hex_out);
int validate_database_pubkey_match(const char *db_path, const char *expected_pubkey);
int set_db_path_from_pubkey(const char *pubkey);
// External validator function declarations
const char *nostr_request_validator_get_last_violation_type(void);
@@ -258,6 +262,76 @@ void handle_delete_request_with_validation(const char *sha256, nostr_request_res
// Key management function implementations
// Derive public key from private key hex string
int derive_pubkey_from_privkey(const char *privkey_hex, char *pubkey_hex_out) {
if (!privkey_hex || strlen(privkey_hex) != 64) {
fprintf(stderr, "ERROR: Invalid private key format\n");
return -1;
}
unsigned char seckey_bytes[32];
if (nostr_hex_to_bytes(privkey_hex, seckey_bytes, 32) != NOSTR_SUCCESS) {
fprintf(stderr, "ERROR: Failed to parse private key hex\n");
return -1;
}
unsigned char pubkey_bytes[32];
if (nostr_ec_public_key_from_private_key(seckey_bytes, pubkey_bytes) != NOSTR_SUCCESS) {
fprintf(stderr, "ERROR: Failed to derive public key\n");
return -1;
}
nostr_bytes_to_hex(pubkey_bytes, 32, pubkey_hex_out);
return 0;
}
// Set database path based on pubkey (db/<pubkey>.db)
int set_db_path_from_pubkey(const char *pubkey) {
if (!pubkey || strlen(pubkey) != 64) {
fprintf(stderr, "ERROR: Invalid pubkey for database naming\n");
return -1;
}
snprintf(g_db_path, sizeof(g_db_path), "db/%s.db", pubkey);
fprintf(stderr, "DATABASE: Set database path to %s\n", g_db_path);
return 0;
}
// Validate that database filename matches the pubkey stored inside
int validate_database_pubkey_match(const char *db_path, const char *expected_pubkey) {
// Extract pubkey from database filename
const char *filename = strrchr(db_path, '/');
if (!filename) {
filename = db_path;
} else {
filename++; // Skip the '/'
}
// Check if filename matches pattern: <pubkey>.db
size_t filename_len = strlen(filename);
if (filename_len != 67) { // 64 chars + ".db" = 67
fprintf(stderr, "WARNING: Database filename doesn't match pubkey pattern: %s\n", filename);
return 0; // Don't enforce for non-standard names (backward compatibility)
}
// Extract pubkey from filename (first 64 chars)
char filename_pubkey[65];
strncpy(filename_pubkey, filename, 64);
filename_pubkey[64] = '\0';
// Compare with expected pubkey
if (strcasecmp(filename_pubkey, expected_pubkey) != 0) {
fprintf(stderr, "ERROR: Database pubkey mismatch!\n");
fprintf(stderr, " Database filename: %s\n", filename_pubkey);
fprintf(stderr, " Expected pubkey: %s\n", expected_pubkey);
fprintf(stderr, " → Database filename doesn't match the keys stored inside\n");
return -1;
}
fprintf(stderr, "DATABASE: Pubkey validation passed: %s\n", filename_pubkey);
return 0;
}
// Generate random private key bytes using /dev/urandom
int generate_random_private_key_bytes(unsigned char *key_bytes, size_t len) {
FILE *fp = fopen("/dev/urandom", "rb");
@@ -310,6 +384,26 @@ int generate_server_keypair(void) {
// Convert public key to hex
nostr_bytes_to_hex(pubkey_bytes, 32, pubkey_hex);
// Store pubkey in global variable
strncpy(g_blossom_pubkey, pubkey_hex, sizeof(g_blossom_pubkey) - 1);
g_blossom_pubkey[64] = '\0';
// Store seckey in global variable
strncpy(g_blossom_seckey, seckey_hex, sizeof(g_blossom_seckey) - 1);
g_blossom_seckey[64] = '\0';
// Set database path based on pubkey (MUST be done before storing keys)
if (set_db_path_from_pubkey(pubkey_hex) != 0) {
fprintf(stderr, "ERROR: Failed to set database path\n");
return -1;
}
// Initialize database with new path
if (initialize_database(g_db_path) != 0) {
fprintf(stderr, "ERROR: Failed to initialize database at %s\n", g_db_path);
return -1;
}
// Store private key securely
if (store_blossom_private_key(seckey_hex) != 0) {
fprintf(stderr, "ERROR: Failed to store blossom private key\n");
@@ -321,7 +415,7 @@ int generate_server_keypair(void) {
sqlite3_stmt *stmt;
int rc;
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL);
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE, NULL);
if (rc) {
fprintf(stderr, "ERROR: Can't open database for config: %s\n", sqlite3_errmsg(db));
return -1;
@@ -354,6 +448,7 @@ int generate_server_keypair(void) {
fprintf(stderr, "========================================\n");
fprintf(stderr, "Blossom Public Key: %s\n", pubkey_hex);
fprintf(stderr, "Blossom Private Key: %s\n", seckey_hex);
fprintf(stderr, "Database Path: %s\n", g_db_path);
fprintf(stderr, "========================================\n");
fprintf(stderr, "IMPORTANT: Save the private key securely!\n");
fprintf(stderr, "This key is used for decrypting admin messages.\n");
@@ -372,26 +467,30 @@ int load_server_keys(void) {
// Try to load blossom private key
fprintf(stderr, "DEBUG: Trying to load blossom private key...\n");
if (get_blossom_private_key(g_blossom_seckey, sizeof(g_blossom_seckey)) != 0) {
fprintf(stderr, "DEBUG: No blossom private key found\n");
// No private key found - check if we should generate one
if (g_generate_keys) {
fprintf(stderr, "STARTUP: No blossom private key found, generating new keypair...\n");
if (generate_server_keypair() != 0) {
fprintf(stderr, "ERROR: Failed to generate server keypair\n");
fprintf(stderr, "DEBUG: No blossom private key found in database\n");
return -1; // Keys must exist in database
}
fprintf(stderr, "STARTUP: Blossom private key loaded successfully\n");
// Derive public key from private key
if (derive_pubkey_from_privkey(g_blossom_seckey, g_blossom_pubkey) != 0) {
fprintf(stderr, "ERROR: Failed to derive public key from private key\n");
return -1;
}
} else {
fprintf(stderr, "WARNING: No blossom private key found. Use --generate-keys to create one.\n");
// This is not fatal - server can still operate without admin features
}
} else {
fprintf(stderr, "STARTUP: Blossom private key loaded successfully\n");
fprintf(stderr, "STARTUP: Derived blossom pubkey: %s\n", g_blossom_pubkey);
// Validate database filename matches pubkey
if (validate_database_pubkey_match(g_db_path, g_blossom_pubkey) != 0) {
fprintf(stderr, "ERROR: Database pubkey validation failed\n");
return -1;
}
// Load admin pubkey from command line or config
if (strlen(g_admin_pubkey) == 0) {
// Try to load from database config
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc == SQLITE_OK) {
const char *sql = "SELECT value FROM config WHERE key = 'admin_pubkey'";
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
@@ -410,7 +509,7 @@ int load_server_keys(void) {
}
} else {
// Store admin pubkey in config if provided via command line
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL);
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE, NULL);
if (rc == SQLITE_OK) {
const char *sql = "INSERT OR REPLACE INTO config (key, value, description) VALUES (?, ?, ?)";
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
@@ -441,7 +540,7 @@ int store_blossom_private_key(const char *seckey) {
}
// Create blossom_seckey table if it doesn't exist
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL);
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL);
if (rc) {
fprintf(stderr, "ERROR: Can't open database: %s\n", sqlite3_errmsg(db));
return -1;
@@ -484,7 +583,7 @@ int get_blossom_private_key(char *seckey_out, size_t max_len) {
sqlite3_stmt *stmt;
int rc;
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc) {
return -1;
}
@@ -520,7 +619,7 @@ int insert_blob_metadata(const char *sha256, long size, const char *type,
sqlite3_stmt *stmt;
int rc;
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL);
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL);
if (rc) {
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
return 0;
@@ -575,7 +674,7 @@ int get_blob_metadata(const char *sha256, blob_metadata_t *metadata) {
sqlite3_stmt *stmt;
int rc;
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY | SQLITE_OPEN_CREATE, NULL);
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY | SQLITE_OPEN_CREATE, NULL);
if (rc) {
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
return 0;
@@ -845,7 +944,7 @@ void handle_list_request(const char *pubkey) {
sqlite3_stmt *stmt;
int rc;
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc) {
send_error_response(500, "database_error", "Failed to access database",
@@ -992,7 +1091,7 @@ void handle_delete_request_with_validation(const char *sha256, nostr_request_res
sqlite3_stmt *stmt;
int rc;
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL);
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL);
if (rc) {
send_error_response(500, "database_error", "Failed to access database",
@@ -1731,16 +1830,29 @@ void handle_auth_challenge_request(void) {
/////////////////////////////////////////////////////////////////////////////////////////
int main(int argc, char *argv[]) {
// Redirect stderr to log file BEFORE any other operations
// This is necessary because spawn-fcgi doesn't preserve stderr redirections
FILE *stderr_log = freopen("logs/app/stderr.log", "a", stderr);
if (!stderr_log) {
// If redirection fails, continue anyway but log to original stderr
perror("Warning: Failed to redirect stderr to log file");
}
// Set stderr to unbuffered mode so all fprintf(stderr, ...) calls flush immediately
setvbuf(stderr, NULL, _IONBF, 0);
fprintf(stderr, "DEBUG: main() started\n");
fflush(stderr);
// Parse command line arguments
int use_test_keys = 0;
char test_server_privkey[65] = "";
char specified_db_path[MAX_PATH_LEN] = "";
int db_path_specified = 0;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--db-path") == 0 && i + 1 < argc) {
strncpy(g_db_path, argv[i + 1], sizeof(g_db_path) - 1);
strncpy(specified_db_path, argv[i + 1], sizeof(specified_db_path) - 1);
db_path_specified = 1;
i++; // Skip next argument
} else if (strcmp(argv[i], "--storage-dir") == 0 && i + 1 < argc) {
strncpy(g_storage_dir, argv[i + 1], sizeof(g_storage_dir) - 1);
@@ -1750,13 +1862,24 @@ int main(int argc, char *argv[]) {
const char *help_text =
"Usage: ginxsom-fcgi [options]\n"
"Options:\n"
" --db-path PATH Database file path (default: db/ginxsom.db)\n"
" --storage-dir DIR Storage directory for files (default: blobs)\n"
" --admin-pubkey KEY Admin public key for management (64 hex chars)\n"
" --server-privkey KEY Server private key (64 hex chars, for testing)\n"
" --db-path PATH Database file path (must match pubkey if keys exist)\n"
" --storage-dir DIR Storage directory for files (default: .)\n"
" --admin-pubkey KEY Admin public key (only used when creating new database)\n"
" --server-privkey KEY Server private key (creates new DB or validates existing)\n"
" --test-keys Use test keys from .test_keys file\n"
" --generate-keys Generate server keypair on startup\n"
" --help, -h Show this help message\n";
" --generate-keys Generate new keypair and create database (deprecated)\n"
" --help, -h Show this help message\n"
"\n"
"Database Naming:\n"
" Databases are named after the server's public key: db/<pubkey>.db\n"
" This ensures database-key consistency and prevents mismatches.\n"
"\n"
"Startup Scenarios:\n"
" 1. No arguments → Generate new keys, create db/<new_pubkey>.db\n"
" 2. --db-path <path> → Open existing database, validate keys match filename\n"
" 3. --server-privkey <key>→ Create new database with those keys\n"
" 4. --test-keys → Use test keys, create/overwrite test database\n"
" 5. --db-path + --server-privkey → Validate keys match or error\n";
ssize_t written = write(STDOUT_FILENO, help_text, strlen(help_text));
(void)written; // Suppress unused variable warning
return 0;
@@ -1770,6 +1893,7 @@ int main(int argc, char *argv[]) {
use_test_keys = 1;
} else if (strcmp(argv[i], "--generate-keys") == 0) {
g_generate_keys = 1;
fprintf(stderr, "WARNING: --generate-keys is deprecated. Keys are generated automatically when needed.\n");
} else {
fprintf(stderr, "Unknown option: %s\n", argv[i]);
fprintf(stderr, "Use --help for usage information\n");
@@ -1777,8 +1901,27 @@ int main(int argc, char *argv[]) {
}
}
// Load test keys if requested
fprintf(stderr, "STARTUP: Using storage directory: %s\n", g_storage_dir);
// CRITICAL: Initialize nostr crypto system BEFORE key operations
fprintf(stderr, "STARTUP: Initializing nostr crypto system...\r\n");
int crypto_init_result = nostr_crypto_init();
fprintf(stderr, "CRYPTO INIT RESULT: %d\r\n", crypto_init_result);
if (crypto_init_result != 0) {
fprintf(stderr, "FATAL ERROR: Failed to initialize nostr crypto system\r\n");
return 1;
}
fprintf(stderr, "STARTUP: nostr crypto system initialized successfully\r\n");
// ========================================================================
// DATABASE AND KEY INITIALIZATION - 5 SCENARIOS
// ========================================================================
// Scenario 4: Test Mode (--test-keys)
if (use_test_keys) {
fprintf(stderr, "\n=== SCENARIO 4: TEST MODE ===\n");
// Load test keys from .test_keys file
FILE *keys_file = fopen(".test_keys", "r");
if (!keys_file) {
fprintf(stderr, "ERROR: Cannot open .test_keys file\n");
@@ -1787,7 +1930,6 @@ int main(int argc, char *argv[]) {
char line[256];
while (fgets(line, sizeof(line), keys_file)) {
// Parse ADMIN_PUBKEY='...'
if (strncmp(line, "ADMIN_PUBKEY='", 14) == 0) {
char *start = line + 14;
char *end = strchr(start, '\'');
@@ -1796,122 +1938,248 @@ int main(int argc, char *argv[]) {
g_admin_pubkey[64] = '\0';
}
}
// Parse SERVER_PRIVKEY='...'
else if (strncmp(line, "SERVER_PRIVKEY='", 16) == 0) {
char *start = line + 16;
char *end = strchr(start, '\'');
if (end && (end - start) == 64) {
strncpy(test_server_privkey, start, 64);
test_server_privkey[64] = '\0';
fprintf(stderr, "TEST MODE: Parsed SERVER_PRIVKEY: %s\n", test_server_privkey);
} else {
fprintf(stderr, "TEST MODE: Failed to parse SERVER_PRIVKEY (length: %ld)\n", end ? (long)(end - start) : -1L);
}
}
}
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, "TEST MODE: Loaded keys from .test_keys\n");
fprintf(stderr, "TEST MODE: Admin pubkey: %s\n", g_admin_pubkey);
// Derive pubkey from test privkey
if (derive_pubkey_from_privkey(test_server_privkey, g_blossom_pubkey) != 0) {
fprintf(stderr, "ERROR: Failed to derive pubkey from test privkey\n");
return 1;
}
fprintf(stderr, "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, "TEST MODE: Server pubkey: %s\n", g_blossom_pubkey);
// Set database path based on test pubkey
if (set_db_path_from_pubkey(g_blossom_pubkey) != 0) {
fprintf(stderr, "ERROR: Failed to set database path\n");
return 1;
}
fprintf(stderr, "DEBUG: About to initialize database\n");
// Test mode ALWAYS overwrites database for clean testing
fprintf(stderr, "TEST MODE: Creating/overwriting database: %s\n", g_db_path);
unlink(g_db_path); // Remove if exists
// Initialize database (create if doesn't exist)
fprintf(stderr, "STARTUP: Initializing database...\n");
// Initialize new database
if (initialize_database(g_db_path) != 0) {
fprintf(stderr, "FATAL ERROR: Failed to initialize database\n");
fprintf(stderr, "ERROR: Failed to initialize test 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
// Store test keys
strncpy(g_blossom_seckey, test_server_privkey, sizeof(g_blossom_seckey) - 1);
g_blossom_seckey[64] = '\0';
fprintf(stderr, "STARTUP: Storing test server private key in database...\n");
if (store_blossom_private_key(test_server_privkey) != 0) {
fprintf(stderr, "ERROR: Failed to store test private key\n");
return 1;
}
// Derive and store public key
unsigned char seckey_bytes[32];
if (nostr_hex_to_bytes(test_server_privkey, seckey_bytes, 32) != NOSTR_SUCCESS) {
fprintf(stderr, "ERROR: Failed to parse test private key\n");
return 1;
}
unsigned char pubkey_bytes[32];
if (nostr_ec_public_key_from_private_key(seckey_bytes, pubkey_bytes) != NOSTR_SUCCESS) {
fprintf(stderr, "ERROR: Failed to derive public key from test private key\n");
return 1;
}
char pubkey_hex[65];
nostr_bytes_to_hex(pubkey_bytes, 32, pubkey_hex);
// Store server public key in config
// Store pubkey and admin pubkey in config
sqlite3 *db;
sqlite3_stmt *stmt;
int rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READWRITE, NULL);
int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE, NULL);
if (rc == SQLITE_OK) {
const char *sql = "INSERT OR REPLACE INTO config (key, value, description) VALUES (?, ?, ?)";
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, "blossom_pubkey", -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, pubkey_hex, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, g_blossom_pubkey, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 3, "Blossom server's public key (TEST MODE)", -1, SQLITE_STATIC);
sqlite3_step(stmt);
sqlite3_finalize(stmt);
}
if (strlen(g_admin_pubkey) > 0) {
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, "admin_pubkey", -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, g_admin_pubkey, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 3, "Admin public key (TEST MODE)", -1, SQLITE_STATIC);
sqlite3_step(stmt);
sqlite3_finalize(stmt);
}
}
sqlite3_close(db);
}
fprintf(stderr, "STARTUP: Test server keys stored in database\n");
fprintf(stderr, "STARTUP: Server pubkey: %s\n", pubkey_hex);
fprintf(stderr, "STARTUP: Admin pubkey: %s\n", g_admin_pubkey);
// 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");
fprintf(stderr, "TEST MODE: Database initialized successfully\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");
// Scenario 3: Keys Specified (--server-privkey)
else if (test_server_privkey[0] != '\0') {
fprintf(stderr, "\n=== SCENARIO 3: KEYS SPECIFIED ===\n");
// Derive pubkey from provided privkey
if (derive_pubkey_from_privkey(test_server_privkey, g_blossom_pubkey) != 0) {
fprintf(stderr, "ERROR: Invalid server private key\n");
return 1;
}
fprintf(stderr, "STARTUP: Server keys initialized successfully\n");
fprintf(stderr, "KEYS: Derived pubkey: %s\n", g_blossom_pubkey);
// Scenario 5: Both database and keys specified - validate match
if (db_path_specified) {
fprintf(stderr, "\n=== SCENARIO 5: DATABASE + KEYS (VALIDATION) ===\n");
strncpy(g_db_path, specified_db_path, sizeof(g_db_path) - 1);
g_db_path[sizeof(g_db_path) - 1] = '\0';
// Check if database exists
struct stat st;
if (stat(g_db_path, &st) != 0) {
fprintf(stderr, "ERROR: Database file not found: %s\n", g_db_path);
return 1;
}
// Load keys from database
if (get_blossom_private_key(g_blossom_seckey, sizeof(g_blossom_seckey)) != 0) {
fprintf(stderr, "ERROR: Invalid database: missing server keys\n");
return 1;
}
// Compare with provided key
if (strcmp(g_blossom_seckey, test_server_privkey) != 0) {
fprintf(stderr, "ERROR: Server private key doesn't match database\n");
fprintf(stderr, " Provided key and database keys are different\n");
return 1;
}
fprintf(stderr, "VALIDATION: Keys match database - continuing\n");
// Validate pubkey matches filename
if (validate_database_pubkey_match(g_db_path, g_blossom_pubkey) != 0) {
return 1;
}
}
// Scenario 3 continued: Create new database with provided keys
else {
// Set database path based on derived pubkey
if (set_db_path_from_pubkey(g_blossom_pubkey) != 0) {
fprintf(stderr, "ERROR: Failed to set database path\n");
return 1;
}
// Check if database already exists
struct stat st;
if (stat(g_db_path, &st) == 0) {
fprintf(stderr, "ERROR: Database already exists for this pubkey: %s\n", g_db_path);
fprintf(stderr, " Use --db-path to open existing database or use different keys\n");
return 1;
}
// Initialize new database
if (initialize_database(g_db_path) != 0) {
fprintf(stderr, "ERROR: Failed to initialize database\n");
return 1;
}
// Store keys
strncpy(g_blossom_seckey, test_server_privkey, sizeof(g_blossom_seckey) - 1);
g_blossom_seckey[64] = '\0';
if (store_blossom_private_key(test_server_privkey) != 0) {
fprintf(stderr, "ERROR: Failed to store private key\n");
return 1;
}
// Store pubkey in config
sqlite3 *db;
sqlite3_stmt *stmt;
int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READWRITE, NULL);
if (rc == SQLITE_OK) {
const char *sql = "INSERT OR REPLACE INTO config (key, value, description) VALUES (?, ?, ?)";
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, "blossom_pubkey", -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, g_blossom_pubkey, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 3, "Blossom server's public key", -1, SQLITE_STATIC);
sqlite3_step(stmt);
sqlite3_finalize(stmt);
}
if (strlen(g_admin_pubkey) > 0) {
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, "admin_pubkey", -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, g_admin_pubkey, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 3, "Admin public key", -1, SQLITE_STATIC);
sqlite3_step(stmt);
sqlite3_finalize(stmt);
}
}
sqlite3_close(db);
}
fprintf(stderr, "KEYS: New database created successfully\n");
}
}
// Scenario 2: Database Specified (--db-path)
else if (db_path_specified) {
fprintf(stderr, "\n=== SCENARIO 2: DATABASE SPECIFIED ===\n");
strncpy(g_db_path, specified_db_path, sizeof(g_db_path) - 1);
g_db_path[sizeof(g_db_path) - 1] = '\0';
// Check if database exists
struct stat st;
if (stat(g_db_path, &st) != 0) {
fprintf(stderr, "ERROR: Database file not found: %s\n", g_db_path);
fprintf(stderr, " → Specify a different database or let the application create a new one\n");
return 1;
}
fprintf(stderr, "DATABASE: Opening existing database: %s\n", g_db_path);
// Load keys from database
if (load_server_keys() != 0) {
fprintf(stderr, "ERROR: Failed to load keys from database\n");
fprintf(stderr, " → Database may be corrupted or not a valid ginxsom database\n");
return 1;
}
fprintf(stderr, "DATABASE: Keys loaded and validated successfully\n");
}
// Scenario 1: No Arguments (Fresh Start)
else {
fprintf(stderr, "\n=== SCENARIO 1: FRESH START (NO ARGUMENTS) ===\n");
fprintf(stderr, "FRESH START: Generating new server keypair...\n");
// Generate new keypair (this will set g_db_path based on pubkey)
if (generate_server_keypair() != 0) {
fprintf(stderr, "ERROR: Failed to generate server keypair\n");
return 1;
}
fprintf(stderr, "FRESH START: New instance created successfully\n");
}
// ========================================================================
// END DATABASE AND KEY INITIALIZATION
// ========================================================================
fprintf(stderr, "\n=== FINAL CONFIGURATION ===\n");
fprintf(stderr, "Database path: %s\n", g_db_path);
fprintf(stderr, "Storage directory: %s\n", g_storage_dir);
fprintf(stderr, "Server pubkey: %s\n", g_blossom_pubkey);
if (strlen(g_admin_pubkey) > 0) {
fprintf(stderr, "Admin pubkey: %s\n", g_admin_pubkey);
}
fprintf(stderr, "===========================\n\n");
fflush(stderr);
// If --generate-keys was specified, exit after key generation
@@ -1940,7 +2208,7 @@ if (!config_loaded /* && !initialize_server_config() */) {
// Initialize request validator system
fprintf(stderr, "STARTUP: Initializing request validator system...\r\n");
int validator_init_result =
ginxsom_request_validator_init(DB_PATH, "ginxsom");
ginxsom_request_validator_init(g_db_path, "ginxsom");
fprintf(stderr, "MAIN: validator init return code: %d\r\n",
validator_init_result);
if (validator_init_result != NOSTR_SUCCESS) {
@@ -1952,6 +2220,32 @@ if (!config_loaded /* && !initialize_server_config() */) {
"STARTUP: Request validator system initialized successfully\r\n");
fflush(stderr);
// Start WebSocket admin server if enabled
sqlite3 *db;
sqlite3_stmt *stmt;
int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc == SQLITE_OK) {
const char *sql = "SELECT value FROM config WHERE key = 'admin_enabled'";
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
const char *admin_enabled = (const char *)sqlite3_column_text(stmt, 0);
if (admin_enabled && (strcmp(admin_enabled, "true") == 0 || strcmp(admin_enabled, "1") == 0)) {
fprintf(stderr, "STARTUP: Starting WebSocket admin server on port 9442...\n");
if (start_admin_websocket_server(9442) == 0) {
fprintf(stderr, "STARTUP: WebSocket admin server started successfully\n");
} else {
fprintf(stderr, "WARNING: Failed to start WebSocket admin server\n");
}
} else {
fprintf(stderr, "STARTUP: Admin interface disabled in config\n");
}
}
sqlite3_finalize(stmt);
}
sqlite3_close(db);
}
/////////////////////////////////////////////////////////////////////
// THIS IS WHERE THE REQUESTS ENTER THE FastCGI
@@ -2054,6 +2348,11 @@ if (!config_loaded /* && !initialize_server_config() */) {
operation = "report";
} else if (strncmp(request_uri, "/api/", 5) == 0) {
operation = "admin";
// Special case: POST /api/admin uses Kind 23456 events for authentication
// Skip centralized validation for these requests
if (strcmp(request_method, "POST") == 0 && strcmp(request_uri, "/api/admin") == 0) {
operation = "admin_event"; // Mark as special case
}
} else if (strcmp(request_method, "GET") == 0 && strncmp(request_uri, "/list/", 6) == 0) {
operation = "list";
} else if (strcmp(request_method, "GET") == 0 && strcmp(request_uri, "/auth") == 0) {
@@ -2095,6 +2394,9 @@ if (!config_loaded /* && !initialize_server_config() */) {
// List operation might be optional auth - let handler decide
} else if (strcmp(operation, "admin") == 0 && strcmp(request_uri, "/api/health") == 0) {
// Health endpoint is public and doesn't require authentication - let handler decide
} else if (strcmp(operation, "admin_event") == 0) {
// POST /api/admin uses Kind 23456 events - authentication handled by admin_event.c
// Skip centralized validation and let the handler validate the event
} else {
// For other operations, validation failure means auth failure
const char *message = result.reason[0] ? result.reason : "Authentication failed";

View File

@@ -32,8 +32,8 @@
// NOSTR_ERROR_NIP42_CHALLENGE_EXPIRED are already defined in
// nostr_core_lib/nostr_core/nostr_common.h
// Database path (consistent with main.c)
#define DB_PATH "db/ginxsom.db"
// Use global database path from main.c
extern char g_db_path[];
// NIP-42 challenge management constants
#define MAX_CHALLENGES 1000
@@ -1064,7 +1064,7 @@ static int reload_auth_config(void) {
memset(&g_auth_cache, 0, sizeof(g_auth_cache));
// Open database
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc != SQLITE_OK) {
validator_debug_log("VALIDATOR: Could not open database\n");
// Use defaults
@@ -1345,7 +1345,7 @@ static int check_database_auth_rules(const char *pubkey, const char *operation,
validator_debug_log(rules_msg);
// Open database
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc != SQLITE_OK) {
validator_debug_log(
"VALIDATOR_DEBUG: RULES ENGINE - Failed to open database\n");

206
tests/admin_event_test.sh Executable file
View File

@@ -0,0 +1,206 @@
#!/bin/bash
# Ginxsom Admin Event Test Script
# Tests Kind 23456/23457 admin command system with NIP-44 encryption
#
# Prerequisites:
# - nak: https://github.com/fiatjaf/nak
# - curl
# - jq (for JSON parsing)
# - Server running with test keys from .test_keys
set -e
# Configuration
GINXSOM_URL="http://localhost:9001"
TEST_KEYS_FILE=".test_keys"
# Load test keys
if [[ ! -f "$TEST_KEYS_FILE" ]]; then
echo "ERROR: $TEST_KEYS_FILE not found"
echo "Run the server with --test-keys to generate test keys"
exit 1
fi
source "$TEST_KEYS_FILE"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Helper functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
check_dependencies() {
log_info "Checking dependencies..."
for cmd in nak curl jq; do
if ! command -v $cmd &> /dev/null; then
log_error "$cmd is not installed"
case $cmd in
nak)
echo "Install from: https://github.com/fiatjaf/nak"
;;
jq)
echo "Install jq for JSON processing"
;;
curl)
echo "curl should be available in most systems"
;;
esac
exit 1
fi
done
log_success "All dependencies found"
}
# Create NIP-44 encrypted admin command event (Kind 23456)
create_admin_command_event() {
local command="$1"
local expiration=$(($(date +%s) + 3600)) # 1 hour from now
log_info "Creating Kind 23456 admin command event..."
log_info "Command: $command"
# For now, we'll create the event structure manually since nak may not support NIP-44 encryption yet
# The content should be NIP-44 encrypted JSON array: ["config_query"]
# We'll use plaintext for initial testing and add encryption later
local content="[\"$command\"]"
# Create event with nak
# Kind 23456 = admin command
# Tags: p = server pubkey, expiration
local event=$(nak event -k 23456 \
-c "$content" \
--tag p="$SERVER_PUBKEY" \
--tag expiration="$expiration" \
--sec "$ADMIN_PRIVKEY")
echo "$event"
}
# Send admin command and parse response
send_admin_command() {
local command="$1"
log_info "=== Testing Admin Command: $command ==="
# Create Kind 23456 event
local event=$(create_admin_command_event "$command")
if [[ -z "$event" ]]; then
log_error "Failed to create admin event"
return 1
fi
log_info "Event created successfully"
echo "$event" | jq . || echo "$event"
# Send to server
log_info "Sending to POST $GINXSOM_URL/api/admin"
local response=$(curl -s -w "\n%{http_code}" \
-X POST \
-H "Content-Type: application/json" \
-d "$event" \
"$GINXSOM_URL/api/admin")
local http_code=$(echo "$response" | tail -n1)
local body=$(echo "$response" | head -n-1)
echo ""
if [[ "$http_code" =~ ^2 ]]; then
log_success "HTTP $http_code - Response received"
echo "$body" | jq . 2>/dev/null || echo "$body"
# Try to parse as Kind 23457 event
local kind=$(echo "$body" | jq -r '.kind // empty' 2>/dev/null)
if [[ "$kind" == "23457" ]]; then
log_success "Received Kind 23457 response event"
local response_content=$(echo "$body" | jq -r '.content // empty' 2>/dev/null)
log_info "Response content (encrypted): $response_content"
# TODO: Decrypt NIP-44 content to see actual response
fi
else
log_error "HTTP $http_code - Request failed"
echo "$body" | jq . 2>/dev/null || echo "$body"
return 1
fi
echo ""
}
test_config_query() {
log_info "=== Testing config_query Command ==="
send_admin_command "config_query"
}
test_server_health() {
log_info "=== Testing Server Health ==="
local response=$(curl -s -w "\n%{http_code}" "$GINXSOM_URL/api/health")
local http_code=$(echo "$response" | tail -n1)
local body=$(echo "$response" | head -n-1)
if [[ "$http_code" =~ ^2 ]]; then
log_success "Server is healthy (HTTP $http_code)"
echo "$body" | jq .
else
log_error "Server health check failed (HTTP $http_code)"
echo "$body"
return 1
fi
echo ""
}
main() {
echo "=== Ginxsom Admin Event Test Suite ==="
echo "Testing Kind 23456/23457 admin command system"
echo ""
log_info "Test Configuration:"
log_info " Admin Pubkey: $ADMIN_PUBKEY"
log_info " Server Pubkey: $SERVER_PUBKEY"
log_info " Server URL: $GINXSOM_URL"
echo ""
check_dependencies
echo ""
# Test server health first
test_server_health
# Test admin commands
test_config_query
echo ""
log_success "Admin event testing complete!"
echo ""
log_warning "NOTE: NIP-44 encryption not yet implemented in test script"
log_warning "Events are sent with plaintext command arrays for initial testing"
log_warning "Production implementation will use full NIP-44 encryption"
}
# Allow sourcing for individual function testing
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi

397
tests/websocket_admin_test.sh Executable file
View File

@@ -0,0 +1,397 @@
#!/bin/bash
# Ginxsom WebSocket Admin Test Script
# Tests Kind 23456/23457 admin command system over WebSocket with NIP-44 encryption
#
# Prerequisites:
# - websocat: WebSocket client (https://github.com/vi/websocat)
# - nak: Nostr Army Knife (https://github.com/fiatjaf/nak)
# - jq: JSON processor
# - Server running with test keys from .test_keys
set -e
# Configuration
WEBSOCKET_URL="wss://localhost:9443/admin" # Secure WebSocket via nginx HTTPS
WEBSOCKET_HTTP_URL="ws://localhost:9001/admin" # Non-secure WebSocket via nginx HTTP
WEBSOCKET_DIRECT_URL="ws://localhost:9442" # Direct connection to WebSocket server (port 9442)
TEST_KEYS_FILE=".test_keys"
TIMEOUT=10 # WebSocket connection timeout in seconds
# Load test keys
if [[ ! -f "$TEST_KEYS_FILE" ]]; then
echo "ERROR: $TEST_KEYS_FILE not found"
echo "Run the server with --test-keys to generate test keys"
exit 1
fi
source "$TEST_KEYS_FILE"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# Helper functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_debug() {
echo -e "${CYAN}[DEBUG]${NC} $1"
}
check_dependencies() {
log_info "Checking dependencies..."
for cmd in websocat nak jq; do
if ! command -v $cmd &> /dev/null; then
log_error "$cmd is not installed"
case $cmd in
websocat)
echo "Install from: https://github.com/vi/websocat"
echo " cargo install websocat"
;;
nak)
echo "Install from: https://github.com/fiatjaf/nak"
echo " go install github.com/fiatjaf/nak@latest"
;;
jq)
echo "Install jq for JSON processing"
echo " apt-get install jq # Debian/Ubuntu"
;;
esac
exit 1
fi
done
log_success "All dependencies found"
log_info " websocat: $(websocat --version 2>&1 | head -n1)"
log_info " nak: $(nak --version 2>&1 | head -n1)"
log_info " jq: $(jq --version 2>&1)"
}
# Test basic WebSocket connection
test_websocket_connection() {
local url="$1"
log_info "=== Testing WebSocket Connection ==="
log_info "Connecting to: $url"
# For wss:// connections, add --insecure flag to skip certificate verification
local websocat_opts=""
if [[ "$url" == wss://* ]]; then
websocat_opts="--insecure"
log_debug "Using --insecure flag for self-signed certificate"
fi
# Try to connect and send a ping
local result=$(timeout $TIMEOUT websocat $websocat_opts -n1 "$url" <<< '{"test":"ping"}' 2>&1 || echo "TIMEOUT")
if [[ "$result" == "TIMEOUT" ]]; then
log_error "Connection timeout after ${TIMEOUT}s"
return 1
elif [[ -z "$result" ]]; then
log_warning "Connected but no response (this may be normal for WebSocket)"
return 0
else
log_success "Connection established"
log_debug "Response: $result"
return 0
fi
}
# Create NIP-44 encrypted admin command event (Kind 23456)
create_admin_command_event() {
local command="$1"
local expiration=$(($(date +%s) + 3600)) # 1 hour from now
log_info "Creating Kind 23456 admin command event..."
log_info "Command: $command"
# Content is a JSON array of commands
local content="[\"$command\"]"
# Create event with nak
# Kind 23456 = admin command
# Tags: p = server pubkey, expiration
local event=$(nak event -k 23456 \
-c "$content" \
--tag p="$SERVER_PUBKEY" \
--tag expiration="$expiration" \
--sec "$ADMIN_PRIVKEY" 2>&1)
if [[ $? -ne 0 ]]; then
log_error "Failed to create event with nak"
log_error "$event"
return 1
fi
echo "$event"
}
# Send admin command via WebSocket and wait for response
send_websocket_admin_command() {
local command="$1"
local url="$2"
log_info "=== Testing Admin Command via WebSocket: $command ==="
# Create Kind 23456 event
local event=$(create_admin_command_event "$command")
if [[ -z "$event" ]]; then
log_error "Failed to create admin event"
return 1
fi
log_success "Event created successfully"
log_debug "Event JSON:"
echo "$event" | jq -C . 2>/dev/null || echo "$event"
echo ""
# Send to WebSocket server and wait for response
log_info "Sending to WebSocket: $url"
log_info "Waiting for Kind 23457 response (timeout: ${TIMEOUT}s)..."
# For wss:// connections, add --insecure flag to skip certificate verification
local websocat_opts=""
if [[ "$url" == wss://* ]]; then
websocat_opts="--insecure"
log_debug "Using --insecure flag for self-signed certificate"
fi
# Use websocat to send event and receive response
local response=$(timeout $TIMEOUT websocat $websocat_opts -n1 "$url" <<< "$event" 2>&1)
local exit_code=$?
echo ""
if [[ $exit_code -eq 124 ]]; then
log_error "Timeout waiting for response after ${TIMEOUT}s"
return 1
elif [[ $exit_code -ne 0 ]]; then
log_error "WebSocket connection failed (exit code: $exit_code)"
log_error "$response"
return 1
fi
if [[ -z "$response" ]]; then
log_warning "No response received (connection may have closed)"
return 1
fi
log_success "Response received"
log_debug "Raw response:"
echo "$response"
echo ""
# Try to parse as JSON
if echo "$response" | jq . &>/dev/null; then
log_success "Valid JSON response"
# Check if it's a Kind 23457 event
local kind=$(echo "$response" | jq -r '.kind // empty' 2>/dev/null)
if [[ "$kind" == "23457" ]]; then
log_success "Received Kind 23457 response event ✓"
# Extract and display response details
local response_id=$(echo "$response" | jq -r '.id // empty')
local response_pubkey=$(echo "$response" | jq -r '.pubkey // empty')
local response_content=$(echo "$response" | jq -r '.content // empty')
local response_sig=$(echo "$response" | jq -r '.sig // empty')
echo ""
log_info "Response Event Details:"
log_info " ID: $response_id"
log_info " Pubkey: $response_pubkey"
log_info " Content: $response_content"
log_info " Sig: ${response_sig:0:32}..."
# Check if content is encrypted (NIP-44)
if [[ ${#response_content} -gt 50 ]]; then
log_info " Content appears to be NIP-44 encrypted"
log_warning " Decryption not yet implemented in test script"
else
log_info " Content (plaintext): $response_content"
fi
# Verify signature
log_info "Verifying event signature..."
if echo "$response" | nak verify 2>&1 | grep -q "signature is valid"; then
log_success "Event signature is valid ✓"
else
log_error "Event signature verification failed"
return 1
fi
else
log_warning "Response is not Kind 23457 (got kind: $kind)"
fi
# Pretty print the full response
echo ""
log_info "Full Response Event:"
echo "$response" | jq -C .
else
log_warning "Response is not valid JSON"
log_debug "Raw response: $response"
fi
echo ""
return 0
}
# Test config_query command
test_config_query() {
log_info "=== Testing config_query Command ==="
send_websocket_admin_command "config_query" "$WEBSOCKET_URL"
}
# Test with HTTP WebSocket connection
test_http_connection() {
log_info "=== Testing HTTP WebSocket Connection ==="
log_info "Connecting via HTTP (port 9001)"
send_websocket_admin_command "config_query" "$WEBSOCKET_HTTP_URL"
}
# Test with direct WebSocket connection (bypassing nginx)
test_direct_connection() {
log_info "=== Testing Direct WebSocket Connection ==="
log_info "Connecting directly to WebSocket server (port 9442)"
send_websocket_admin_command "config_query" "$WEBSOCKET_DIRECT_URL"
}
# Test invalid command
test_invalid_command() {
log_info "=== Testing Invalid Command ==="
send_websocket_admin_command "invalid_command_xyz" "$WEBSOCKET_URL" || log_warning "Expected failure for invalid command"
}
# Test connection persistence
test_connection_persistence() {
log_info "=== Testing Connection Persistence ==="
log_info "Sending multiple commands over same connection..."
# Create two events
local event1=$(create_admin_command_event "config_query")
local event2=$(create_admin_command_event "config_query")
if [[ -z "$event1" ]] || [[ -z "$event2" ]]; then
log_error "Failed to create events"
return 1
fi
# For wss:// connections, add --insecure flag
local websocat_opts=""
if [[ "$WEBSOCKET_URL" == wss://* ]]; then
websocat_opts="--insecure"
fi
# Send both events and collect responses
log_info "Sending two events sequentially..."
local responses=$(timeout $((TIMEOUT * 2)) websocat $websocat_opts -n2 "$WEBSOCKET_URL" <<EOF
$event1
$event2
EOF
)
if [[ $? -eq 0 ]]; then
log_success "Received responses for both events"
echo "$responses" | while IFS= read -r line; do
if [[ -n "$line" ]]; then
echo "$line" | jq -C . 2>/dev/null || echo "$line"
fi
done
else
log_warning "Connection persistence test inconclusive"
fi
echo ""
}
main() {
echo "=========================================="
echo " Ginxsom WebSocket Admin Test Suite"
echo " Kind 23456/23457 over WebSocket"
echo "=========================================="
echo ""
log_info "Test Configuration:"
log_info " Admin Privkey: ${ADMIN_PRIVKEY:0:16}...${ADMIN_PRIVKEY: -16}"
log_info " Admin Pubkey: $ADMIN_PUBKEY"
log_info " Server Pubkey: $SERVER_PUBKEY"
log_info " HTTPS URL: $WEBSOCKET_URL"
log_info " HTTP URL: $WEBSOCKET_HTTP_URL"
log_info " Direct URL: $WEBSOCKET_DIRECT_URL"
log_info " Timeout: ${TIMEOUT}s"
echo ""
check_dependencies
echo ""
# Test basic WebSocket connectivity
if ! test_websocket_connection "$WEBSOCKET_URL"; then
log_error "Basic WebSocket connection failed"
log_info "Trying direct connection to port 9442..."
if ! test_websocket_connection "$WEBSOCKET_DIRECT_URL"; then
log_error "Direct connection also failed"
log_error "Make sure the server is running with WebSocket admin enabled"
exit 1
fi
fi
echo ""
# Test admin commands via HTTPS
test_config_query
echo ""
# Test via HTTP
test_http_connection
echo ""
# Test direct connection (bypassing nginx)
test_direct_connection
echo ""
# Test invalid command
test_invalid_command
echo ""
# Test connection persistence
test_connection_persistence
echo ""
echo "=========================================="
log_success "WebSocket admin testing complete!"
echo "=========================================="
echo ""
log_info "Summary:"
log_info " ✓ WebSocket connection established"
log_info " ✓ Kind 23456 events sent"
log_info " ✓ Kind 23457 responses received"
log_info " ✓ Event signatures verified"
echo ""
log_warning "NOTE: NIP-44 encryption/decryption not yet implemented in test script"
log_warning "Events use plaintext command arrays for initial testing"
log_warning "Production implementation uses full NIP-44 encryption"
}
# Allow sourcing for individual function testing
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi