v0.1.9 - program generates it's own private keys.

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