6 Commits

Author SHA1 Message Date
Your Name
db7621a293 v0.1.10 - In the middle of working on getting admin api working 2025-11-21 11:54:17 -04:00
Your Name
e693fe3caa v0.1.9 - program generates it's own private keys. 2025-11-20 07:53:58 -04:00
Your Name
c1b615de32 v0.1.8 - Removed cache functionality for now 2025-11-13 10:59:14 -04:00
Your Name
455aab1eac v0.1.7 - Fixing black and white lists 2025-11-13 10:21:26 -04:00
Your Name
533c7f29f2 v0.1.6 - Just catching up 2025-11-11 17:02:14 -04:00
Your Name
35f8385508 v0.1.5 - Make versioning system 2025-11-11 07:16:33 -04:00
71 changed files with 7091 additions and 265 deletions

View File

@@ -1,4 +0,0 @@
ADMIN_PRIVKEY='31d3fd4bb38f4f6b60fb66e0a2e5063703bb3394579ce820d5aaf3773b96633f'
ADMIN_PUBKEY='bd109762a8185716ec0fe0f887e911c30d40e36cf7b6bb99f6eef3301e9f6f99'
SERVER_PRIVKEY='c4e0d2ed7d36277d6698650f68a6e9199f91f3abb476a67f07303e81309c48f1'
SERVER_PUBKEY='52e366edfa4e9cc6a6d4653828e51ccf828a2f5a05227d7a768f33b5a198681a'

1
.gitignore vendored
View File

@@ -2,4 +2,5 @@ blossom/
logs/
nostr_core_lib/
blobs/
c-relay/

4
.test_keys Normal file
View File

@@ -0,0 +1,4 @@
ADMIN_PRIVKEY='22cc83aa57928a2800234c939240c9a6f0f44a33ea3838a860ed38930b195afd'
ADMIN_PUBKEY='8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e'
SERVER_PRIVKEY='c4e0d2ed7d36277d6698650f68a6e9199f91f3abb476a67f07303e81309c48f1'
SERVER_PUBKEY='ebe82fbff0ff79b2973892eb285cafc767863e434f894838a548580266b70254'

View File

@@ -8,7 +8,7 @@ BUILDDIR = build
TARGET = $(BUILDDIR)/ginxsom-fcgi
# Source files
SOURCES = $(SRCDIR)/main.c $(SRCDIR)/admin_api.c $(SRCDIR)/bud04.c $(SRCDIR)/bud06.c $(SRCDIR)/bud08.c $(SRCDIR)/bud09.c $(SRCDIR)/request_validator.c
SOURCES = $(SRCDIR)/main.c $(SRCDIR)/admin_api.c $(SRCDIR)/admin_auth.c $(SRCDIR)/admin_event.c $(SRCDIR)/admin_websocket.c $(SRCDIR)/admin_handlers.c $(SRCDIR)/bud04.c $(SRCDIR)/bud06.c $(SRCDIR)/bud08.c $(SRCDIR)/bud09.c $(SRCDIR)/request_validator.c
OBJECTS = $(SOURCES:$(SRCDIR)/%.c=$(BUILDDIR)/%.o)
# Default target

File diff suppressed because it is too large Load Diff

View File

@@ -38,14 +38,48 @@ INSERT OR IGNORE INTO config (key, value, description) VALUES
('auth_rules_enabled', 'false', 'Whether authentication rules are enabled for uploads'),
('server_name', 'ginxsom', 'Server name for responses'),
('admin_pubkey', '', 'Admin public key for API access'),
('admin_enabled', 'false', 'Whether admin API is enabled'),
('admin_enabled', 'true', 'Whether admin API is enabled'),
('nip42_require_auth', 'false', 'Enable NIP-42 challenge/response authentication'),
('nip42_challenge_timeout', '600', 'NIP-42 challenge timeout in seconds'),
('nip42_time_tolerance', '300', 'NIP-42 timestamp tolerance in seconds');
-- Authentication rules table for whitelist/blacklist functionality
CREATE TABLE IF NOT EXISTS auth_rules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
rule_type TEXT NOT NULL, -- 'pubkey_blacklist', 'pubkey_whitelist',
-- 'hash_blacklist', 'mime_blacklist', 'mime_whitelist'
rule_target TEXT NOT NULL, -- The pubkey, hash, or MIME type to match
operation TEXT NOT NULL DEFAULT '*', -- 'upload', 'delete', 'list', or '*' for all
enabled INTEGER NOT NULL DEFAULT 1, -- 1 = enabled, 0 = disabled
priority INTEGER NOT NULL DEFAULT 100,-- Lower number = higher priority
description TEXT, -- Human-readable description
created_by TEXT, -- Admin pubkey who created the rule
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
-- Constraints
CHECK (rule_type IN ('pubkey_blacklist', 'pubkey_whitelist',
'hash_blacklist', 'mime_blacklist', 'mime_whitelist')),
CHECK (operation IN ('upload', 'delete', 'list', '*')),
CHECK (enabled IN (0, 1)),
CHECK (priority >= 0),
-- Unique constraint: one rule per type/target/operation combination
UNIQUE(rule_type, rule_target, operation)
);
-- Indexes for performance optimization
CREATE INDEX IF NOT EXISTS idx_auth_rules_type_target ON auth_rules(rule_type, rule_target);
CREATE INDEX IF NOT EXISTS idx_auth_rules_operation ON auth_rules(operation);
CREATE INDEX IF NOT EXISTS idx_auth_rules_enabled ON auth_rules(enabled);
CREATE INDEX IF NOT EXISTS idx_auth_rules_priority ON auth_rules(priority);
CREATE INDEX IF NOT EXISTS idx_auth_rules_type_operation ON auth_rules(rule_type, operation, enabled);
-- View for storage statistics
CREATE VIEW IF NOT EXISTS storage_stats AS
SELECT
SELECT
COUNT(*) as total_blobs,
SUM(size) as total_bytes,
AVG(size) as avg_blob_size,

Binary file not shown.

BIN
build/admin_auth.o Normal file

Binary file not shown.

BIN
build/admin_event.o Normal file

Binary file not shown.

BIN
build/admin_handlers.o Normal file

Binary file not shown.

BIN
build/admin_websocket.o Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -2,7 +2,7 @@
# Comprehensive Blossom Protocol Implementation
# Main context - specify error log here to override system default
error_log logs/nginx/error.log debug;
error_log logs/nginx/error.log info;
pid logs/nginx/nginx.pid;
events {
@@ -221,7 +221,7 @@ http {
# Admin API endpoints (/api/*)
location /api/ {
if ($request_method !~ ^(GET|PUT)$) {
if ($request_method !~ ^(GET|PUT|POST)$) {
return 405;
}
fastcgi_pass fastcgi_backend;
@@ -351,14 +351,33 @@ http {
autoindex_format json;
}
# Root redirect - Server info endpoint
# Root endpoint - Server info from FastCGI
location = / {
return 200 '{\n "server": "ginxsom",\n "version": "1.0.0",\n "description": "Ginxsom Blossom Server",\n "endpoints": {\n "blob_get": "GET /<sha256>",\n "blob_head": "HEAD /<sha256>",\n "upload": "PUT /upload",\n "upload_requirements": "HEAD /upload",\n "list": "GET /list/<pubkey>",\n "delete": "DELETE /<sha256>",\n "mirror": "PUT /mirror",\n "report": "PUT /report",\n "health": "GET /health"\n },\n "supported_buds": [\n "BUD-01",\n "BUD-02",\n "BUD-04",\n "BUD-06",\n "BUD-08",\n "BUD-09"\n ],\n "limits": {\n "max_upload_size": 104857600,\n "supported_mime_types": [\n "image/jpeg",\n "image/png",\n "image/webp",\n "image/gif",\n "video/mp4",\n "video/webm",\n "audio/mpeg",\n "audio/ogg",\n "text/plain",\n "application/pdf"\n ]\n },\n "authentication": {\n "required_for_upload": false,\n "required_for_delete": true,\n "required_for_list": false,\n "nip42_enabled": true\n }\n}';
add_header Content-Type application/json;
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type, Content-Length, Accept, Origin, User-Agent, DNT, Cache-Control, X-Mx-ReqToken, Keep-Alive, X-Requested-With, If-Modified-Since, *" always;
add_header Access-Control-Max-Age 86400 always;
if ($request_method !~ ^(GET)$) {
return 405;
}
fastcgi_pass fastcgi_backend;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param DOCUMENT_URI $document_uri;
fastcgi_param DOCUMENT_ROOT $document_root;
fastcgi_param SERVER_PROTOCOL $server_protocol;
fastcgi_param REQUEST_SCHEME $scheme;
fastcgi_param HTTPS $https if_not_empty;
fastcgi_param GATEWAY_INTERFACE CGI/1.1;
fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;
fastcgi_param REMOTE_ADDR $remote_addr;
fastcgi_param REMOTE_PORT $remote_port;
fastcgi_param SERVER_ADDR $server_addr;
fastcgi_param SERVER_PORT $server_port;
fastcgi_param SERVER_NAME $server_name;
fastcgi_param REDIRECT_STATUS 200;
fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi;
fastcgi_param HTTP_AUTHORIZATION $http_authorization;
}
}
@@ -553,7 +572,7 @@ http {
# Admin API endpoints (/api/*)
location /api/ {
if ($request_method !~ ^(GET|PUT)$) {
if ($request_method !~ ^(GET|PUT|POST)$) {
return 405;
}
fastcgi_pass fastcgi_backend;
@@ -683,14 +702,33 @@ http {
autoindex_format json;
}
# Root redirect - Server info endpoint
# Root endpoint - Server info from FastCGI
location = / {
return 200 '{\n "server": "ginxsom",\n "version": "1.0.0",\n "description": "Ginxsom Blossom Server (HTTPS)",\n "endpoints": {\n "blob_get": "GET /<sha256>",\n "blob_head": "HEAD /<sha256>",\n "upload": "PUT /upload",\n "upload_requirements": "HEAD /upload",\n "list": "GET /list/<pubkey>",\n "delete": "DELETE /<sha256>",\n "mirror": "PUT /mirror",\n "report": "PUT /report",\n "health": "GET /health"\n },\n "supported_buds": [\n "BUD-01",\n "BUD-02",\n "BUD-04",\n "BUD-06",\n "BUD-08",\n "BUD-09"\n ],\n "limits": {\n "max_upload_size": 104857600,\n "supported_mime_types": [\n "image/jpeg",\n "image/png",\n "image/webp",\n "image/gif",\n "video/mp4",\n "video/webm",\n "audio/mpeg",\n "audio/ogg",\n "text/plain",\n "application/pdf"\n ]\n },\n "authentication": {\n "required_for_upload": false,\n "required_for_delete": true,\n "required_for_list": false,\n "nip42_enabled": true\n }\n}';
add_header Content-Type application/json;
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type, Content-Length, Accept, Origin, User-Agent, DNT, Cache-Control, X-Mx-ReqToken, Keep-Alive, X-Requested-With, If-Modified-Since, *" always;
add_header Access-Control-Max-Age 86400 always;
if ($request_method !~ ^(GET)$) {
return 405;
}
fastcgi_pass fastcgi_backend;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param DOCUMENT_URI $document_uri;
fastcgi_param DOCUMENT_ROOT $document_root;
fastcgi_param SERVER_PROTOCOL $server_protocol;
fastcgi_param REQUEST_SCHEME $scheme;
fastcgi_param HTTPS $https if_not_empty;
fastcgi_param GATEWAY_INTERFACE CGI/1.1;
fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;
fastcgi_param REMOTE_ADDR $remote_addr;
fastcgi_param REMOTE_PORT $remote_port;
fastcgi_param SERVER_ADDR $server_addr;
fastcgi_param SERVER_PORT $server_port;
fastcgi_param SERVER_NAME $server_name;
fastcgi_param REDIRECT_STATUS 200;
fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi;
fastcgi_param HTTP_AUTHORIZATION $http_authorization;
}
}
}

Binary file not shown.

View File

@@ -13,6 +13,12 @@ print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
print_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Parse command line arguments
FRESH_INSTALL=false
if [[ "$1" == "--fresh" ]]; then
FRESH_INSTALL=true
fi
# Configuration
REMOTE_HOST="laantungir.net"
REMOTE_USER="ubuntu"
@@ -68,7 +74,7 @@ print_status "Copying files to remote server..."
# Copy entire project directory (excluding unnecessary files)
print_status "Copying entire ginxsom project..."
rsync -avz --exclude='.git' --exclude='build' --exclude='logs' --exclude='Trash' --exclude='blobs' --exclude='db/ginxsom.db' --no-g --no-o --no-perms --omit-dir-times . $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/
rsync -avz --exclude='.git' --exclude='build' --exclude='logs' --exclude='Trash' --exclude='blobs' --exclude='db' --no-g --no-o --no-perms --omit-dir-times . $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/
# Build on remote server to ensure compatibility
print_status "Building ginxsom on remote server..."
@@ -97,8 +103,10 @@ ssh $REMOTE_USER@$REMOTE_HOST << EOF
echo "Binary copied successfully"
EOF
# Skip nginx config update - user prefers manual control
print_status "Skipping nginx configuration update (manual control preferred)"
# NOTE: Do NOT update nginx configuration automatically
# The deployment script should only update ginxsom binaries and do nothing else with the system
# Nginx configuration should be managed manually by the system administrator
print_status "Skipping nginx configuration update (manual control required)"
print_success "Files copied to remote server"
@@ -159,25 +167,35 @@ print_status "Setting up database directory..."
ssh $REMOTE_USER@$REMOTE_HOST << EOF
# Create db directory if it doesn't exist
mkdir -p $REMOTE_DIR/db
# Backup current database if it exists in old location
if [ -f /var/www/html/blossom/ginxsom.db ]; then
echo "Backing up existing database..."
cp /var/www/html/blossom/ginxsom.db /var/www/html/blossom/ginxsom.db.backup.\$(date +%Y%m%d_%H%M%S)
# Migrate database to new location if not already there
if [ ! -f $REMOTE_DB_PATH ]; then
echo "Migrating database to new location..."
cp /var/www/html/blossom/ginxsom.db $REMOTE_DB_PATH
else
echo "Database already exists at new location"
fi
elif [ ! -f $REMOTE_DB_PATH ]; then
echo "No existing database found - will be created on first run"
if [ "$FRESH_INSTALL" = "true" ]; then
echo "Fresh install: removing existing database and blobs..."
# Remove existing database
sudo rm -f $REMOTE_DB_PATH
sudo rm -f /var/www/html/blossom/ginxsom.db
# Remove existing blobs
sudo rm -rf $REMOTE_DATA_DIR/*
echo "Existing data removed"
else
echo "Database already exists at $REMOTE_DB_PATH"
# Backup current database if it exists in old location
if [ -f /var/www/html/blossom/ginxsom.db ]; then
echo "Backing up existing database..."
cp /var/www/html/blossom/ginxsom.db /var/www/html/blossom/ginxsom.db.backup.\$(date +%Y%m%d_%H%M%S)
# Migrate database to new location if not already there
if [ ! -f $REMOTE_DB_PATH ]; then
echo "Migrating database to new location..."
cp /var/www/html/blossom/ginxsom.db $REMOTE_DB_PATH
else
echo "Database already exists at new location"
fi
elif [ ! -f $REMOTE_DB_PATH ]; then
echo "No existing database found - will be created on first run"
else
echo "Database already exists at $REMOTE_DB_PATH"
fi
fi
# Set proper permissions - www-data needs write access to db directory for SQLite journal files
sudo chown -R www-data:www-data $REMOTE_DIR/db
sudo chmod 755 $REMOTE_DIR/db
@@ -185,7 +203,7 @@ ssh $REMOTE_USER@$REMOTE_HOST << EOF
# Allow www-data to access the application directory for spawn-fcgi chdir
chmod 755 $REMOTE_DIR
echo "Database directory setup complete"
EOF
@@ -282,4 +300,7 @@ print_status "Ginxsom should now be available at: https://blossom.laantungir.net
print_status "Test endpoints:"
echo " Health: curl -k https://blossom.laantungir.net/health"
echo " Root: curl -k https://blossom.laantungir.net/"
echo " List: curl -k https://blossom.laantungir.net/list"
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

View File

@@ -0,0 +1,496 @@
# Authentication Rules Implementation Plan
## Executive Summary
This document outlines the implementation plan for adding whitelist/blacklist functionality to the Ginxsom Blossom server. The authentication rules system is **already coded** in [`src/request_validator.c`](src/request_validator.c) but lacks the database schema to function. This plan focuses on completing the implementation by adding the missing database tables and Admin API endpoints.
## Current State Analysis
### ✅ Already Implemented
- **Nostr event validation** - Full cryptographic verification (NIP-42 and Blossom)
- **Rule evaluation engine** - Complete priority-based logic in [`check_database_auth_rules()`](src/request_validator.c:1309-1471)
- **Configuration system** - `auth_rules_enabled` flag in config table
- **Admin API framework** - Authentication and endpoint structure in place
- **Documentation** - Comprehensive flow diagrams in [`docs/AUTH_API.md`](docs/AUTH_API.md)
### ❌ Missing Components
- **Database schema** - `auth_rules` table doesn't exist
- **Cache table** - `auth_rules_cache` for performance optimization
- **Admin API endpoints** - CRUD operations for managing rules
- **Migration script** - Database schema updates
- **Test suite** - Validation of rule enforcement
## Database Schema Design
### 1. auth_rules Table
```sql
-- Authentication rules for whitelist/blacklist functionality
CREATE TABLE IF NOT EXISTS auth_rules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
rule_type TEXT NOT NULL, -- 'pubkey_blacklist', 'pubkey_whitelist',
-- 'hash_blacklist', 'mime_blacklist', 'mime_whitelist'
rule_target TEXT NOT NULL, -- The pubkey, hash, or MIME type to match
operation TEXT NOT NULL DEFAULT '*', -- 'upload', 'delete', 'list', or '*' for all
enabled INTEGER NOT NULL DEFAULT 1, -- 1 = enabled, 0 = disabled
priority INTEGER NOT NULL DEFAULT 100,-- Lower number = higher priority
description TEXT, -- Human-readable description
created_by TEXT, -- Admin pubkey who created the rule
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
-- Constraints
CHECK (rule_type IN ('pubkey_blacklist', 'pubkey_whitelist',
'hash_blacklist', 'mime_blacklist', 'mime_whitelist')),
CHECK (operation IN ('upload', 'delete', 'list', '*')),
CHECK (enabled IN (0, 1)),
CHECK (priority >= 0),
-- Unique constraint: one rule per type/target/operation combination
UNIQUE(rule_type, rule_target, operation)
);
-- Indexes for performance
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);
```
### 2. auth_rules_cache Table
```sql
-- Cache for authentication decisions (5-minute TTL)
CREATE TABLE IF NOT EXISTS auth_rules_cache (
cache_key TEXT PRIMARY KEY NOT NULL, -- SHA-256 hash of request parameters
decision INTEGER NOT NULL, -- 1 = allow, 0 = deny
reason TEXT, -- Reason for decision
pubkey TEXT, -- Public key from request
operation TEXT, -- Operation type
resource_hash TEXT, -- Resource hash (if applicable)
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
expires_at INTEGER NOT NULL, -- Expiration timestamp
CHECK (decision IN (0, 1))
);
-- Index for cache expiration cleanup
CREATE INDEX IF NOT EXISTS idx_auth_cache_expires ON auth_rules_cache(expires_at);
```
### 3. Rule Type Definitions
| Rule Type | Purpose | Target Format | Priority Range |
|-----------|---------|---------------|----------------|
| `pubkey_blacklist` | Block specific users | 64-char hex pubkey | 1-99 (highest) |
| `hash_blacklist` | Block specific files | 64-char hex SHA-256 | 100-199 |
| `mime_blacklist` | Block file types | MIME type string | 200-299 |
| `pubkey_whitelist` | Allow specific users | 64-char hex pubkey | 300-399 |
| `mime_whitelist` | Allow file types | MIME type string | 400-499 |
### 4. Operation Types
- `upload` - File upload operations
- `delete` - File deletion operations
- `list` - File listing operations
- `*` - All operations (wildcard)
## Admin API Endpoints
### GET /api/rules
**Purpose**: List all authentication rules with filtering
**Authentication**: Required (admin pubkey)
**Query Parameters**:
- `rule_type` (optional): Filter by rule type
- `operation` (optional): Filter by operation
- `enabled` (optional): Filter by enabled status (true/false)
- `limit` (default: 100): Number of rules to return
- `offset` (default: 0): Pagination offset
**Response**:
```json
{
"status": "success",
"data": {
"rules": [
{
"id": 1,
"rule_type": "pubkey_blacklist",
"rule_target": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
"operation": "upload",
"enabled": true,
"priority": 10,
"description": "Blocked spammer account",
"created_by": "admin_pubkey_here",
"created_at": 1704067200,
"updated_at": 1704067200
}
],
"total": 1,
"limit": 100,
"offset": 0
}
}
```
### POST /api/rules
**Purpose**: Create a new authentication rule
**Authentication**: Required (admin pubkey)
**Request Body**:
```json
{
"rule_type": "pubkey_blacklist",
"rule_target": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
"operation": "upload",
"priority": 10,
"description": "Blocked spammer account"
}
```
**Response**:
```json
{
"status": "success",
"message": "Rule created successfully",
"data": {
"id": 1,
"rule_type": "pubkey_blacklist",
"rule_target": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
"operation": "upload",
"enabled": true,
"priority": 10,
"description": "Blocked spammer account",
"created_at": 1704067200
}
}
```
### PUT /api/rules/:id
**Purpose**: Update an existing rule
**Authentication**: Required (admin pubkey)
**Request Body**:
```json
{
"enabled": false,
"priority": 20,
"description": "Updated description"
}
```
**Response**:
```json
{
"status": "success",
"message": "Rule updated successfully",
"data": {
"id": 1,
"updated_fields": ["enabled", "priority", "description"]
}
}
```
### DELETE /api/rules/:id
**Purpose**: Delete an authentication rule
**Authentication**: Required (admin pubkey)
**Response**:
```json
{
"status": "success",
"message": "Rule deleted successfully",
"data": {
"id": 1
}
}
```
### POST /api/rules/clear-cache
**Purpose**: Clear the authentication rules cache
**Authentication**: Required (admin pubkey)
**Response**:
```json
{
"status": "success",
"message": "Authentication cache cleared",
"data": {
"entries_cleared": 42
}
}
```
### GET /api/rules/test
**Purpose**: Test if a specific request would be allowed
**Authentication**: Required (admin pubkey)
**Query Parameters**:
- `pubkey` (required): Public key to test
- `operation` (required): Operation type (upload/delete/list)
- `hash` (optional): Resource hash
- `mime` (optional): MIME type
**Response**:
```json
{
"status": "success",
"data": {
"allowed": false,
"reason": "Public key blacklisted",
"matched_rule": {
"id": 1,
"rule_type": "pubkey_blacklist",
"description": "Blocked spammer account"
}
}
}
```
## Implementation Phases
### Phase 1: Database Schema (Priority: HIGH)
**Estimated Time**: 2-4 hours
**Tasks**:
1. Create migration script `db/migrations/001_add_auth_rules.sql`
2. Add `auth_rules` table with indexes
3. Add `auth_rules_cache` table with indexes
4. Create migration runner script
5. Test migration on clean database
6. Test migration on existing database
**Deliverables**:
- Migration SQL script
- Migration runner bash script
- Migration documentation
**Validation**:
- Verify tables created successfully
- Verify indexes exist
- Verify constraints work correctly
- Test with sample data
### Phase 2: Admin API Endpoints (Priority: HIGH)
**Estimated Time**: 6-8 hours
**Tasks**:
1. Implement `GET /api/rules` endpoint
2. Implement `POST /api/rules` endpoint
3. Implement `PUT /api/rules/:id` endpoint
4. Implement `DELETE /api/rules/:id` endpoint
5. Implement `POST /api/rules/clear-cache` endpoint
6. Implement `GET /api/rules/test` endpoint
7. Add input validation for all endpoints
8. Add error handling and logging
**Deliverables**:
- C implementation in `src/admin_api.c`
- Header declarations in `src/ginxsom.h`
- API documentation updates
**Validation**:
- Test each endpoint with valid data
- Test error cases (invalid input, missing auth, etc.)
- Verify database operations work correctly
- Check response formats match specification
### Phase 3: Integration & Testing (Priority: HIGH)
**Estimated Time**: 4-6 hours
**Tasks**:
1. Create comprehensive test suite
2. Test rule creation and enforcement
3. Test cache functionality
4. Test priority ordering
5. Test whitelist default-deny behavior
6. Test performance with many rules
7. Document test scenarios
**Deliverables**:
- Test script `tests/auth_rules_test.sh`
- Performance benchmarks
- Test documentation
**Validation**:
- All test cases pass
- Performance meets requirements (<3ms per request)
- Cache hit rate >80% under load
- No memory leaks detected
### Phase 4: Documentation & Examples (Priority: MEDIUM)
**Estimated Time**: 2-3 hours
**Tasks**:
1. Update [`docs/AUTH_API.md`](docs/AUTH_API.md) with rule management
2. Create usage examples
3. Document common patterns (blocking users, allowing file types)
4. Create migration guide for existing deployments
5. Add troubleshooting section
**Deliverables**:
- Updated documentation
- Example scripts
- Migration guide
- Troubleshooting guide
## Code Changes Required
### 1. src/request_validator.c
**Status**: ✅ Already implemented - NO CHANGES NEEDED
The rule evaluation logic is complete in [`check_database_auth_rules()`](src/request_validator.c:1309-1471). Once the database tables exist, this code will work immediately.
### 2. src/admin_api.c
**Status**: ❌ Needs new endpoints
Add new functions:
```c
// Rule management endpoints
int handle_get_rules(FCGX_Request *request);
int handle_create_rule(FCGX_Request *request);
int handle_update_rule(FCGX_Request *request);
int handle_delete_rule(FCGX_Request *request);
int handle_clear_cache(FCGX_Request *request);
int handle_test_rule(FCGX_Request *request);
```
### 3. src/ginxsom.h
**Status**: ❌ Needs new declarations
Add function prototypes for new admin endpoints.
### 4. db/schema.sql
**Status**: ❌ Needs new tables
Add `auth_rules` and `auth_rules_cache` table definitions.
## Migration Strategy
### For New Installations
1. Run updated `db/init.sh` which includes new tables
2. No additional steps needed
### For Existing Installations
1. Create backup: `cp db/ginxsom.db db/ginxsom.db.backup`
2. Run migration: `sqlite3 db/ginxsom.db < db/migrations/001_add_auth_rules.sql`
3. Verify migration: `sqlite3 db/ginxsom.db ".schema auth_rules"`
4. Restart server to load new schema
### Rollback Procedure
1. Stop server
2. Restore backup: `cp db/ginxsom.db.backup db/ginxsom.db`
3. Restart server
## Performance Considerations
### Cache Strategy
- **5-minute TTL** balances freshness with performance
- **SHA-256 cache keys** prevent collision attacks
- **Automatic cleanup** of expired entries every 5 minutes
- **Cache hit target**: >80% under normal load
### Database Optimization
- **Indexes on all query columns** for fast lookups
- **Prepared statements** prevent SQL injection
- **Single connection** with proper cleanup
- **Query optimization** for rule evaluation order
### Expected Performance
- **Cache hit**: ~100μs (SQLite SELECT)
- **Cache miss**: ~2.4ms (full validation + rule checks)
- **Rule creation**: ~50ms (INSERT + cache invalidation)
- **Rule update**: ~30ms (UPDATE + cache invalidation)
## Security Considerations
### Input Validation
- Validate all rule_type values against enum
- Validate pubkey format (64 hex chars)
- Validate hash format (64 hex chars)
- Validate MIME type format
- Sanitize description text
### Authorization
- All rule management requires admin pubkey
- Verify Nostr event signatures
- Check event expiration
- Log all rule changes with admin pubkey
### Attack Mitigation
- **Rule flooding**: Limit total rules per type
- **Cache poisoning**: Cryptographic cache keys
- **Priority manipulation**: Validate priority ranges
- **Whitelist bypass**: Default-deny when whitelist exists
## Testing Strategy
### Unit Tests
- Rule creation with valid data
- Rule creation with invalid data
- Rule update operations
- Rule deletion
- Cache operations
- Priority ordering
### Integration Tests
- End-to-end request flow
- Multiple rules interaction
- Cache hit/miss scenarios
- Whitelist default-deny behavior
- Performance under load
### Security Tests
- Invalid admin pubkey rejection
- Expired event rejection
- SQL injection attempts
- Cache poisoning attempts
- Priority bypass attempts
## Success Criteria
### Functional Requirements
- ✅ Rules can be created via Admin API
- ✅ Rules can be updated via Admin API
- ✅ Rules can be deleted via Admin API
- ✅ Rules are enforced during request validation
- ✅ Cache improves performance significantly
- ✅ Priority ordering works correctly
- ✅ Whitelist default-deny works correctly
### Performance Requirements
- ✅ Cache hit latency <200μs
- Full validation latency <3ms
- Cache hit rate >80% under load
- ✅ No memory leaks
- ✅ Database queries optimized
### Security Requirements
- ✅ Admin authentication required
- ✅ Input validation prevents injection
- ✅ Audit logging of all changes
- ✅ Cache keys prevent poisoning
- ✅ Whitelist bypass prevented
## Timeline Estimate
| Phase | Duration | Dependencies |
|-------|----------|--------------|
| Phase 1: Database Schema | 2-4 hours | None |
| Phase 2: Admin API | 6-8 hours | Phase 1 |
| Phase 3: Testing | 4-6 hours | Phase 2 |
| Phase 4: Documentation | 2-3 hours | Phase 3 |
| **Total** | **14-21 hours** | Sequential |
## Next Steps
1. **Review this plan** with stakeholders
2. **Create Phase 1 migration script** in `db/migrations/`
3. **Test migration** on development database
4. **Implement Phase 2 endpoints** in `src/admin_api.c`
5. **Create test suite** in `tests/auth_rules_test.sh`
6. **Update documentation** in `docs/`
7. **Deploy to production** with migration guide
## Conclusion
The authentication rules system is **90% complete** - the core logic exists and is well-tested. This implementation plan focuses on the final 10%: adding database tables and Admin API endpoints. The work is straightforward, well-scoped, and can be completed in 2-3 days of focused development.
The system will provide powerful whitelist/blacklist functionality while maintaining the performance and security characteristics already present in the codebase.

View File

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

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

View File

@@ -131,21 +131,48 @@ increment_version() {
export NEW_VERSION
}
# Function to update version in header file
update_version_in_header() {
local version="$1"
print_status "Updating version in src/ginxsom.h to $version..."
# Extract version components (remove 'v' prefix)
local version_no_v=${version#v}
# Parse major.minor.patch using regex
if [[ $version_no_v =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
local major=${BASH_REMATCH[1]}
local minor=${BASH_REMATCH[2]}
local patch=${BASH_REMATCH[3]}
# Update the header file
sed -i "s/#define VERSION_MAJOR [0-9]\+/#define VERSION_MAJOR $major/" src/ginxsom.h
sed -i "s/#define VERSION_MINOR [0-9]\+/#define VERSION_MINOR $minor/" src/ginxsom.h
sed -i "s/#define VERSION_PATCH [0-9]\+/#define VERSION_PATCH $patch/" src/ginxsom.h
sed -i "s/#define VERSION \"v[0-9]\+\.[0-9]\+\.[0-9]\+\"/#define VERSION \"$version\"/" src/ginxsom.h
print_success "Updated version in header file"
else
print_error "Invalid version format: $version"
exit 1
fi
}
# Function to compile the Ginxsom project
compile_project() {
print_status "Compiling Ginxsom FastCGI server..."
# Clean previous build
if make clean > /dev/null 2>&1; then
print_success "Cleaned previous build"
else
print_warning "Clean failed or no Makefile found"
fi
# Compile the project
if make > /dev/null 2>&1; then
print_success "Ginxsom compiled successfully"
# Verify the binary was created
if [[ -f "build/ginxsom-fcgi" ]]; then
print_success "Binary created: build/ginxsom-fcgi"
@@ -390,9 +417,12 @@ main() {
git tag "$NEW_VERSION" > /dev/null 2>&1
fi
# Update version in header file
update_version_in_header "$NEW_VERSION"
# Compile project
compile_project
# Build release binary
build_release_binary
@@ -423,9 +453,12 @@ main() {
git tag "$NEW_VERSION" > /dev/null 2>&1
fi
# Update version in header file
update_version_in_header "$NEW_VERSION"
# Compile project
compile_project
# Commit and push (but skip tag creation since we already did it)
git_commit_and_push_no_tag

View File

@@ -15,6 +15,10 @@ server {
root /var/www/html;
index index.html index.htm;
# CORS for Nostr NIP-05 verification
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Methods "GET, OPTIONS" always;
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range" always;
location / {
try_files $uri $uri/ =404;
@@ -42,6 +46,10 @@ server {
root /var/www/html;
index index.html index.htm;
# CORS for Nostr NIP-05 verification
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Methods "GET, OPTIONS" always;
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range" always;
location / {
try_files $uri $uri/ =404;
@@ -58,7 +66,7 @@ server {
# Blossom subdomains HTTP - redirect to HTTPS (keep for ACME)
server {
listen 80;
server_name blossom.laantungir.com blossom.laantungir.net blossom.laantungir.org;
server_name blossom.laantungir.net;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
@@ -72,7 +80,7 @@ server {
# Blossom subdomains HTTPS - ginxsom FastCGI
server {
listen 443 ssl;
server_name blossom.laantungir.com blossom.laantungir.net blossom.laantungir.org;
server_name blossom.laantungir.net;
ssl_certificate /etc/letsencrypt/live/git.laantungir.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/git.laantungir.net/privkey.pem;
@@ -210,14 +218,14 @@ server {
add_header Access-Control-Max-Age 86400 always;
}
# Default location - Server info endpoint
# Default location - Server info from FastCGI
location / {
return 200 '{\n "server": "ginxsom",\n "version": "1.0.0",\n "description": "Ginxsom Blossom Server",\n "endpoints": {\n "blob_get": "GET /<sha256>",\n "blob_head": "HEAD /<sha256>",\n "upload": "PUT /upload",\n "upload_requirements": "HEAD /upload",\n "list": "GET /list/<pubkey>",\n "delete": "DELETE /<sha256>",\n "mirror": "PUT /mirror",\n "report": "PUT /report",\n "health": "GET /health"\n },\n "supported_buds": [\n "BUD-01",\n "BUD-02",\n "BUD-04",\n "BUD-06",\n "BUD-08",\n "BUD-09"\n ],\n "limits": {\n "max_upload_size": 104857600,\n "supported_mime_types": [\n "image/jpeg",\n "image/png",\n "image/webp",\n "image/gif",\n "video/mp4",\n "video/webm",\n "audio/mpeg",\n "audio/ogg",\n "text/plain",\n "application/pdf"\n ]\n },\n "authentication": {\n "required_for_upload": false,\n "required_for_delete": true,\n "required_for_list": false,\n "nip42_enabled": true\n }\n}';
add_header Content-Type application/json;
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type, Content-Length, Accept, Origin, User-Agent, DNT, Cache-Control, X-Mx-ReqToken, Keep-Alive, X-Requested-With, If-Modified-Since, *" always;
add_header Access-Control-Max-Age 86400 always;
if ($request_method !~ ^(GET)$) {
return 405;
}
fastcgi_pass ginxsom_backend;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi;
}
}
@@ -241,7 +249,7 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 86400s;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
proxy_connect_timeout 60s;
proxy_buffering off;
@@ -256,8 +264,8 @@ server {
listen 443 ssl;
server_name relay.laantungir.com relay.laantungir.net relay.laantungir.org;
ssl_certificate /etc/letsencrypt/live/blossom.laantungir.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/blossom.laantungir.net/privkey.pem;
ssl_certificate /etc/letsencrypt/live/git.laantungir.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/git.laantungir.net/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8888;
@@ -373,4 +381,4 @@ server {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
}

View File

@@ -1,11 +1,36 @@
#!/bin/bash
# Restart Ginxsom Development Environment
# Combines nginx and FastCGI restart operations for debugging
# WARNING: This script DELETES all databases in db/ for fresh testing
# Configuration
# Parse command line arguments
TEST_MODE=0
FOLLOW_LOGS=0
while [[ $# -gt 0 ]]; do
case $1 in
-t|--test-keys)
TEST_MODE=1
shift
;;
--follow)
FOLLOW_LOGS=1
shift
;;
*)
echo "Unknown option: $1"
echo "Usage: $0 [-t|--test-keys] [--follow]"
echo " -t, --test-keys Use test mode with keys from .test_keys"
echo " --follow Follow logs in real-time"
exit 1
;;
esac
done
# Check for --follow flag
if [[ "$1" == "--follow" ]]; then
if [[ $FOLLOW_LOGS -eq 1 ]]; then
echo "=== Following logs in real-time ==="
echo "Monitoring: nginx error, nginx access, app stderr, app stdout"
echo "Press Ctrl+C to stop following logs"
@@ -37,7 +62,12 @@ touch logs/app/stderr.log logs/app/stdout.log logs/nginx/error.log logs/nginx/ac
chmod 644 logs/app/stderr.log logs/app/stdout.log logs/nginx/error.log logs/nginx/access.log
chmod 755 logs/nginx logs/app
echo -e "${YELLOW}=== Ginxsom Development Environment Restart ===${NC}"
if [ $TEST_MODE -eq 1 ]; then
echo -e "${YELLOW}=== Ginxsom Development Environment Restart (TEST MODE) ===${NC}"
echo "Using test keys from .test_keys file"
else
echo -e "${YELLOW}=== Ginxsom Development Environment Restart ===${NC}"
fi
echo "Starting full restart sequence..."
# Function to check if a process is running
@@ -148,6 +178,46 @@ if [ $? -ne 0 ]; then
fi
echo -e "${GREEN}Clean rebuild complete${NC}"
# Step 3.5: Clean database directory for fresh testing
echo -e "\n${YELLOW}3.5. Cleaning database directory...${NC}"
echo "Removing all existing databases for fresh start..."
# Remove all .db files in db/ directory
if ls db/*.db 1> /dev/null 2>&1; then
echo "Found databases to remove:"
ls -lh db/*.db
rm -f db/*.db
echo -e "${GREEN}Database cleanup complete${NC}"
else
echo "No existing databases found"
fi
# Step 3.75: Handle keys based on mode
echo -e "\n${YELLOW}3.75. Configuring server keys...${NC}"
if [ $TEST_MODE -eq 1 ]; then
# Test mode: verify .test_keys file exists
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
# Extract test server pubkey to determine database name
TEST_PUBKEY=$(grep "^SERVER_PUBKEY=" .test_keys | cut -d"'" -f2)
if [ -z "$TEST_PUBKEY" ]; then
echo -e "${RED}ERROR: Could not extract SERVER_PUBKEY from .test_keys${NC}"
exit 1
fi
echo -e "${GREEN}Test mode: Will use keys from .test_keys${NC}"
echo -e "${GREEN}Fresh test database will be created as: db/${TEST_PUBKEY}.db${NC}"
else
# Production mode: databases were cleaned, will generate new keypair
echo -e "${YELLOW}Production mode: Fresh start with new keypair${NC}"
echo -e "${YELLOW}New database will be created as db/<new_pubkey>.db${NC}"
fi
# Step 4: Start FastCGI
echo -e "\n${YELLOW}4. Starting FastCGI application...${NC}"
echo "Socket: $SOCKET_PATH"
@@ -166,9 +236,19 @@ fi
echo "Setting GINX_DEBUG environment for pubkey extraction diagnostics"
export GINX_DEBUG=1
# Build command line arguments based on mode
FCGI_ARGS="--storage-dir blobs"
if [ $TEST_MODE -eq 1 ]; then
FCGI_ARGS="$FCGI_ARGS --test-keys"
echo -e "${YELLOW}Starting FastCGI in TEST MODE with test keys${NC}"
else
# Production mode: databases were cleaned, will generate new keys
echo -e "${YELLOW}Starting FastCGI in production mode - will generate new keys and create database${NC}"
fi
# Start FastCGI application with proper logging (daemonized but with redirected streams)
echo "FastCGI starting at $(date)" >> logs/app/stderr.log
spawn-fcgi -s "$SOCKET_PATH" -M 666 -u "$USER" -g "$USER" -P "$PID_FILE" -- "$FCGI_BINARY" --storage-dir blobs 1>>logs/app/stdout.log 2>>logs/app/stderr.log
spawn-fcgi -s "$SOCKET_PATH" -M 666 -u "$USER" -g "$USER" -P "$PID_FILE" -- "$FCGI_BINARY" $FCGI_ARGS 1>>logs/app/stdout.log 2>>logs/app/stderr.log
if [ $? -eq 0 ] && [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE")

View File

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

509
src/admin_auth.c Normal file
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);
}
}

471
src/admin_event.c Normal file
View File

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

216
src/admin_handlers.c Normal file
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
src/admin_websocket.c Normal file
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);
}

View File

@@ -427,8 +427,16 @@ void handle_mirror_request(void) {
const char* extension = mime_to_extension(content_type_final);
// Save file to storage directory using global g_storage_dir variable
char filepath[512];
snprintf(filepath, sizeof(filepath), "%s/%s%s", g_storage_dir, sha256_hex, extension);
char filepath[4096];
int filepath_len = snprintf(filepath, sizeof(filepath), "%s/%s%s", g_storage_dir, sha256_hex, extension);
if (filepath_len >= (int)sizeof(filepath)) {
free_mirror_download(download);
send_error_response(500, "file_error",
"File path too long",
"Internal server error during file path construction");
log_request("PUT", "/mirror", uploader_pubkey ? "authenticated" : "anonymous", 500);
return;
}
FILE* outfile = fopen(filepath, "wb");
if (!outfile) {

View File

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

View File

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

View File

@@ -7,6 +7,11 @@
#ifndef GINXSOM_H
#define GINXSOM_H
// Version information (auto-updated by build system)
#define VERSION_MAJOR 0
#define VERSION_MINOR 1
#define VERSION_PATCH 10
#define VERSION "v0.1.10"
#include <stddef.h>
#include <stdint.h>
@@ -257,6 +262,9 @@ int validate_sha256_format(const char* sha256);
// Admin API request handler
void handle_admin_api_request(const char* method, const char* uri, const char* validated_pubkey, int is_authenticated);
// Admin event handler (Kind 23456/23457)
void handle_admin_event_request(void);
// Individual endpoint handlers
void handle_stats_api(void);
void handle_config_get_api(void);

1041
src/main.c

File diff suppressed because it is too large Load Diff

View File

@@ -32,8 +32,8 @@
// NOSTR_ERROR_NIP42_CHALLENGE_EXPIRED are already defined in
// nostr_core_lib/nostr_core/nostr_common.h
// Database path (consistent with main.c)
#define DB_PATH "db/ginxsom.db"
// Use global database path from main.c
extern char g_db_path[];
// NIP-42 challenge management constants
#define MAX_CHALLENGES 1000
@@ -115,7 +115,7 @@ static int validate_nip42_event(cJSON *event, const char *relay_url,
const char *challenge_id);
static int validate_admin_event(cJSON *event, const char *method, const char *endpoint);
static int check_database_auth_rules(const char *pubkey, const char *operation,
const char *resource_hash);
const char *resource_hash, const char *mime_type);
void nostr_request_validator_clear_violation(void);
// NIP-42 challenge management functions
@@ -283,6 +283,16 @@ int nostr_validate_unified_request(const nostr_unified_request_t *request,
// PHASE 2: NOSTR EVENT VALIDATION (CPU Intensive ~2ms)
/////////////////////////////////////////////////////////////////////
// Check if authentication is disabled first (regardless of header presence)
if (!g_auth_cache.auth_required) {
validator_debug_log("VALIDATOR_DEBUG: STEP 4 PASSED - Authentication "
"disabled, allowing request\n");
result->valid = 1;
result->error_code = NOSTR_SUCCESS;
strcpy(result->reason, "Authentication disabled");
return NOSTR_SUCCESS;
}
// Check if this is a BUD-09 report request - allow anonymous reporting
if (request->operation && strcmp(request->operation, "report") == 0) {
// BUD-09 allows anonymous reporting - pass through to bud09.c for validation
@@ -810,8 +820,17 @@ int nostr_validate_unified_request(const nostr_unified_request_t *request,
"checking database rules\n");
// Check database rules for authorization
// For Blossom uploads, use hash from event 'x' tag instead of URI
const char *hash_for_rules = request->resource_hash;
if (event_kind == 24242 && strlen(expected_hash_from_event) == 64) {
hash_for_rules = expected_hash_from_event;
char hash_msg[256];
sprintf(hash_msg, "VALIDATOR_DEBUG: Using hash from Blossom event for rules: %.16s...\n", hash_for_rules);
validator_debug_log(hash_msg);
}
int rules_result = check_database_auth_rules(
extracted_pubkey, request->operation, request->resource_hash);
extracted_pubkey, request->operation, hash_for_rules, request->mime_type);
if (rules_result != NOSTR_SUCCESS) {
validator_debug_log(
"VALIDATOR_DEBUG: STEP 14 FAILED - Database rules denied request\n");
@@ -1045,7 +1064,7 @@ static int reload_auth_config(void) {
memset(&g_auth_cache, 0, sizeof(g_auth_cache));
// Open database
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc != SQLITE_OK) {
validator_debug_log("VALIDATOR: Could not open database\n");
// Use defaults
@@ -1307,7 +1326,7 @@ static int validate_blossom_event(cJSON *event, const char *expected_hash,
* Implements the 6-step rule evaluation engine from AUTH_API.md
*/
static int check_database_auth_rules(const char *pubkey, const char *operation,
const char *resource_hash) {
const char *resource_hash, const char *mime_type) {
sqlite3 *db = NULL;
sqlite3_stmt *stmt = NULL;
int rc;
@@ -1321,12 +1340,12 @@ static int check_database_auth_rules(const char *pubkey, const char *operation,
char rules_msg[256];
sprintf(rules_msg,
"VALIDATOR_DEBUG: RULES ENGINE - Checking rules for pubkey=%.32s..., "
"operation=%s\n",
pubkey, operation ? operation : "NULL");
"operation=%s, mime_type=%s\n",
pubkey, operation ? operation : "NULL", mime_type ? mime_type : "NULL");
validator_debug_log(rules_msg);
// Open database
rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL);
rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc != SQLITE_OK) {
validator_debug_log(
"VALIDATOR_DEBUG: RULES ENGINE - Failed to open database\n");
@@ -1334,9 +1353,10 @@ static int check_database_auth_rules(const char *pubkey, const char *operation,
}
// Step 1: Check pubkey blacklist (highest priority)
// Match both exact operation and wildcard '*'
const char *blacklist_sql =
"SELECT rule_type, description FROM auth_rules WHERE rule_type = "
"'pubkey_blacklist' AND rule_target = ? AND operation = ? AND enabled = "
"'pubkey_blacklist' AND rule_target = ? AND (operation = ? OR operation = '*') AND enabled = "
"1 ORDER BY priority LIMIT 1";
rc = sqlite3_prepare_v2(db, blacklist_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
@@ -1369,9 +1389,10 @@ static int check_database_auth_rules(const char *pubkey, const char *operation,
// Step 2: Check hash blacklist
if (resource_hash) {
// Match both exact operation and wildcard '*'
const char *hash_blacklist_sql =
"SELECT rule_type, description FROM auth_rules WHERE rule_type = "
"'hash_blacklist' AND rule_target = ? AND operation = ? AND enabled = "
"'hash_blacklist' AND rule_target = ? AND (operation = ? OR operation = '*') AND enabled = "
"1 ORDER BY priority LIMIT 1";
rc = sqlite3_prepare_v2(db, hash_blacklist_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
@@ -1407,10 +1428,53 @@ static int check_database_auth_rules(const char *pubkey, const char *operation,
"resource hash provided\n");
}
// Step 3: Check pubkey whitelist
// Step 3: Check MIME type blacklist
if (mime_type) {
// Match both exact MIME type and wildcard patterns (e.g., 'image/*')
const char *mime_blacklist_sql =
"SELECT rule_type, description FROM auth_rules WHERE rule_type = "
"'mime_blacklist' AND (rule_target = ? OR rule_target LIKE '%/*' AND ? LIKE REPLACE(rule_target, '*', '%')) AND (operation = ? OR operation = '*') AND enabled = "
"1 ORDER BY priority LIMIT 1";
rc = sqlite3_prepare_v2(db, mime_blacklist_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, mime_type, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, mime_type, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 3, operation ? operation : "", -1, SQLITE_STATIC);
if (sqlite3_step(stmt) == SQLITE_ROW) {
const char *description = (const char *)sqlite3_column_text(stmt, 1);
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 3 FAILED - "
"MIME type blacklisted\n");
char mime_blacklist_msg[256];
sprintf(
mime_blacklist_msg,
"VALIDATOR_DEBUG: RULES ENGINE - MIME blacklist rule matched: %s\n",
description ? description : "Unknown");
validator_debug_log(mime_blacklist_msg);
// Set specific violation details for status code mapping
strcpy(g_last_rule_violation.violation_type, "mime_blacklist");
sprintf(g_last_rule_violation.reason, "%s: MIME type blacklisted",
description ? description : "TEST_MIME_BLACKLIST");
sqlite3_finalize(stmt);
sqlite3_close(db);
return NOSTR_ERROR_AUTH_REQUIRED;
}
sqlite3_finalize(stmt);
}
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 3 PASSED - MIME "
"type not blacklisted\n");
} else {
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 3 SKIPPED - No "
"MIME type provided\n");
}
// Step 4: Check pubkey whitelist
// Match both exact operation and wildcard '*'
const char *whitelist_sql =
"SELECT rule_type, description FROM auth_rules WHERE rule_type = "
"'pubkey_whitelist' AND rule_target = ? AND operation = ? AND enabled = "
"'pubkey_whitelist' AND rule_target = ? AND (operation = ? OR operation = '*') AND enabled = "
"1 ORDER BY priority LIMIT 1";
rc = sqlite3_prepare_v2(db, whitelist_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
@@ -1435,10 +1499,76 @@ static int check_database_auth_rules(const char *pubkey, const char *operation,
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 3 FAILED - Pubkey "
"not whitelisted\n");
// Step 4: Check if any whitelist rules exist - if yes, deny by default
// Step 5: Check MIME type whitelist (only if not already denied)
if (mime_type) {
// Match both exact MIME type and wildcard patterns (e.g., 'image/*')
const char *mime_whitelist_sql =
"SELECT rule_type, description FROM auth_rules WHERE rule_type = "
"'mime_whitelist' AND (rule_target = ? OR rule_target LIKE '%/*' AND ? LIKE REPLACE(rule_target, '*', '%')) AND (operation = ? OR operation = '*') AND enabled = "
"1 ORDER BY priority LIMIT 1";
rc = sqlite3_prepare_v2(db, mime_whitelist_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, mime_type, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, mime_type, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 3, operation ? operation : "", -1, SQLITE_STATIC);
if (sqlite3_step(stmt) == SQLITE_ROW) {
const char *description = (const char *)sqlite3_column_text(stmt, 1);
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 5 PASSED - "
"MIME type whitelisted\n");
char mime_whitelist_msg[256];
sprintf(mime_whitelist_msg,
"VALIDATOR_DEBUG: RULES ENGINE - MIME whitelist rule matched: %s\n",
description ? description : "Unknown");
validator_debug_log(mime_whitelist_msg);
sqlite3_finalize(stmt);
sqlite3_close(db);
return NOSTR_SUCCESS; // Allow whitelisted MIME type
}
sqlite3_finalize(stmt);
}
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 5 FAILED - MIME "
"type not whitelisted\n");
} else {
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 5 SKIPPED - No "
"MIME type provided\n");
}
// Step 6: Check if any MIME whitelist rules exist - if yes, deny by default
// Match both exact operation and wildcard '*'
const char *mime_whitelist_exists_sql =
"SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'mime_whitelist' "
"AND (operation = ? OR operation = '*') AND enabled = 1 LIMIT 1";
rc = sqlite3_prepare_v2(db, mime_whitelist_exists_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, operation ? operation : "", -1, SQLITE_STATIC);
if (sqlite3_step(stmt) == SQLITE_ROW) {
int mime_whitelist_count = sqlite3_column_int(stmt, 0);
if (mime_whitelist_count > 0) {
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 6 FAILED - "
"MIME whitelist exists but type not in it\n");
// Set specific violation details for status code mapping
strcpy(g_last_rule_violation.violation_type, "mime_whitelist_violation");
strcpy(g_last_rule_violation.reason,
"MIME type not whitelisted for this operation");
sqlite3_finalize(stmt);
sqlite3_close(db);
return NOSTR_ERROR_AUTH_REQUIRED;
}
}
sqlite3_finalize(stmt);
}
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 6 PASSED - No "
"MIME whitelist restrictions apply\n");
// Step 7: Check if any whitelist rules exist - if yes, deny by default
// Match both exact operation and wildcard '*'
const char *whitelist_exists_sql =
"SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'pubkey_whitelist' "
"AND operation = ? AND enabled = 1 LIMIT 1";
"AND (operation = ? OR operation = '*') AND enabled = 1 LIMIT 1";
rc = sqlite3_prepare_v2(db, whitelist_exists_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, operation ? operation : "", -1, SQLITE_STATIC);
@@ -1465,7 +1595,7 @@ static int check_database_auth_rules(const char *pubkey, const char *operation,
"whitelist restrictions apply\n");
sqlite3_close(db);
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 5 PASSED - All "
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 7 PASSED - All "
"rule checks completed, default ALLOW\n");
return NOSTR_SUCCESS; // Default allow if no restrictive rules matched
}

199
src/test_keygen.c Normal file
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
src/test_main.c Normal file
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;
}

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

View File

@@ -1 +0,0 @@
test file content

25
test_key_generation.sh Executable file
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
test_mode_verification.sh Executable file
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"

206
tests/admin_event_test.sh Executable file
View File

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

View File

@@ -14,9 +14,9 @@ TESTS_PASSED=0
TESTS_FAILED=0
TOTAL_TESTS=0
# Test keys for different scenarios
TEST_USER1_PRIVKEY="5c0c523f52a5b6fad39ed2403092df8cebc36318b39383bca6c00808626fab3a"
TEST_USER1_PUBKEY="87d3561f19b74adbe8bf840682992466068830a9d8c36b4a0c99d36f826cb6cb"
# Test keys for different scenarios - Using WSB's keys for TEST_USER1
TEST_USER1_PRIVKEY="22cc83aa57928a2800234c939240c9a6f0f44a33ea3838a860ed38930b195afd"
TEST_USER1_PUBKEY="8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e"
TEST_USER2_PRIVKEY="182c3a5e3b7a1b7e4f5c6b7c8b4a5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
TEST_USER2_PUBKEY="c95195e5e7de1ad8c4d3c0ac4e8b5c0c4e0c4d3c1e5c8d4c2e7e9f4a5b6c7d8e"

View File

@@ -0,0 +1 @@
Content from blacklisted user

View File

@@ -0,0 +1 @@
Content from allowed user

View File

@@ -0,0 +1 @@
First request - cache miss

View File

@@ -0,0 +1 @@
Second request - cache hit

View File

@@ -0,0 +1 @@
Testing after cleanup

View File

@@ -0,0 +1 @@
Testing disabled rule

View File

@@ -0,0 +1 @@
Testing enabled rule

View File

@@ -0,0 +1 @@
This specific file is blacklisted

View File

@@ -0,0 +1 @@
This file is allowed

View File

@@ -0,0 +1 @@
Plain text file

View File

@@ -0,0 +1 @@
Text file with whitelist active

View File

@@ -1 +1 @@
e3ba927d32ca105a8a4cafa2e013b97945a165c38e9ce573446a2332dc312fdb
299c28eeb15df327c30c9afd952d4e35c3777443d2094b2caab2fc94599ce607

View File

@@ -0,0 +1 @@
Testing operation-specific rules

View File

@@ -0,0 +1 @@
Testing priority ordering

View File

@@ -0,0 +1 @@
test content

View File

@@ -0,0 +1 @@
Content from whitelisted user

View File

@@ -0,0 +1 @@
Content from non-whitelisted user

View File

@@ -0,0 +1 @@
Testing wildcard operation

View File

@@ -5,12 +5,14 @@
set -e # Exit on any error
# Configuration
# Configuration - Using WSB's keys
# SERVER_URL="http://localhost:9001"
SERVER_URL="https://localhost:9443"
UPLOAD_ENDPOINT="${SERVER_URL}/upload"
TEST_FILE="test_blob_$(date +%s).txt"
CLEANUP_FILES=()
NOSTR_PRIVKEY="22cc83aa57928a2800234c939240c9a6f0f44a33ea3838a860ed38930b195afd"
NOSTR_PUBKEY="8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e"
# Colors for output
RED='\033[0;31m'
@@ -128,22 +130,23 @@ calculate_hash() {
# Generate nostr event
generate_nostr_event() {
log_info "Generating kind 24242 nostr event with nak..."
log_info "Generating kind 24242 nostr event with nak using Alice's private key..."
# Calculate expiration time (1 hour from now)
EXPIRATION=$(date -d '+1 hour' +%s)
# Generate the event using nak
# Generate the event using nak with Alice's private key
EVENT_JSON=$(nak event -k 24242 -c "" \
--sec "$NOSTR_PRIVKEY" \
-t "t=upload" \
-t "x=${HASH}" \
-t "expiration=${EXPIRATION}")
if [[ -z "$EVENT_JSON" ]]; then
log_error "Failed to generate nostr event"
exit 1
fi
log_success "Generated nostr event"
echo "Event JSON: $EVENT_JSON"
}

View File

@@ -126,22 +126,23 @@ calculate_hash() {
# Generate nostr event
generate_nostr_event() {
log_info "Generating kind 24242 nostr event with nak..."
log_info "Generating kind 24242 nostr event with nak using WSB's private key..."
# Calculate expiration time (1 hour from now)
EXPIRATION=$(date -d '+1 hour' +%s)
# Generate the event using nak
# Generate the event using nak with WSB's private key
EVENT_JSON=$(nak event -k 24242 -c "" \
--sec "22cc83aa57928a2800234c939240c9a6f0f44a33ea3838a860ed38930b195afd" \
-t "t=upload" \
-t "x=${HASH}" \
-t "expiration=${EXPIRATION}")
if [[ -z "$EVENT_JSON" ]]; then
log_error "Failed to generate nostr event"
exit 1
fi
log_success "Generated nostr event"
echo "Event JSON: $EVENT_JSON"
}

View File

@@ -4,8 +4,8 @@
# This script tests the blob listing functionality
BASE_URL="http://localhost:9001"
NOSTR_PRIVKEY="0000000000000000000000000000000000000000000000000000000000000001"
NOSTR_PUBKEY="79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
NOSTR_PRIVKEY="22cc83aa57928a2800234c939240c9a6f0f44a33ea3838a860ed38930b195afd"
NOSTR_PUBKEY="8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e"
# Colors for output
RED='\033[0;31m'
@@ -19,128 +19,117 @@ echo
# Function to generate a Nostr event for list authorization
generate_list_auth() {
local content="$1"
local created_at=$(date +%s)
local expiration=$((created_at + 3600)) # 1 hour from now
# Note: This is a placeholder - in real implementation, you'd use nostr tools
# to generate properly signed events. For now, we'll create the structure.
cat << EOF
{
"id": "placeholder_id",
"pubkey": "$NOSTR_PUBKEY",
"kind": 24242,
"content": "$content",
"created_at": $created_at,
"tags": [
["t", "list"],
["expiration", "$expiration"]
],
"sig": "placeholder_signature"
}
EOF
# Use nak to generate properly signed events with Alice's private key
nak event -k 24242 -c "$content" \
--sec "$NOSTR_PRIVKEY" \
-t "t=list" \
-t "expiration=$(( $(date +%s) + 3600 ))"
}
# Test 1: List blobs without authorization (should work if optional auth)
echo -e "${YELLOW}Test 1: GET /list/<pubkey> without authorization${NC}"
RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" "$BASE_URL/list/$NOSTR_PUBKEY")
HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
echo "Using pubkey: $NOSTR_PUBKEY"
echo "HTTP Status: $HTTP_STATUS"
echo "Response: $BODY"
echo
# Test 2: List blobs with authorization
echo -e "${YELLOW}Test 2: GET /list/<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 "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')
# Test 3: List blobs with since parameter
echo -e "${YELLOW}Test 3: GET /list/<pubkey> with since parameter${NC}"
SINCE_TIMESTAMP=$(($(date +%s) - 86400)) # 24 hours ago
RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \
"$BASE_URL/list/$NOSTR_PUBKEY?since=$SINCE_TIMESTAMP")
HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
# echo "HTTP Status: $HTTP_STATUS"
# echo "Response: $BODY"
# echo
echo "HTTP Status: $HTTP_STATUS"
echo "Response: $BODY"
echo
# # Test 3: List blobs with since parameter
# echo -e "${YELLOW}Test 3: GET /list/<pubkey> with since parameter${NC}"
# SINCE_TIMESTAMP=$(($(date +%s) - 86400)) # 24 hours ago
# RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \
# "$BASE_URL/list/$NOSTR_PUBKEY?since=$SINCE_TIMESTAMP")
# HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
# BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
# Test 4: List blobs with until parameter
echo -e "${YELLOW}Test 4: GET /list/<pubkey> with until parameter${NC}"
UNTIL_TIMESTAMP=$(date +%s) # now
RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \
"$BASE_URL/list/$NOSTR_PUBKEY?until=$UNTIL_TIMESTAMP")
HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
# echo "HTTP Status: $HTTP_STATUS"
# echo "Response: $BODY"
# echo
echo "HTTP Status: $HTTP_STATUS"
echo "Response: $BODY"
echo
# # Test 4: List blobs with until parameter
# echo -e "${YELLOW}Test 4: GET /list/<pubkey> with until parameter${NC}"
# UNTIL_TIMESTAMP=$(date +%s) # now
# RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \
# "$BASE_URL/list/$NOSTR_PUBKEY?until=$UNTIL_TIMESTAMP")
# HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
# BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
# Test 5: List blobs with both since and until parameters
echo -e "${YELLOW}Test 5: GET /list/<pubkey> with since and until parameters${NC}"
SINCE_TIMESTAMP=$(($(date +%s) - 86400)) # 24 hours ago
UNTIL_TIMESTAMP=$(date +%s) # now
RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \
"$BASE_URL/list/$NOSTR_PUBKEY?since=$SINCE_TIMESTAMP&until=$UNTIL_TIMESTAMP")
HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
# echo "HTTP Status: $HTTP_STATUS"
# echo "Response: $BODY"
# echo
echo "HTTP Status: $HTTP_STATUS"
echo "Response: $BODY"
echo
# # Test 5: List blobs with both since and until parameters
# echo -e "${YELLOW}Test 5: GET /list/<pubkey> with since and until parameters${NC}"
# SINCE_TIMESTAMP=$(($(date +%s) - 86400)) # 24 hours ago
# UNTIL_TIMESTAMP=$(date +%s) # now
# RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \
# "$BASE_URL/list/$NOSTR_PUBKEY?since=$SINCE_TIMESTAMP&until=$UNTIL_TIMESTAMP")
# HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
# BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
# Test 6: List blobs for non-existent pubkey
echo -e "${YELLOW}Test 6: GET /list/<nonexistent_pubkey>${NC}"
FAKE_PUBKEY="1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" "$BASE_URL/list/$FAKE_PUBKEY")
HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
# echo "HTTP Status: $HTTP_STATUS"
# echo "Response: $BODY"
# echo
if [ "$HTTP_STATUS" = "200" ]; then
echo -e "${GREEN}✓ Correctly returned 200 with empty array${NC}"
else
echo "HTTP Status: $HTTP_STATUS"
fi
echo "Response: $BODY"
echo
# # Test 6: List blobs for non-existent pubkey
# echo -e "${YELLOW}Test 6: GET /list/<nonexistent_pubkey>${NC}"
# FAKE_PUBKEY="1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
# RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" "$BASE_URL/list/$FAKE_PUBKEY")
# HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
# BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
# Test 7: List blobs with invalid pubkey format
echo -e "${YELLOW}Test 7: GET /list/<invalid_pubkey_format>${NC}"
INVALID_PUBKEY="invalid_pubkey"
RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" "$BASE_URL/list/$INVALID_PUBKEY")
HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
# if [ "$HTTP_STATUS" = "200" ]; then
# echo -e "${GREEN}✓ Correctly returned 200 with empty array${NC}"
# else
# echo "HTTP Status: $HTTP_STATUS"
# fi
# echo "Response: $BODY"
# echo
if [ "$HTTP_STATUS" = "400" ]; then
echo -e "${GREEN}✓ Correctly returned 400 for invalid pubkey format${NC}"
else
echo "HTTP Status: $HTTP_STATUS"
fi
echo "Response: $BODY"
echo
# # Test 7: List blobs with invalid pubkey format
# echo -e "${YELLOW}Test 7: GET /list/<invalid_pubkey_format>${NC}"
# INVALID_PUBKEY="invalid_pubkey"
# RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" "$BASE_URL/list/$INVALID_PUBKEY")
# HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP_STATUS" | cut -d: -f2)
# BODY=$(echo "$RESPONSE" | sed '/HTTP_STATUS/d')
# Test 8: List blobs with invalid since/until parameters
echo -e "${YELLOW}Test 8: GET /list/<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')
# if [ "$HTTP_STATUS" = "400" ]; then
# echo -e "${GREEN}✓ Correctly returned 400 for invalid pubkey format${NC}"
# else
# echo "HTTP Status: $HTTP_STATUS"
# fi
# echo "Response: $BODY"
# echo
# # Test 8: List blobs with invalid since/until parameters
# echo -e "${YELLOW}Test 8: GET /list/<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 "HTTP Status: $HTTP_STATUS"
echo "Response: $BODY"
echo
# echo "=== List Tests Complete ==="
# echo

353
tests/white_black_list_test.sh Executable file
View File

@@ -0,0 +1,353 @@
#!/bin/bash
# white_black_list_test.sh - Whitelist/Blacklist Rules Test Suite
# Tests the auth_rules table functionality for pubkey and MIME type filtering
# Configuration
SERVER_URL="http://localhost:9001"
UPLOAD_ENDPOINT="${SERVER_URL}/upload"
DB_PATH="db/ginxsom.db"
TEST_DIR="tests/auth_test_tmp"
# Test results tracking
TESTS_PASSED=0
TESTS_FAILED=0
TOTAL_TESTS=0
# Test keys for different scenarios - Using WSB's keys for TEST_USER1
# Generated using: nak key public <privkey>
TEST_USER1_PRIVKEY="22cc83aa57928a2800234c939240c9a6f0f44a33ea3838a860ed38930b195afd"
TEST_USER1_PUBKEY="8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e"
TEST_USER2_PRIVKEY="182c3a5e3b7a1b7e4f5c6b7c8b4a5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
TEST_USER2_PUBKEY="0396b426090284a28294078dce53fe73791ab623c3fc46ab4409fea05109a6db"
TEST_USER3_PRIVKEY="abcd1234567890abcd1234567890abcd1234567890abcd1234567890abcd1234"
TEST_USER3_PUBKEY="769a740386211c76f81bb235de50a5e6fa463cb4fae25e62625607fc2cfc0f28"
# Helper function to record test results
record_test_result() {
local test_name="$1"
local expected="$2"
local actual="$3"
TOTAL_TESTS=$((TOTAL_TESTS + 1))
if [[ "$actual" == "$expected" ]]; then
echo "$test_name - PASSED"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
echo "$test_name - FAILED (Expected: $expected, Got: $actual)"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
}
# Check prerequisites
for cmd in nak curl jq sqlite3; do
if ! command -v $cmd &> /dev/null; then
echo "$cmd command not found"
exit 1
fi
done
# Check if server is running
if ! curl -s -f "${SERVER_URL}/" > /dev/null 2>&1; then
echo "❌ Server not running at $SERVER_URL"
echo "Start with: ./restart-all.sh"
exit 1
fi
# Check if database exists
if [[ ! -f "$DB_PATH" ]]; then
echo "❌ Database not found at $DB_PATH"
exit 1
fi
# Setup test environment
mkdir -p "$TEST_DIR"
echo "=========================================="
echo " WHITELIST/BLACKLIST RULES TEST SUITE"
echo "=========================================="
echo
# Helper functions
create_test_file() {
local filename="$1"
local content="${2:-test content for $filename}"
local filepath="$TEST_DIR/$filename"
echo "$content" > "$filepath"
echo "$filepath"
}
create_auth_event() {
local privkey="$1"
local operation="$2"
local hash="$3"
local expiration_offset="${4:-3600}" # 1 hour default
local expiration=$(date -d "+${expiration_offset} seconds" +%s)
local event_args=(-k 24242 -c "" --tag "t=$operation" --tag "expiration=$expiration" --sec "$privkey")
if [[ -n "$hash" ]]; then
event_args+=(--tag "x=$hash")
fi
nak event "${event_args[@]}"
}
test_upload() {
local test_name="$1"
local privkey="$2"
local file_path="$3"
local expected_status="${4:-200}"
local file_hash=$(sha256sum "$file_path" | cut -d' ' -f1)
# Create auth event
local event=$(create_auth_event "$privkey" "upload" "$file_hash")
local auth_header="Nostr $(echo "$event" | base64 -w 0)"
# Make upload request
local response_file=$(mktemp)
local http_status=$(curl -s -w "%{http_code}" \
-H "Authorization: $auth_header" \
-H "Content-Type: text/plain" \
--data-binary "@$file_path" \
-X PUT "$UPLOAD_ENDPOINT" \
-o "$response_file" 2>/dev/null)
# Show response if test fails
if [[ "$http_status" != "$expected_status" ]]; then
echo " Response: $(cat "$response_file")"
fi
rm -f "$response_file"
# Record result
record_test_result "$test_name" "$expected_status" "$http_status"
}
# Clean up any existing rules from previous tests
echo "Cleaning up existing auth rules..."
sqlite3 "$DB_PATH" "DELETE FROM auth_rules;" 2>/dev/null
# Enable authentication rules
echo "Enabling authentication rules..."
sqlite3 "$DB_PATH" "UPDATE config SET value = 'true' WHERE key = 'auth_rules_enabled';"
echo
echo "=== SECTION 1: PUBKEY BLACKLIST TESTS ==="
echo
# Test 1: Add pubkey blacklist rule
echo "Adding blacklist rule for TEST_USER3..."
sqlite3 "$DB_PATH" "INSERT INTO auth_rules (rule_type, rule_target, operation, priority, description) VALUES ('pubkey_blacklist', '$TEST_USER3_PUBKEY', 'upload', 10, 'Test blacklist');"
# Test 1a: Blacklisted user should be denied
test_file1=$(create_test_file "blacklist_test1.txt" "Content from blacklisted user")
test_upload "Test 1a: Blacklisted Pubkey Upload" "$TEST_USER3_PRIVKEY" "$test_file1" "403"
# Test 1b: Non-blacklisted user should succeed
test_file2=$(create_test_file "blacklist_test2.txt" "Content from allowed user")
test_upload "Test 1b: Non-Blacklisted Pubkey Upload" "$TEST_USER1_PRIVKEY" "$test_file2" "200"
echo
echo "=== SECTION 2: PUBKEY WHITELIST TESTS ==="
echo
# Clean rules
sqlite3 "$DB_PATH" "DELETE FROM auth_rules;"
sqlite3 "$DB_PATH" "DELETE FROM auth_rules_cache;"
# Test 2: Add pubkey whitelist rule
echo "Adding whitelist rule for TEST_USER1..."
sqlite3 "$DB_PATH" "INSERT INTO auth_rules (rule_type, rule_target, operation, priority, description) VALUES ('pubkey_whitelist', '$TEST_USER1_PUBKEY', 'upload', 300, 'Test whitelist');"
# Test 2a: Whitelisted user should succeed
test_file3=$(create_test_file "whitelist_test1.txt" "Content from whitelisted user")
test_upload "Test 2a: Whitelisted Pubkey Upload" "$TEST_USER1_PRIVKEY" "$test_file3" "200"
# Test 2b: Non-whitelisted user should be denied (whitelist default-deny)
test_file4=$(create_test_file "whitelist_test2.txt" "Content from non-whitelisted user")
test_upload "Test 2b: Non-Whitelisted Pubkey Upload" "$TEST_USER2_PRIVKEY" "$test_file4" "403"
echo
echo "=== SECTION 3: HASH BLACKLIST TESTS ==="
echo
# Clean rules
sqlite3 "$DB_PATH" "DELETE FROM auth_rules;"
# Test 3: Create a file and blacklist its hash
test_file5=$(create_test_file "hash_blacklist_test.txt" "This specific file is blacklisted")
BLACKLISTED_HASH=$(sha256sum "$test_file5" | cut -d' ' -f1)
echo "Adding hash blacklist rule for $BLACKLISTED_HASH..."
sqlite3 "$DB_PATH" "INSERT INTO auth_rules (rule_type, rule_target, operation, priority, description) VALUES ('hash_blacklist', '$BLACKLISTED_HASH', 'upload', 100, 'Test hash blacklist');"
# Test 3a: Blacklisted hash should be denied
test_upload "Test 3a: Blacklisted Hash Upload" "$TEST_USER1_PRIVKEY" "$test_file5" "403"
# Test 3b: Different file should succeed
test_file6=$(create_test_file "hash_blacklist_test2.txt" "This file is allowed")
test_upload "Test 3b: Non-Blacklisted Hash Upload" "$TEST_USER1_PRIVKEY" "$test_file6" "200"
echo
echo "=== SECTION 4: MIME TYPE BLACKLIST TESTS ==="
echo
# Clean rules
sqlite3 "$DB_PATH" "DELETE FROM auth_rules;"
sqlite3 "$DB_PATH" "DELETE FROM auth_rules_cache;"
# Test 4: Blacklist executable MIME types
echo "Adding MIME type blacklist rules..."
sqlite3 "$DB_PATH" "INSERT INTO auth_rules (rule_type, rule_target, operation, priority, description) VALUES ('mime_blacklist', 'application/x-executable', 'upload', 200, 'Block executables');"
# Note: This test would require the server to detect MIME types from file content
# For now, we'll test with text/plain which should be allowed
test_file7=$(create_test_file "mime_test1.txt" "Plain text file")
test_upload "Test 4a: Allowed MIME Type Upload" "$TEST_USER1_PRIVKEY" "$test_file7" "200"
echo
echo "=== SECTION 5: MIME TYPE WHITELIST TESTS ==="
echo
# Clean rules
sqlite3 "$DB_PATH" "DELETE FROM auth_rules;"
sqlite3 "$DB_PATH" "DELETE FROM auth_rules_cache;"
# Test 5: Whitelist only image MIME types
echo "Adding MIME type whitelist rules..."
sqlite3 "$DB_PATH" "INSERT INTO auth_rules (rule_type, rule_target, operation, priority, description) VALUES ('mime_whitelist', 'image/jpeg', 'upload', 400, 'Allow JPEG');"
sqlite3 "$DB_PATH" "INSERT INTO auth_rules (rule_type, rule_target, operation, priority, description) VALUES ('mime_whitelist', 'image/png', 'upload', 400, 'Allow PNG');"
# Note: MIME type detection would need to be implemented in the server
# For now, text/plain should be denied if whitelist exists
test_file8=$(create_test_file "mime_whitelist_test.txt" "Text file with whitelist active")
test_upload "Test 5a: Non-Whitelisted MIME Type Upload" "$TEST_USER1_PRIVKEY" "$test_file8" "403"
echo
echo "=== SECTION 6: PRIORITY ORDERING TESTS ==="
echo
# Clean rules
sqlite3 "$DB_PATH" "DELETE FROM auth_rules;"
sqlite3 "$DB_PATH" "DELETE FROM auth_rules_cache;"
# Test 6: Blacklist should override whitelist (priority ordering)
echo "Adding both blacklist (priority 10) and whitelist (priority 300) for same pubkey..."
sqlite3 "$DB_PATH" "INSERT INTO auth_rules (rule_type, rule_target, operation, priority, description) VALUES ('pubkey_blacklist', '$TEST_USER1_PUBKEY', 'upload', 10, 'Blacklist priority test');"
sqlite3 "$DB_PATH" "INSERT INTO auth_rules (rule_type, rule_target, operation, priority, description) VALUES ('pubkey_whitelist', '$TEST_USER1_PUBKEY', 'upload', 300, 'Whitelist priority test');"
# Test 6a: Blacklist should win (lower priority number = higher priority)
test_file9=$(create_test_file "priority_test.txt" "Testing priority ordering")
test_upload "Test 6a: Blacklist Priority Over Whitelist" "$TEST_USER1_PRIVKEY" "$test_file9" "403"
echo
echo "=== SECTION 7: OPERATION-SPECIFIC RULES ==="
echo
# Clean rules
sqlite3 "$DB_PATH" "DELETE FROM auth_rules;"
sqlite3 "$DB_PATH" "DELETE FROM auth_rules_cache;"
# Test 7: Blacklist only for upload operation
echo "Adding blacklist rule for upload operation only..."
sqlite3 "$DB_PATH" "INSERT INTO auth_rules (rule_type, rule_target, operation, priority, description) VALUES ('pubkey_blacklist', '$TEST_USER2_PUBKEY', 'upload', 10, 'Upload-only blacklist');"
# Test 7a: Upload should be denied
test_file10=$(create_test_file "operation_test.txt" "Testing operation-specific rules")
test_upload "Test 7a: Operation-Specific Blacklist" "$TEST_USER2_PRIVKEY" "$test_file10" "403"
echo
echo "=== SECTION 8: WILDCARD OPERATION TESTS ==="
echo
# Clean rules
sqlite3 "$DB_PATH" "DELETE FROM auth_rules;"
sqlite3 "$DB_PATH" "DELETE FROM auth_rules_cache;"
# Test 8: Blacklist for all operations using wildcard
echo "Adding blacklist rule for all operations (*)..."
sqlite3 "$DB_PATH" "INSERT INTO auth_rules (rule_type, rule_target, operation, priority, description) VALUES ('pubkey_blacklist', '$TEST_USER3_PUBKEY', '*', 10, 'All operations blacklist');"
# Test 8a: Upload should be denied
test_file11=$(create_test_file "wildcard_test.txt" "Testing wildcard operation")
test_upload "Test 8a: Wildcard Operation Blacklist" "$TEST_USER3_PRIVKEY" "$test_file11" "403"
echo
echo "=== SECTION 9: ENABLED/DISABLED RULES ==="
echo
# Clean rules
sqlite3 "$DB_PATH" "DELETE FROM auth_rules;"
sqlite3 "$DB_PATH" "DELETE FROM auth_rules_cache;"
# Test 9: Disabled rule should not be enforced
echo "Adding disabled blacklist rule..."
sqlite3 "$DB_PATH" "INSERT INTO auth_rules (rule_type, rule_target, operation, priority, enabled, description) VALUES ('pubkey_blacklist', '$TEST_USER1_PUBKEY', 'upload', 10, 0, 'Disabled blacklist');"
# Test 9a: Upload should succeed (rule is disabled)
test_file12=$(create_test_file "disabled_rule_test.txt" "Testing disabled rule")
test_upload "Test 9a: Disabled Rule Not Enforced" "$TEST_USER1_PRIVKEY" "$test_file12" "200"
# Test 9b: Enable the rule
echo "Enabling the blacklist rule..."
sqlite3 "$DB_PATH" "UPDATE auth_rules SET enabled = 1 WHERE rule_target = '$TEST_USER1_PUBKEY';"
# Test 9c: Upload should now be denied
test_file13=$(create_test_file "enabled_rule_test.txt" "Testing enabled rule")
test_upload "Test 9c: Enabled Rule Enforced" "$TEST_USER1_PRIVKEY" "$test_file13" "403"
echo
echo "=== SECTION 11: CLEANUP AND RESET ==="
echo
# Clean up all test rules
echo "Cleaning up test rules..."
sqlite3 "$DB_PATH" "DELETE FROM auth_rules;"
# Verify cleanup
RULE_COUNT=$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM auth_rules;" 2>/dev/null)
if [[ "$RULE_COUNT" -eq 0 ]]; then
record_test_result "Test 10a: Rules Cleanup" "0" "0"
else
record_test_result "Test 10a: Rules Cleanup" "0" "$RULE_COUNT"
fi
# Test that uploads work again after cleanup
test_file16=$(create_test_file "cleanup_test.txt" "Testing after cleanup")
test_upload "Test 10b: Upload After Cleanup" "$TEST_USER1_PRIVKEY" "$test_file16" "200"
echo
echo "=========================================="
echo " TEST SUITE RESULTS"
echo "=========================================="
echo
echo "Total Tests: $TOTAL_TESTS"
echo "✅ Passed: $TESTS_PASSED"
echo "❌ Failed: $TESTS_FAILED"
echo
if [[ $TESTS_FAILED -eq 0 ]]; then
echo "🎉 ALL TESTS PASSED!"
echo
echo "Whitelist/Blacklist functionality verified:"
echo "- Pubkey blacklist: Working"
echo "- Pubkey whitelist: Working"
echo "- Hash blacklist: Working"
echo "- MIME type rules: Working"
echo "- Priority ordering: Working"
echo "- Operation-specific rules: Working"
echo "- Wildcard operations: Working"
echo "- Enable/disable rules: Working"
else
echo "⚠️ Some tests failed. Check output above for details."
echo "Success rate: $(( (TESTS_PASSED * 100) / TOTAL_TESTS ))%"
fi
echo
echo "To clean up test data: rm -rf $TEST_DIR"
echo "=========================================="

54
update_remote_nginx_conf.sh Executable file
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."