Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70c91ec858 | ||
|
|
b7c4609c2d | ||
|
|
7f69367666 | ||
|
|
fa17aa1f78 | ||
|
|
7e560b4247 | ||
|
|
9a29ea51e3 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,3 +1,6 @@
|
|||||||
nostr_core_lib/
|
nostr_core_lib/
|
||||||
nips/
|
nips/
|
||||||
build/
|
build/
|
||||||
|
relay.log
|
||||||
|
Trash/
|
||||||
|
src/version.h
|
||||||
|
|||||||
1
.roo/rules-code/rules.md
Normal file
1
.roo/rules-code/rules.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Use ./make_and_restart_relay.sh instead of make to build the project.
|
||||||
61
Makefile
61
Makefile
@@ -9,7 +9,7 @@ LIBS = -lsqlite3 -lwebsockets -lz -ldl -lpthread -lm -L/usr/local/lib -lsecp256k
|
|||||||
BUILD_DIR = build
|
BUILD_DIR = build
|
||||||
|
|
||||||
# Source files
|
# Source files
|
||||||
MAIN_SRC = src/main.c
|
MAIN_SRC = src/main.c src/config.c
|
||||||
NOSTR_CORE_LIB = nostr_core_lib/libnostr_core_x64.a
|
NOSTR_CORE_LIB = nostr_core_lib/libnostr_core_x64.a
|
||||||
|
|
||||||
# Architecture detection
|
# Architecture detection
|
||||||
@@ -36,19 +36,68 @@ $(NOSTR_CORE_LIB):
|
|||||||
@echo "Building nostr_core_lib..."
|
@echo "Building nostr_core_lib..."
|
||||||
cd nostr_core_lib && ./build.sh
|
cd nostr_core_lib && ./build.sh
|
||||||
|
|
||||||
|
# Generate version.h from git tags
|
||||||
|
src/version.h:
|
||||||
|
@if [ -d .git ]; then \
|
||||||
|
echo "Generating version.h from git tags..."; \
|
||||||
|
VERSION=$$(git describe --tags --always --dirty 2>/dev/null || echo "unknown"); \
|
||||||
|
if echo "$$VERSION" | grep -q "^v[0-9]"; then \
|
||||||
|
CLEAN_VERSION=$$(echo "$$VERSION" | sed 's/^v//'); \
|
||||||
|
MAJOR=$$(echo "$$CLEAN_VERSION" | cut -d. -f1); \
|
||||||
|
MINOR=$$(echo "$$CLEAN_VERSION" | cut -d. -f2); \
|
||||||
|
PATCH=$$(echo "$$CLEAN_VERSION" | cut -d. -f3 | cut -d- -f1); \
|
||||||
|
else \
|
||||||
|
CLEAN_VERSION="0.0.0-$$VERSION"; \
|
||||||
|
MAJOR=0; MINOR=0; PATCH=0; \
|
||||||
|
fi; \
|
||||||
|
echo "/* Auto-generated version information */" > src/version.h; \
|
||||||
|
echo "#ifndef VERSION_H" >> src/version.h; \
|
||||||
|
echo "#define VERSION_H" >> src/version.h; \
|
||||||
|
echo "" >> src/version.h; \
|
||||||
|
echo "#define VERSION \"$$VERSION\"" >> src/version.h; \
|
||||||
|
echo "#define VERSION_MAJOR $$MAJOR" >> src/version.h; \
|
||||||
|
echo "#define VERSION_MINOR $$MINOR" >> src/version.h; \
|
||||||
|
echo "#define VERSION_PATCH $$PATCH" >> src/version.h; \
|
||||||
|
echo "" >> src/version.h; \
|
||||||
|
echo "#endif /* VERSION_H */" >> src/version.h; \
|
||||||
|
echo "Generated version.h with version: $$VERSION"; \
|
||||||
|
elif [ ! -f src/version.h ]; then \
|
||||||
|
echo "Git not available and version.h missing, creating fallback version.h..."; \
|
||||||
|
VERSION="unknown"; \
|
||||||
|
echo "/* Auto-generated version information */" > src/version.h; \
|
||||||
|
echo "#ifndef VERSION_H" >> src/version.h; \
|
||||||
|
echo "#define VERSION_H" >> src/version.h; \
|
||||||
|
echo "" >> src/version.h; \
|
||||||
|
echo "#define VERSION \"$$VERSION\"" >> src/version.h; \
|
||||||
|
echo "#define VERSION_MAJOR 0" >> src/version.h; \
|
||||||
|
echo "#define VERSION_MINOR 0" >> src/version.h; \
|
||||||
|
echo "#define VERSION_PATCH 0" >> src/version.h; \
|
||||||
|
echo "" >> src/version.h; \
|
||||||
|
echo "#endif /* VERSION_H */" >> src/version.h; \
|
||||||
|
echo "Created fallback version.h with version: $$VERSION"; \
|
||||||
|
else \
|
||||||
|
echo "Git not available, preserving existing version.h"; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Force version.h regeneration (useful for development)
|
||||||
|
force-version:
|
||||||
|
@echo "Force regenerating version.h..."
|
||||||
|
@rm -f src/version.h
|
||||||
|
@$(MAKE) src/version.h
|
||||||
|
|
||||||
# Build the relay
|
# Build the relay
|
||||||
$(TARGET): $(BUILD_DIR) $(MAIN_SRC) $(NOSTR_CORE_LIB)
|
$(TARGET): $(BUILD_DIR) src/version.h $(MAIN_SRC) $(NOSTR_CORE_LIB)
|
||||||
@echo "Compiling C-Relay for architecture: $(ARCH)"
|
@echo "Compiling C-Relay for architecture: $(ARCH)"
|
||||||
$(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(TARGET) $(NOSTR_CORE_LIB) $(LIBS)
|
$(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(TARGET) $(NOSTR_CORE_LIB) $(LIBS)
|
||||||
@echo "Build complete: $(TARGET)"
|
@echo "Build complete: $(TARGET)"
|
||||||
|
|
||||||
# Build for specific architectures
|
# Build for specific architectures
|
||||||
x86: $(BUILD_DIR) $(MAIN_SRC) $(NOSTR_CORE_LIB)
|
x86: $(BUILD_DIR) src/version.h $(MAIN_SRC) $(NOSTR_CORE_LIB)
|
||||||
@echo "Building C-Relay for x86_64..."
|
@echo "Building C-Relay for x86_64..."
|
||||||
$(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(BUILD_DIR)/c_relay_x86 $(NOSTR_CORE_LIB) $(LIBS)
|
$(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(BUILD_DIR)/c_relay_x86 $(NOSTR_CORE_LIB) $(LIBS)
|
||||||
@echo "Build complete: $(BUILD_DIR)/c_relay_x86"
|
@echo "Build complete: $(BUILD_DIR)/c_relay_x86"
|
||||||
|
|
||||||
arm64: $(BUILD_DIR) $(MAIN_SRC) $(NOSTR_CORE_LIB)
|
arm64: $(BUILD_DIR) src/version.h $(MAIN_SRC) $(NOSTR_CORE_LIB)
|
||||||
@echo "Cross-compiling C-Relay for ARM64..."
|
@echo "Cross-compiling C-Relay for ARM64..."
|
||||||
@if ! command -v aarch64-linux-gnu-gcc >/dev/null 2>&1; then \
|
@if ! command -v aarch64-linux-gnu-gcc >/dev/null 2>&1; then \
|
||||||
echo "ERROR: ARM64 cross-compiler not found."; \
|
echo "ERROR: ARM64 cross-compiler not found."; \
|
||||||
@@ -120,6 +169,7 @@ init-db:
|
|||||||
# Clean build artifacts
|
# Clean build artifacts
|
||||||
clean:
|
clean:
|
||||||
rm -rf $(BUILD_DIR)
|
rm -rf $(BUILD_DIR)
|
||||||
|
rm -f src/version.h
|
||||||
@echo "Clean complete"
|
@echo "Clean complete"
|
||||||
|
|
||||||
# Clean everything including nostr_core_lib
|
# Clean everything including nostr_core_lib
|
||||||
@@ -158,5 +208,6 @@ help:
|
|||||||
@echo " make check-toolchain # Check what compilers are available"
|
@echo " make check-toolchain # Check what compilers are available"
|
||||||
@echo " make test # Run tests"
|
@echo " make test # Run tests"
|
||||||
@echo " make init-db # Set up database"
|
@echo " make init-db # Set up database"
|
||||||
|
@echo " make force-version # Force regenerate version.h from git"
|
||||||
|
|
||||||
.PHONY: all x86 arm64 test init-db clean clean-all install-deps install-cross-tools install-arm64-deps check-toolchain help
|
.PHONY: all x86 arm64 test init-db clean clean-all install-deps install-cross-tools install-arm64-deps check-toolchain help force-version
|
||||||
387
admin_spec.md
387
admin_spec.md
@@ -1,387 +0,0 @@
|
|||||||
# Ginxsom Admin System - Comprehensive Specification
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The Ginxsom admin system provides both programmatic (API-based) and interactive (web-based) administration capabilities for the Ginxsom Blossom server. The system is designed around Nostr-based authentication and supports multiple administration workflows including first-run setup, ongoing configuration management, and operational monitoring.
|
|
||||||
|
|
||||||
## Architecture Components
|
|
||||||
|
|
||||||
### 1. Configuration System
|
|
||||||
- **File-based configuration**: Signed Nostr events stored as JSON files following XDG Base Directory specification
|
|
||||||
- **Database configuration**: Key-value pairs stored in SQLite for runtime configuration
|
|
||||||
- **Interactive setup**: Command-line wizard for initial server configuration
|
|
||||||
- **Manual setup**: Scripts for generating signed configuration events
|
|
||||||
|
|
||||||
### 2. Authentication & Authorization
|
|
||||||
- **Nostr-based auth**: All admin operations require valid Nostr event signatures
|
|
||||||
- **Admin pubkey verification**: Only configured admin public keys can perform admin operations
|
|
||||||
- **Event validation**: Full cryptographic verification of Nostr events including structure, signature, and expiration
|
|
||||||
- **Method-specific authorization**: Different event types for different operations (upload, admin, delete, etc.)
|
|
||||||
|
|
||||||
### 3. API System
|
|
||||||
- **RESTful endpoints**: `/api/*` routes for programmatic administration
|
|
||||||
- **Command-line testing**: Complete test suite using `nak` and `curl`
|
|
||||||
- **JSON responses**: Structured data for all admin operations
|
|
||||||
- **CORS support**: Cross-origin requests for web admin interface
|
|
||||||
|
|
||||||
### 4. Web Interface (Future)
|
|
||||||
- **Single-page application**: Self-contained HTML file with inline CSS/JS
|
|
||||||
- **Real-time monitoring**: Statistics and system health dashboards
|
|
||||||
- **Configuration management**: GUI for server settings
|
|
||||||
- **File management**: Browse and manage uploaded blobs
|
|
||||||
|
|
||||||
## Configuration System Architecture
|
|
||||||
|
|
||||||
### File-based Configuration (Priority 1)
|
|
||||||
|
|
||||||
**Location**: Follows XDG Base Directory Specification
|
|
||||||
- `$XDG_CONFIG_HOME/ginxsom/ginxsom_config_event.json`
|
|
||||||
- Falls back to `$HOME/.config/ginxsom/ginxsom_config_event.json`
|
|
||||||
|
|
||||||
**Format**: Signed Nostr event containing server configuration
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"kind": 33333,
|
|
||||||
"created_at": 1704067200,
|
|
||||||
"tags": [
|
|
||||||
["server_privkey", "server_private_key_hex"],
|
|
||||||
["cdn_origin", "https://cdn.example.com"],
|
|
||||||
["max_file_size", "104857600"],
|
|
||||||
["nip94_enabled", "true"]
|
|
||||||
],
|
|
||||||
"content": "Ginxsom server configuration",
|
|
||||||
"pubkey": "admin_public_key_hex",
|
|
||||||
"id": "event_id_hash",
|
|
||||||
"sig": "event_signature"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Loading Process**:
|
|
||||||
1. Check for file-based config at XDG location
|
|
||||||
2. Validate Nostr event structure and signature
|
|
||||||
3. Extract configuration from event tags
|
|
||||||
4. Apply settings to server (database storage)
|
|
||||||
5. Fall back to database-only config if file missing/invalid
|
|
||||||
|
|
||||||
### Database Configuration (Priority 2)
|
|
||||||
|
|
||||||
**Table**: `server_config`
|
|
||||||
```sql
|
|
||||||
CREATE TABLE server_config (
|
|
||||||
key TEXT PRIMARY KEY,
|
|
||||||
value TEXT NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key Configuration Items**:
|
|
||||||
- `admin_pubkey`: Authorized admin public key
|
|
||||||
- `admin_enabled`: Enable/disable admin interface
|
|
||||||
- `cdn_origin`: Base URL for blob access
|
|
||||||
- `max_file_size`: Maximum upload size in bytes
|
|
||||||
- `nip94_enabled`: Enable NIP-94 metadata emission
|
|
||||||
- `auth_rules_enabled`: Enable authentication rules system
|
|
||||||
|
|
||||||
### Setup Workflows
|
|
||||||
|
|
||||||
#### Interactive Setup (Command Line)
|
|
||||||
```bash
|
|
||||||
# First-run detection
|
|
||||||
if [[ ! -f "$XDG_CONFIG_HOME/ginxsom/ginxsom_config_event.json" ]]; then
|
|
||||||
echo "=== Ginxsom First-Time Setup Required ==="
|
|
||||||
echo "1. Run interactive setup wizard"
|
|
||||||
echo "2. Exit and create config manually"
|
|
||||||
read -p "Choice (1/2): " choice
|
|
||||||
|
|
||||||
if [[ "$choice" == "1" ]]; then
|
|
||||||
./scripts/setup.sh
|
|
||||||
else
|
|
||||||
echo "Manual setup: Run ./scripts/generate_config.sh"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Manual Setup (Script-based)
|
|
||||||
```bash
|
|
||||||
# Generate configuration event
|
|
||||||
./scripts/generate_config.sh --admin-key <admin_pubkey> \
|
|
||||||
--server-key <server_privkey> \
|
|
||||||
--cdn-origin "https://cdn.example.com" \
|
|
||||||
--output "$XDG_CONFIG_HOME/ginxsom/ginxsom_config_event.json"
|
|
||||||
```
|
|
||||||
|
|
||||||
### C Implementation Functions
|
|
||||||
|
|
||||||
#### Configuration Loading
|
|
||||||
```c
|
|
||||||
// Get XDG-compliant config file path
|
|
||||||
int get_config_file_path(char* path, size_t path_size);
|
|
||||||
|
|
||||||
// Load and validate config event from file
|
|
||||||
int load_server_config(const char* config_path);
|
|
||||||
|
|
||||||
// Extract config from validated event and apply to server
|
|
||||||
int apply_config_from_event(cJSON* event);
|
|
||||||
|
|
||||||
// Interactive setup runner for first-run
|
|
||||||
int run_interactive_setup(const char* config_path);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Security Features
|
|
||||||
- Server private key stored only in memory (never in database)
|
|
||||||
- Config file must be signed Nostr event
|
|
||||||
- Full cryptographic validation of config events
|
|
||||||
- Admin pubkey verification for all operations
|
|
||||||
|
|
||||||
## Admin API Specification
|
|
||||||
|
|
||||||
### Authentication Model
|
|
||||||
|
|
||||||
All admin API endpoints (except `/api/health`) require Nostr authentication:
|
|
||||||
|
|
||||||
**Authorization Header Format**:
|
|
||||||
```
|
|
||||||
Authorization: Nostr <base64-encoded-event>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Required Event Structure**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"kind": 24242,
|
|
||||||
"created_at": 1704067200,
|
|
||||||
"tags": [
|
|
||||||
["t", "GET"],
|
|
||||||
["expiration", "1704070800"]
|
|
||||||
],
|
|
||||||
"content": "admin_request",
|
|
||||||
"pubkey": "admin_public_key",
|
|
||||||
"id": "event_id",
|
|
||||||
"sig": "event_signature"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### API Endpoints
|
|
||||||
|
|
||||||
#### GET /api/health
|
|
||||||
**Purpose**: System health check (no authentication required)
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "success",
|
|
||||||
"data": {
|
|
||||||
"database": "connected",
|
|
||||||
"blob_directory": "accessible",
|
|
||||||
"server_time": 1704067200,
|
|
||||||
"uptime": 3600,
|
|
||||||
"disk_usage": {
|
|
||||||
"total_bytes": 1073741824,
|
|
||||||
"used_bytes": 536870912,
|
|
||||||
"available_bytes": 536870912,
|
|
||||||
"usage_percent": 50.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### GET /api/stats
|
|
||||||
**Purpose**: Server statistics and metrics
|
|
||||||
**Authentication**: Required (admin pubkey)
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "success",
|
|
||||||
"data": {
|
|
||||||
"total_files": 1234,
|
|
||||||
"total_bytes": 104857600,
|
|
||||||
"total_size_mb": 100.0,
|
|
||||||
"unique_uploaders": 56,
|
|
||||||
"first_upload": 1693929600,
|
|
||||||
"last_upload": 1704067200,
|
|
||||||
"avg_file_size": 85049,
|
|
||||||
"file_types": {
|
|
||||||
"image/png": 45,
|
|
||||||
"image/jpeg": 32,
|
|
||||||
"application/pdf": 12,
|
|
||||||
"other": 8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### GET /api/config
|
|
||||||
**Purpose**: Retrieve current server configuration
|
|
||||||
**Authentication**: Required (admin pubkey)
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "success",
|
|
||||||
"data": {
|
|
||||||
"cdn_origin": "http://localhost:9001",
|
|
||||||
"max_file_size": "104857600",
|
|
||||||
"nip94_enabled": "true",
|
|
||||||
"auth_rules_enabled": "false",
|
|
||||||
"auth_cache_ttl": "300"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### PUT /api/config
|
|
||||||
**Purpose**: Update server configuration
|
|
||||||
**Authentication**: Required (admin pubkey)
|
|
||||||
**Request Body**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"max_file_size": "209715200",
|
|
||||||
"nip94_enabled": "true",
|
|
||||||
"cdn_origin": "https://cdn.example.com"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "success",
|
|
||||||
"message": "Configuration updated successfully",
|
|
||||||
"updated_keys": ["max_file_size", "cdn_origin"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### GET /api/files
|
|
||||||
**Purpose**: List recent files with pagination
|
|
||||||
**Authentication**: Required (admin pubkey)
|
|
||||||
**Parameters**:
|
|
||||||
- `limit` (default: 50): Number of files to return
|
|
||||||
- `offset` (default: 0): Pagination offset
|
|
||||||
**Response**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "success",
|
|
||||||
"data": {
|
|
||||||
"files": [
|
|
||||||
{
|
|
||||||
"sha256": "b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553",
|
|
||||||
"size": 184292,
|
|
||||||
"type": "application/pdf",
|
|
||||||
"uploaded_at": 1725105921,
|
|
||||||
"uploader_pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
|
|
||||||
"filename": "document.pdf",
|
|
||||||
"url": "http://localhost:9001/b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"total": 1234,
|
|
||||||
"limit": 50,
|
|
||||||
"offset": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Implementation Status
|
|
||||||
|
|
||||||
### ✅ Completed Components
|
|
||||||
1. **Database-based configuration loading** - Implemented in main.c
|
|
||||||
2. **Admin API authentication system** - Implemented in admin_api.c
|
|
||||||
3. **Nostr event validation** - Full cryptographic verification
|
|
||||||
4. **Admin pubkey verification** - Database-backed authorization
|
|
||||||
5. **Basic API endpoints** - Health, stats, config, files
|
|
||||||
|
|
||||||
### ✅ Recently Completed Components
|
|
||||||
1. **File-based configuration system** - Fully implemented in main.c with XDG compliance
|
|
||||||
2. **Interactive setup wizard** - Complete shell script with guided setup process (`scripts/setup.sh`)
|
|
||||||
3. **Manual config generation** - Full-featured command-line config generator (`scripts/generate_config.sh`)
|
|
||||||
4. **Testing infrastructure** - Comprehensive admin API test suite (`scripts/test_admin.sh`)
|
|
||||||
5. **Documentation system** - Complete setup and usage documentation (`scripts/README.md`)
|
|
||||||
|
|
||||||
### 📋 Planned Components
|
|
||||||
1. **Web admin interface** - Single-page HTML application
|
|
||||||
2. **Enhanced monitoring** - Real-time statistics dashboard
|
|
||||||
3. **Bulk operations** - Multi-file management APIs
|
|
||||||
4. **Configuration validation** - Advanced config checking
|
|
||||||
5. **Audit logging** - Admin action tracking
|
|
||||||
|
|
||||||
## Setup Instructions
|
|
||||||
|
|
||||||
### 1. Enable Admin Interface
|
|
||||||
```bash
|
|
||||||
# Configure admin pubkey and enable interface
|
|
||||||
sqlite3 db/ginxsom.db << EOF
|
|
||||||
INSERT OR REPLACE INTO server_config (key, value, description) VALUES
|
|
||||||
('admin_pubkey', 'your_admin_public_key_here', 'Authorized admin public key'),
|
|
||||||
('admin_enabled', 'true', 'Enable admin interface');
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Test API Access
|
|
||||||
```bash
|
|
||||||
# Generate admin authentication event
|
|
||||||
ADMIN_PRIVKEY="your_admin_private_key"
|
|
||||||
EVENT=$(nak event -k 24242 -c "admin_request" \
|
|
||||||
--tag t="GET" \
|
|
||||||
--tag expiration="$(date -d '+1 hour' +%s)" \
|
|
||||||
--sec "$ADMIN_PRIVKEY")
|
|
||||||
|
|
||||||
# Test admin API
|
|
||||||
AUTH_HEADER="Nostr $(echo "$EVENT" | base64 -w 0)"
|
|
||||||
curl -H "Authorization: $AUTH_HEADER" http://localhost:9001/api/stats
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Configure File-based Setup (Future)
|
|
||||||
```bash
|
|
||||||
# Create XDG config directory
|
|
||||||
mkdir -p "$XDG_CONFIG_HOME/ginxsom"
|
|
||||||
|
|
||||||
# Generate signed config event
|
|
||||||
./scripts/generate_config.sh \
|
|
||||||
--admin-key "your_admin_pubkey" \
|
|
||||||
--server-key "generated_server_privkey" \
|
|
||||||
--output "$XDG_CONFIG_HOME/ginxsom/ginxsom_config_event.json"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
|
|
||||||
### Authentication Security
|
|
||||||
- **Event expiration**: All admin events must include expiration timestamps
|
|
||||||
- **Signature validation**: Full secp256k1 cryptographic verification
|
|
||||||
- **Replay protection**: Event IDs tracked to prevent reuse
|
|
||||||
- **Admin key rotation**: Support for updating admin pubkeys
|
|
||||||
|
|
||||||
### Configuration Security
|
|
||||||
- **File permissions**: Config files should be readable only by server user
|
|
||||||
- **Private key handling**: Server private keys never stored in database
|
|
||||||
- **Config validation**: All configuration changes validated before application
|
|
||||||
- **Backup verification**: Config events cryptographically verifiable
|
|
||||||
|
|
||||||
### Operational Security
|
|
||||||
- **Access logging**: All admin operations logged with timestamps
|
|
||||||
- **Rate limiting**: API endpoints protected against abuse
|
|
||||||
- **Input validation**: All user input sanitized and validated
|
|
||||||
- **Database security**: Prepared statements prevent SQL injection
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
### 1. Web Admin Interface
|
|
||||||
- Self-contained HTML file with inline CSS/JavaScript
|
|
||||||
- Real-time monitoring dashboards
|
|
||||||
- Visual configuration management
|
|
||||||
- File upload/management interface
|
|
||||||
|
|
||||||
### 2. Advanced Monitoring
|
|
||||||
- Performance metrics collection
|
|
||||||
- Alert system for critical events
|
|
||||||
- Historical data trending
|
|
||||||
- Resource usage tracking
|
|
||||||
|
|
||||||
### 3. Multi-admin Support
|
|
||||||
- Multiple authorized admin pubkeys
|
|
||||||
- Role-based permissions (read-only vs full admin)
|
|
||||||
- Admin action audit trails
|
|
||||||
- Delegation capabilities
|
|
||||||
|
|
||||||
### 4. Integration Features
|
|
||||||
- Nostr relay integration for admin events
|
|
||||||
- Webhook notifications for admin actions
|
|
||||||
- External authentication providers
|
|
||||||
- API key management for programmatic access
|
|
||||||
|
|
||||||
This specification represents the current understanding and planned development of the Ginxsom admin system, focusing on security, usability, and maintainability.
|
|
||||||
@@ -139,6 +139,13 @@ compile_project() {
|
|||||||
print_warning "Clean failed or no Makefile found"
|
print_warning "Clean failed or no Makefile found"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Force regenerate version.h to pick up new tags
|
||||||
|
if make force-version > /dev/null 2>&1; then
|
||||||
|
print_success "Regenerated version.h"
|
||||||
|
else
|
||||||
|
print_warning "Failed to regenerate version.h"
|
||||||
|
fi
|
||||||
|
|
||||||
# Compile the project
|
# Compile the project
|
||||||
if make > /dev/null 2>&1; then
|
if make > /dev/null 2>&1; then
|
||||||
print_success "C-Relay compiled successfully"
|
print_success "C-Relay compiled successfully"
|
||||||
@@ -229,10 +236,55 @@ git_commit_and_push() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if git push --tags > /dev/null 2>&1; then
|
# Push only the new tag to avoid conflicts with existing tags
|
||||||
print_success "Pushed tags"
|
if git push origin "$NEW_VERSION" > /dev/null 2>&1; then
|
||||||
|
print_success "Pushed tag: $NEW_VERSION"
|
||||||
else
|
else
|
||||||
print_warning "Failed to push tags"
|
print_error "Failed to push tag: $NEW_VERSION"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to commit and push changes without creating a tag (tag already created)
|
||||||
|
git_commit_and_push_no_tag() {
|
||||||
|
print_status "Preparing git commit..."
|
||||||
|
|
||||||
|
# Stage all changes
|
||||||
|
if git add . > /dev/null 2>&1; then
|
||||||
|
print_success "Staged all changes"
|
||||||
|
else
|
||||||
|
print_error "Failed to stage changes"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if there are changes to commit
|
||||||
|
if git diff --staged --quiet; then
|
||||||
|
print_warning "No changes to commit"
|
||||||
|
else
|
||||||
|
# Commit changes
|
||||||
|
if git commit -m "$NEW_VERSION - $COMMIT_MESSAGE" > /dev/null 2>&1; then
|
||||||
|
print_success "Committed changes"
|
||||||
|
else
|
||||||
|
print_error "Failed to commit changes"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Push changes and tags
|
||||||
|
print_status "Pushing to remote repository..."
|
||||||
|
if git push > /dev/null 2>&1; then
|
||||||
|
print_success "Pushed changes"
|
||||||
|
else
|
||||||
|
print_error "Failed to push changes"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Push only the new tag to avoid conflicts with existing tags
|
||||||
|
if git push origin "$NEW_VERSION" > /dev/null 2>&1; then
|
||||||
|
print_success "Pushed tag: $NEW_VERSION"
|
||||||
|
else
|
||||||
|
print_error "Failed to push tag: $NEW_VERSION"
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,14 +404,23 @@ main() {
|
|||||||
# Increment minor version for releases
|
# Increment minor version for releases
|
||||||
increment_version "minor"
|
increment_version "minor"
|
||||||
|
|
||||||
# Compile project first
|
# Create new git tag BEFORE compilation so version.h picks it up
|
||||||
|
if git tag "$NEW_VERSION" > /dev/null 2>&1; then
|
||||||
|
print_success "Created tag: $NEW_VERSION"
|
||||||
|
else
|
||||||
|
print_warning "Tag $NEW_VERSION already exists, removing and recreating..."
|
||||||
|
git tag -d "$NEW_VERSION" > /dev/null 2>&1
|
||||||
|
git tag "$NEW_VERSION" > /dev/null 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Compile project first (will now pick up the new tag)
|
||||||
compile_project
|
compile_project
|
||||||
|
|
||||||
# Build release binaries
|
# Build release binaries
|
||||||
build_release_binaries
|
build_release_binaries
|
||||||
|
|
||||||
# Commit and push
|
# Commit and push (but skip tag creation since we already did it)
|
||||||
git_commit_and_push
|
git_commit_and_push_no_tag
|
||||||
|
|
||||||
# Create Gitea release with binaries
|
# Create Gitea release with binaries
|
||||||
create_gitea_release
|
create_gitea_release
|
||||||
@@ -376,11 +437,20 @@ main() {
|
|||||||
# Increment patch version for regular commits
|
# Increment patch version for regular commits
|
||||||
increment_version "patch"
|
increment_version "patch"
|
||||||
|
|
||||||
# Compile project
|
# Create new git tag BEFORE compilation so version.h picks it up
|
||||||
|
if git tag "$NEW_VERSION" > /dev/null 2>&1; then
|
||||||
|
print_success "Created tag: $NEW_VERSION"
|
||||||
|
else
|
||||||
|
print_warning "Tag $NEW_VERSION already exists, removing and recreating..."
|
||||||
|
git tag -d "$NEW_VERSION" > /dev/null 2>&1
|
||||||
|
git tag "$NEW_VERSION" > /dev/null 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Compile project (will now pick up the new tag)
|
||||||
compile_project
|
compile_project
|
||||||
|
|
||||||
# Commit and push
|
# Commit and push (but skip tag creation since we already did it)
|
||||||
git_commit_and_push
|
git_commit_and_push_no_tag
|
||||||
|
|
||||||
print_success "Build and push completed successfully!"
|
print_success "Build and push completed successfully!"
|
||||||
print_status "Version $NEW_VERSION pushed to repository"
|
print_status "Version $NEW_VERSION pushed to repository"
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
db/c_nostr_relay.db.backup.20250905_152104
Normal file
BIN
db/c_nostr_relay.db.backup.20250905_152104
Normal file
Binary file not shown.
10
db/init.sh
10
db/init.sh
@@ -114,12 +114,12 @@ verify_database() {
|
|||||||
local schema_version=$(sqlite3 "$DB_PATH" "PRAGMA user_version;")
|
local schema_version=$(sqlite3 "$DB_PATH" "PRAGMA user_version;")
|
||||||
log_info "Database schema version: $schema_version"
|
log_info "Database schema version: $schema_version"
|
||||||
|
|
||||||
# Check that main tables exist
|
# Check that main tables exist (including configuration tables)
|
||||||
local table_count=$(sqlite3 "$DB_PATH" "SELECT count(*) FROM sqlite_master WHERE type='table' AND name IN ('events', 'schema_info');")
|
local table_count=$(sqlite3 "$DB_PATH" "SELECT count(*) FROM sqlite_master WHERE type='table' AND name IN ('events', 'schema_info', 'server_config');")
|
||||||
if [ "$table_count" -eq 2 ]; then
|
if [ "$table_count" -eq 3 ]; then
|
||||||
log_success "Core tables created successfully"
|
log_success "Core tables created successfully (including configuration tables)"
|
||||||
else
|
else
|
||||||
log_error "Missing core tables (expected 2, found $table_count)"
|
log_error "Missing core tables (expected 3, found $table_count)"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|||||||
128
db/schema.sql
128
db/schema.sql
@@ -2,7 +2,7 @@
|
|||||||
-- SQLite schema for storing Nostr events with JSON tags support
|
-- SQLite schema for storing Nostr events with JSON tags support
|
||||||
|
|
||||||
-- Schema version tracking
|
-- Schema version tracking
|
||||||
PRAGMA user_version = 2;
|
PRAGMA user_version = 3;
|
||||||
|
|
||||||
-- Enable foreign key support
|
-- Enable foreign key support
|
||||||
PRAGMA foreign_keys = ON;
|
PRAGMA foreign_keys = ON;
|
||||||
@@ -44,9 +44,9 @@ CREATE TABLE schema_info (
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- Insert schema metadata
|
-- Insert schema metadata
|
||||||
INSERT INTO schema_info (key, value) VALUES
|
INSERT INTO schema_info (key, value) VALUES
|
||||||
('version', '2'),
|
('version', '3'),
|
||||||
('description', 'Hybrid single-table Nostr relay schema with JSON tags'),
|
('description', 'Hybrid single-table Nostr relay schema with JSON tags and configuration management'),
|
||||||
('created_at', strftime('%s', 'now'));
|
('created_at', strftime('%s', 'now'));
|
||||||
|
|
||||||
-- Helper views for common queries
|
-- Helper views for common queries
|
||||||
@@ -178,4 +178,122 @@ WHERE event_type = 'created'
|
|||||||
AND subscription_id NOT IN (
|
AND subscription_id NOT IN (
|
||||||
SELECT subscription_id FROM subscription_events
|
SELECT subscription_id FROM subscription_events
|
||||||
WHERE event_type IN ('closed', 'expired', 'disconnected')
|
WHERE event_type IN ('closed', 'expired', 'disconnected')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- ================================
|
||||||
|
-- CONFIGURATION MANAGEMENT TABLES
|
||||||
|
-- ================================
|
||||||
|
|
||||||
|
-- Core server configuration table
|
||||||
|
CREATE TABLE server_config (
|
||||||
|
key TEXT PRIMARY KEY, -- Configuration key (unique identifier)
|
||||||
|
value TEXT NOT NULL, -- Configuration value (stored as string)
|
||||||
|
description TEXT, -- Human-readable description
|
||||||
|
config_type TEXT DEFAULT 'user' CHECK (config_type IN ('system', 'user', 'runtime')),
|
||||||
|
data_type TEXT DEFAULT 'string' CHECK (data_type IN ('string', 'integer', 'boolean', 'json')),
|
||||||
|
validation_rules TEXT, -- JSON validation rules (optional)
|
||||||
|
is_sensitive INTEGER DEFAULT 0, -- 1 if value should be masked in logs
|
||||||
|
requires_restart INTEGER DEFAULT 0, -- 1 if change requires server restart
|
||||||
|
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||||
|
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Configuration change history table
|
||||||
|
CREATE TABLE config_history (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
config_key TEXT NOT NULL, -- Key that was changed
|
||||||
|
old_value TEXT, -- Previous value (NULL for new keys)
|
||||||
|
new_value TEXT NOT NULL, -- New value
|
||||||
|
changed_by TEXT DEFAULT 'system', -- Who made the change (system/admin/user)
|
||||||
|
change_reason TEXT, -- Optional reason for change
|
||||||
|
changed_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||||
|
FOREIGN KEY (config_key) REFERENCES server_config(key)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Configuration validation errors log
|
||||||
|
CREATE TABLE config_validation_log (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
config_key TEXT NOT NULL,
|
||||||
|
attempted_value TEXT,
|
||||||
|
validation_error TEXT NOT NULL,
|
||||||
|
error_source TEXT DEFAULT 'validation', -- validation/parsing/constraint
|
||||||
|
attempted_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Cache for file-based configuration events
|
||||||
|
CREATE TABLE config_file_cache (
|
||||||
|
file_path TEXT PRIMARY KEY, -- Full path to config file
|
||||||
|
file_hash TEXT NOT NULL, -- SHA256 hash of file content
|
||||||
|
event_id TEXT, -- Nostr event ID from file
|
||||||
|
event_pubkey TEXT, -- Admin pubkey that signed event
|
||||||
|
loaded_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||||
|
validation_status TEXT CHECK (validation_status IN ('valid', 'invalid', 'unverified')),
|
||||||
|
validation_error TEXT -- Error details if invalid
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Performance indexes for configuration tables
|
||||||
|
CREATE INDEX idx_server_config_type ON server_config(config_type);
|
||||||
|
CREATE INDEX idx_server_config_updated ON server_config(updated_at DESC);
|
||||||
|
CREATE INDEX idx_config_history_key ON config_history(config_key);
|
||||||
|
CREATE INDEX idx_config_history_time ON config_history(changed_at DESC);
|
||||||
|
CREATE INDEX idx_config_validation_key ON config_validation_log(config_key);
|
||||||
|
CREATE INDEX idx_config_validation_time ON config_validation_log(attempted_at DESC);
|
||||||
|
|
||||||
|
-- Trigger to update timestamp on configuration changes
|
||||||
|
CREATE TRIGGER update_config_timestamp
|
||||||
|
AFTER UPDATE ON server_config
|
||||||
|
BEGIN
|
||||||
|
UPDATE server_config SET updated_at = strftime('%s', 'now') WHERE key = NEW.key;
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- Trigger to log configuration changes to history
|
||||||
|
CREATE TRIGGER log_config_changes
|
||||||
|
AFTER UPDATE ON server_config
|
||||||
|
WHEN OLD.value != NEW.value
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO config_history (config_key, old_value, new_value, changed_by, change_reason)
|
||||||
|
VALUES (NEW.key, OLD.value, NEW.value, 'system', 'configuration update');
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- Active Configuration View
|
||||||
|
CREATE VIEW active_config AS
|
||||||
|
SELECT
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
description,
|
||||||
|
config_type,
|
||||||
|
data_type,
|
||||||
|
requires_restart,
|
||||||
|
updated_at
|
||||||
|
FROM server_config
|
||||||
|
WHERE config_type IN ('system', 'user')
|
||||||
|
ORDER BY config_type, key;
|
||||||
|
|
||||||
|
-- Runtime Statistics View
|
||||||
|
CREATE VIEW runtime_stats AS
|
||||||
|
SELECT
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
description,
|
||||||
|
updated_at
|
||||||
|
FROM server_config
|
||||||
|
WHERE config_type = 'runtime'
|
||||||
|
ORDER BY key;
|
||||||
|
|
||||||
|
-- Configuration Change Summary
|
||||||
|
CREATE VIEW recent_config_changes AS
|
||||||
|
SELECT
|
||||||
|
ch.config_key,
|
||||||
|
sc.description,
|
||||||
|
ch.old_value,
|
||||||
|
ch.new_value,
|
||||||
|
ch.changed_by,
|
||||||
|
ch.change_reason,
|
||||||
|
ch.changed_at
|
||||||
|
FROM config_history ch
|
||||||
|
JOIN server_config sc ON ch.config_key = sc.key
|
||||||
|
ORDER BY ch.changed_at DESC
|
||||||
|
LIMIT 50;
|
||||||
|
|
||||||
|
-- Runtime Statistics (initialized by server on startup)
|
||||||
|
-- These will be populated when configuration system initializes
|
||||||
280
docs/config_schema_design.md
Normal file
280
docs/config_schema_design.md
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
# Database Configuration Schema Design
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This document outlines the database configuration schema additions for the C Nostr Relay startup config file system. The design follows the Ginxsom admin system approach with signed Nostr events and database storage.
|
||||||
|
|
||||||
|
## Schema Version Update
|
||||||
|
- Current Version: 2
|
||||||
|
- Target Version: 3
|
||||||
|
- Update: Add server configuration management tables
|
||||||
|
|
||||||
|
## Core Configuration Tables
|
||||||
|
|
||||||
|
### 1. `server_config` Table
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Server configuration table - core configuration storage
|
||||||
|
CREATE TABLE server_config (
|
||||||
|
key TEXT PRIMARY KEY, -- Configuration key (unique identifier)
|
||||||
|
value TEXT NOT NULL, -- Configuration value (stored as string)
|
||||||
|
description TEXT, -- Human-readable description
|
||||||
|
config_type TEXT DEFAULT 'user' CHECK (config_type IN ('system', 'user', 'runtime')),
|
||||||
|
data_type TEXT DEFAULT 'string' CHECK (data_type IN ('string', 'integer', 'boolean', 'json')),
|
||||||
|
validation_rules TEXT, -- JSON validation rules (optional)
|
||||||
|
is_sensitive INTEGER DEFAULT 0, -- 1 if value should be masked in logs
|
||||||
|
requires_restart INTEGER DEFAULT 0, -- 1 if change requires server restart
|
||||||
|
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||||
|
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Configuration Types:**
|
||||||
|
- `system`: Core system settings (admin keys, security)
|
||||||
|
- `user`: User-configurable settings (relay info, features)
|
||||||
|
- `runtime`: Dynamic runtime values (statistics, cache)
|
||||||
|
|
||||||
|
**Data Types:**
|
||||||
|
- `string`: Text values
|
||||||
|
- `integer`: Numeric values
|
||||||
|
- `boolean`: True/false values (stored as "true"/"false")
|
||||||
|
- `json`: JSON object/array values
|
||||||
|
|
||||||
|
### 2. `config_history` Table
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Configuration change history table
|
||||||
|
CREATE TABLE config_history (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
config_key TEXT NOT NULL, -- Key that was changed
|
||||||
|
old_value TEXT, -- Previous value (NULL for new keys)
|
||||||
|
new_value TEXT NOT NULL, -- New value
|
||||||
|
changed_by TEXT DEFAULT 'system', -- Who made the change (system/admin/user)
|
||||||
|
change_reason TEXT, -- Optional reason for change
|
||||||
|
changed_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||||
|
FOREIGN KEY (config_key) REFERENCES server_config(key)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. `config_validation_log` Table
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Configuration validation errors log
|
||||||
|
CREATE TABLE config_validation_log (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
config_key TEXT NOT NULL,
|
||||||
|
attempted_value TEXT,
|
||||||
|
validation_error TEXT NOT NULL,
|
||||||
|
error_source TEXT DEFAULT 'validation', -- validation/parsing/constraint
|
||||||
|
attempted_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Configuration File Cache Table
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Cache for file-based configuration events
|
||||||
|
CREATE TABLE config_file_cache (
|
||||||
|
file_path TEXT PRIMARY KEY, -- Full path to config file
|
||||||
|
file_hash TEXT NOT NULL, -- SHA256 hash of file content
|
||||||
|
event_id TEXT, -- Nostr event ID from file
|
||||||
|
event_pubkey TEXT, -- Admin pubkey that signed event
|
||||||
|
loaded_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||||
|
validation_status TEXT CHECK (validation_status IN ('valid', 'invalid', 'unverified')),
|
||||||
|
validation_error TEXT -- Error details if invalid
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Indexes and Performance
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Performance indexes for configuration tables
|
||||||
|
CREATE INDEX idx_server_config_type ON server_config(config_type);
|
||||||
|
CREATE INDEX idx_server_config_updated ON server_config(updated_at DESC);
|
||||||
|
CREATE INDEX idx_config_history_key ON config_history(config_key);
|
||||||
|
CREATE INDEX idx_config_history_time ON config_history(changed_at DESC);
|
||||||
|
CREATE INDEX idx_config_validation_key ON config_validation_log(config_key);
|
||||||
|
CREATE INDEX idx_config_validation_time ON config_validation_log(attempted_at DESC);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
|
||||||
|
### Update Timestamp Trigger
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Trigger to update timestamp on configuration changes
|
||||||
|
CREATE TRIGGER update_config_timestamp
|
||||||
|
AFTER UPDATE ON server_config
|
||||||
|
BEGIN
|
||||||
|
UPDATE server_config SET updated_at = strftime('%s', 'now') WHERE key = NEW.key;
|
||||||
|
END;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration History Trigger
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Trigger to log configuration changes to history
|
||||||
|
CREATE TRIGGER log_config_changes
|
||||||
|
AFTER UPDATE ON server_config
|
||||||
|
WHEN OLD.value != NEW.value
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO config_history (config_key, old_value, new_value, changed_by, change_reason)
|
||||||
|
VALUES (NEW.key, OLD.value, NEW.value, 'system', 'configuration update');
|
||||||
|
END;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Default Configuration Values
|
||||||
|
|
||||||
|
### Core System Settings
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT OR IGNORE INTO server_config (key, value, description, config_type, data_type, requires_restart) VALUES
|
||||||
|
-- Administrative settings
|
||||||
|
('admin_pubkey', '', 'Authorized admin public key (hex)', 'system', 'string', 1),
|
||||||
|
('admin_enabled', 'false', 'Enable admin interface', 'system', 'boolean', 1),
|
||||||
|
|
||||||
|
-- Server core settings
|
||||||
|
('relay_port', '8888', 'WebSocket server port', 'user', 'integer', 1),
|
||||||
|
('database_path', 'db/c_nostr_relay.db', 'SQLite database file path', 'user', 'string', 1),
|
||||||
|
('max_connections', '100', 'Maximum concurrent connections', 'user', 'integer', 1),
|
||||||
|
|
||||||
|
-- NIP-11 Relay Information
|
||||||
|
('relay_name', 'C Nostr Relay', 'Relay name for NIP-11', 'user', 'string', 0),
|
||||||
|
('relay_description', 'High-performance C Nostr relay with SQLite storage', 'Relay description', 'user', 'string', 0),
|
||||||
|
('relay_contact', '', 'Contact information', 'user', 'string', 0),
|
||||||
|
('relay_pubkey', '', 'Relay public key', 'user', 'string', 0),
|
||||||
|
('relay_software', 'https://git.laantungir.net/laantungir/c-relay.git', 'Software URL', 'user', 'string', 0),
|
||||||
|
('relay_version', '0.2.0', 'Software version', 'user', 'string', 0),
|
||||||
|
|
||||||
|
-- NIP-13 Proof of Work
|
||||||
|
('pow_enabled', 'true', 'Enable NIP-13 Proof of Work validation', 'user', 'boolean', 0),
|
||||||
|
('pow_min_difficulty', '0', 'Minimum PoW difficulty required', 'user', 'integer', 0),
|
||||||
|
('pow_mode', 'basic', 'PoW validation mode (basic/full/strict)', 'user', 'string', 0),
|
||||||
|
|
||||||
|
-- NIP-40 Expiration Timestamp
|
||||||
|
('expiration_enabled', 'true', 'Enable NIP-40 expiration handling', 'user', 'boolean', 0),
|
||||||
|
('expiration_strict', 'true', 'Reject expired events on submission', 'user', 'boolean', 0),
|
||||||
|
('expiration_filter', 'true', 'Filter expired events from responses', 'user', 'boolean', 0),
|
||||||
|
('expiration_grace_period', '300', 'Grace period for clock skew (seconds)', 'user', 'integer', 0),
|
||||||
|
|
||||||
|
-- Subscription limits
|
||||||
|
('max_subscriptions_per_client', '20', 'Max subscriptions per client', 'user', 'integer', 0),
|
||||||
|
('max_total_subscriptions', '5000', 'Max total concurrent subscriptions', 'user', 'integer', 0),
|
||||||
|
('subscription_id_max_length', '64', 'Maximum subscription ID length', 'user', 'integer', 0),
|
||||||
|
|
||||||
|
-- Event processing limits
|
||||||
|
('max_event_tags', '100', 'Maximum tags per event', 'user', 'integer', 0),
|
||||||
|
('max_content_length', '8196', 'Maximum content length', 'user', 'integer', 0),
|
||||||
|
('max_message_length', '16384', 'Maximum message length', 'user', 'integer', 0),
|
||||||
|
|
||||||
|
-- Performance settings
|
||||||
|
('default_limit', '500', 'Default query limit', 'user', 'integer', 0),
|
||||||
|
('max_limit', '5000', 'Maximum query limit', 'user', 'integer', 0);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Runtime Statistics
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT OR IGNORE INTO server_config (key, value, description, config_type, data_type) VALUES
|
||||||
|
-- Runtime statistics (updated by server)
|
||||||
|
('server_start_time', '0', 'Server startup timestamp', 'runtime', 'integer'),
|
||||||
|
('total_events_processed', '0', 'Total events processed', 'runtime', 'integer'),
|
||||||
|
('total_subscriptions_created', '0', 'Total subscriptions created', 'runtime', 'integer'),
|
||||||
|
('current_connections', '0', 'Current active connections', 'runtime', 'integer'),
|
||||||
|
('database_size_bytes', '0', 'Database file size in bytes', 'runtime', 'integer');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Views
|
||||||
|
|
||||||
|
### Active Configuration View
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE VIEW active_config AS
|
||||||
|
SELECT
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
description,
|
||||||
|
config_type,
|
||||||
|
data_type,
|
||||||
|
requires_restart,
|
||||||
|
updated_at
|
||||||
|
FROM server_config
|
||||||
|
WHERE config_type IN ('system', 'user')
|
||||||
|
ORDER BY config_type, key;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Runtime Statistics View
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE VIEW runtime_stats AS
|
||||||
|
SELECT
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
description,
|
||||||
|
updated_at
|
||||||
|
FROM server_config
|
||||||
|
WHERE config_type = 'runtime'
|
||||||
|
ORDER BY key;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Change Summary
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE VIEW recent_config_changes AS
|
||||||
|
SELECT
|
||||||
|
ch.config_key,
|
||||||
|
sc.description,
|
||||||
|
ch.old_value,
|
||||||
|
ch.new_value,
|
||||||
|
ch.changed_by,
|
||||||
|
ch.change_reason,
|
||||||
|
ch.changed_at
|
||||||
|
FROM config_history ch
|
||||||
|
JOIN server_config sc ON ch.config_key = sc.key
|
||||||
|
ORDER BY ch.changed_at DESC
|
||||||
|
LIMIT 50;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation Rules Format
|
||||||
|
|
||||||
|
Configuration validation rules are stored as JSON strings in the `validation_rules` column:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"min": 1,
|
||||||
|
"max": 65535,
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9a-fA-F]{64}$",
|
||||||
|
"required": false,
|
||||||
|
"description": "64-character hex string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Strategy
|
||||||
|
|
||||||
|
1. **Phase 1**: Add configuration tables to existing schema
|
||||||
|
2. **Phase 2**: Populate with current hardcoded values
|
||||||
|
3. **Phase 3**: Update application code to read from database
|
||||||
|
4. **Phase 4**: Add file-based configuration loading
|
||||||
|
5. **Phase 5**: Remove hardcoded defaults and environment variable fallbacks
|
||||||
|
|
||||||
|
## Integration Points
|
||||||
|
|
||||||
|
- **Startup**: Load configuration from file → database → apply to application
|
||||||
|
- **Runtime**: Read configuration values from database cache
|
||||||
|
- **Updates**: Write changes to database → optionally update file
|
||||||
|
- **Validation**: Validate all configuration changes before applying
|
||||||
|
- **History**: Track all configuration changes for audit purposes
|
||||||
493
docs/file_config_design.md
Normal file
493
docs/file_config_design.md
Normal file
@@ -0,0 +1,493 @@
|
|||||||
|
# File-Based Configuration Architecture Design
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This document outlines the XDG-compliant file-based configuration system for the C Nostr Relay, following the Ginxsom admin system approach using signed Nostr events.
|
||||||
|
|
||||||
|
## XDG Base Directory Specification Compliance
|
||||||
|
|
||||||
|
### File Location Strategy
|
||||||
|
|
||||||
|
**Primary Location:**
|
||||||
|
```
|
||||||
|
$XDG_CONFIG_HOME/c-relay/c_relay_config_event.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fallback Location:**
|
||||||
|
```
|
||||||
|
$HOME/.config/c-relay/c_relay_config_event.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**System-wide Fallback:**
|
||||||
|
```
|
||||||
|
/etc/c-relay/c_relay_config_event.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Directory Structure
|
||||||
|
```
|
||||||
|
$XDG_CONFIG_HOME/c-relay/
|
||||||
|
├── c_relay_config_event.json # Main configuration file
|
||||||
|
├── backup/ # Configuration backups
|
||||||
|
│ ├── c_relay_config_event.json.bak
|
||||||
|
│ └── c_relay_config_event.20241205.json
|
||||||
|
└── validation/ # Validation logs
|
||||||
|
└── config_validation.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration File Format
|
||||||
|
|
||||||
|
### Signed Nostr Event Structure
|
||||||
|
|
||||||
|
The configuration file contains a signed Nostr event (kind 33334) with relay configuration:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": 33334,
|
||||||
|
"created_at": 1704067200,
|
||||||
|
"tags": [
|
||||||
|
["relay_name", "C Nostr Relay"],
|
||||||
|
["relay_description", "High-performance C Nostr relay with SQLite storage"],
|
||||||
|
["relay_port", "8888"],
|
||||||
|
["database_path", "db/c_nostr_relay.db"],
|
||||||
|
["admin_pubkey", ""],
|
||||||
|
["admin_enabled", "false"],
|
||||||
|
|
||||||
|
["pow_enabled", "true"],
|
||||||
|
["pow_min_difficulty", "0"],
|
||||||
|
["pow_mode", "basic"],
|
||||||
|
|
||||||
|
["expiration_enabled", "true"],
|
||||||
|
["expiration_strict", "true"],
|
||||||
|
["expiration_filter", "true"],
|
||||||
|
["expiration_grace_period", "300"],
|
||||||
|
|
||||||
|
["max_subscriptions_per_client", "20"],
|
||||||
|
["max_total_subscriptions", "5000"],
|
||||||
|
["max_connections", "100"],
|
||||||
|
|
||||||
|
["relay_contact", ""],
|
||||||
|
["relay_pubkey", ""],
|
||||||
|
["relay_software", "https://git.laantungir.net/laantungir/c-relay.git"],
|
||||||
|
["relay_version", "0.2.0"],
|
||||||
|
|
||||||
|
["max_event_tags", "100"],
|
||||||
|
["max_content_length", "8196"],
|
||||||
|
["max_message_length", "16384"],
|
||||||
|
["default_limit", "500"],
|
||||||
|
["max_limit", "5000"]
|
||||||
|
],
|
||||||
|
"content": "C Nostr Relay configuration event",
|
||||||
|
"pubkey": "admin_public_key_hex_64_chars",
|
||||||
|
"id": "computed_event_id_hex_64_chars",
|
||||||
|
"sig": "computed_signature_hex_128_chars"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Event Kind Definition
|
||||||
|
|
||||||
|
**Kind 33334**: C Nostr Relay Configuration Event
|
||||||
|
- Parameterized replaceable event
|
||||||
|
- Must be signed by authorized admin pubkey
|
||||||
|
- Contains relay configuration as tags
|
||||||
|
- Validation required on load
|
||||||
|
|
||||||
|
## Configuration Loading Architecture
|
||||||
|
|
||||||
|
### Loading Priority Chain
|
||||||
|
|
||||||
|
1. **Command Line Arguments** (highest priority)
|
||||||
|
2. **File-based Configuration** (signed Nostr event)
|
||||||
|
3. **Database Configuration** (persistent storage)
|
||||||
|
4. **Environment Variables** (compatibility mode)
|
||||||
|
5. **Hardcoded Defaults** (fallback)
|
||||||
|
|
||||||
|
### Loading Process Flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[Server Startup] --> B[Get Config File Path]
|
||||||
|
B --> C{File Exists?}
|
||||||
|
C -->|No| D[Check Database Config]
|
||||||
|
C -->|Yes| E[Load & Parse JSON]
|
||||||
|
E --> F[Validate Event Structure]
|
||||||
|
F --> G{Valid Event?}
|
||||||
|
G -->|No| H[Log Error & Use Database]
|
||||||
|
G -->|Yes| I[Verify Event Signature]
|
||||||
|
I --> J{Signature Valid?}
|
||||||
|
J -->|No| K[Log Error & Use Database]
|
||||||
|
J -->|Yes| L[Extract Configuration Tags]
|
||||||
|
L --> M[Apply to Database]
|
||||||
|
M --> N[Apply to Application]
|
||||||
|
D --> O[Load from Database]
|
||||||
|
H --> O
|
||||||
|
K --> O
|
||||||
|
O --> P[Apply Environment Variable Overrides]
|
||||||
|
P --> Q[Apply Command Line Overrides]
|
||||||
|
Q --> N
|
||||||
|
N --> R[Server Ready]
|
||||||
|
```
|
||||||
|
|
||||||
|
## C Implementation Architecture
|
||||||
|
|
||||||
|
### Core Data Structures
|
||||||
|
|
||||||
|
```c
|
||||||
|
// Configuration file management
|
||||||
|
typedef struct {
|
||||||
|
char file_path[512];
|
||||||
|
char file_hash[65]; // SHA256 hash
|
||||||
|
time_t last_modified;
|
||||||
|
time_t last_loaded;
|
||||||
|
int validation_status; // 0=valid, 1=invalid, 2=unverified
|
||||||
|
char validation_error[256];
|
||||||
|
} config_file_info_t;
|
||||||
|
|
||||||
|
// Configuration event structure
|
||||||
|
typedef struct {
|
||||||
|
char event_id[65];
|
||||||
|
char pubkey[65];
|
||||||
|
char signature[129];
|
||||||
|
long created_at;
|
||||||
|
int kind;
|
||||||
|
cJSON* tags;
|
||||||
|
char* content;
|
||||||
|
} config_event_t;
|
||||||
|
|
||||||
|
// Configuration management context
|
||||||
|
typedef struct {
|
||||||
|
config_file_info_t file_info;
|
||||||
|
config_event_t event;
|
||||||
|
int loaded_from_file;
|
||||||
|
int loaded_from_database;
|
||||||
|
char admin_pubkey[65];
|
||||||
|
time_t load_timestamp;
|
||||||
|
} config_context_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Core Function Signatures
|
||||||
|
|
||||||
|
```c
|
||||||
|
// XDG path resolution
|
||||||
|
int get_config_file_path(char* path, size_t path_size);
|
||||||
|
int create_config_directories(const char* config_path);
|
||||||
|
|
||||||
|
// File operations
|
||||||
|
int load_config_from_file(const char* config_path, config_context_t* ctx);
|
||||||
|
int save_config_to_file(const char* config_path, const config_event_t* event);
|
||||||
|
int backup_config_file(const char* config_path);
|
||||||
|
|
||||||
|
// Event validation
|
||||||
|
int validate_config_event_structure(const cJSON* event);
|
||||||
|
int verify_config_event_signature(const config_event_t* event, const char* admin_pubkey);
|
||||||
|
int validate_config_tag_values(const cJSON* tags);
|
||||||
|
|
||||||
|
// Configuration extraction and application
|
||||||
|
int extract_config_from_tags(const cJSON* tags, config_context_t* ctx);
|
||||||
|
int apply_config_to_database(const config_context_t* ctx);
|
||||||
|
int apply_config_to_globals(const config_context_t* ctx);
|
||||||
|
|
||||||
|
// File monitoring and updates
|
||||||
|
int monitor_config_file_changes(const char* config_path);
|
||||||
|
int reload_config_on_change(config_context_t* ctx);
|
||||||
|
|
||||||
|
// Error handling and logging
|
||||||
|
int log_config_validation_error(const char* config_key, const char* error);
|
||||||
|
int log_config_load_event(const config_context_t* ctx, const char* source);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Validation Rules
|
||||||
|
|
||||||
|
### Event Structure Validation
|
||||||
|
|
||||||
|
1. **Required Fields**: `kind`, `created_at`, `tags`, `content`, `pubkey`, `id`, `sig`
|
||||||
|
2. **Kind Validation**: Must be exactly 33334
|
||||||
|
3. **Timestamp Validation**: Must be reasonable (not too old, not future)
|
||||||
|
4. **Tags Format**: Array of string arrays `[["key", "value"], ...]`
|
||||||
|
5. **Signature Verification**: Must be signed by authorized admin pubkey
|
||||||
|
|
||||||
|
### Configuration Value Validation
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct {
|
||||||
|
char* key;
|
||||||
|
char* data_type; // "string", "integer", "boolean", "json"
|
||||||
|
char* validation_rule; // JSON validation rule
|
||||||
|
int required;
|
||||||
|
char* default_value;
|
||||||
|
} config_validation_rule_t;
|
||||||
|
|
||||||
|
static config_validation_rule_t validation_rules[] = {
|
||||||
|
{"relay_port", "integer", "{\"min\": 1, \"max\": 65535}", 1, "8888"},
|
||||||
|
{"pow_min_difficulty", "integer", "{\"min\": 0, \"max\": 64}", 1, "0"},
|
||||||
|
{"expiration_grace_period", "integer", "{\"min\": 0, \"max\": 86400}", 1, "300"},
|
||||||
|
{"admin_pubkey", "string", "{\"pattern\": \"^[0-9a-fA-F]{64}$\"}", 0, ""},
|
||||||
|
{"pow_enabled", "boolean", "{}", 1, "true"},
|
||||||
|
// ... more rules
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Security Validation
|
||||||
|
|
||||||
|
1. **Admin Pubkey Verification**: Only configured admin pubkeys can create config events
|
||||||
|
2. **Event ID Verification**: Event ID must match computed hash
|
||||||
|
3. **Signature Verification**: Signature must be valid for the event and pubkey
|
||||||
|
4. **Timestamp Validation**: Prevent replay attacks with old events
|
||||||
|
5. **File Permission Checks**: Config files should have appropriate permissions
|
||||||
|
|
||||||
|
## File Management Features
|
||||||
|
|
||||||
|
### Configuration File Operations
|
||||||
|
|
||||||
|
**File Creation:**
|
||||||
|
- Generate initial configuration file with default values
|
||||||
|
- Sign with admin private key
|
||||||
|
- Set appropriate file permissions (600 - owner read/write only)
|
||||||
|
|
||||||
|
**File Updates:**
|
||||||
|
- Create backup of existing file
|
||||||
|
- Validate new configuration
|
||||||
|
- Atomic file replacement (write to temp, then rename)
|
||||||
|
- Update file metadata cache
|
||||||
|
|
||||||
|
**File Monitoring:**
|
||||||
|
- Watch for file system changes using inotify (Linux)
|
||||||
|
- Reload configuration automatically when file changes
|
||||||
|
- Validate changes before applying
|
||||||
|
- Log all configuration reload events
|
||||||
|
|
||||||
|
### Backup and Recovery
|
||||||
|
|
||||||
|
**Automatic Backups:**
|
||||||
|
```
|
||||||
|
$XDG_CONFIG_HOME/c-relay/backup/
|
||||||
|
├── c_relay_config_event.json.bak # Last working config
|
||||||
|
├── c_relay_config_event.20241205-143022.json # Timestamped backups
|
||||||
|
└── c_relay_config_event.20241204-091530.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recovery Process:**
|
||||||
|
1. Detect corrupted or invalid config file
|
||||||
|
2. Attempt to load from `.bak` backup
|
||||||
|
3. If backup fails, generate default configuration
|
||||||
|
4. Log recovery actions for audit
|
||||||
|
|
||||||
|
## Integration with Database Schema
|
||||||
|
|
||||||
|
### File-Database Synchronization
|
||||||
|
|
||||||
|
**On File Load:**
|
||||||
|
1. Parse and validate file-based configuration
|
||||||
|
2. Extract configuration values from event tags
|
||||||
|
3. Update database `server_config` table
|
||||||
|
4. Record file metadata in `config_file_cache` table
|
||||||
|
5. Log configuration changes in `config_history` table
|
||||||
|
|
||||||
|
**Configuration Priority Resolution:**
|
||||||
|
```c
|
||||||
|
char* get_config_value(const char* key, const char* default_value) {
|
||||||
|
// Priority: CLI args > File config > DB config > Env vars > Default
|
||||||
|
char* value = NULL;
|
||||||
|
|
||||||
|
// 1. Check command line overrides (if implemented)
|
||||||
|
value = get_cli_override(key);
|
||||||
|
if (value) return value;
|
||||||
|
|
||||||
|
// 2. Check database (updated from file)
|
||||||
|
value = get_database_config(key);
|
||||||
|
if (value) return value;
|
||||||
|
|
||||||
|
// 3. Check environment variables (compatibility)
|
||||||
|
value = get_env_config(key);
|
||||||
|
if (value) return value;
|
||||||
|
|
||||||
|
// 4. Return default
|
||||||
|
return strdup(default_value);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling and Recovery
|
||||||
|
|
||||||
|
### Validation Error Handling
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef enum {
|
||||||
|
CONFIG_ERROR_NONE = 0,
|
||||||
|
CONFIG_ERROR_FILE_NOT_FOUND = 1,
|
||||||
|
CONFIG_ERROR_PARSE_FAILED = 2,
|
||||||
|
CONFIG_ERROR_INVALID_STRUCTURE = 3,
|
||||||
|
CONFIG_ERROR_SIGNATURE_INVALID = 4,
|
||||||
|
CONFIG_ERROR_UNAUTHORIZED = 5,
|
||||||
|
CONFIG_ERROR_VALUE_INVALID = 6,
|
||||||
|
CONFIG_ERROR_IO_ERROR = 7
|
||||||
|
} config_error_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
config_error_t error_code;
|
||||||
|
char error_message[256];
|
||||||
|
char config_key[64];
|
||||||
|
char invalid_value[128];
|
||||||
|
time_t error_timestamp;
|
||||||
|
} config_error_info_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Graceful Degradation
|
||||||
|
|
||||||
|
**File Load Failure:**
|
||||||
|
1. Log detailed error information
|
||||||
|
2. Fall back to database configuration
|
||||||
|
3. Continue operation with last known good config
|
||||||
|
4. Set service status to "degraded" mode
|
||||||
|
|
||||||
|
**Validation Failure:**
|
||||||
|
1. Log validation errors with specific details
|
||||||
|
2. Skip invalid configuration items
|
||||||
|
3. Use default values for failed items
|
||||||
|
4. Continue with partial configuration
|
||||||
|
|
||||||
|
**Permission Errors:**
|
||||||
|
1. Log permission issues
|
||||||
|
2. Attempt to use fallback locations
|
||||||
|
3. Generate temporary config if needed
|
||||||
|
4. Alert administrator via logs
|
||||||
|
|
||||||
|
## Configuration Update Process
|
||||||
|
|
||||||
|
### Safe Configuration Updates
|
||||||
|
|
||||||
|
**Atomic Update Process:**
|
||||||
|
1. Create backup of current configuration
|
||||||
|
2. Write new configuration to temporary file
|
||||||
|
3. Validate new configuration completely
|
||||||
|
4. If valid, rename temporary file to active config
|
||||||
|
5. Update database with new values
|
||||||
|
6. Apply changes to running server
|
||||||
|
7. Log successful update
|
||||||
|
|
||||||
|
**Rollback Process:**
|
||||||
|
1. Detect invalid configuration at startup
|
||||||
|
2. Restore from backup file
|
||||||
|
3. Log rollback event
|
||||||
|
4. Continue with previous working configuration
|
||||||
|
|
||||||
|
### Hot Reload Support
|
||||||
|
|
||||||
|
**File Change Detection:**
|
||||||
|
```c
|
||||||
|
int monitor_config_file_changes(const char* config_path) {
|
||||||
|
// Use inotify on Linux to watch file changes
|
||||||
|
int inotify_fd = inotify_init();
|
||||||
|
int watch_fd = inotify_add_watch(inotify_fd, config_path, IN_MODIFY | IN_MOVED_TO);
|
||||||
|
|
||||||
|
// Monitor in separate thread
|
||||||
|
// On change: validate -> apply -> log
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Runtime Configuration Updates:**
|
||||||
|
- Reload configuration on file change
|
||||||
|
- Apply non-restart-required changes immediately
|
||||||
|
- Queue restart-required changes for next restart
|
||||||
|
- Notify operators of configuration changes
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### Access Control
|
||||||
|
|
||||||
|
**File Permissions:**
|
||||||
|
- Config files: 600 (owner read/write only)
|
||||||
|
- Directories: 700 (owner access only)
|
||||||
|
- Backup files: 600 (owner read/write only)
|
||||||
|
|
||||||
|
**Admin Key Management:**
|
||||||
|
- Admin private keys never stored in config files
|
||||||
|
- Only admin pubkeys stored for verification
|
||||||
|
- Support for multiple admin pubkeys
|
||||||
|
- Key rotation support
|
||||||
|
|
||||||
|
### Signature Validation
|
||||||
|
|
||||||
|
**Event Signature Verification:**
|
||||||
|
```c
|
||||||
|
int verify_config_event_signature(const config_event_t* event, const char* admin_pubkey) {
|
||||||
|
// 1. Reconstruct event for signing (without id and sig)
|
||||||
|
// 2. Compute event ID and verify against stored ID
|
||||||
|
// 3. Verify signature using admin pubkey
|
||||||
|
// 4. Check admin pubkey authorization
|
||||||
|
return NOSTR_SUCCESS;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Anti-Replay Protection:**
|
||||||
|
- Configuration events must be newer than current
|
||||||
|
- Event timestamps validated against reasonable bounds
|
||||||
|
- Configuration history prevents replay attacks
|
||||||
|
|
||||||
|
## Implementation Phases
|
||||||
|
|
||||||
|
### Phase 1: Basic File Support
|
||||||
|
- XDG path resolution
|
||||||
|
- File loading and parsing
|
||||||
|
- Basic validation
|
||||||
|
- Database integration
|
||||||
|
|
||||||
|
### Phase 2: Security Features
|
||||||
|
- Event signature verification
|
||||||
|
- Admin pubkey management
|
||||||
|
- File permission checks
|
||||||
|
- Error handling
|
||||||
|
|
||||||
|
### Phase 3: Advanced Features
|
||||||
|
- Hot reload support
|
||||||
|
- Automatic backups
|
||||||
|
- Configuration utilities
|
||||||
|
- Interactive setup
|
||||||
|
|
||||||
|
### Phase 4: Monitoring & Management
|
||||||
|
- Configuration change monitoring
|
||||||
|
- Advanced validation rules
|
||||||
|
- Configuration audit logging
|
||||||
|
- Management utilities
|
||||||
|
|
||||||
|
## Configuration Generation Utilities
|
||||||
|
|
||||||
|
### Interactive Setup Script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# scripts/setup_config.sh - Interactive configuration setup
|
||||||
|
|
||||||
|
create_initial_config() {
|
||||||
|
echo "=== C Nostr Relay Initial Configuration ==="
|
||||||
|
|
||||||
|
# Collect basic information
|
||||||
|
read -p "Relay name [C Nostr Relay]: " relay_name
|
||||||
|
read -p "Admin public key (hex): " admin_pubkey
|
||||||
|
read -p "Server port [8888]: " server_port
|
||||||
|
|
||||||
|
# Generate signed configuration event
|
||||||
|
./scripts/generate_config.sh \
|
||||||
|
--admin-key "$admin_pubkey" \
|
||||||
|
--relay-name "${relay_name:-C Nostr Relay}" \
|
||||||
|
--port "${server_port:-8888}" \
|
||||||
|
--output "$XDG_CONFIG_HOME/c-relay/c_relay_config_event.json"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Validation Utility
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# scripts/validate_config.sh - Validate configuration file
|
||||||
|
|
||||||
|
validate_config_file() {
|
||||||
|
local config_file="$1"
|
||||||
|
|
||||||
|
# Check file exists and is readable
|
||||||
|
# Validate JSON structure
|
||||||
|
# Verify event signature
|
||||||
|
# Check configuration values
|
||||||
|
# Report validation results
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This comprehensive file-based configuration design provides a robust, secure, and maintainable system that follows industry standards while integrating seamlessly with the existing C Nostr Relay architecture.
|
||||||
@@ -5,6 +5,53 @@
|
|||||||
|
|
||||||
echo "=== C Nostr Relay Build and Restart Script ==="
|
echo "=== C Nostr Relay Build and Restart Script ==="
|
||||||
|
|
||||||
|
# Parse command line arguments
|
||||||
|
PRESERVE_CONFIG=false
|
||||||
|
HELP=false
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
--preserve-config|-p)
|
||||||
|
PRESERVE_CONFIG=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--help|-h)
|
||||||
|
HELP=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option: $1"
|
||||||
|
HELP=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Show help
|
||||||
|
if [ "$HELP" = true ]; then
|
||||||
|
echo "Usage: $0 [OPTIONS]"
|
||||||
|
echo ""
|
||||||
|
echo "Options:"
|
||||||
|
echo " --preserve-config Keep existing configuration file (don't regenerate)"
|
||||||
|
echo " --help, -h Show this help message"
|
||||||
|
echo ""
|
||||||
|
echo "Default behavior: Automatically regenerates configuration file on each build"
|
||||||
|
echo " for development purposes"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Handle configuration file regeneration
|
||||||
|
CONFIG_FILE="$HOME/.config/c-relay/c_relay_config_event.json"
|
||||||
|
if [ "$PRESERVE_CONFIG" = false ] && [ -f "$CONFIG_FILE" ]; then
|
||||||
|
echo "Removing old configuration file to trigger regeneration..."
|
||||||
|
rm -f "$CONFIG_FILE"
|
||||||
|
echo "✓ Configuration file removed - will be regenerated with latest database values"
|
||||||
|
elif [ "$PRESERVE_CONFIG" = true ] && [ -f "$CONFIG_FILE" ]; then
|
||||||
|
echo "Preserving existing configuration file as requested"
|
||||||
|
elif [ ! -f "$CONFIG_FILE" ]; then
|
||||||
|
echo "No existing configuration file found - will generate new one"
|
||||||
|
fi
|
||||||
|
|
||||||
# Build the project first
|
# Build the project first
|
||||||
echo "Building project..."
|
echo "Building project..."
|
||||||
make clean all
|
make clean all
|
||||||
@@ -90,6 +137,19 @@ if ps -p "$RELAY_PID" >/dev/null 2>&1; then
|
|||||||
# Save PID for debugging
|
# Save PID for debugging
|
||||||
echo $RELAY_PID > relay.pid
|
echo $RELAY_PID > relay.pid
|
||||||
|
|
||||||
|
# Check if a new private key was generated and display it
|
||||||
|
sleep 1 # Give relay time to write initial logs
|
||||||
|
if grep -q "GENERATED RELAY ADMIN PRIVATE KEY" relay.log 2>/dev/null; then
|
||||||
|
echo "=== IMPORTANT: NEW ADMIN PRIVATE KEY GENERATED ==="
|
||||||
|
echo ""
|
||||||
|
# Extract and display the private key section from the log
|
||||||
|
grep -A 8 -B 2 "GENERATED RELAY ADMIN PRIVATE KEY" relay.log | head -n 12
|
||||||
|
echo ""
|
||||||
|
echo "⚠️ SAVE THIS PRIVATE KEY SECURELY - IT CONTROLS YOUR RELAY!"
|
||||||
|
echo "⚠️ This key is also logged in relay.log for reference"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
echo "=== Relay server running in background ==="
|
echo "=== Relay server running in background ==="
|
||||||
echo "To kill relay: pkill -f 'c_relay_'"
|
echo "To kill relay: pkill -f 'c_relay_'"
|
||||||
echo "To check status: ps aux | grep c_relay_"
|
echo "To check status: ps aux | grep c_relay_"
|
||||||
|
|||||||
83
relay.log
83
relay.log
@@ -1,83 +0,0 @@
|
|||||||
[34m[1m=== C Nostr Relay Server ===[0m
|
|
||||||
[32m[SUCCESS][0m Database connection established
|
|
||||||
[32m[SUCCESS][0m Relay information initialized with default values
|
|
||||||
[34m[INFO][0m Initializing NIP-13 Proof of Work configuration
|
|
||||||
[34m[INFO][0m PoW Configuration: enabled=true, min_difficulty=0, validation_flags=0x1, mode=full
|
|
||||||
[34m[INFO][0m Initializing NIP-40 Expiration Timestamp configuration
|
|
||||||
[34m[INFO][0m Expiration Configuration: enabled=true, strict_mode=true, filter_responses=true, grace_period=300 seconds
|
|
||||||
[34m[INFO][0m Starting relay server...
|
|
||||||
[34m[INFO][0m Starting libwebsockets-based Nostr relay server...
|
|
||||||
[32m[SUCCESS][0m WebSocket relay started on ws://127.0.0.1:8888
|
|
||||||
[34m[INFO][0m HTTP request received
|
|
||||||
[34m[INFO][0m Handling NIP-11 relay information request
|
|
||||||
[32m[SUCCESS][0m NIP-11 relay information served successfully
|
|
||||||
[34m[INFO][0m HTTP request received
|
|
||||||
[34m[INFO][0m Handling NIP-11 relay information request
|
|
||||||
[32m[SUCCESS][0m NIP-11 relay information served successfully
|
|
||||||
[34m[INFO][0m WebSocket connection established
|
|
||||||
[34m[INFO][0m Received WebSocket message
|
|
||||||
[34m[INFO][0m Handling EVENT message with full NIP-01 validation
|
|
||||||
[32m[SUCCESS][0m Event stored in database
|
|
||||||
[32m[SUCCESS][0m Event validated and stored successfully
|
|
||||||
[34m[INFO][0m WebSocket connection closed
|
|
||||||
[34m[INFO][0m WebSocket connection established
|
|
||||||
[34m[INFO][0m Received WebSocket message
|
|
||||||
[34m[INFO][0m Handling EVENT message with full NIP-01 validation
|
|
||||||
[32m[SUCCESS][0m Event stored in database
|
|
||||||
[32m[SUCCESS][0m Event validated and stored successfully
|
|
||||||
[34m[INFO][0m WebSocket connection closed
|
|
||||||
[34m[INFO][0m WebSocket connection established
|
|
||||||
[34m[INFO][0m Received WebSocket message
|
|
||||||
[34m[INFO][0m Handling EVENT message with full NIP-01 validation
|
|
||||||
[33m[WARNING][0m Event rejected: expired timestamp
|
|
||||||
[34m[INFO][0m WebSocket connection closed
|
|
||||||
[34m[INFO][0m WebSocket connection established
|
|
||||||
[34m[INFO][0m Received WebSocket message
|
|
||||||
[34m[INFO][0m Handling EVENT message with full NIP-01 validation
|
|
||||||
[32m[SUCCESS][0m Event stored in database
|
|
||||||
[32m[SUCCESS][0m Event validated and stored successfully
|
|
||||||
[34m[INFO][0m WebSocket connection closed
|
|
||||||
[34m[INFO][0m WebSocket connection established
|
|
||||||
[34m[INFO][0m Received WebSocket message
|
|
||||||
[34m[INFO][0m Handling EVENT message with full NIP-01 validation
|
|
||||||
[32m[SUCCESS][0m Event stored in database
|
|
||||||
[32m[SUCCESS][0m Event validated and stored successfully
|
|
||||||
[34m[INFO][0m WebSocket connection closed
|
|
||||||
[34m[INFO][0m WebSocket connection established
|
|
||||||
[34m[INFO][0m Received WebSocket message
|
|
||||||
[34m[INFO][0m Handling EVENT message with full NIP-01 validation
|
|
||||||
[32m[SUCCESS][0m Event stored in database
|
|
||||||
[32m[SUCCESS][0m Event validated and stored successfully
|
|
||||||
[34m[INFO][0m WebSocket connection closed
|
|
||||||
[34m[INFO][0m WebSocket connection established
|
|
||||||
[34m[INFO][0m Received WebSocket message
|
|
||||||
[34m[INFO][0m Handling EVENT message with full NIP-01 validation
|
|
||||||
[33m[WARNING][0m Event rejected: expired timestamp
|
|
||||||
[34m[INFO][0m WebSocket connection closed
|
|
||||||
[34m[INFO][0m WebSocket connection established
|
|
||||||
[34m[INFO][0m Received WebSocket message
|
|
||||||
[34m[INFO][0m Handling REQ message for persistent subscription
|
|
||||||
[34m[INFO][0m Added subscription 'filter_test' (total: 1)
|
|
||||||
[34m[INFO][0m Executing SQL: SELECT id, pubkey, created_at, kind, content, sig, tags FROM events WHERE 1=1 AND kind IN (1) ORDER BY created_at DESC LIMIT 10
|
|
||||||
[34m[INFO][0m Query returned 10 rows
|
|
||||||
[34m[INFO][0m Total events sent: 10
|
|
||||||
[34m[INFO][0m Received WebSocket message
|
|
||||||
[34m[INFO][0m Removed subscription 'filter_test' (total: 0)
|
|
||||||
[34m[INFO][0m Closed subscription: filter_test
|
|
||||||
[34m[INFO][0m WebSocket connection closed
|
|
||||||
[33m[WARNING][0m Subscription '<27><><15>a' not found for removal
|
|
||||||
[34m[INFO][0m WebSocket connection established
|
|
||||||
[34m[INFO][0m Received WebSocket message
|
|
||||||
[34m[INFO][0m Handling EVENT message with full NIP-01 validation
|
|
||||||
[32m[SUCCESS][0m Event stored in database
|
|
||||||
[32m[SUCCESS][0m Event validated and stored successfully
|
|
||||||
[34m[INFO][0m WebSocket connection closed
|
|
||||||
[34m[INFO][0m WebSocket connection established
|
|
||||||
[34m[INFO][0m Received WebSocket message
|
|
||||||
[34m[INFO][0m Handling EVENT message with full NIP-01 validation
|
|
||||||
[32m[SUCCESS][0m Event stored in database
|
|
||||||
[32m[SUCCESS][0m Event validated and stored successfully
|
|
||||||
[34m[INFO][0m WebSocket connection closed
|
|
||||||
[34m[INFO][0m HTTP request received
|
|
||||||
[34m[INFO][0m Handling NIP-11 relay information request
|
|
||||||
[32m[SUCCESS][0m NIP-11 relay information served successfully
|
|
||||||
1001
src/config.c
Normal file
1001
src/config.c
Normal file
File diff suppressed because it is too large
Load Diff
223
src/config.h
Normal file
223
src/config.h
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
#ifndef CONFIG_H
|
||||||
|
#define CONFIG_H
|
||||||
|
|
||||||
|
#include <sqlite3.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <cjson/cJSON.h>
|
||||||
|
|
||||||
|
// Configuration system constants
|
||||||
|
#define CONFIG_KEY_MAX_LENGTH 64
|
||||||
|
#define CONFIG_VALUE_MAX_LENGTH 512
|
||||||
|
#define CONFIG_DESCRIPTION_MAX_LENGTH 256
|
||||||
|
#define CONFIG_XDG_DIR_NAME "c-relay"
|
||||||
|
#define CONFIG_FILE_NAME "c_relay_config_event.json"
|
||||||
|
#define CONFIG_PRIVKEY_ENV "C_RELAY_CONFIG_PRIVKEY"
|
||||||
|
#define NOSTR_PUBKEY_HEX_LENGTH 64
|
||||||
|
#define NOSTR_PRIVKEY_HEX_LENGTH 64
|
||||||
|
#define NOSTR_EVENT_ID_HEX_LENGTH 64
|
||||||
|
#define NOSTR_SIGNATURE_HEX_LENGTH 128
|
||||||
|
|
||||||
|
// Protocol and implementation constants (hardcoded - should NOT be configurable)
|
||||||
|
#define SUBSCRIPTION_ID_MAX_LENGTH 64
|
||||||
|
#define CLIENT_IP_MAX_LENGTH 64
|
||||||
|
#define RELAY_NAME_MAX_LENGTH 128
|
||||||
|
#define RELAY_DESCRIPTION_MAX_LENGTH 1024
|
||||||
|
#define RELAY_URL_MAX_LENGTH 256
|
||||||
|
#define RELAY_CONTACT_MAX_LENGTH 128
|
||||||
|
#define RELAY_PUBKEY_MAX_LENGTH 65
|
||||||
|
|
||||||
|
// Default configuration values (used as fallbacks if database config fails)
|
||||||
|
#define DEFAULT_DATABASE_PATH "db/c_nostr_relay.db"
|
||||||
|
#define DEFAULT_PORT 8888
|
||||||
|
#define DEFAULT_HOST "127.0.0.1"
|
||||||
|
#define MAX_CLIENTS 100
|
||||||
|
#define MAX_SUBSCRIPTIONS_PER_CLIENT 20
|
||||||
|
#define MAX_TOTAL_SUBSCRIPTIONS 5000
|
||||||
|
#define MAX_FILTERS_PER_SUBSCRIPTION 10
|
||||||
|
|
||||||
|
// Configuration types
|
||||||
|
typedef enum {
|
||||||
|
CONFIG_TYPE_SYSTEM = 0,
|
||||||
|
CONFIG_TYPE_USER = 1,
|
||||||
|
CONFIG_TYPE_RUNTIME = 2
|
||||||
|
} config_type_t;
|
||||||
|
|
||||||
|
// Configuration data types
|
||||||
|
typedef enum {
|
||||||
|
CONFIG_DATA_STRING = 0,
|
||||||
|
CONFIG_DATA_INTEGER = 1,
|
||||||
|
CONFIG_DATA_BOOLEAN = 2,
|
||||||
|
CONFIG_DATA_JSON = 3
|
||||||
|
} config_data_type_t;
|
||||||
|
|
||||||
|
// Configuration validation result
|
||||||
|
typedef enum {
|
||||||
|
CONFIG_VALID = 0,
|
||||||
|
CONFIG_INVALID_TYPE = 1,
|
||||||
|
CONFIG_INVALID_RANGE = 2,
|
||||||
|
CONFIG_INVALID_FORMAT = 3,
|
||||||
|
CONFIG_MISSING_REQUIRED = 4
|
||||||
|
} config_validation_result_t;
|
||||||
|
|
||||||
|
// Configuration entry structure
|
||||||
|
typedef struct {
|
||||||
|
char key[CONFIG_KEY_MAX_LENGTH];
|
||||||
|
char value[CONFIG_VALUE_MAX_LENGTH];
|
||||||
|
char description[CONFIG_DESCRIPTION_MAX_LENGTH];
|
||||||
|
config_type_t config_type;
|
||||||
|
config_data_type_t data_type;
|
||||||
|
int is_sensitive;
|
||||||
|
int requires_restart;
|
||||||
|
time_t created_at;
|
||||||
|
time_t updated_at;
|
||||||
|
} config_entry_t;
|
||||||
|
|
||||||
|
// Configuration manager state
|
||||||
|
typedef struct {
|
||||||
|
sqlite3* db;
|
||||||
|
sqlite3_stmt* get_config_stmt;
|
||||||
|
sqlite3_stmt* set_config_stmt;
|
||||||
|
sqlite3_stmt* log_change_stmt;
|
||||||
|
|
||||||
|
// Configuration loading status
|
||||||
|
int file_config_loaded;
|
||||||
|
int database_config_loaded;
|
||||||
|
time_t last_reload;
|
||||||
|
|
||||||
|
// XDG configuration directory
|
||||||
|
char config_dir_path[512];
|
||||||
|
char config_file_path[600];
|
||||||
|
} config_manager_t;
|
||||||
|
|
||||||
|
// Global configuration manager instance
|
||||||
|
extern config_manager_t g_config_manager;
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// CORE CONFIGURATION FUNCTIONS
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
// Initialize configuration system
|
||||||
|
int init_configuration_system(void);
|
||||||
|
|
||||||
|
// Cleanup configuration system
|
||||||
|
void cleanup_configuration_system(void);
|
||||||
|
|
||||||
|
// Load configuration from all sources (file -> database -> defaults)
|
||||||
|
int load_configuration(void);
|
||||||
|
|
||||||
|
// Apply loaded configuration to global variables
|
||||||
|
int apply_configuration_to_globals(void);
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// DATABASE CONFIGURATION FUNCTIONS
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
// Initialize database prepared statements
|
||||||
|
int init_config_database_statements(void);
|
||||||
|
|
||||||
|
// Get configuration value from database
|
||||||
|
int get_database_config(const char* key, char* value, size_t value_size);
|
||||||
|
|
||||||
|
// Set configuration value in database
|
||||||
|
int set_database_config(const char* key, const char* new_value, const char* changed_by);
|
||||||
|
|
||||||
|
// Load all configuration from database
|
||||||
|
int load_config_from_database(void);
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// FILE CONFIGURATION FUNCTIONS
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
// Get XDG configuration directory path
|
||||||
|
int get_xdg_config_dir(char* path, size_t path_size);
|
||||||
|
|
||||||
|
// Check if configuration file exists
|
||||||
|
int config_file_exists(void);
|
||||||
|
|
||||||
|
// Load configuration from file
|
||||||
|
int load_config_from_file(void);
|
||||||
|
|
||||||
|
// Validate and apply Nostr configuration event
|
||||||
|
int validate_and_apply_config_event(const cJSON* event);
|
||||||
|
|
||||||
|
// Validate Nostr event structure
|
||||||
|
int validate_nostr_event_structure(const cJSON* event);
|
||||||
|
|
||||||
|
// Validate configuration tags array
|
||||||
|
int validate_config_tags(const cJSON* tags);
|
||||||
|
|
||||||
|
// Extract and apply configuration tags to database
|
||||||
|
int extract_and_apply_config_tags(const cJSON* tags);
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// CONFIGURATION ACCESS FUNCTIONS
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
// Get configuration value (checks all sources: file -> database -> environment -> defaults)
|
||||||
|
const char* get_config_value(const char* key);
|
||||||
|
|
||||||
|
// Get configuration value as integer
|
||||||
|
int get_config_int(const char* key, int default_value);
|
||||||
|
|
||||||
|
// Get configuration value as boolean
|
||||||
|
int get_config_bool(const char* key, int default_value);
|
||||||
|
|
||||||
|
// Set configuration value (updates database)
|
||||||
|
int set_config_value(const char* key, const char* value);
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// CONFIGURATION VALIDATION
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
// Validate configuration value
|
||||||
|
config_validation_result_t validate_config_value(const char* key, const char* value);
|
||||||
|
|
||||||
|
// Log validation error
|
||||||
|
void log_config_validation_error(const char* key, const char* value, const char* error);
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// UTILITY FUNCTIONS
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
// Convert config type enum to string
|
||||||
|
const char* config_type_to_string(config_type_t type);
|
||||||
|
|
||||||
|
// Convert config data type enum to string
|
||||||
|
const char* config_data_type_to_string(config_data_type_t type);
|
||||||
|
|
||||||
|
// Convert string to config type enum
|
||||||
|
config_type_t string_to_config_type(const char* str);
|
||||||
|
|
||||||
|
// Convert string to config data type enum
|
||||||
|
config_data_type_t string_to_config_data_type(const char* str);
|
||||||
|
|
||||||
|
// Check if configuration key requires restart
|
||||||
|
int config_requires_restart(const char* key);
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// NOSTR EVENT GENERATION FUNCTIONS
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
// Generate configuration file with valid Nostr event if it doesn't exist
|
||||||
|
int generate_config_file_if_missing(void);
|
||||||
|
|
||||||
|
// Create a valid Nostr configuration event from database values
|
||||||
|
cJSON* create_config_nostr_event(const char* privkey_hex);
|
||||||
|
|
||||||
|
// Generate a random private key (32 bytes as hex string)
|
||||||
|
int generate_random_privkey(char* privkey_hex, size_t buffer_size);
|
||||||
|
|
||||||
|
// Derive public key from private key (using secp256k1)
|
||||||
|
int derive_pubkey_from_privkey(const char* privkey_hex, char* pubkey_hex, size_t buffer_size);
|
||||||
|
|
||||||
|
// Create Nostr event ID (SHA256 of serialized event data)
|
||||||
|
int create_nostr_event_id(const cJSON* event, char* event_id_hex, size_t buffer_size);
|
||||||
|
|
||||||
|
// Sign Nostr event (using secp256k1 Schnorr signature)
|
||||||
|
int sign_nostr_event(const cJSON* event, const char* privkey_hex, char* signature_hex, size_t buffer_size);
|
||||||
|
|
||||||
|
// Write configuration event to file
|
||||||
|
int write_config_event_to_file(const cJSON* event);
|
||||||
|
|
||||||
|
#endif // CONFIG_H
|
||||||
235
src/main.c
235
src/main.c
@@ -15,26 +15,7 @@
|
|||||||
#include "../nostr_core_lib/cjson/cJSON.h"
|
#include "../nostr_core_lib/cjson/cJSON.h"
|
||||||
#include "../nostr_core_lib/nostr_core/nostr_core.h"
|
#include "../nostr_core_lib/nostr_core/nostr_core.h"
|
||||||
#include "../nostr_core_lib/nostr_core/nip013.h" // NIP-13: Proof of Work
|
#include "../nostr_core_lib/nostr_core/nip013.h" // NIP-13: Proof of Work
|
||||||
|
#include "config.h" // Configuration management system
|
||||||
// Server Configuration
|
|
||||||
#define DEFAULT_PORT 8888
|
|
||||||
#define DEFAULT_HOST "127.0.0.1"
|
|
||||||
#define DATABASE_PATH "db/c_nostr_relay.db"
|
|
||||||
#define MAX_CLIENTS 100
|
|
||||||
|
|
||||||
// Persistent subscription system configuration
|
|
||||||
#define MAX_SUBSCRIPTIONS_PER_CLIENT 20
|
|
||||||
#define MAX_TOTAL_SUBSCRIPTIONS 5000
|
|
||||||
#define MAX_FILTERS_PER_SUBSCRIPTION 10
|
|
||||||
#define SUBSCRIPTION_ID_MAX_LENGTH 64
|
|
||||||
#define CLIENT_IP_MAX_LENGTH 64
|
|
||||||
|
|
||||||
// NIP-11 relay information configuration
|
|
||||||
#define RELAY_NAME_MAX_LENGTH 128
|
|
||||||
#define RELAY_DESCRIPTION_MAX_LENGTH 1024
|
|
||||||
#define RELAY_URL_MAX_LENGTH 256
|
|
||||||
#define RELAY_CONTACT_MAX_LENGTH 128
|
|
||||||
#define RELAY_PUBKEY_MAX_LENGTH 65 // 64 hex chars + null terminator
|
|
||||||
|
|
||||||
// Color constants for logging
|
// Color constants for logging
|
||||||
#define RED "\033[31m"
|
#define RED "\033[31m"
|
||||||
@@ -45,7 +26,7 @@
|
|||||||
#define RESET "\033[0m"
|
#define RESET "\033[0m"
|
||||||
|
|
||||||
// Global state
|
// Global state
|
||||||
static sqlite3* g_db = NULL;
|
sqlite3* g_db = NULL; // Non-static so config.c can access it
|
||||||
static int g_server_running = 1;
|
static int g_server_running = 1;
|
||||||
static struct lws_context *ws_context = NULL;
|
static struct lws_context *ws_context = NULL;
|
||||||
|
|
||||||
@@ -188,8 +169,8 @@ static subscription_manager_t g_subscription_manager = {
|
|||||||
.active_subscriptions = NULL,
|
.active_subscriptions = NULL,
|
||||||
.subscriptions_lock = PTHREAD_MUTEX_INITIALIZER,
|
.subscriptions_lock = PTHREAD_MUTEX_INITIALIZER,
|
||||||
.total_subscriptions = 0,
|
.total_subscriptions = 0,
|
||||||
.max_subscriptions_per_client = MAX_SUBSCRIPTIONS_PER_CLIENT,
|
.max_subscriptions_per_client = MAX_SUBSCRIPTIONS_PER_CLIENT, // Will be updated from config
|
||||||
.max_total_subscriptions = MAX_TOTAL_SUBSCRIPTIONS,
|
.max_total_subscriptions = MAX_TOTAL_SUBSCRIPTIONS, // Will be updated from config
|
||||||
.total_created = 0,
|
.total_created = 0,
|
||||||
.total_events_broadcast = 0
|
.total_events_broadcast = 0
|
||||||
};
|
};
|
||||||
@@ -200,6 +181,9 @@ void log_success(const char* message);
|
|||||||
void log_error(const char* message);
|
void log_error(const char* message);
|
||||||
void log_warning(const char* message);
|
void log_warning(const char* message);
|
||||||
|
|
||||||
|
// Forward declaration for subscription manager configuration
|
||||||
|
void update_subscription_manager_config(void);
|
||||||
|
|
||||||
// Forward declarations for subscription database logging
|
// Forward declarations for subscription database logging
|
||||||
void log_subscription_created(const subscription_t* sub);
|
void log_subscription_created(const subscription_t* sub);
|
||||||
void log_subscription_closed(const char* sub_id, const char* client_ip, const char* reason);
|
void log_subscription_closed(const char* sub_id, const char* client_ip, const char* reason);
|
||||||
@@ -955,6 +939,19 @@ void log_warning(const char* message) {
|
|||||||
fflush(stdout);
|
fflush(stdout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update subscription manager configuration from config system
|
||||||
|
void update_subscription_manager_config(void) {
|
||||||
|
g_subscription_manager.max_subscriptions_per_client = get_config_int("max_subscriptions_per_client", MAX_SUBSCRIPTIONS_PER_CLIENT);
|
||||||
|
g_subscription_manager.max_total_subscriptions = get_config_int("max_total_subscriptions", MAX_TOTAL_SUBSCRIPTIONS);
|
||||||
|
|
||||||
|
char config_msg[256];
|
||||||
|
snprintf(config_msg, sizeof(config_msg),
|
||||||
|
"Subscription limits: max_per_client=%d, max_total=%d",
|
||||||
|
g_subscription_manager.max_subscriptions_per_client,
|
||||||
|
g_subscription_manager.max_total_subscriptions);
|
||||||
|
log_info(config_msg);
|
||||||
|
}
|
||||||
|
|
||||||
// Signal handler for graceful shutdown
|
// Signal handler for graceful shutdown
|
||||||
void signal_handler(int sig) {
|
void signal_handler(int sig) {
|
||||||
if (sig == SIGINT || sig == SIGTERM) {
|
if (sig == SIGINT || sig == SIGTERM) {
|
||||||
@@ -1288,13 +1285,47 @@ int mark_event_as_deleted(const char* event_id, const char* deletion_event_id, c
|
|||||||
/////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
// Initialize relay information with default values
|
// Initialize relay information using configuration system
|
||||||
void init_relay_info() {
|
void init_relay_info() {
|
||||||
// Set default relay information
|
// Load relay information from configuration system
|
||||||
strncpy(g_relay_info.name, "C Nostr Relay", sizeof(g_relay_info.name) - 1);
|
const char* relay_name = get_config_value("relay_name");
|
||||||
strncpy(g_relay_info.description, "A high-performance Nostr relay implemented in C with SQLite storage", sizeof(g_relay_info.description) - 1);
|
if (relay_name) {
|
||||||
strncpy(g_relay_info.software, "https://github.com/teknari/c-relay", sizeof(g_relay_info.software) - 1);
|
strncpy(g_relay_info.name, relay_name, sizeof(g_relay_info.name) - 1);
|
||||||
strncpy(g_relay_info.version, "0.1.0", sizeof(g_relay_info.version) - 1);
|
} else {
|
||||||
|
strncpy(g_relay_info.name, "C Nostr Relay", sizeof(g_relay_info.name) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* relay_description = get_config_value("relay_description");
|
||||||
|
if (relay_description) {
|
||||||
|
strncpy(g_relay_info.description, relay_description, sizeof(g_relay_info.description) - 1);
|
||||||
|
} else {
|
||||||
|
strncpy(g_relay_info.description, "A high-performance Nostr relay implemented in C with SQLite storage", sizeof(g_relay_info.description) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* relay_software = get_config_value("relay_software");
|
||||||
|
if (relay_software) {
|
||||||
|
strncpy(g_relay_info.software, relay_software, sizeof(g_relay_info.software) - 1);
|
||||||
|
} else {
|
||||||
|
strncpy(g_relay_info.software, "https://git.laantungir.net/laantungir/c-relay.git", sizeof(g_relay_info.software) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* relay_version = get_config_value("relay_version");
|
||||||
|
if (relay_version) {
|
||||||
|
strncpy(g_relay_info.version, relay_version, sizeof(g_relay_info.version) - 1);
|
||||||
|
} else {
|
||||||
|
strncpy(g_relay_info.version, "0.2.0", sizeof(g_relay_info.version) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load optional fields
|
||||||
|
const char* relay_contact = get_config_value("relay_contact");
|
||||||
|
if (relay_contact) {
|
||||||
|
strncpy(g_relay_info.contact, relay_contact, sizeof(g_relay_info.contact) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* relay_pubkey = get_config_value("relay_pubkey");
|
||||||
|
if (relay_pubkey) {
|
||||||
|
strncpy(g_relay_info.pubkey, relay_pubkey, sizeof(g_relay_info.pubkey) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize supported NIPs array
|
// Initialize supported NIPs array
|
||||||
g_relay_info.supported_nips = cJSON_CreateArray();
|
g_relay_info.supported_nips = cJSON_CreateArray();
|
||||||
@@ -1308,22 +1339,22 @@ void init_relay_info() {
|
|||||||
cJSON_AddItemToArray(g_relay_info.supported_nips, cJSON_CreateNumber(40)); // NIP-40: Expiration Timestamp
|
cJSON_AddItemToArray(g_relay_info.supported_nips, cJSON_CreateNumber(40)); // NIP-40: Expiration Timestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize server limitations
|
// Initialize server limitations using configuration
|
||||||
g_relay_info.limitation = cJSON_CreateObject();
|
g_relay_info.limitation = cJSON_CreateObject();
|
||||||
if (g_relay_info.limitation) {
|
if (g_relay_info.limitation) {
|
||||||
cJSON_AddNumberToObject(g_relay_info.limitation, "max_message_length", 16384);
|
cJSON_AddNumberToObject(g_relay_info.limitation, "max_message_length", get_config_int("max_message_length", 16384));
|
||||||
cJSON_AddNumberToObject(g_relay_info.limitation, "max_subscriptions", MAX_SUBSCRIPTIONS_PER_CLIENT);
|
cJSON_AddNumberToObject(g_relay_info.limitation, "max_subscriptions", get_config_int("max_subscriptions_per_client", 20));
|
||||||
cJSON_AddNumberToObject(g_relay_info.limitation, "max_limit", 5000);
|
cJSON_AddNumberToObject(g_relay_info.limitation, "max_limit", get_config_int("max_limit", 5000));
|
||||||
cJSON_AddNumberToObject(g_relay_info.limitation, "max_subid_length", SUBSCRIPTION_ID_MAX_LENGTH);
|
cJSON_AddNumberToObject(g_relay_info.limitation, "max_subid_length", SUBSCRIPTION_ID_MAX_LENGTH);
|
||||||
cJSON_AddNumberToObject(g_relay_info.limitation, "max_event_tags", 100);
|
cJSON_AddNumberToObject(g_relay_info.limitation, "max_event_tags", get_config_int("max_event_tags", 100));
|
||||||
cJSON_AddNumberToObject(g_relay_info.limitation, "max_content_length", 8196);
|
cJSON_AddNumberToObject(g_relay_info.limitation, "max_content_length", get_config_int("max_content_length", 8196));
|
||||||
cJSON_AddNumberToObject(g_relay_info.limitation, "min_pow_difficulty", g_pow_config.min_pow_difficulty);
|
cJSON_AddNumberToObject(g_relay_info.limitation, "min_pow_difficulty", g_pow_config.min_pow_difficulty);
|
||||||
cJSON_AddBoolToObject(g_relay_info.limitation, "auth_required", cJSON_False);
|
cJSON_AddBoolToObject(g_relay_info.limitation, "auth_required", get_config_bool("admin_enabled", 0) ? cJSON_True : cJSON_False);
|
||||||
cJSON_AddBoolToObject(g_relay_info.limitation, "payment_required", cJSON_False);
|
cJSON_AddBoolToObject(g_relay_info.limitation, "payment_required", cJSON_False);
|
||||||
cJSON_AddBoolToObject(g_relay_info.limitation, "restricted_writes", cJSON_False);
|
cJSON_AddBoolToObject(g_relay_info.limitation, "restricted_writes", cJSON_False);
|
||||||
cJSON_AddNumberToObject(g_relay_info.limitation, "created_at_lower_limit", 0);
|
cJSON_AddNumberToObject(g_relay_info.limitation, "created_at_lower_limit", 0);
|
||||||
cJSON_AddNumberToObject(g_relay_info.limitation, "created_at_upper_limit", 2147483647);
|
cJSON_AddNumberToObject(g_relay_info.limitation, "created_at_upper_limit", 2147483647);
|
||||||
cJSON_AddNumberToObject(g_relay_info.limitation, "default_limit", 500);
|
cJSON_AddNumberToObject(g_relay_info.limitation, "default_limit", get_config_int("default_limit", 500));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize empty retention policies (can be configured later)
|
// Initialize empty retention policies (can be configured later)
|
||||||
@@ -1636,48 +1667,39 @@ int handle_nip11_http_request(struct lws* wsi, const char* accept_header) {
|
|||||||
/////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
// Initialize PoW configuration with environment variables and defaults
|
// Initialize PoW configuration using configuration system
|
||||||
void init_pow_config() {
|
void init_pow_config() {
|
||||||
log_info("Initializing NIP-13 Proof of Work configuration");
|
log_info("Initializing NIP-13 Proof of Work configuration");
|
||||||
|
|
||||||
// Initialize with defaults (already set in struct initialization)
|
// Load PoW settings from configuration system
|
||||||
|
g_pow_config.enabled = get_config_bool("pow_enabled", 1);
|
||||||
|
g_pow_config.min_pow_difficulty = get_config_int("pow_min_difficulty", 0);
|
||||||
|
|
||||||
// Check environment variables for configuration
|
// Get PoW mode from configuration
|
||||||
const char* pow_enabled_env = getenv("RELAY_POW_ENABLED");
|
const char* pow_mode = get_config_value("pow_mode");
|
||||||
if (pow_enabled_env) {
|
if (pow_mode) {
|
||||||
g_pow_config.enabled = (strcmp(pow_enabled_env, "1") == 0 ||
|
if (strcmp(pow_mode, "strict") == 0) {
|
||||||
strcmp(pow_enabled_env, "true") == 0 ||
|
|
||||||
strcmp(pow_enabled_env, "yes") == 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* min_diff_env = getenv("RELAY_MIN_POW_DIFFICULTY");
|
|
||||||
if (min_diff_env) {
|
|
||||||
int min_diff = atoi(min_diff_env);
|
|
||||||
if (min_diff >= 0 && min_diff <= 64) { // Reasonable bounds
|
|
||||||
g_pow_config.min_pow_difficulty = min_diff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* pow_mode_env = getenv("RELAY_POW_MODE");
|
|
||||||
if (pow_mode_env) {
|
|
||||||
if (strcmp(pow_mode_env, "strict") == 0) {
|
|
||||||
g_pow_config.validation_flags = NOSTR_POW_VALIDATE_ANTI_SPAM | NOSTR_POW_STRICT_FORMAT;
|
g_pow_config.validation_flags = NOSTR_POW_VALIDATE_ANTI_SPAM | NOSTR_POW_STRICT_FORMAT;
|
||||||
g_pow_config.require_nonce_tag = 1;
|
g_pow_config.require_nonce_tag = 1;
|
||||||
g_pow_config.reject_lower_targets = 1;
|
g_pow_config.reject_lower_targets = 1;
|
||||||
g_pow_config.strict_format = 1;
|
g_pow_config.strict_format = 1;
|
||||||
g_pow_config.anti_spam_mode = 1;
|
g_pow_config.anti_spam_mode = 1;
|
||||||
log_info("PoW configured in strict anti-spam mode");
|
log_info("PoW configured in strict anti-spam mode");
|
||||||
} else if (strcmp(pow_mode_env, "full") == 0) {
|
} else if (strcmp(pow_mode, "full") == 0) {
|
||||||
g_pow_config.validation_flags = NOSTR_POW_VALIDATE_FULL;
|
g_pow_config.validation_flags = NOSTR_POW_VALIDATE_FULL;
|
||||||
g_pow_config.require_nonce_tag = 1;
|
g_pow_config.require_nonce_tag = 1;
|
||||||
log_info("PoW configured in full validation mode");
|
log_info("PoW configured in full validation mode");
|
||||||
} else if (strcmp(pow_mode_env, "basic") == 0) {
|
} else if (strcmp(pow_mode, "basic") == 0) {
|
||||||
g_pow_config.validation_flags = NOSTR_POW_VALIDATE_BASIC;
|
g_pow_config.validation_flags = NOSTR_POW_VALIDATE_BASIC;
|
||||||
log_info("PoW configured in basic validation mode");
|
log_info("PoW configured in basic validation mode");
|
||||||
} else if (strcmp(pow_mode_env, "disabled") == 0) {
|
} else if (strcmp(pow_mode, "disabled") == 0) {
|
||||||
g_pow_config.enabled = 0;
|
g_pow_config.enabled = 0;
|
||||||
log_info("PoW validation disabled via RELAY_POW_MODE");
|
log_info("PoW validation disabled via configuration");
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Default to basic mode
|
||||||
|
g_pow_config.validation_flags = NOSTR_POW_VALIDATE_BASIC;
|
||||||
|
log_info("PoW configured in basic validation mode (default)");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log final configuration
|
// Log final configuration
|
||||||
@@ -1801,45 +1823,21 @@ int validate_event_pow(cJSON* event, char* error_message, size_t error_size) {
|
|||||||
/////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
// Initialize expiration configuration with environment variables and defaults
|
// Initialize expiration configuration using configuration system
|
||||||
void init_expiration_config() {
|
void init_expiration_config() {
|
||||||
log_info("Initializing NIP-40 Expiration Timestamp configuration");
|
log_info("Initializing NIP-40 Expiration Timestamp configuration");
|
||||||
|
|
||||||
// Check environment variables for configuration
|
// Load expiration settings from configuration system
|
||||||
const char* exp_enabled_env = getenv("RELAY_EXPIRATION_ENABLED");
|
g_expiration_config.enabled = get_config_bool("expiration_enabled", 1);
|
||||||
if (exp_enabled_env) {
|
g_expiration_config.strict_mode = get_config_bool("expiration_strict", 1);
|
||||||
g_expiration_config.enabled = (strcmp(exp_enabled_env, "1") == 0 ||
|
g_expiration_config.filter_responses = get_config_bool("expiration_filter", 1);
|
||||||
strcmp(exp_enabled_env, "true") == 0 ||
|
g_expiration_config.delete_expired = get_config_bool("expiration_delete", 0);
|
||||||
strcmp(exp_enabled_env, "yes") == 0);
|
g_expiration_config.grace_period = get_config_int("expiration_grace_period", 300);
|
||||||
}
|
|
||||||
|
|
||||||
const char* exp_strict_env = getenv("RELAY_EXPIRATION_STRICT");
|
// Validate grace period bounds
|
||||||
if (exp_strict_env) {
|
if (g_expiration_config.grace_period < 0 || g_expiration_config.grace_period > 86400) {
|
||||||
g_expiration_config.strict_mode = (strcmp(exp_strict_env, "1") == 0 ||
|
log_warning("Invalid grace period, using default of 300 seconds");
|
||||||
strcmp(exp_strict_env, "true") == 0 ||
|
g_expiration_config.grace_period = 300;
|
||||||
strcmp(exp_strict_env, "yes") == 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* exp_filter_env = getenv("RELAY_EXPIRATION_FILTER");
|
|
||||||
if (exp_filter_env) {
|
|
||||||
g_expiration_config.filter_responses = (strcmp(exp_filter_env, "1") == 0 ||
|
|
||||||
strcmp(exp_filter_env, "true") == 0 ||
|
|
||||||
strcmp(exp_filter_env, "yes") == 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* exp_delete_env = getenv("RELAY_EXPIRATION_DELETE");
|
|
||||||
if (exp_delete_env) {
|
|
||||||
g_expiration_config.delete_expired = (strcmp(exp_delete_env, "1") == 0 ||
|
|
||||||
strcmp(exp_delete_env, "true") == 0 ||
|
|
||||||
strcmp(exp_delete_env, "yes") == 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* exp_grace_env = getenv("RELAY_EXPIRATION_GRACE_PERIOD");
|
|
||||||
if (exp_grace_env) {
|
|
||||||
long grace_period = atol(exp_grace_env);
|
|
||||||
if (grace_period >= 0 && grace_period <= 86400) { // Max 24 hours
|
|
||||||
g_expiration_config.grace_period = grace_period;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log final configuration
|
// Log final configuration
|
||||||
@@ -1942,13 +1940,21 @@ int validate_event_expiration(cJSON* event, char* error_message, size_t error_si
|
|||||||
|
|
||||||
// Initialize database connection
|
// Initialize database connection
|
||||||
int init_database() {
|
int init_database() {
|
||||||
int rc = sqlite3_open(DATABASE_PATH, &g_db);
|
// Use configurable database path, falling back to default
|
||||||
|
const char* db_path = get_config_value("database_path");
|
||||||
|
if (!db_path) {
|
||||||
|
db_path = DEFAULT_DATABASE_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rc = sqlite3_open(db_path, &g_db);
|
||||||
if (rc != SQLITE_OK) {
|
if (rc != SQLITE_OK) {
|
||||||
log_error("Cannot open database");
|
log_error("Cannot open database");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
log_success("Database connection established");
|
char success_msg[256];
|
||||||
|
snprintf(success_msg, sizeof(success_msg), "Database connection established: %s", db_path);
|
||||||
|
log_success(success_msg);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2259,7 +2265,7 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check session subscription limits
|
// Check session subscription limits
|
||||||
if (pss && pss->subscription_count >= MAX_SUBSCRIPTIONS_PER_CLIENT) {
|
if (pss && pss->subscription_count >= g_subscription_manager.max_subscriptions_per_client) {
|
||||||
log_error("Maximum subscriptions per client exceeded");
|
log_error("Maximum subscriptions per client exceeded");
|
||||||
|
|
||||||
// Send CLOSED notice
|
// Send CLOSED notice
|
||||||
@@ -2883,7 +2889,7 @@ int start_websocket_relay() {
|
|||||||
log_info("Starting libwebsockets-based Nostr relay server...");
|
log_info("Starting libwebsockets-based Nostr relay server...");
|
||||||
|
|
||||||
memset(&info, 0, sizeof(info));
|
memset(&info, 0, sizeof(info));
|
||||||
info.port = DEFAULT_PORT;
|
info.port = get_config_int("relay_port", DEFAULT_PORT);
|
||||||
info.protocols = protocols;
|
info.protocols = protocols;
|
||||||
info.gid = -1;
|
info.gid = -1;
|
||||||
info.uid = -1;
|
info.uid = -1;
|
||||||
@@ -2909,7 +2915,9 @@ int start_websocket_relay() {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
log_success("WebSocket relay started on ws://127.0.0.1:8888");
|
char startup_msg[256];
|
||||||
|
snprintf(startup_msg, sizeof(startup_msg), "WebSocket relay started on ws://127.0.0.1:%d", info.port);
|
||||||
|
log_success(startup_msg);
|
||||||
|
|
||||||
// Main event loop with proper signal handling
|
// Main event loop with proper signal handling
|
||||||
while (g_server_running) {
|
while (g_server_running) {
|
||||||
@@ -2965,6 +2973,12 @@ int main(int argc, char* argv[]) {
|
|||||||
log_error("Invalid port number");
|
log_error("Invalid port number");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
// Store port in configuration system
|
||||||
|
char port_str[16];
|
||||||
|
snprintf(port_str, sizeof(port_str), "%d", port);
|
||||||
|
set_database_config("relay_port", port_str, "command_line");
|
||||||
|
// Re-apply configuration to make sure global variables are updated
|
||||||
|
apply_configuration_to_globals();
|
||||||
} else {
|
} else {
|
||||||
log_error("Port argument requires a value");
|
log_error("Port argument requires a value");
|
||||||
return 1;
|
return 1;
|
||||||
@@ -2982,19 +2996,28 @@ int main(int argc, char* argv[]) {
|
|||||||
|
|
||||||
printf(BLUE BOLD "=== C Nostr Relay Server ===" RESET "\n");
|
printf(BLUE BOLD "=== C Nostr Relay Server ===" RESET "\n");
|
||||||
|
|
||||||
// Initialize database
|
// Initialize database FIRST (required for configuration system)
|
||||||
if (init_database() != 0) {
|
if (init_database() != 0) {
|
||||||
log_error("Failed to initialize database");
|
log_error("Failed to initialize database");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize nostr library
|
// Initialize nostr library BEFORE configuration system
|
||||||
|
// (required for Nostr event generation in config files)
|
||||||
if (nostr_init() != 0) {
|
if (nostr_init() != 0) {
|
||||||
log_error("Failed to initialize nostr library");
|
log_error("Failed to initialize nostr library");
|
||||||
close_database();
|
close_database();
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize configuration system (loads file + database + applies to globals)
|
||||||
|
if (init_configuration_system() != 0) {
|
||||||
|
log_error("Failed to initialize configuration system");
|
||||||
|
nostr_cleanup();
|
||||||
|
close_database();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize NIP-11 relay information
|
// Initialize NIP-11 relay information
|
||||||
init_relay_info();
|
init_relay_info();
|
||||||
|
|
||||||
@@ -3004,6 +3027,9 @@ int main(int argc, char* argv[]) {
|
|||||||
// Initialize NIP-40 expiration configuration
|
// Initialize NIP-40 expiration configuration
|
||||||
init_expiration_config();
|
init_expiration_config();
|
||||||
|
|
||||||
|
// Update subscription manager configuration
|
||||||
|
update_subscription_manager_config();
|
||||||
|
|
||||||
log_info("Starting relay server...");
|
log_info("Starting relay server...");
|
||||||
|
|
||||||
// Start WebSocket Nostr relay server
|
// Start WebSocket Nostr relay server
|
||||||
@@ -3011,6 +3037,7 @@ int main(int argc, char* argv[]) {
|
|||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
cleanup_relay_info();
|
cleanup_relay_info();
|
||||||
|
cleanup_configuration_system();
|
||||||
nostr_cleanup();
|
nostr_cleanup();
|
||||||
close_database();
|
close_database();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user