Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c23d81b740 | ||
|
|
6dac231040 | ||
|
|
6fd3e531c3 | ||
|
|
c1c05991cf | ||
|
|
ab378e14d1 | ||
|
|
c0f9bf9ef5 | ||
|
|
bc6a7b3f20 | ||
|
|
036b0823b9 | ||
|
|
be99595bde | ||
|
|
01836a4b4c | ||
|
|
9f3b3dd773 | ||
|
|
3210b9e752 | ||
|
|
2d66b8bf1d | ||
|
|
f3d6afead1 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -8,3 +8,5 @@ src/version.h
|
||||
dev-config/
|
||||
db/
|
||||
copy_executable_local.sh
|
||||
nostr_login_lite/
|
||||
style_guide/
|
||||
298
.roo/architect/AGENTS.md
Normal file
298
.roo/architect/AGENTS.md
Normal file
@@ -0,0 +1,298 @@
|
||||
|
||||
# AGENTS.md - AI Agent Integration Guide for Architect Mode
|
||||
|
||||
**Project-Specific Information for AI Agents Working with C-Relay in Architect Mode**
|
||||
|
||||
## Critical Architecture Understanding
|
||||
|
||||
### System Architecture Overview
|
||||
C-Relay implements a **unique event-based configuration architecture** that fundamentally differs from traditional Nostr relays:
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
||||
│ WebSocket │ │ Configuration │ │ Database │
|
||||
│ + HTTP │◄──►│ Event System │◄──►│ (SQLite) │
|
||||
│ (Port 8888) │ │ (Kind 33334) │ │ Schema v4 │
|
||||
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
||||
│ nostr_core_lib │ │ Admin Key │ │ Event Storage │
|
||||
│ (Crypto/Sigs) │ │ Management │ │ + Subscriptions │
|
||||
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
### Core Architectural Principles
|
||||
|
||||
#### 1. Event-Driven Configuration
|
||||
**Design Philosophy**: Configuration as cryptographically signed events rather than files
|
||||
- **Benefits**: Auditability, remote management, tamper-evidence
|
||||
- **Trade-offs**: Complexity in configuration changes, admin key management burden
|
||||
- **Implementation**: Kind 33334 events stored in same database as relay events
|
||||
|
||||
#### 2. Identity-Based Database Naming
|
||||
**Design Philosophy**: Database file named by relay's generated public key
|
||||
- **Benefits**: Prevents database conflicts, enables multi-relay deployments
|
||||
- **Trade-offs**: Cannot predict database filename, complicates backup strategies
|
||||
- **Implementation**: `<relay_pubkey>.db` created in build/ directory
|
||||
|
||||
#### 3. Single-Binary Deployment
|
||||
**Design Philosophy**: All functionality embedded in one executable
|
||||
- **Benefits**: Simple deployment, no external dependencies to manage
|
||||
- **Trade-offs**: Larger binary size, harder to modularize
|
||||
- **Implementation**: SQL schema embedded as header file, nostr_core_lib as submodule
|
||||
|
||||
#### 4. Dual-Protocol Support
|
||||
**Design Philosophy**: WebSocket (Nostr) and HTTP (NIP-11) on same port
|
||||
- **Benefits**: Simplified port management, reduced infrastructure complexity
|
||||
- **Trade-offs**: Protocol detection overhead, libwebsockets dependency
|
||||
- **Implementation**: Request routing based on HTTP headers and upgrade requests
|
||||
|
||||
## Architectural Decision Analysis
|
||||
|
||||
### Configuration System Design
|
||||
**Traditional Approach vs C-Relay:**
|
||||
```
|
||||
Traditional: C-Relay:
|
||||
config.json → kind 33334 events
|
||||
ENV variables → cryptographically signed tags
|
||||
File watching → database polling/restart
|
||||
```
|
||||
|
||||
**Implications for Extensions:**
|
||||
- Configuration changes require event signing capabilities
|
||||
- No hot-reloading without architectural changes
|
||||
- Admin key loss = complete database reset required
|
||||
|
||||
### Database Architecture Decisions
|
||||
**Schema Design Philosophy:**
|
||||
- **Event Tags as JSON**: Separate table with JSON column instead of normalized relations
|
||||
- **Application-Level Filtering**: NIP-40 expiration handled in C, not SQL
|
||||
- **Embedded Schema**: Version 4 schema compiled into binary
|
||||
|
||||
**Scaling Considerations:**
|
||||
- SQLite suitable for small-to-medium relays (< 10k concurrent connections)
|
||||
- Single-writer limitation of SQLite affects write-heavy workloads
|
||||
- JSON tag storage optimizes for read performance over write normalization
|
||||
|
||||
### Memory Management Architecture
|
||||
**Thread Safety Model:**
|
||||
- Global subscription manager with mutex protection
|
||||
- Per-client subscription limits enforced in memory
|
||||
- WebSocket connection state managed by libwebsockets
|
||||
|
||||
**Resource Management:**
|
||||
- JSON objects use reference counting (jansson library)
|
||||
- String duplication pattern for configuration values
|
||||
- Automatic cleanup on client disconnect
|
||||
|
||||
## Architectural Extension Points
|
||||
|
||||
### Adding New Configuration Options
|
||||
**Required Changes:**
|
||||
1. Update [`default_config_event.h`](src/default_config_event.h) template
|
||||
2. Add parsing logic in [`config.c`](src/config.c) `load_config_from_database()`
|
||||
3. Add global config struct field in [`config.h`](src/config.h)
|
||||
4. Update documentation in [`docs/configuration_guide.md`](docs/configuration_guide.md)
|
||||
|
||||
### Adding New NIP Support
|
||||
**Integration Pattern:**
|
||||
1. Event validation in [`request_validator.c`](src/request_validator.c)
|
||||
2. Protocol handling in [`main.c`](src/main.c) WebSocket callback
|
||||
3. Database storage considerations in schema
|
||||
4. Add test in `tests/` directory
|
||||
|
||||
### Scaling Architecture
|
||||
**Current Limitations:**
|
||||
- Single process, no horizontal scaling
|
||||
- SQLite single-writer bottleneck
|
||||
- Memory-based subscription management
|
||||
|
||||
**Potential Extensions:**
|
||||
- Redis for subscription state sharing
|
||||
- PostgreSQL for better concurrent write performance
|
||||
- Load balancer for read scaling with multiple instances
|
||||
|
||||
## Deployment Architecture Patterns
|
||||
|
||||
### Development Deployment
|
||||
```
|
||||
Developer Machine:
|
||||
├── ./make_and_restart_relay.sh
|
||||
├── build/c_relay_x86
|
||||
├── build/<relay_pubkey>.db
|
||||
└── relay.log
|
||||
```
|
||||
|
||||
### Production SystemD Deployment
|
||||
```
|
||||
/opt/c-relay/:
|
||||
├── c_relay_x86
|
||||
├── <relay_pubkey>.db
|
||||
├── systemd service (c-relay.service)
|
||||
└── c-relay user isolation
|
||||
```
|
||||
|
||||
### Container Deployment Architecture
|
||||
```
|
||||
Container:
|
||||
├── Multi-stage build (deps + binary)
|
||||
├── Volume mount for database persistence
|
||||
├── Health checks via NIP-11 endpoint
|
||||
└── Signal handling for graceful shutdown
|
||||
```
|
||||
|
||||
### Reverse Proxy Architecture
|
||||
```
|
||||
Internet → Nginx/HAProxy → C-Relay
|
||||
├── WebSocket upgrade handling
|
||||
├── SSL termination
|
||||
└── Rate limiting
|
||||
```
|
||||
|
||||
## Security Architecture Considerations
|
||||
|
||||
### Key Management Design
|
||||
**Admin Key Security Model:**
|
||||
- Generated once, displayed once, never stored
|
||||
- Required for all configuration changes
|
||||
- Loss requires complete database reset
|
||||
|
||||
**Relay Identity Model:**
|
||||
- Separate keypair for relay identity
|
||||
- Public key used for database naming
|
||||
- Private key never exposed to clients
|
||||
|
||||
### Event Validation Pipeline
|
||||
```
|
||||
WebSocket Input → JSON Parse → Schema Validate → Signature Verify → Store
|
||||
↓ ↓ ↓
|
||||
reject reject reject success
|
||||
```
|
||||
|
||||
### Attack Surface Analysis
|
||||
**Network Attack Vectors:**
|
||||
- WebSocket connection flooding (mitigated by libwebsockets limits)
|
||||
- JSON parsing attacks (handled by jansson library bounds checking)
|
||||
- SQLite injection (prevented by prepared statements)
|
||||
|
||||
**Configuration Attack Vectors:**
|
||||
- Admin key compromise (complete relay control)
|
||||
- Event signature forgery (prevented by nostr_core_lib validation)
|
||||
- Replay attacks (event timestamp validation required)
|
||||
|
||||
## Non-Obvious Architectural Considerations
|
||||
|
||||
### Database Evolution Strategy
|
||||
**Current Limitations:**
|
||||
- Schema changes require database recreation
|
||||
- No migration system for configuration events
|
||||
- Version 4 schema embedded in binary
|
||||
|
||||
**Future Architecture Needs:**
|
||||
- Schema versioning and migration system
|
||||
- Backward compatibility for configuration events
|
||||
- Database backup/restore procedures
|
||||
|
||||
### Configuration Event Lifecycle
|
||||
**Event Flow:**
|
||||
```
|
||||
Admin Signs Event → WebSocket Submit → Validate → Store → Restart Required
|
||||
↓ ↓ ↓
|
||||
Signature Check Database Config Reload
|
||||
```
|
||||
|
||||
**Architectural Implications:**
|
||||
- No hot configuration reloading
|
||||
- Configuration changes require planned downtime
|
||||
- Event ordering matters for multiple simultaneous changes
|
||||
|
||||
### Cross-Architecture Deployment
|
||||
**Build System Architecture:**
|
||||
- Auto-detection of host architecture
|
||||
- Cross-compilation support for ARM64
|
||||
- Architecture-specific binary outputs
|
||||
|
||||
**Deployment Implications:**
|
||||
- Binary must match target architecture
|
||||
- Dependencies must be available for target architecture
|
||||
- Debug tooling architecture-specific
|
||||
|
||||
### Performance Architecture Characteristics
|
||||
**Bottlenecks:**
|
||||
1. **SQLite Write Performance**: Single writer limitation
|
||||
2. **JSON Parsing**: Per-event parsing overhead
|
||||
3. **Signature Validation**: Cryptographic operations per event
|
||||
4. **Memory Management**: JSON object lifecycle management
|
||||
|
||||
**Optimization Points:**
|
||||
- Prepared statement reuse
|
||||
- Connection pooling for concurrent reads
|
||||
- Event batching for bulk operations
|
||||
- Subscription indexing strategies
|
||||
|
||||
### Integration Architecture Patterns
|
||||
**Monitoring Integration:**
|
||||
- NIP-11 endpoint for health checks
|
||||
- Log file monitoring for operational metrics
|
||||
- Database query monitoring for performance
|
||||
- Process monitoring for resource usage
|
||||
|
||||
**Backup Architecture:**
|
||||
- Database file backup (SQLite file copy)
|
||||
- Configuration event export/import
|
||||
- Admin key secure storage (external to relay)
|
||||
|
||||
### Future Extension Architectures
|
||||
**Multi-Relay Coordination:**
|
||||
- Database sharding by event kind
|
||||
- Cross-relay event synchronization
|
||||
- Distributed configuration management
|
||||
|
||||
**Plugin Architecture Possibilities:**
|
||||
- Event processing pipeline hooks
|
||||
- Custom validation plugins
|
||||
- External authentication providers
|
||||
|
||||
**Scaling Architecture Options:**
|
||||
- Read replicas with PostgreSQL migration
|
||||
- Event stream processing with message queues
|
||||
- Microservice decomposition (auth, storage, validation)
|
||||
|
||||
## Architectural Anti-Patterns to Avoid
|
||||
|
||||
1. **Configuration File Addition**: Breaks event-based config paradigm
|
||||
2. **Direct Database Modification**: Bypasses signature validation
|
||||
3. **Hard-Coded Ports**: Conflicts with auto-fallback system
|
||||
4. **Schema Modifications**: Requires database recreation
|
||||
5. **Admin Key Storage**: Violates security model
|
||||
6. **Blocking Operations**: Interferes with WebSocket event loop
|
||||
7. **Memory Leaks**: JSON objects must be properly reference counted
|
||||
8. **Thread Unsafe Operations**: Global state requires proper synchronization
|
||||
|
||||
## Architecture Decision Records (Implicit)
|
||||
|
||||
### Decision: Event-Based Configuration
|
||||
**Context**: Traditional config files vs. cryptographic auditability
|
||||
**Decision**: Store configuration as signed Nostr events
|
||||
**Consequences**: Complex configuration changes, enhanced security, remote management capability
|
||||
|
||||
### Decision: SQLite Database
|
||||
**Context**: Database choice for relay storage
|
||||
**Decision**: Embedded SQLite with JSON tag storage
|
||||
**Consequences**: Simple deployment, single-writer limitation, application-level filtering
|
||||
|
||||
### Decision: Single Binary Deployment
|
||||
**Context**: Dependency management vs. deployment simplicity
|
||||
**Decision**: Embed all dependencies and schema in binary
|
||||
**Consequences**: Larger binary, simple deployment, version coupling
|
||||
|
||||
### Decision: Dual Protocol Support
|
||||
**Context**: WebSocket for Nostr, HTTP for NIP-11
|
||||
**Decision**: Same port serves both protocols
|
||||
**Consequences**: Simplified deployment, protocol detection overhead, libwebsockets dependency
|
||||
|
||||
These architectural decisions form the foundation of C-Relay's unique approach to Nostr relay implementation and should be carefully considered when planning extensions or modifications.
|
||||
**
|
||||
|
||||
[Response interrupted by a tool use result. Only one tool may be used at a time and should be placed at the end of the message.]
|
||||
32
07.md
Normal file
32
07.md
Normal file
@@ -0,0 +1,32 @@
|
||||
NIP-07
|
||||
======
|
||||
|
||||
`window.nostr` capability for web browsers
|
||||
------------------------------------------
|
||||
|
||||
`draft` `optional`
|
||||
|
||||
The `window.nostr` object may be made available by web browsers or extensions and websites or web-apps may make use of it after checking its availability.
|
||||
|
||||
That object must define the following methods:
|
||||
|
||||
```
|
||||
async window.nostr.getPublicKey(): string // returns a public key as hex
|
||||
async window.nostr.signEvent(event: { created_at: number, kind: number, tags: string[][], content: string }): Event // takes an event object, adds `id`, `pubkey` and `sig` and returns it
|
||||
```
|
||||
|
||||
Aside from these two basic above, the following functions can also be implemented optionally:
|
||||
```
|
||||
async window.nostr.nip04.encrypt(pubkey, plaintext): string // returns ciphertext and iv as specified in nip-04 (deprecated)
|
||||
async window.nostr.nip04.decrypt(pubkey, ciphertext): string // takes ciphertext and iv as specified in nip-04 (deprecated)
|
||||
async window.nostr.nip44.encrypt(pubkey, plaintext): string // returns ciphertext as specified in nip-44
|
||||
async window.nostr.nip44.decrypt(pubkey, ciphertext): string // takes ciphertext as specified in nip-44
|
||||
```
|
||||
|
||||
### Recommendation to Extension Authors
|
||||
To make sure that the `window.nostr` is available to nostr clients on page load, the authors who create Chromium and Firefox extensions should load their scripts by specifying `"run_at": "document_end"` in the extension's manifest.
|
||||
|
||||
|
||||
### Implementation
|
||||
|
||||
See https://github.com/aljazceru/awesome-nostr#nip-07-browser-extensions.
|
||||
152
AGENTS.md
Normal file
152
AGENTS.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# AGENTS.md - AI Agent Integration Guide
|
||||
|
||||
**Project-Specific Information for AI Agents Working with C-Relay**
|
||||
|
||||
## Critical Build Commands
|
||||
|
||||
### Primary Build Command
|
||||
```bash
|
||||
./make_and_restart_relay.sh
|
||||
```
|
||||
**Never use `make` directly.** The project requires the custom restart script which:
|
||||
- Handles database preservation/cleanup based on flags
|
||||
- Manages architecture-specific binary detection (x86/ARM64)
|
||||
- Performs automatic process cleanup and port management
|
||||
- Starts relay in background with proper logging
|
||||
|
||||
### Architecture-Specific Binary Outputs
|
||||
- **x86_64**: `./build/c_relay_x86`
|
||||
- **ARM64**: `./build/c_relay_arm64`
|
||||
- **Other**: `./build/c_relay_$(ARCH)`
|
||||
|
||||
### Database File Naming Convention
|
||||
- **Format**: `<relay_pubkey>.db` (NOT `.nrdb` as shown in docs)
|
||||
- **Location**: Created in `build/` directory during execution
|
||||
- **Cleanup**: Use `--preserve-database` flag to retain between builds
|
||||
|
||||
## Critical Integration Issues
|
||||
|
||||
### Event-Based Configuration System
|
||||
- **No traditional config files** - all configuration stored in config table
|
||||
- Admin private key shown **only once** on first startup
|
||||
- Configuration changes require cryptographically signed events
|
||||
- Database path determined by generated relay pubkey
|
||||
|
||||
### First-Time Startup Sequence
|
||||
1. Relay generates admin keypair and relay keypair
|
||||
2. Creates database file with relay pubkey as filename
|
||||
3. Stores default configuration in config table
|
||||
4. **CRITICAL**: Admin private key displayed once and never stored on disk
|
||||
|
||||
### Port Management
|
||||
- Default port 8888 with automatic fallback (8889, 8890, etc.)
|
||||
- Script performs port availability checking before libwebsockets binding
|
||||
- Process cleanup includes force-killing processes on port 8888
|
||||
|
||||
### Database Schema Dependencies
|
||||
- Uses embedded SQL schema (`sql_schema.h`)
|
||||
- Schema version 4 with JSON tag storage
|
||||
- **Critical**: Event expiration filtering done at application level, not SQL level
|
||||
|
||||
### Admin API Event Structure
|
||||
```json
|
||||
{
|
||||
"kind": 23456,
|
||||
"content": "base64_nip44_encrypted_command_array",
|
||||
"tags": [
|
||||
["p", "<relay_pubkey>"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Configuration Commands** (encrypted in content):
|
||||
- `["relay_description", "My Relay"]`
|
||||
- `["max_subscriptions_per_client", "25"]`
|
||||
- `["pow_min_difficulty", "16"]`
|
||||
|
||||
**Auth Rule Commands** (encrypted in content):
|
||||
- `["blacklist", "pubkey", "hex_pubkey_value"]`
|
||||
- `["whitelist", "pubkey", "hex_pubkey_value"]`
|
||||
|
||||
**Query Commands** (encrypted in content):
|
||||
- `["auth_query", "all"]`
|
||||
- `["system_command", "system_status"]`
|
||||
|
||||
### Process Management
|
||||
```bash
|
||||
# Kill existing relay processes
|
||||
pkill -f "c_relay_"
|
||||
|
||||
# Check running processes
|
||||
ps aux | grep c_relay_
|
||||
|
||||
# Force kill port binding
|
||||
fuser -k 8888/tcp
|
||||
```
|
||||
|
||||
### Cross-Compilation Specifics
|
||||
- ARM64 requires explicit dependency installation: `make install-arm64-deps`
|
||||
- Uses `aarch64-linux-gnu-gcc` with specific library paths
|
||||
- PKG_CONFIG_PATH must be set for ARM64: `/usr/lib/aarch64-linux-gnu/pkgconfig`
|
||||
|
||||
### Testing Integration
|
||||
- Tests expect relay running on default port
|
||||
- Use `tests/quick_error_tests.sh` for validation
|
||||
- Event configuration tests: `tests/event_config_tests.sh`
|
||||
|
||||
### SystemD Integration Considerations
|
||||
- Service runs as `c-relay` user in `/opt/c-relay`
|
||||
- Database files created in WorkingDirectory automatically
|
||||
- No environment variables needed (event-based config)
|
||||
- Resource limits: 65536 file descriptors, 4096 processes
|
||||
|
||||
### Development vs Production Differences
|
||||
- Development: `make_and_restart_relay.sh` (default database cleanup)
|
||||
- Production: `make_and_restart_relay.sh --preserve-database`
|
||||
- Debug build requires manual gdb attachment to architecture-specific binary
|
||||
|
||||
### Critical File Dependencies
|
||||
- `nostr_core_lib/` submodule must be initialized and built first
|
||||
- Version header auto-generated from git tags: `src/version.h`
|
||||
- Schema embedded in binary from `src/sql_schema.h`
|
||||
|
||||
### WebSocket Protocol Specifics
|
||||
- Supports both WebSocket (Nostr protocol) and HTTP (NIP-11)
|
||||
- NIP-11 requires `Accept: application/nostr+json` header
|
||||
- CORS headers automatically added for NIP-11 compliance
|
||||
|
||||
### Memory Management Notes
|
||||
- Persistent subscription system with thread-safe global manager
|
||||
- Per-session subscription limits enforced
|
||||
- Event filtering done at C level, not SQL level for NIP-40 expiration
|
||||
|
||||
### Configuration Override Behavior
|
||||
- CLI port override only affects first-time startup
|
||||
- After database creation, all config comes from events
|
||||
- Database path cannot be changed after initialization
|
||||
|
||||
## Non-Obvious Pitfalls
|
||||
|
||||
1. **Database Lock Issues**: Script handles SQLite locking by killing existing processes first
|
||||
2. **Port Race Conditions**: Pre-check + libwebsockets binding can still fail due to timing
|
||||
3. **Key Loss**: Admin private key loss requires complete database deletion and restart
|
||||
4. **Architecture Detection**: Build system auto-detects but cross-compilation requires manual setup
|
||||
5. **Event Storage**: Ephemeral events (kind 20000-29999) accepted but not stored
|
||||
6. **Signature Validation**: All events validated with `nostr_verify_event_signature()` from nostr_core_lib
|
||||
|
||||
## Quick Debugging Commands
|
||||
```bash
|
||||
# Check relay status
|
||||
ps aux | grep c_relay_ && netstat -tln | grep 8888
|
||||
|
||||
# View logs
|
||||
tail -f relay.log
|
||||
|
||||
# Test WebSocket connection
|
||||
wscat -c ws://localhost:8888
|
||||
|
||||
# Test NIP-11 endpoint
|
||||
curl -H "Accept: application/nostr+json" http://localhost:8888
|
||||
|
||||
# Find database files
|
||||
find . -name "*.db" -type f
|
||||
2
Makefile
2
Makefile
@@ -9,7 +9,7 @@ LIBS = -lsqlite3 -lwebsockets -lz -ldl -lpthread -lm -L/usr/local/lib -lsecp256k
|
||||
BUILD_DIR = build
|
||||
|
||||
# Source files
|
||||
MAIN_SRC = src/main.c src/config.c
|
||||
MAIN_SRC = src/main.c src/config.c src/request_validator.c
|
||||
NOSTR_CORE_LIB = nostr_core_lib/libnostr_core_x64.a
|
||||
|
||||
# Architecture detection
|
||||
|
||||
468
README.md
468
README.md
@@ -2,265 +2,6 @@
|
||||
|
||||
A high-performance Nostr relay implemented in C with SQLite backend, featuring a revolutionary **zero-configuration** approach using event-based configuration management.
|
||||
|
||||
## 🌟 Key Features
|
||||
|
||||
- **🔧 Zero Configuration**: No config files or command line arguments needed
|
||||
- **🔑 Event-Based Config**: All settings stored as kind 33334 Nostr events
|
||||
- **🚀 Real-Time Updates**: Configuration changes applied instantly via WebSocket
|
||||
- **🛡️ Cryptographic Security**: Configuration events cryptographically signed and validated
|
||||
- **📊 SQLite Backend**: High-performance event storage with optimized schema
|
||||
- **🔄 Auto Key Generation**: Secure admin and relay keypairs generated on first startup
|
||||
- **💾 Database Per Relay**: Each relay instance uses `<relay_pubkey>.nrdb` database naming
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### 1. Build the Relay
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd c-relay
|
||||
git submodule update --init --recursive
|
||||
make
|
||||
```
|
||||
|
||||
### 2. Start the Relay
|
||||
```bash
|
||||
./build/c_relay_x86
|
||||
```
|
||||
|
||||
**That's it!** No configuration files, no command line arguments needed.
|
||||
|
||||
### 3. Save Your Admin Keys (IMPORTANT!)
|
||||
On first startup, the relay will display:
|
||||
|
||||
```
|
||||
=================================================================
|
||||
IMPORTANT: SAVE THIS ADMIN PRIVATE KEY SECURELY!
|
||||
=================================================================
|
||||
Admin Private Key: f8491814ea288260dad2ab52c09b3b037e75e83e8b24feb9bdc328423922be44
|
||||
Admin Public Key: 07fc2cdd8bdc0c60eefcc9e37e67fef88206bc84fadb894c283b006554ac687b
|
||||
|
||||
Relay Private Key: a1b2c3d4e5f6...
|
||||
Relay Public Key: 1a2b3c4d5e6f...
|
||||
|
||||
Database: dc9a93fd0ffba7041f6df0602e5021913a42fcaf6dbf40f43ecdc011177b4d94.nrdb
|
||||
=================================================================
|
||||
```
|
||||
|
||||
⚠️ **Save the admin private key securely** - it's needed to update relay configuration and is only displayed once!
|
||||
|
||||
## 📋 System Requirements
|
||||
|
||||
- **OS**: Linux, macOS, or Windows (WSL)
|
||||
- **Dependencies**:
|
||||
- SQLite 3
|
||||
- libwebsockets
|
||||
- OpenSSL/LibreSSL
|
||||
- libsecp256k1
|
||||
- libcurl
|
||||
- zlib
|
||||
|
||||
## 🏗️ Event-Based Configuration System
|
||||
|
||||
### How It Works
|
||||
|
||||
Traditional Nostr relays require configuration files, environment variables, or command line arguments. This relay uses a **revolutionary approach**:
|
||||
|
||||
1. **First-Time Startup**: Generates cryptographically secure admin and relay keypairs
|
||||
2. **Database Creation**: Creates `<relay_pubkey>.nrdb` database file
|
||||
3. **Default Configuration**: Creates initial kind 33334 configuration event with sensible defaults
|
||||
4. **Real-Time Updates**: Administrators send new kind 33334 events to update configuration
|
||||
5. **Instant Application**: Changes are applied immediately without restart
|
||||
|
||||
### Configuration Updates
|
||||
|
||||
To update relay configuration, send a signed kind 33334 event:
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": 33334,
|
||||
"content": "C Nostr Relay Configuration",
|
||||
"tags": [
|
||||
["d", "<relay_pubkey>"],
|
||||
["relay_description", "My awesome Nostr relay"],
|
||||
["max_subscriptions_per_client", "25"],
|
||||
["pow_min_difficulty", "16"],
|
||||
["nip40_expiration_enabled", "true"]
|
||||
],
|
||||
"created_at": 1234567890,
|
||||
"pubkey": "<admin_pubkey>",
|
||||
"id": "...",
|
||||
"sig": "..."
|
||||
}
|
||||
```
|
||||
|
||||
Send this event to your relay via WebSocket, and changes are applied instantly.
|
||||
|
||||
### Configurable Parameters
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|-----------|-------------|---------|
|
||||
| `relay_description` | Relay description (NIP-11) | "C Nostr Relay" |
|
||||
| `relay_contact` | Admin contact info | "" |
|
||||
| `max_subscriptions_per_client` | Max subscriptions per client | "25" |
|
||||
| `max_total_subscriptions` | Total subscription limit | "5000" |
|
||||
| `pow_min_difficulty` | NIP-13 PoW difficulty | "0" |
|
||||
| `pow_mode` | PoW validation mode | "optional" |
|
||||
| `nip40_expiration_enabled` | Enable NIP-40 expiration | "true" |
|
||||
| `nip40_expiration_strict` | Strict expiration mode | "false" |
|
||||
| `max_message_length` | Max message size | "65536" |
|
||||
| `max_event_tags` | Max tags per event | "2000" |
|
||||
| `max_content_length` | Max content length | "65536" |
|
||||
|
||||
## 🔧 Deployment
|
||||
|
||||
### Manual Installation
|
||||
```bash
|
||||
# Build the relay
|
||||
make
|
||||
|
||||
# Run directly
|
||||
./build/c_relay_x86
|
||||
```
|
||||
|
||||
### SystemD Service (Recommended)
|
||||
```bash
|
||||
# Install as system service
|
||||
sudo systemd/install-service.sh
|
||||
|
||||
# Start the service
|
||||
sudo systemctl start c-relay
|
||||
|
||||
# Enable auto-start on boot
|
||||
sudo systemctl enable c-relay
|
||||
|
||||
# View logs
|
||||
sudo journalctl -u c-relay -f
|
||||
```
|
||||
|
||||
See [`systemd/README.md`](systemd/README.md) for detailed deployment documentation.
|
||||
|
||||
### Docker (Coming Soon)
|
||||
Docker support is planned for future releases.
|
||||
|
||||
## 📊 Database Schema
|
||||
|
||||
The relay uses an optimized SQLite schema (version 4) with these key features:
|
||||
|
||||
- **Event-based storage**: All Nostr events in single `events` table
|
||||
- **JSON tags support**: Native JSON storage for event tags
|
||||
- **Performance optimized**: Multiple indexes for fast queries
|
||||
- **Subscription logging**: Optional detailed subscription analytics
|
||||
- **Auto-cleanup**: Automatic ephemeral event cleanup
|
||||
- **Replaceable events**: Proper handling of replaceable/addressable events
|
||||
|
||||
## 🛡️ Security Features
|
||||
|
||||
- **Cryptographic validation**: All configuration events cryptographically verified
|
||||
- **Admin-only config**: Only authorized admin pubkey can update configuration
|
||||
- **Signature verification**: Uses `nostr_verify_event_signature()` for validation
|
||||
- **Event structure validation**: Complete event structure validation
|
||||
- **Secure key generation**: Uses `/dev/urandom` for cryptographically secure keys
|
||||
- **No secrets storage**: Admin private key never stored on disk
|
||||
|
||||
## 🔌 Network Configuration
|
||||
|
||||
- **Default Port**: 8888 (WebSocket)
|
||||
- **Protocol**: WebSocket with Nostr message format
|
||||
- **Endpoints**:
|
||||
- `ws://localhost:8888` - WebSocket relay
|
||||
- `http://localhost:8888` - NIP-11 relay information (HTTP GET)
|
||||
|
||||
## 🏃♂️ Usage Examples
|
||||
|
||||
### Connect with a Nostr Client
|
||||
```javascript
|
||||
const relay = new WebSocket('ws://localhost:8888');
|
||||
relay.send(JSON.stringify(["REQ", "sub1", {"kinds": [1], "limit": 10}]));
|
||||
```
|
||||
|
||||
### Update Configuration (using `nostrtool` or similar)
|
||||
```bash
|
||||
# Create configuration event with nostrtool
|
||||
nostrtool event --kind 33334 --content "Updated config" \
|
||||
--tag d <relay_pubkey> \
|
||||
--tag relay_description "My updated relay" \
|
||||
--private-key <admin_private_key>
|
||||
|
||||
# Send to relay
|
||||
nostrtool send ws://localhost:8888 <event_json>
|
||||
```
|
||||
|
||||
## 📈 Monitoring and Analytics
|
||||
|
||||
### View Relay Status
|
||||
```bash
|
||||
# Check if relay is running
|
||||
ps aux | grep c_relay
|
||||
|
||||
# Check network port
|
||||
netstat -tln | grep 8888
|
||||
|
||||
# View recent logs
|
||||
tail -f relay.log
|
||||
```
|
||||
|
||||
### Database Analytics
|
||||
```bash
|
||||
# Connect to relay database
|
||||
sqlite3 <relay_pubkey>.nrdb
|
||||
|
||||
# View relay statistics
|
||||
SELECT * FROM event_stats;
|
||||
|
||||
# View configuration events
|
||||
SELECT * FROM configuration_events;
|
||||
|
||||
# View recent events
|
||||
SELECT * FROM recent_events LIMIT 10;
|
||||
```
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### Run Error Handling Tests
|
||||
```bash
|
||||
# Comprehensive test suite
|
||||
tests/event_config_tests.sh
|
||||
|
||||
# Quick validation tests
|
||||
tests/quick_error_tests.sh
|
||||
```
|
||||
|
||||
### Manual Testing
|
||||
```bash
|
||||
# Test WebSocket connection
|
||||
wscat -c ws://localhost:8888
|
||||
|
||||
# Test NIP-11 information
|
||||
curl http://localhost:8888
|
||||
```
|
||||
|
||||
## 🔧 Development
|
||||
|
||||
### Build from Source
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd c-relay
|
||||
git submodule update --init --recursive
|
||||
make clean && make
|
||||
```
|
||||
|
||||
### Debug Build
|
||||
```bash
|
||||
make debug
|
||||
gdb ./build/c_relay_x86
|
||||
```
|
||||
|
||||
### Contributing
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make changes with tests
|
||||
4. Submit a pull request
|
||||
|
||||
## 📜 Supported NIPs
|
||||
|
||||
<!--
|
||||
@@ -276,68 +17,189 @@ Do NOT modify the formatting, add emojis, or change the text. Keep the simple fo
|
||||
- [x] NIP-20: Command Results
|
||||
- [x] NIP-33: Parameterized Replaceable Events
|
||||
- [x] NIP-40: Expiration Timestamp
|
||||
- [ ] NIP-42: Authentication of clients to relays
|
||||
- [x] NIP-42: Authentication of clients to relays
|
||||
- [ ] NIP-45: Counting results
|
||||
- [ ] NIP-50: Keywords filter
|
||||
- [ ] NIP-70: Protected Events
|
||||
|
||||
## 🆘 Troubleshooting
|
||||
## 🔧 Administrator API
|
||||
|
||||
### Common Issues
|
||||
C-Relay uses an innovative **event-based administration system** where all configuration and management commands are sent as signed Nostr events using the admin private key generated during first startup. All admin commands use **NIP-44 encrypted command arrays** for security and compatibility.
|
||||
|
||||
**Relay won't start**
|
||||
```bash
|
||||
# Check for port conflicts
|
||||
netstat -tln | grep 8888
|
||||
### Authentication
|
||||
|
||||
# Check permissions
|
||||
ls -la build/c_relay_x86
|
||||
All admin commands require signing with the admin private key displayed during first-time startup. **Save this key securely** - it cannot be recovered and is needed for all administrative operations.
|
||||
|
||||
# Check dependencies
|
||||
ldd build/c_relay_x86
|
||||
### Event Structure
|
||||
|
||||
All admin commands use the same unified event structure with NIP-44 encrypted content:
|
||||
|
||||
**Admin Command Event:**
|
||||
```json
|
||||
{
|
||||
"id": "event_id",
|
||||
"pubkey": "admin_public_key",
|
||||
"created_at": 1234567890,
|
||||
"kind": 23456,
|
||||
"content": "AqHBUgcM7dXFYLQuDVzGwMST1G8jtWYyVvYxXhVGEu4nAb4LVw...",
|
||||
"tags": [
|
||||
["p", "relay_public_key"]
|
||||
],
|
||||
"sig": "event_signature"
|
||||
}
|
||||
```
|
||||
|
||||
**Lost admin private key**
|
||||
- If you lose the admin private key, you cannot update configuration
|
||||
- You must delete the database file and restart (loses all events)
|
||||
- The relay will generate new keys on first startup
|
||||
The `content` field contains a NIP-44 encrypted JSON array representing the command.
|
||||
|
||||
**Database corruption**
|
||||
```bash
|
||||
# Check database integrity
|
||||
sqlite3 <relay_pubkey>.nrdb "PRAGMA integrity_check;"
|
||||
|
||||
# If corrupted, remove database (loses all events)
|
||||
rm <relay_pubkey>.nrdb*
|
||||
./build/c_relay_x86 # Will create fresh database
|
||||
**Admin Response Event:**
|
||||
```json
|
||||
["EVENT", "temp_sub_id", {
|
||||
"id": "response_event_id",
|
||||
"pubkey": "relay_public_key",
|
||||
"created_at": 1234567890,
|
||||
"kind": 23457,
|
||||
"content": "BpKCVhfN8eYtRmPqSvWxZnMkL2gHjUiOp3rTyEwQaS5dFg...",
|
||||
"tags": [
|
||||
["p", "admin_public_key"]
|
||||
],
|
||||
"sig": "response_event_signature"
|
||||
}]
|
||||
```
|
||||
|
||||
**Configuration not updating**
|
||||
- Ensure configuration events are properly signed
|
||||
- Check that admin pubkey matches the one from first startup
|
||||
- Verify WebSocket connection is active
|
||||
- Check relay logs for validation errors
|
||||
The `content` field contains a NIP-44 encrypted JSON response object.
|
||||
|
||||
## 📄 License
|
||||
### Admin Commands
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||
All commands are sent as NIP-44 encrypted JSON arrays in the event content. The following table lists all available commands:
|
||||
|
||||
## 🤝 Support
|
||||
| Command Type | Command Format | Description |
|
||||
|--------------|----------------|-------------|
|
||||
| **Configuration Management** |
|
||||
| `config_update` | `["config_update", [{"key": "auth_enabled", "value": "true", "data_type": "boolean", "category": "auth"}, {"key": "relay_description", "value": "My Relay", "data_type": "string", "category": "relay"}, ...]]` | Update relay configuration parameters (supports multiple updates) |
|
||||
| `config_query` | `["config_query", "all"]` | Query all configuration parameters |
|
||||
| **Auth Rules Management** |
|
||||
| `auth_add_blacklist` | `["blacklist", "pubkey", "abc123..."]` | Add pubkey to blacklist |
|
||||
| `auth_add_whitelist` | `["whitelist", "pubkey", "def456..."]` | Add pubkey to whitelist |
|
||||
| `auth_delete_rule` | `["delete_auth_rule", "blacklist", "pubkey", "abc123..."]` | Delete specific auth rule |
|
||||
| `auth_query_all` | `["auth_query", "all"]` | Query all auth rules |
|
||||
| `auth_query_type` | `["auth_query", "whitelist"]` | Query specific rule type |
|
||||
| `auth_query_pattern` | `["auth_query", "pattern", "abc123..."]` | Query specific pattern |
|
||||
| **System Commands** |
|
||||
| `system_clear_auth` | `["system_command", "clear_all_auth_rules"]` | Clear all auth rules |
|
||||
| `system_status` | `["system_command", "system_status"]` | Get system status |
|
||||
|
||||
- **Issues**: Report bugs and feature requests on GitHub
|
||||
- **Documentation**: See `docs/` directory for technical details
|
||||
- **Deployment**: See `systemd/README.md` for production deployment
|
||||
- **Community**: Join the Nostr development community
|
||||
### Available Configuration Keys
|
||||
|
||||
## 🚀 Future Roadmap
|
||||
**Basic Relay Settings:**
|
||||
- `relay_description`: Relay description text
|
||||
- `relay_contact`: Contact information
|
||||
- `max_connections`: Maximum concurrent connections
|
||||
- `max_subscriptions_per_client`: Max subscriptions per client
|
||||
- `max_event_tags`: Maximum tags per event
|
||||
- `max_content_length`: Maximum event content length
|
||||
|
||||
- [ ] Docker containerization
|
||||
- [ ] NIP-42 authentication support
|
||||
- [ ] Advanced analytics dashboard
|
||||
- [ ] Clustering support for high availability
|
||||
- [ ] Performance optimizations
|
||||
- [ ] Additional NIP implementations
|
||||
**Authentication & Access Control:**
|
||||
- `auth_enabled`: Enable whitelist/blacklist auth rules (`true`/`false`)
|
||||
- `nip42_auth_required`: Enable NIP-42 cryptographic authentication (`true`/`false`)
|
||||
- `nip42_auth_required_kinds`: Event kinds requiring NIP-42 auth (comma-separated)
|
||||
- `nip42_challenge_timeout`: NIP-42 challenge expiration seconds
|
||||
|
||||
---
|
||||
**Proof of Work & Validation:**
|
||||
- `pow_min_difficulty`: Minimum proof-of-work difficulty
|
||||
- `nip40_expiration_enabled`: Enable event expiration (`true`/`false`)
|
||||
|
||||
**The C Nostr Relay represents the future of Nostr infrastructure - zero configuration, event-based management, and cryptographically secure administration.**
|
||||
### Response Format
|
||||
|
||||
All admin commands return **signed EVENT responses** via WebSocket following standard Nostr protocol. Responses use JSON content with structured data.
|
||||
|
||||
#### Response Examples
|
||||
|
||||
**Success Response:**
|
||||
```json
|
||||
["EVENT", "temp_sub_id", {
|
||||
"id": "response_event_id",
|
||||
"pubkey": "relay_public_key",
|
||||
"created_at": 1234567890,
|
||||
"kind": 23457,
|
||||
"content": "nip44 encrypted:{\"query_type\": \"config_update\", \"status\": \"success\", \"message\": \"Operation completed successfully\", \"timestamp\": 1234567890}",
|
||||
"tags": [
|
||||
["p", "admin_public_key"]
|
||||
],
|
||||
"sig": "response_event_signature"
|
||||
}]
|
||||
```
|
||||
|
||||
**Error Response:**
|
||||
```json
|
||||
["EVENT", "temp_sub_id", {
|
||||
"id": "response_event_id",
|
||||
"pubkey": "relay_public_key",
|
||||
"created_at": 1234567890,
|
||||
"kind": 23457,
|
||||
"content": "nip44 encrypted:{\"query_type\": \"config_update\", \"status\": \"error\", \"error\": \"invalid configuration value\", \"timestamp\": 1234567890}",
|
||||
"tags": [
|
||||
["p", "admin_public_key"]
|
||||
],
|
||||
"sig": "response_event_signature"
|
||||
}]
|
||||
```
|
||||
|
||||
**Auth Rules Query Response:**
|
||||
```json
|
||||
["EVENT", "temp_sub_id", {
|
||||
"id": "response_event_id",
|
||||
"pubkey": "relay_public_key",
|
||||
"created_at": 1234567890,
|
||||
"kind": 23457,
|
||||
"content": "nip44 encrypted:{\"query_type\": \"auth_rules_all\", \"total_results\": 2, \"timestamp\": 1234567890, \"data\": [{\"rule_type\": \"blacklist\", \"pattern_type\": \"pubkey\", \"pattern_value\": \"abc123...\", \"action\": \"allow\"}]}",
|
||||
"tags": [
|
||||
["p", "admin_public_key"]
|
||||
],
|
||||
"sig": "response_event_signature"
|
||||
}]
|
||||
```
|
||||
|
||||
**Configuration Query Response:**
|
||||
```json
|
||||
["EVENT", "temp_sub_id", {
|
||||
"id": "response_event_id",
|
||||
"pubkey": "relay_public_key",
|
||||
"created_at": 1234567890,
|
||||
"kind": 23457,
|
||||
"content": "nip44 encrypted:{\"query_type\": \"config_all\", \"total_results\": 27, \"timestamp\": 1234567890, \"data\": [{\"key\": \"auth_enabled\", \"value\": \"false\", \"data_type\": \"boolean\", \"category\": \"auth\", \"description\": \"Enable NIP-42 authentication\"}, {\"key\": \"relay_description\", \"value\": \"My Relay\", \"data_type\": \"string\", \"category\": \"relay\", \"description\": \"Relay description text\"}]}",
|
||||
"tags": [
|
||||
["p", "admin_public_key"]
|
||||
],
|
||||
"sig": "response_event_signature"
|
||||
}]
|
||||
```
|
||||
|
||||
**Configuration Update Success Response:**
|
||||
```json
|
||||
["EVENT", "temp_sub_id", {
|
||||
"id": "response_event_id",
|
||||
"pubkey": "relay_public_key",
|
||||
"created_at": 1234567890,
|
||||
"kind": 23457,
|
||||
"content": "nip44 encrypted:{\"query_type\": \"config_update\", \"total_results\": 2, \"timestamp\": 1234567890, \"status\": \"success\", \"data\": [{\"key\": \"auth_enabled\", \"value\": \"true\", \"status\": \"updated\"}, {\"key\": \"relay_description\", \"value\": \"My Updated Relay\", \"status\": \"updated\"}]}",
|
||||
"tags": [
|
||||
["p", "admin_public_key"]
|
||||
],
|
||||
"sig": "response_event_signature"
|
||||
}]
|
||||
```
|
||||
|
||||
**Configuration Update Error Response:**
|
||||
```json
|
||||
["EVENT", "temp_sub_id", {
|
||||
"id": "response_event_id",
|
||||
"pubkey": "relay_public_key",
|
||||
"created_at": 1234567890,
|
||||
"kind": 23457,
|
||||
"content": "nip44 encrypted:{\"query_type\": \"config_update\", \"status\": \"error\", \"error\": \"field validation failed: invalid port number '99999' (must be 1-65535)\", \"timestamp\": 1234567890}",
|
||||
"tags": [
|
||||
["p", "admin_public_key"]
|
||||
],
|
||||
"sig": "response_event_signature"
|
||||
}]
|
||||
```
|
||||
|
||||
134
api/button.html
Normal file
134
api/button.html
Normal file
@@ -0,0 +1,134 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Embedded NOSTR_LOGIN_LITE</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
margin: 0;
|
||||
padding: 40px;
|
||||
background: white;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 90vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#login-button {
|
||||
background: #0066cc;
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
#login-button:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div id="login-button">Login</div>
|
||||
</div>
|
||||
|
||||
<script src="../lite/nostr.bundle.js"></script>
|
||||
<script src="../lite/nostr-lite.js"></script>
|
||||
|
||||
<script>
|
||||
let isAuthenticated = false;
|
||||
let currentUser = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
await window.NOSTR_LOGIN_LITE.init({
|
||||
|
||||
methods: {
|
||||
extension: true,
|
||||
local: true,
|
||||
readonly: true,
|
||||
connect: true,
|
||||
remote: true,
|
||||
otp: true
|
||||
},
|
||||
floatingTab: {
|
||||
enabled: false
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for authentication events
|
||||
window.addEventListener('nlMethodSelected', handleAuthEvent);
|
||||
window.addEventListener('nlLogout', handleLogoutEvent);
|
||||
|
||||
// Check for existing authentication state
|
||||
checkAuthState();
|
||||
|
||||
// Initialize button
|
||||
updateButtonState();
|
||||
});
|
||||
|
||||
function handleAuthEvent(event) {
|
||||
const { pubkey, method } = event.detail;
|
||||
console.log(`Authenticated with ${method}, pubkey: ${pubkey}`);
|
||||
|
||||
isAuthenticated = true;
|
||||
currentUser = event.detail;
|
||||
updateButtonState();
|
||||
}
|
||||
|
||||
function handleLogoutEvent() {
|
||||
console.log('Logout event received');
|
||||
|
||||
isAuthenticated = false;
|
||||
currentUser = null;
|
||||
updateButtonState();
|
||||
}
|
||||
|
||||
function checkAuthState() {
|
||||
// Check if user is already authenticated (from persistent storage)
|
||||
try {
|
||||
// Try to get public key - this will work if already authenticated
|
||||
window.nostr.getPublicKey().then(pubkey => {
|
||||
console.log('Found existing authentication, pubkey:', pubkey);
|
||||
isAuthenticated = true;
|
||||
currentUser = { pubkey, method: 'persistent' };
|
||||
updateButtonState();
|
||||
}).catch(error => {
|
||||
console.log('No existing authentication found:', error.message);
|
||||
// User is not authenticated, button stays in login state
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('No existing authentication found');
|
||||
// User is not authenticated, button stays in login state
|
||||
}
|
||||
}
|
||||
|
||||
function updateButtonState() {
|
||||
const button = document.getElementById('login-button');
|
||||
|
||||
if (isAuthenticated) {
|
||||
button.textContent = 'Logout';
|
||||
button.onclick = () => window.NOSTR_LOGIN_LITE.logout();
|
||||
button.style.background = '#dc3545'; // Red for logout
|
||||
} else {
|
||||
button.textContent = 'Login';
|
||||
button.onclick = () => window.NOSTR_LOGIN_LITE.launch('login');
|
||||
button.style.background = '#0066cc'; // Blue for login
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
3892
api/index.html
Normal file
3892
api/index.html
Normal file
File diff suppressed because it is too large
Load Diff
3190
api/nostr-lite.js
Normal file
3190
api/nostr-lite.js
Normal file
File diff suppressed because it is too large
Load Diff
11534
api/nostr.bundle.js
Normal file
11534
api/nostr.bundle.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
||||
Only README.md will remain
|
||||
295
docs/NIP-42_Authentication.md
Normal file
295
docs/NIP-42_Authentication.md
Normal file
@@ -0,0 +1,295 @@
|
||||
# NIP-42 Authentication Implementation
|
||||
|
||||
## Overview
|
||||
|
||||
This relay implements NIP-42 (Authentication of clients to relays) providing granular authentication controls for event submission and subscription operations. The implementation supports both challenge-response authentication and per-connection state management.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
1. **Per-Session Authentication State** (`struct per_session_data`)
|
||||
- `authenticated`: Boolean flag indicating authentication status
|
||||
- `authenticated_pubkey[65]`: Hex-encoded public key of authenticated user
|
||||
- `active_challenge[65]`: Current authentication challenge
|
||||
- `challenge_created`: Timestamp when challenge was generated
|
||||
- `challenge_expires`: Challenge expiration timestamp
|
||||
- `nip42_auth_required_events`: Whether auth is required for EVENT submission
|
||||
- `nip42_auth_required_subscriptions`: Whether auth is required for REQ operations
|
||||
- `auth_challenge_sent`: Flag indicating if challenge has been sent
|
||||
|
||||
2. **Challenge Management** (via `request_validator.c`)
|
||||
- `nostr_nip42_generate_challenge()`: Generates cryptographically secure challenges
|
||||
- `nostr_nip42_verify_auth_event()`: Validates signed authentication events
|
||||
- Challenge storage and cleanup with expiration handling
|
||||
|
||||
3. **WebSocket Protocol Integration**
|
||||
- AUTH message handling in `nostr_relay_callback()`
|
||||
- Challenge generation and transmission
|
||||
- Authentication verification and session state updates
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Event-Based Configuration
|
||||
|
||||
NIP-42 authentication is configured using kind 33334 configuration events with the following tags:
|
||||
|
||||
| Tag | Description | Default | Values |
|
||||
|-----|-------------|---------|--------|
|
||||
| `nip42_auth_required_events` | Require auth for EVENT submission | `false` | `true`/`false` |
|
||||
| `nip42_auth_required_subscriptions` | Require auth for REQ operations | `false` | `true`/`false` |
|
||||
|
||||
### Example Configuration Event
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": 33334,
|
||||
"content": "C Nostr Relay Configuration",
|
||||
"tags": [
|
||||
["d", "<relay_pubkey>"],
|
||||
["nip42_auth_required_events", "true"],
|
||||
["nip42_auth_required_subscriptions", "false"],
|
||||
["relay_description", "Authenticated Nostr Relay"]
|
||||
],
|
||||
"created_at": 1640995200,
|
||||
"pubkey": "<admin_pubkey>",
|
||||
"id": "<event_id>",
|
||||
"sig": "<signature>"
|
||||
}
|
||||
```
|
||||
|
||||
## Authentication Flow
|
||||
|
||||
### 1. Challenge Generation
|
||||
|
||||
When authentication is required and client is not authenticated:
|
||||
|
||||
```
|
||||
Client -> Relay: ["EVENT", <event>] (unauthenticated)
|
||||
Relay -> Client: ["AUTH", <challenge>]
|
||||
```
|
||||
|
||||
The challenge is a 64-character hex string generated using cryptographically secure random numbers.
|
||||
|
||||
### 2. Authentication Response
|
||||
|
||||
Client creates and signs an authentication event (kind 22242):
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": 22242,
|
||||
"content": "",
|
||||
"tags": [
|
||||
["relay", "ws://relay.example.com"],
|
||||
["challenge", "<challenge_from_relay>"]
|
||||
],
|
||||
"created_at": <current_timestamp>,
|
||||
"pubkey": "<client_pubkey>",
|
||||
"id": "<event_id>",
|
||||
"sig": "<signature>"
|
||||
}
|
||||
```
|
||||
|
||||
Client sends this event back to relay:
|
||||
|
||||
```
|
||||
Client -> Relay: ["AUTH", <signed_auth_event>]
|
||||
```
|
||||
|
||||
### 3. Verification and Session Update
|
||||
|
||||
The relay:
|
||||
1. Validates the authentication event signature
|
||||
2. Verifies the challenge matches the one sent
|
||||
3. Checks challenge expiration (default: 10 minutes)
|
||||
4. Updates session state with authenticated public key
|
||||
5. Sends confirmation notice
|
||||
|
||||
```
|
||||
Relay -> Client: ["NOTICE", "NIP-42 authentication successful"]
|
||||
```
|
||||
|
||||
## Granular Authentication Controls
|
||||
|
||||
### Separate Controls for Events vs Subscriptions
|
||||
|
||||
The implementation provides separate authentication requirements:
|
||||
|
||||
- **Event Submission**: Control whether clients must authenticate to publish events
|
||||
- **Subscription Access**: Control whether clients must authenticate to create subscriptions
|
||||
|
||||
This allows flexible relay policies:
|
||||
- **Public Read, Authenticated Write**: `events=true, subscriptions=false`
|
||||
- **Fully Authenticated**: `events=true, subscriptions=true`
|
||||
- **Public Access**: `events=false, subscriptions=false` (default)
|
||||
- **Authenticated Read Only**: `events=false, subscriptions=true`
|
||||
|
||||
### Per-Connection State
|
||||
|
||||
Each WebSocket connection maintains its own authentication state:
|
||||
- Authentication persists for the lifetime of the connection
|
||||
- Challenges expire after 10 minutes
|
||||
- Session cleanup on connection close
|
||||
|
||||
## Security Features
|
||||
|
||||
### Challenge Security
|
||||
- 64-character hexadecimal challenges (256 bits of entropy)
|
||||
- Cryptographically secure random generation
|
||||
- Challenge expiration to prevent replay attacks
|
||||
- One-time use challenges
|
||||
|
||||
### Event Validation
|
||||
- Complete signature verification using secp256k1
|
||||
- Event ID validation
|
||||
- Challenge-response binding verification
|
||||
- Timestamp validation with configurable tolerance
|
||||
|
||||
### Session Management
|
||||
- Thread-safe per-session state management
|
||||
- Automatic cleanup on disconnection
|
||||
- Challenge expiration handling
|
||||
|
||||
## Client Integration
|
||||
|
||||
### Using nak Client
|
||||
|
||||
```bash
|
||||
# Generate keypair
|
||||
PRIVKEY=$(nak key --gen)
|
||||
PUBKEY=$(nak key --pub $PRIVKEY)
|
||||
|
||||
# Connect and authenticate automatically
|
||||
nak event -k 1 --content "Authenticated message" --sec $PRIVKEY --relay ws://localhost:8888
|
||||
|
||||
# nak handles NIP-42 authentication automatically when required
|
||||
```
|
||||
|
||||
### Manual WebSocket Integration
|
||||
|
||||
```javascript
|
||||
const ws = new WebSocket('ws://localhost:8888');
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const message = JSON.parse(event.data);
|
||||
|
||||
if (message[0] === 'AUTH') {
|
||||
const challenge = message[1];
|
||||
|
||||
// Create auth event (kind 22242)
|
||||
const authEvent = {
|
||||
kind: 22242,
|
||||
content: "",
|
||||
tags: [
|
||||
["relay", "ws://localhost:8888"],
|
||||
["challenge", challenge]
|
||||
],
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
pubkey: clientPubkey,
|
||||
// ... calculate id and signature
|
||||
};
|
||||
|
||||
// Send auth response
|
||||
ws.send(JSON.stringify(["AUTH", authEvent]));
|
||||
}
|
||||
};
|
||||
|
||||
// Send event (may trigger AUTH challenge)
|
||||
ws.send(JSON.stringify(["EVENT", myEvent]));
|
||||
```
|
||||
|
||||
## Administration
|
||||
|
||||
### Enabling Authentication
|
||||
|
||||
1. **Get Admin Private Key**: Extract from relay startup logs (shown once)
|
||||
2. **Create Configuration Event**: Use nak or custom tooling
|
||||
3. **Publish Configuration**: Send to relay with admin signature
|
||||
|
||||
```bash
|
||||
# Enable auth for events only
|
||||
nak event -k 33334 \
|
||||
--content "C Nostr Relay Configuration" \
|
||||
--tag "d=$RELAY_PUBKEY" \
|
||||
--tag "nip42_auth_required_events=true" \
|
||||
--tag "nip42_auth_required_subscriptions=false" \
|
||||
--sec $ADMIN_PRIVKEY \
|
||||
--relay ws://localhost:8888
|
||||
```
|
||||
|
||||
### Monitoring Authentication
|
||||
|
||||
- Check relay logs for authentication events
|
||||
- Monitor `NOTICE` messages for auth status
|
||||
- Use `get_settings.sh` script to view current configuration
|
||||
|
||||
```bash
|
||||
./get_settings.sh
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Challenge Expiration**
|
||||
- Default: 10 minutes
|
||||
- Client must respond within expiration window
|
||||
- Generate new challenge for expired attempts
|
||||
|
||||
2. **Signature Verification Failures**
|
||||
- Verify event structure matches NIP-42 specification
|
||||
- Check challenge value matches exactly
|
||||
- Ensure proper secp256k1 signature generation
|
||||
|
||||
3. **Configuration Not Applied**
|
||||
- Verify admin private key is correct
|
||||
- Check configuration event signature
|
||||
- Ensure relay pubkey in 'd' tag matches relay
|
||||
|
||||
### Debug Commands
|
||||
|
||||
```bash
|
||||
# Check supported NIPs
|
||||
curl -H "Accept: application/nostr+json" http://localhost:8888 | jq .supported_nips
|
||||
|
||||
# View current configuration
|
||||
nak req -k 33334 ws://localhost:8888 | jq .
|
||||
|
||||
# Test authentication flow
|
||||
./tests/42_nip_test.sh
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- Challenge generation: ~1ms overhead per unauthenticated connection
|
||||
- Authentication verification: ~2-5ms per auth event
|
||||
- Memory overhead: ~200 bytes per connection for auth state
|
||||
- Database impact: Configuration events cached, minimal query overhead
|
||||
|
||||
## Integration with Other NIPs
|
||||
|
||||
### NIP-01 (Basic Protocol)
|
||||
- AUTH messages integrated into standard WebSocket flow
|
||||
- Compatible with existing EVENT/REQ/CLOSE message handling
|
||||
|
||||
### NIP-11 (Relay Information)
|
||||
- NIP-42 advertised in `supported_nips` array
|
||||
- Authentication requirements reflected in relay metadata
|
||||
|
||||
### NIP-20 (Command Results)
|
||||
- OK responses include authentication-related error messages
|
||||
- NOTICE messages provide authentication status updates
|
||||
|
||||
## Future Extensions
|
||||
|
||||
### Potential Enhancements
|
||||
- Role-based authentication (admin, user, read-only)
|
||||
- Time-based access controls
|
||||
- Rate limiting based on authentication status
|
||||
- Integration with external authentication providers
|
||||
|
||||
### Configuration Extensions
|
||||
- Per-kind authentication requirements
|
||||
- Whitelist/blacklist integration
|
||||
- Custom challenge expiration times
|
||||
- Authentication logging and metrics
|
||||
460
docs/admin_api_plan.md
Normal file
460
docs/admin_api_plan.md
Normal file
@@ -0,0 +1,460 @@
|
||||
# C-Relay Administrator API Implementation Plan
|
||||
|
||||
## Problem Analysis
|
||||
|
||||
### Current Issues Identified:
|
||||
|
||||
1. **Schema Mismatch**: Storage system (config.c) vs Validation system (request_validator.c) use different column names and values
|
||||
2. **Missing API Endpoint**: No way to clear auth_rules table for testing
|
||||
3. **Configuration Gap**: Auth rules enforcement may not be properly enabled
|
||||
4. **Documentation Gap**: Admin API commands not documented
|
||||
|
||||
### Root Cause: Auth Rules Schema Inconsistency
|
||||
|
||||
**Current Schema (sql_schema.h lines 140-150):**
|
||||
```sql
|
||||
CREATE TABLE auth_rules (
|
||||
rule_type TEXT CHECK (rule_type IN ('whitelist', 'blacklist')),
|
||||
pattern_type TEXT CHECK (pattern_type IN ('pubkey', 'hash')),
|
||||
pattern_value TEXT,
|
||||
action TEXT CHECK (action IN ('allow', 'deny')),
|
||||
active INTEGER DEFAULT 1
|
||||
);
|
||||
```
|
||||
|
||||
**Storage Implementation (config.c):**
|
||||
- Stores: `rule_type='blacklist'`, `pattern_type='pubkey'`, `pattern_value='hex'`, `action='allow'`
|
||||
|
||||
**Validation Implementation (request_validator.c):**
|
||||
- Queries: `rule_type='pubkey_blacklist'`, `rule_target='hex'`, `operation='event'`, `enabled=1`
|
||||
|
||||
**MISMATCH**: Validator looks for non-existent columns and wrong rule_type values!
|
||||
|
||||
## Proposed Solution Architecture
|
||||
|
||||
### Phase 1: API Documentation & Standardization
|
||||
|
||||
#### Admin API Commands (via WebSocket with admin private key)
|
||||
|
||||
**Kind 23456: Unified Admin API (Ephemeral)**
|
||||
- Configuration management: Update relay settings, limits, authentication policies
|
||||
- Auth rules: Add/remove/query whitelist/blacklist rules
|
||||
- System commands: clear rules, status, cache management
|
||||
- **Unified Format**: All commands use NIP-44 encrypted content with `["p", "relay_pubkey"]` tags
|
||||
- **Command Types**:
|
||||
- Configuration: `["config_key", "config_value"]`
|
||||
- Auth rules: `["rule_type", "pattern_type", "pattern_value"]`
|
||||
- Queries: `["auth_query", "filter"]` or `["system_command", "command_name"]`
|
||||
- **Security**: All admin commands use NIP-44 encryption for privacy and security
|
||||
|
||||
#### Configuration Commands (using Kind 23456)
|
||||
|
||||
1. **Update Configuration**:
|
||||
```json
|
||||
{
|
||||
"kind": 23456,
|
||||
"content": "base64_nip44_encrypted_command_array",
|
||||
"tags": [["p", "relay_pubkey"]]
|
||||
}
|
||||
```
|
||||
*Encrypted content contains:* `["relay_description", "My Relay"]`
|
||||
|
||||
2. **Query System Status**:
|
||||
```json
|
||||
{
|
||||
"kind": 23456,
|
||||
"content": "base64_nip44_encrypted_command_array",
|
||||
"tags": [["p", "relay_pubkey"]]
|
||||
}
|
||||
```
|
||||
*Encrypted content contains:* `["system_command", "system_status"]`
|
||||
|
||||
#### Auth Rules and System Commands (using Kind 23456)
|
||||
|
||||
1. **Clear All Auth Rules**:
|
||||
```json
|
||||
{
|
||||
"kind": 23456,
|
||||
"content": "base64_nip44_encrypted_command_array",
|
||||
"tags": [["p", "relay_pubkey"]]
|
||||
}
|
||||
```
|
||||
*Encrypted content contains:* `["system_command", "clear_all_auth_rules"]`
|
||||
|
||||
2. **Query All Auth Rules**:
|
||||
```json
|
||||
{
|
||||
"kind": 23456,
|
||||
"content": "base64_nip44_encrypted_command_array",
|
||||
"tags": [["p", "relay_pubkey"]]
|
||||
}
|
||||
```
|
||||
*Encrypted content contains:* `["auth_query", "all"]`
|
||||
|
||||
3. **Add Blacklist Rule**:
|
||||
```json
|
||||
{
|
||||
"kind": 23456,
|
||||
"content": "base64_nip44_encrypted_command_array",
|
||||
"tags": [["p", "relay_pubkey"]]
|
||||
}
|
||||
```
|
||||
*Encrypted content contains:* `["blacklist", "pubkey", "deadbeef1234abcd..."]`
|
||||
|
||||
### Phase 2: Auth Rules Schema Alignment
|
||||
|
||||
#### Option A: Fix Validator to Match Schema (RECOMMENDED)
|
||||
|
||||
**Update request_validator.c:**
|
||||
```sql
|
||||
-- OLD (broken):
|
||||
WHERE rule_type = 'pubkey_blacklist' AND rule_target = ? AND operation = ? AND enabled = 1
|
||||
|
||||
-- NEW (correct):
|
||||
WHERE rule_type = 'blacklist' AND pattern_type = 'pubkey' AND pattern_value = ? AND active = 1
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Matches actual database schema
|
||||
- Simpler rule_type values ('blacklist' vs 'pubkey_blacklist')
|
||||
- Uses existing columns (pattern_value vs rule_target)
|
||||
- Consistent with storage implementation
|
||||
|
||||
#### Option B: Update Schema to Match Validator (NOT RECOMMENDED)
|
||||
|
||||
Would require changing schema, migration scripts, and storage logic.
|
||||
|
||||
### Phase 3: Implementation Priority
|
||||
|
||||
#### High Priority (Critical for blacklist functionality):
|
||||
1. Fix request_validator.c schema mismatch
|
||||
2. Ensure auth_required configuration is enabled
|
||||
3. Update tests to use unified ephemeral event kind (23456)
|
||||
4. Test blacklist enforcement
|
||||
|
||||
#### Medium Priority (Enhanced Admin Features):
|
||||
1. **Implement NIP-44 Encryption Support**:
|
||||
- Detect NIP-44 encrypted content for Kind 23456 events
|
||||
- Parse `encrypted_tags` field from content JSON
|
||||
- Decrypt using admin privkey and relay pubkey
|
||||
- Process decrypted tags as normal commands
|
||||
2. Add clear_all_auth_rules system command
|
||||
3. Add auth rule query functionality (both standard and encrypted modes)
|
||||
4. Add configuration discovery (list available config keys)
|
||||
5. Enhanced error reporting in admin API
|
||||
6. Conflict resolution (same pubkey in whitelist + blacklist)
|
||||
|
||||
#### Security Priority (NIP-44 Implementation):
|
||||
1. **Encryption Detection Logic**: Check for empty tags + encrypted_tags field
|
||||
2. **Key Pair Management**: Use admin private key + relay public key for NIP-44
|
||||
3. **Backward Compatibility**: Support both standard and encrypted modes
|
||||
4. **Error Handling**: Graceful fallback if decryption fails
|
||||
5. **Performance**: Cache decrypted results to avoid repeated decryption
|
||||
|
||||
#### Low Priority (Documentation & Polish):
|
||||
1. Complete README.md API documentation
|
||||
2. Example usage scripts
|
||||
3. Admin client tools
|
||||
|
||||
### Phase 4: Expected API Structure
|
||||
|
||||
#### README.md Documentation Format:
|
||||
|
||||
```markdown
|
||||
# C-Relay Administrator API
|
||||
|
||||
## Authentication
|
||||
All admin commands require signing with the admin private key generated during first startup.
|
||||
|
||||
## Unified Admin API (Kind 23456 - Ephemeral)
|
||||
Update relay configuration parameters or query available settings.
|
||||
|
||||
**Configuration Update Event:**
|
||||
```json
|
||||
{
|
||||
"kind": 23456,
|
||||
"content": "base64_nip44_encrypted_command_array",
|
||||
"tags": [["p", "relay_pubkey"]]
|
||||
}
|
||||
```
|
||||
*Encrypted content contains:* `["relay_description", "My Relay Description"]`
|
||||
|
||||
**Auth Rules Management:**
|
||||
|
||||
**Add Rule Event:**
|
||||
```json
|
||||
{
|
||||
"kind": 23456,
|
||||
"content": "{\"action\":\"add\",\"description\":\"Block malicious user\"}",
|
||||
"tags": [
|
||||
["blacklist", "pubkey", "deadbeef1234..."]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Remove Rule Event:**
|
||||
```json
|
||||
{
|
||||
"kind": 23456,
|
||||
"content": "{\"action\":\"remove\",\"description\":\"Unblock user\"}",
|
||||
"tags": [
|
||||
["blacklist", "pubkey", "deadbeef1234..."]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Query All Auth Rules:**
|
||||
```json
|
||||
{
|
||||
"kind": 23456,
|
||||
"content": "{\"query\":\"list_auth_rules\",\"description\":\"Get all rules\"}",
|
||||
"tags": [
|
||||
["auth_query", "all"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Query Whitelist Rules Only:**
|
||||
```json
|
||||
{
|
||||
"kind": 23456,
|
||||
"content": "{\"query\":\"list_auth_rules\",\"description\":\"Get whitelist\"}",
|
||||
"tags": [
|
||||
["auth_query", "whitelist"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Check Specific Pattern:**
|
||||
```json
|
||||
{
|
||||
"kind": 23456,
|
||||
"content": "{\"query\":\"check_pattern\",\"description\":\"Check if pattern exists\"}",
|
||||
"tags": [
|
||||
["auth_query", "pattern", "deadbeef1234..."]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## System Management (Kind 23456 - Ephemeral)
|
||||
System administration commands using the same kind as auth rules.
|
||||
|
||||
**Clear All Auth Rules:**
|
||||
```json
|
||||
{
|
||||
"kind": 23456,
|
||||
"content": "{\"action\":\"clear_all\",\"description\":\"Clear all auth rules\"}",
|
||||
"tags": [
|
||||
["system_command", "clear_all_auth_rules"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**System Status:**
|
||||
```json
|
||||
{
|
||||
"kind": 23456,
|
||||
"content": "{\"action\":\"system_status\",\"description\":\"Get system status\"}",
|
||||
"tags": [
|
||||
["system_command", "system_status"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Response Format
|
||||
All admin commands return JSON responses via WebSocket:
|
||||
|
||||
**Success Response:**
|
||||
```json
|
||||
["OK", "event_id", true, "success_message"]
|
||||
```
|
||||
|
||||
**Error Response:**
|
||||
```json
|
||||
["OK", "event_id", false, "error_message"]
|
||||
```
|
||||
|
||||
## Configuration Keys
|
||||
- `relay_description`: Relay description text
|
||||
- `relay_contact`: Contact information
|
||||
- `auth_enabled`: Enable authentication system
|
||||
- `max_connections`: Maximum concurrent connections
|
||||
- `pow_min_difficulty`: Minimum proof-of-work difficulty
|
||||
- ... (full list of config keys)
|
||||
|
||||
## Examples
|
||||
|
||||
### Enable Authentication & Add Blacklist
|
||||
```bash
|
||||
# 1. Enable auth system
|
||||
nak event -k 23456 --content "base64_nip44_encrypted_command" \
|
||||
-t "auth_enabled=true" \
|
||||
--sec $ADMIN_PRIVKEY | nak event ws://localhost:8888
|
||||
|
||||
# 2. Add user to blacklist
|
||||
nak event -k 23456 --content '{"action":"add","description":"Spam user"}' \
|
||||
-t "blacklist=pubkey;$SPAM_USER_PUBKEY" \
|
||||
--sec $ADMIN_PRIVKEY | nak event ws://localhost:8888
|
||||
|
||||
# 3. Query all auth rules
|
||||
nak event -k 23456 --content '{"query":"list_auth_rules","description":"Get all rules"}' \
|
||||
-t "auth_query=all" \
|
||||
--sec $ADMIN_PRIVKEY | nak event ws://localhost:8888
|
||||
|
||||
# 4. Clear all rules for testing
|
||||
nak event -k 23456 --content '{"action":"clear_all","description":"Clear all rules"}' \
|
||||
-t "system_command=clear_all_auth_rules" \
|
||||
--sec $ADMIN_PRIVKEY | nak event ws://localhost:8888
|
||||
```
|
||||
|
||||
## Expected Response Formats
|
||||
|
||||
### Configuration Query Response
|
||||
```json
|
||||
["EVENT", "subscription_id", {
|
||||
"kind": 23457,
|
||||
"content": "base64_nip44_encrypted_response",
|
||||
"tags": [["p", "admin_pubkey"]]
|
||||
}]
|
||||
```
|
||||
|
||||
### Current Config Response
|
||||
```json
|
||||
["EVENT", "subscription_id", {
|
||||
"kind": 23457,
|
||||
"content": "base64_nip44_encrypted_response",
|
||||
"tags": [["p", "admin_pubkey"]]
|
||||
}]
|
||||
```
|
||||
|
||||
### Auth Rules Query Response
|
||||
```json
|
||||
["EVENT", "subscription_id", {
|
||||
"kind": 23456,
|
||||
"content": "{\"auth_rules\": [{\"rule_type\": \"blacklist\", \"pattern_type\": \"pubkey\", \"pattern_value\": \"deadbeef...\"}, {\"rule_type\": \"whitelist\", \"pattern_type\": \"pubkey\", \"pattern_value\": \"cafebabe...\"}]}",
|
||||
"tags": [["response_type", "auth_rules_list"], ["query_type", "all"]]
|
||||
}]
|
||||
```
|
||||
|
||||
### Pattern Check Response
|
||||
```json
|
||||
["EVENT", "subscription_id", {
|
||||
"kind": 23456,
|
||||
"content": "{\"pattern_exists\": true, \"rule_type\": \"blacklist\", \"pattern_value\": \"deadbeef...\"}",
|
||||
"tags": [["response_type", "pattern_check"], ["pattern", "deadbeef..."]]
|
||||
}]
|
||||
```
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. **Document API** (this file) ✅
|
||||
2. **Update to ephemeral event kinds** ✅
|
||||
3. **Fix request_validator.c** schema mismatch
|
||||
4. **Update tests** to use unified Kind 23456
|
||||
5. **Add auth rule query functionality**
|
||||
6. **Add configuration discovery feature**
|
||||
7. **Test blacklist functionality**
|
||||
8. **Add remaining system commands**
|
||||
|
||||
## Testing Plan
|
||||
|
||||
1. Fix schema mismatch and test basic blacklist
|
||||
2. Add clear_auth_rules and test table cleanup
|
||||
3. Test whitelist/blacklist conflict scenarios
|
||||
4. Test all admin API commands end-to-end
|
||||
5. Update integration tests
|
||||
|
||||
This plan addresses the immediate blacklist issue while establishing a comprehensive admin API framework for future expansion.
|
||||
|
||||
## NIP-44 Encryption Implementation Details
|
||||
|
||||
### Server-Side Detection Logic
|
||||
```c
|
||||
// In admin event processing function
|
||||
bool is_encrypted_command(struct nostr_event *event) {
|
||||
// Check if Kind 23456 with NIP-44 encrypted content
|
||||
if (event->kind == 23456 &&
|
||||
event->tags_count == 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
cJSON *decrypt_admin_tags(struct nostr_event *event) {
|
||||
cJSON *content_json = cJSON_Parse(event->content);
|
||||
if (!content_json) return NULL;
|
||||
|
||||
cJSON *encrypted_tags = cJSON_GetObjectItem(content_json, "encrypted_tags");
|
||||
if (!encrypted_tags) {
|
||||
cJSON_Delete(content_json);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Decrypt using NIP-44 with admin pubkey and relay privkey
|
||||
char *decrypted = nip44_decrypt(
|
||||
cJSON_GetStringValue(encrypted_tags),
|
||||
admin_pubkey, // Shared secret with admin
|
||||
relay_private_key // Our private key
|
||||
);
|
||||
|
||||
cJSON *decrypted_tags = cJSON_Parse(decrypted);
|
||||
free(decrypted);
|
||||
cJSON_Delete(content_json);
|
||||
|
||||
return decrypted_tags; // Returns tag array: [["key1", "val1"], ["key2", "val2"]]
|
||||
}
|
||||
```
|
||||
|
||||
### Admin Event Processing Flow
|
||||
1. **Receive Event**: Kind 23456 with admin signature
|
||||
2. **Check Mode**: Empty tags = encrypted, populated tags = standard
|
||||
3. **Decrypt if Needed**: Extract and decrypt `encrypted_tags` from content
|
||||
4. **Process Commands**: Use decrypted/standard tags for command processing
|
||||
5. **Execute**: Same logic for both modes after tag extraction
|
||||
6. **Respond**: Standard response format (optionally encrypt response)
|
||||
|
||||
### Security Benefits
|
||||
- **Command Privacy**: Admin operations invisible in event tags
|
||||
- **Replay Protection**: NIP-44 includes timestamp/randomness
|
||||
- **Key Management**: Uses existing admin/relay key pair
|
||||
- **Backward Compatible**: Standard mode still works
|
||||
- **Performance**: Only decrypt when needed (empty tags detection)
|
||||
|
||||
### NIP-44 Library Integration
|
||||
The relay will need to integrate a NIP-44 encryption/decryption library:
|
||||
|
||||
```c
|
||||
// Required NIP-44 functions
|
||||
char* nip44_encrypt(const char* plaintext, const char* sender_privkey, const char* recipient_pubkey);
|
||||
char* nip44_decrypt(const char* ciphertext, const char* recipient_privkey, const char* sender_pubkey);
|
||||
```
|
||||
|
||||
### Implementation Priority (Updated)
|
||||
|
||||
#### Phase 1: Core Infrastructure (Complete)
|
||||
- [x] Event-based admin authentication system
|
||||
- [x] Kind 23456 (Unified Admin API) processing
|
||||
- [x] Basic configuration parameter updates
|
||||
- [x] Auth rule add/remove/clear functionality
|
||||
- [x] Updated to ephemeral event kinds
|
||||
- [x] Designed NIP-44 encryption support
|
||||
|
||||
#### Phase 2: NIP-44 Encryption Support (Next Priority)
|
||||
- [ ] **Add NIP-44 library dependency** to project
|
||||
- [ ] **Implement encryption detection logic** (`is_encrypted_command()`)
|
||||
- [ ] **Add decrypt_admin_tags() function** with NIP-44 support
|
||||
- [ ] **Update admin command processing** to handle both modes
|
||||
- [ ] **Test encrypted admin commands** end-to-end
|
||||
|
||||
#### Phase 3: Enhanced Features
|
||||
- [ ] **Auth rule query functionality** (both standard and encrypted modes)
|
||||
- [ ] **Configuration discovery API** (list available config keys)
|
||||
- [ ] **Enhanced error messages** with encryption status
|
||||
- [ ] **Performance optimization** (caching, async decrypt)
|
||||
|
||||
#### Phase 4: Schema Fixes (Critical)
|
||||
- [ ] **Fix request_validator.c** schema mismatch
|
||||
- [ ] **Enable blacklist enforcement** with encrypted commands
|
||||
- [ ] **Update tests** to use both standard and encrypted modes
|
||||
|
||||
This enhanced admin API provides enterprise-grade security while maintaining ease of use for basic operations.
|
||||
19
get_settings.sh
Executable file
19
get_settings.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
# get_settings.sh - Query relay configuration events using nak
|
||||
# Uses admin test key to query kind 33334 configuration events
|
||||
|
||||
# Test key configuration
|
||||
ADMIN_PRIVATE_KEY="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
ADMIN_PUBLIC_KEY="6a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3"
|
||||
RELAY_PUBLIC_KEY="4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"
|
||||
RELAY_URL="ws://localhost:8888"
|
||||
|
||||
echo "Querying configuration events (kind 33334) from relay at $RELAY_URL"
|
||||
echo "Using admin public key: $ADMIN_PUBLIC_KEY"
|
||||
echo "Looking for relay config: $RELAY_PUBLIC_KEY"
|
||||
echo ""
|
||||
|
||||
# Query for kind 33334 configuration events
|
||||
# These events contain the relay configuration with d-tag matching the relay pubkey
|
||||
nak req -k 33334 "$RELAY_URL" | jq .
|
||||
@@ -8,13 +8,69 @@ echo "=== C Nostr Relay Build and Restart Script ==="
|
||||
# Parse command line arguments
|
||||
PRESERVE_DATABASE=false
|
||||
HELP=false
|
||||
USE_TEST_KEYS=false
|
||||
ADMIN_KEY=""
|
||||
RELAY_KEY=""
|
||||
PORT_OVERRIDE=""
|
||||
|
||||
# Key validation function
|
||||
validate_hex_key() {
|
||||
local key="$1"
|
||||
local key_type="$2"
|
||||
|
||||
if [ ${#key} -ne 64 ]; then
|
||||
echo "ERROR: $key_type key must be exactly 64 characters"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! [[ "$key" =~ ^[0-9a-fA-F]{64}$ ]]; then
|
||||
echo "ERROR: $key_type key must contain only hex characters (0-9, a-f, A-F)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--preserve-database|-p)
|
||||
-a|--admin-key)
|
||||
if [ -z "$2" ]; then
|
||||
echo "ERROR: Admin key option requires a value"
|
||||
HELP=true
|
||||
shift
|
||||
else
|
||||
ADMIN_KEY="$2"
|
||||
shift 2
|
||||
fi
|
||||
;;
|
||||
-r|--relay-key)
|
||||
if [ -z "$2" ]; then
|
||||
echo "ERROR: Relay key option requires a value"
|
||||
HELP=true
|
||||
shift
|
||||
else
|
||||
RELAY_KEY="$2"
|
||||
shift 2
|
||||
fi
|
||||
;;
|
||||
-p|--port)
|
||||
if [ -z "$2" ]; then
|
||||
echo "ERROR: Port option requires a value"
|
||||
HELP=true
|
||||
shift
|
||||
else
|
||||
PORT_OVERRIDE="$2"
|
||||
shift 2
|
||||
fi
|
||||
;;
|
||||
--preserve-database)
|
||||
PRESERVE_DATABASE=true
|
||||
shift
|
||||
;;
|
||||
--test-keys|-t)
|
||||
USE_TEST_KEYS=true
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
HELP=true
|
||||
shift
|
||||
@@ -27,13 +83,38 @@ while [[ $# -gt 0 ]]; do
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate custom keys if provided
|
||||
if [ -n "$ADMIN_KEY" ]; then
|
||||
if ! validate_hex_key "$ADMIN_KEY" "Admin"; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$RELAY_KEY" ]; then
|
||||
if ! validate_hex_key "$RELAY_KEY" "Relay"; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Validate port if provided
|
||||
if [ -n "$PORT_OVERRIDE" ]; then
|
||||
if ! [[ "$PORT_OVERRIDE" =~ ^[0-9]+$ ]] || [ "$PORT_OVERRIDE" -lt 1 ] || [ "$PORT_OVERRIDE" -gt 65535 ]; then
|
||||
echo "ERROR: Port must be a number between 1 and 65535"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Show help
|
||||
if [ "$HELP" = true ]; then
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --preserve-database, -p Keep existing database files (don't delete for fresh start)"
|
||||
echo " --help, -h Show this help message"
|
||||
echo " -a, --admin-key <hex> 64-character hex admin private key"
|
||||
echo " -r, --relay-key <hex> 64-character hex relay private key"
|
||||
echo " -p, --port <port> Custom port override (default: 8888)"
|
||||
echo " --preserve-database Keep existing database files (don't delete for fresh start)"
|
||||
echo " --test-keys, -t Use deterministic test keys for development (admin: all 'a's, relay: all '1's)"
|
||||
echo " --help, -h Show this help message"
|
||||
echo ""
|
||||
echo "Event-Based Configuration:"
|
||||
echo " This relay now uses event-based configuration stored directly in the database."
|
||||
@@ -41,9 +122,14 @@ if [ "$HELP" = true ]; then
|
||||
echo " Database file: <relay_pubkey>.db (created automatically)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 # Fresh start with new keys (default)"
|
||||
echo " $0 -p # Preserve existing database and keys"
|
||||
echo " $0 # Fresh start with random keys"
|
||||
echo " $0 -a <admin-hex> -r <relay-hex> # Use custom keys"
|
||||
echo " $0 -a <admin-hex> -p 9000 # Custom admin key on port 9000"
|
||||
echo " $0 --preserve-database # Preserve existing database and keys"
|
||||
echo " $0 --test-keys # Use test keys for consistent development"
|
||||
echo " $0 -t --preserve-database # Use test keys and preserve database"
|
||||
echo ""
|
||||
echo "Key Format: Keys must be exactly 64 hexadecimal characters (0-9, a-f, A-F)"
|
||||
echo "Default behavior: Deletes existing database files to start fresh with new keys"
|
||||
echo " for development purposes"
|
||||
exit 0
|
||||
@@ -112,25 +198,54 @@ fi
|
||||
|
||||
echo "Build successful. Proceeding with relay restart..."
|
||||
|
||||
# Kill existing relay if running
|
||||
# Kill existing relay if running - start aggressive immediately
|
||||
echo "Stopping any existing relay servers..."
|
||||
pkill -f "c_relay_" 2>/dev/null
|
||||
sleep 2 # Give time for shutdown
|
||||
|
||||
# Check if port is still bound
|
||||
if lsof -i :8888 >/dev/null 2>&1; then
|
||||
echo "Port 8888 still in use, force killing..."
|
||||
fuser -k 8888/tcp 2>/dev/null || echo "No process on port 8888"
|
||||
# Get all relay processes and kill them immediately with -9
|
||||
RELAY_PIDS=$(pgrep -f "c_relay_" || echo "")
|
||||
if [ -n "$RELAY_PIDS" ]; then
|
||||
echo "Force killing relay processes immediately: $RELAY_PIDS"
|
||||
kill -9 $RELAY_PIDS 2>/dev/null
|
||||
else
|
||||
echo "No existing relay processes found"
|
||||
fi
|
||||
|
||||
# Get any remaining processes
|
||||
REMAINING_PIDS=$(pgrep -f "c_relay_" || echo "")
|
||||
if [ -n "$REMAINING_PIDS" ]; then
|
||||
echo "Force killing remaining processes: $REMAINING_PIDS"
|
||||
kill -9 $REMAINING_PIDS 2>/dev/null
|
||||
# Ensure port 8888 is completely free with retry loop
|
||||
echo "Ensuring port 8888 is available..."
|
||||
for attempt in {1..15}; do
|
||||
if ! lsof -i :8888 >/dev/null 2>&1; then
|
||||
echo "Port 8888 is now free"
|
||||
break
|
||||
fi
|
||||
|
||||
echo "Attempt $attempt: Port 8888 still in use, force killing..."
|
||||
# Kill anything using port 8888
|
||||
fuser -k 8888/tcp 2>/dev/null || true
|
||||
|
||||
# Double-check for any remaining relay processes
|
||||
REMAINING_PIDS=$(pgrep -f "c_relay_" || echo "")
|
||||
if [ -n "$REMAINING_PIDS" ]; then
|
||||
echo "Killing remaining relay processes: $REMAINING_PIDS"
|
||||
kill -9 $REMAINING_PIDS 2>/dev/null || true
|
||||
fi
|
||||
|
||||
sleep 2
|
||||
|
||||
if [ $attempt -eq 15 ]; then
|
||||
echo "ERROR: Could not free port 8888 after 15 attempts"
|
||||
echo "Current processes using port:"
|
||||
lsof -i :8888 2>/dev/null || echo "No process details available"
|
||||
echo "You may need to manually kill processes or reboot"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Final safety check - ensure no relay processes remain
|
||||
FINAL_PIDS=$(pgrep -f "c_relay_" || echo "")
|
||||
if [ -n "$FINAL_PIDS" ]; then
|
||||
echo "Final cleanup: killing processes $FINAL_PIDS"
|
||||
kill -9 $FINAL_PIDS 2>/dev/null || true
|
||||
sleep 1
|
||||
else
|
||||
echo "No existing relay found"
|
||||
fi
|
||||
|
||||
# Clean up PID file
|
||||
@@ -144,10 +259,38 @@ echo "Database will be initialized automatically on startup if needed"
|
||||
echo "Starting relay server..."
|
||||
echo "Debug: Current processes: $(ps aux | grep 'c_relay_' | grep -v grep || echo 'None')"
|
||||
|
||||
# Build command line arguments for relay binary
|
||||
RELAY_ARGS=""
|
||||
|
||||
if [ -n "$ADMIN_KEY" ]; then
|
||||
RELAY_ARGS="$RELAY_ARGS -a $ADMIN_KEY"
|
||||
echo "Using custom admin key: ${ADMIN_KEY:0:16}..."
|
||||
fi
|
||||
|
||||
if [ -n "$RELAY_KEY" ]; then
|
||||
RELAY_ARGS="$RELAY_ARGS -r $RELAY_KEY"
|
||||
echo "Using custom relay key: ${RELAY_KEY:0:16}..."
|
||||
fi
|
||||
|
||||
if [ -n "$PORT_OVERRIDE" ]; then
|
||||
RELAY_ARGS="$RELAY_ARGS -p $PORT_OVERRIDE"
|
||||
echo "Using custom port: $PORT_OVERRIDE"
|
||||
fi
|
||||
|
||||
# Change to build directory before starting relay so database files are created there
|
||||
cd build
|
||||
# Start relay in background and capture its PID (no command line arguments needed)
|
||||
./$(basename $BINARY_PATH) > ../relay.log 2>&1 &
|
||||
# Start relay in background and capture its PID
|
||||
if [ "$USE_TEST_KEYS" = true ]; then
|
||||
echo "Using deterministic test keys for development..."
|
||||
./$(basename $BINARY_PATH) -a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -r 1111111111111111111111111111111111111111111111111111111111111111 --strict-port > ../relay.log 2>&1 &
|
||||
elif [ -n "$RELAY_ARGS" ]; then
|
||||
echo "Starting relay with custom configuration..."
|
||||
./$(basename $BINARY_PATH) $RELAY_ARGS --strict-port > ../relay.log 2>&1 &
|
||||
else
|
||||
# No command line arguments needed for random key generation
|
||||
echo "Starting relay with random key generation..."
|
||||
./$(basename $BINARY_PATH) --strict-port > ../relay.log 2>&1 &
|
||||
fi
|
||||
RELAY_PID=$!
|
||||
# Change back to original directory
|
||||
cd ..
|
||||
@@ -161,7 +304,34 @@ sleep 3
|
||||
if ps -p "$RELAY_PID" >/dev/null 2>&1; then
|
||||
echo "Relay started successfully!"
|
||||
echo "PID: $RELAY_PID"
|
||||
echo "WebSocket endpoint: ws://127.0.0.1:8888"
|
||||
|
||||
# Wait for relay to fully initialize and detect the actual port it's using
|
||||
sleep 2
|
||||
|
||||
# Extract actual port from relay logs
|
||||
ACTUAL_PORT=""
|
||||
if [ -f relay.log ]; then
|
||||
# Look for the success message with actual port
|
||||
ACTUAL_PORT=$(grep "WebSocket relay started on ws://127.0.0.1:" relay.log 2>/dev/null | tail -1 | sed -n 's/.*ws:\/\/127\.0\.0\.1:\([0-9]*\).*/\1/p')
|
||||
|
||||
# If we couldn't find the port in logs, try to detect from netstat
|
||||
if [ -z "$ACTUAL_PORT" ]; then
|
||||
ACTUAL_PORT=$(netstat -tln 2>/dev/null | grep -E ":888[0-9]" | head -1 | sed -n 's/.*:\([0-9]*\).*/\1/p')
|
||||
fi
|
||||
fi
|
||||
|
||||
# Display the actual endpoint
|
||||
if [ -n "$ACTUAL_PORT" ]; then
|
||||
if [ "$ACTUAL_PORT" = "8888" ]; then
|
||||
echo "WebSocket endpoint: ws://127.0.0.1:$ACTUAL_PORT"
|
||||
else
|
||||
echo "WebSocket endpoint: ws://127.0.0.1:$ACTUAL_PORT (fell back from port 8888)"
|
||||
fi
|
||||
else
|
||||
echo "WebSocket endpoint: ws://127.0.0.1:8888 (port detection failed - check logs)"
|
||||
fi
|
||||
|
||||
echo "HTTP endpoint: http://127.0.0.1:${ACTUAL_PORT:-8888}"
|
||||
echo "Log file: relay.log"
|
||||
echo ""
|
||||
|
||||
|
||||
455
nip11_relay_connection_implementation_plan.md
Normal file
455
nip11_relay_connection_implementation_plan.md
Normal file
@@ -0,0 +1,455 @@
|
||||
# NIP-11 Relay Connection Implementation Plan
|
||||
|
||||
## Overview
|
||||
Implement NIP-11 relay information fetching in the web admin interface to replace hardcoded relay pubkey and provide proper relay connection flow.
|
||||
|
||||
## Current Issues
|
||||
1. **Hardcoded Relay Pubkey**: `getRelayPubkey()` returns hardcoded value `'4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa'`
|
||||
2. **Relay URL in Debug Section**: Currently in "DEBUG - TEST FETCH WITHOUT LOGIN" section (lines 336-385)
|
||||
3. **No Relay Verification**: Users can attempt admin operations without verifying relay identity
|
||||
4. **Missing NIP-11 Support**: No fetching of relay information document
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### 1. New Relay Connection Section (HTML Structure)
|
||||
|
||||
Add after User Info section (around line 332):
|
||||
|
||||
```html
|
||||
<!-- Relay Connection Section -->
|
||||
<div class="section">
|
||||
<h2>RELAY CONNECTION</h2>
|
||||
<div class="input-group">
|
||||
<label for="relay-url-input">Relay URL:</label>
|
||||
<input type="text" id="relay-url-input" value="ws://localhost:8888" placeholder="ws://localhost:8888 or wss://relay.example.com">
|
||||
</div>
|
||||
<div class="inline-buttons">
|
||||
<button type="button" id="connect-relay-btn">CONNECT TO RELAY</button>
|
||||
<button type="button" id="disconnect-relay-btn" style="display: none;">DISCONNECT</button>
|
||||
</div>
|
||||
<div class="status disconnected" id="relay-connection-status">NOT CONNECTED</div>
|
||||
|
||||
<!-- Relay Information Display -->
|
||||
<div id="relay-info-display" class="hidden">
|
||||
<h3>Relay Information</h3>
|
||||
<div class="user-info">
|
||||
<div><strong>Name:</strong> <span id="relay-name">-</span></div>
|
||||
<div><strong>Description:</strong> <span id="relay-description">-</span></div>
|
||||
<div><strong>Public Key:</strong>
|
||||
<div class="user-pubkey" id="relay-pubkey-display">-</div>
|
||||
</div>
|
||||
<div><strong>Software:</strong> <span id="relay-software">-</span></div>
|
||||
<div><strong>Version:</strong> <span id="relay-version">-</span></div>
|
||||
<div><strong>Contact:</strong> <span id="relay-contact">-</span></div>
|
||||
<div><strong>Supported NIPs:</strong> <span id="relay-nips">-</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 2. JavaScript Implementation
|
||||
|
||||
#### Global State Variables
|
||||
Add to global state section (around line 535):
|
||||
|
||||
```javascript
|
||||
// Relay connection state
|
||||
let relayInfo = null;
|
||||
let isRelayConnected = false;
|
||||
let relayWebSocket = null;
|
||||
```
|
||||
|
||||
#### NIP-11 Fetching Function
|
||||
Add new function:
|
||||
|
||||
```javascript
|
||||
// Fetch relay information using NIP-11
|
||||
async function fetchRelayInfo(relayUrl) {
|
||||
try {
|
||||
console.log('=== FETCHING RELAY INFO VIA NIP-11 ===');
|
||||
console.log('Relay URL:', relayUrl);
|
||||
|
||||
// Convert WebSocket URL to HTTP URL for NIP-11
|
||||
let httpUrl = relayUrl;
|
||||
if (relayUrl.startsWith('ws://')) {
|
||||
httpUrl = relayUrl.replace('ws://', 'http://');
|
||||
} else if (relayUrl.startsWith('wss://')) {
|
||||
httpUrl = relayUrl.replace('wss://', 'https://');
|
||||
}
|
||||
|
||||
console.log('HTTP URL for NIP-11:', httpUrl);
|
||||
|
||||
// Fetch relay information document
|
||||
const response = await fetch(httpUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/nostr+json'
|
||||
},
|
||||
// Add timeout
|
||||
signal: AbortSignal.timeout(10000) // 10 second timeout
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (!contentType || !contentType.includes('application/json')) {
|
||||
throw new Error(`Invalid content type: ${contentType}. Expected application/json or application/nostr+json`);
|
||||
}
|
||||
|
||||
const relayInfoData = await response.json();
|
||||
console.log('Fetched relay info:', relayInfoData);
|
||||
|
||||
// Validate required fields
|
||||
if (!relayInfoData.pubkey) {
|
||||
throw new Error('Relay information missing required pubkey field');
|
||||
}
|
||||
|
||||
// Validate pubkey format (64 hex characters)
|
||||
if (!/^[0-9a-fA-F]{64}$/.test(relayInfoData.pubkey)) {
|
||||
throw new Error(`Invalid relay pubkey format: ${relayInfoData.pubkey}`);
|
||||
}
|
||||
|
||||
return relayInfoData;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch relay info:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Relay Connection Function
|
||||
Add new function:
|
||||
|
||||
```javascript
|
||||
// Connect to relay and fetch information
|
||||
async function connectToRelay() {
|
||||
try {
|
||||
const relayUrlInput = document.getElementById('relay-url-input');
|
||||
const connectBtn = document.getElementById('connect-relay-btn');
|
||||
const disconnectBtn = document.getElementById('disconnect-relay-btn');
|
||||
const statusDiv = document.getElementById('relay-connection-status');
|
||||
const infoDisplay = document.getElementById('relay-info-display');
|
||||
|
||||
const url = relayUrlInput.value.trim();
|
||||
if (!url) {
|
||||
throw new Error('Please enter a relay URL');
|
||||
}
|
||||
|
||||
// Update UI to show connecting state
|
||||
connectBtn.disabled = true;
|
||||
statusDiv.textContent = 'CONNECTING...';
|
||||
statusDiv.className = 'status connected';
|
||||
|
||||
console.log('Connecting to relay:', url);
|
||||
|
||||
// Fetch relay information via NIP-11
|
||||
console.log('Fetching relay information...');
|
||||
const fetchedRelayInfo = await fetchRelayInfo(url);
|
||||
|
||||
// Test WebSocket connection
|
||||
console.log('Testing WebSocket connection...');
|
||||
await testWebSocketConnection(url);
|
||||
|
||||
// Store relay information
|
||||
relayInfo = fetchedRelayInfo;
|
||||
isRelayConnected = true;
|
||||
|
||||
// Update UI with relay information
|
||||
displayRelayInfo(relayInfo);
|
||||
|
||||
// Update connection status
|
||||
statusDiv.textContent = 'CONNECTED';
|
||||
statusDiv.className = 'status connected';
|
||||
|
||||
// Update button states
|
||||
connectBtn.style.display = 'none';
|
||||
disconnectBtn.style.display = 'inline-block';
|
||||
relayUrlInput.disabled = true;
|
||||
|
||||
// Show relay info
|
||||
infoDisplay.classList.remove('hidden');
|
||||
|
||||
console.log('Successfully connected to relay:', relayInfo.name || url);
|
||||
log(`Connected to relay: ${relayInfo.name || url}`, 'INFO');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to connect to relay:', error);
|
||||
|
||||
// Reset UI state
|
||||
const connectBtn = document.getElementById('connect-relay-btn');
|
||||
const statusDiv = document.getElementById('relay-connection-status');
|
||||
|
||||
connectBtn.disabled = false;
|
||||
statusDiv.textContent = `CONNECTION FAILED: ${error.message}`;
|
||||
statusDiv.className = 'status error';
|
||||
|
||||
// Clear any partial state
|
||||
relayInfo = null;
|
||||
isRelayConnected = false;
|
||||
|
||||
log(`Failed to connect to relay: ${error.message}`, 'ERROR');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### WebSocket Connection Test
|
||||
Add new function:
|
||||
|
||||
```javascript
|
||||
// Test WebSocket connection to relay
|
||||
async function testWebSocketConnection(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
ws.close();
|
||||
reject(new Error('WebSocket connection timeout'));
|
||||
}, 5000);
|
||||
|
||||
const ws = new WebSocket(url);
|
||||
|
||||
ws.onopen = () => {
|
||||
clearTimeout(timeout);
|
||||
console.log('WebSocket connection successful');
|
||||
ws.close();
|
||||
resolve();
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
clearTimeout(timeout);
|
||||
console.error('WebSocket connection failed:', error);
|
||||
reject(new Error('WebSocket connection failed'));
|
||||
};
|
||||
|
||||
ws.onclose = (event) => {
|
||||
if (event.code !== 1000) {
|
||||
clearTimeout(timeout);
|
||||
reject(new Error(`WebSocket closed with code ${event.code}: ${event.reason}`));
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### Display Relay Information
|
||||
Add new function:
|
||||
|
||||
```javascript
|
||||
// Display relay information in the UI
|
||||
function displayRelayInfo(info) {
|
||||
document.getElementById('relay-name').textContent = info.name || 'Unknown';
|
||||
document.getElementById('relay-description').textContent = info.description || 'No description';
|
||||
document.getElementById('relay-pubkey-display').textContent = info.pubkey || 'Unknown';
|
||||
document.getElementById('relay-software').textContent = info.software || 'Unknown';
|
||||
document.getElementById('relay-version').textContent = info.version || 'Unknown';
|
||||
document.getElementById('relay-contact').textContent = info.contact || 'No contact info';
|
||||
|
||||
// Format supported NIPs
|
||||
let nipsText = 'None specified';
|
||||
if (info.supported_nips && Array.isArray(info.supported_nips) && info.supported_nips.length > 0) {
|
||||
nipsText = info.supported_nips.map(nip => `NIP-${nip.toString().padStart(2, '0')}`).join(', ');
|
||||
}
|
||||
document.getElementById('relay-nips').textContent = nipsText;
|
||||
}
|
||||
```
|
||||
|
||||
#### Disconnect Function
|
||||
Add new function:
|
||||
|
||||
```javascript
|
||||
// Disconnect from relay
|
||||
function disconnectFromRelay() {
|
||||
console.log('Disconnecting from relay...');
|
||||
|
||||
// Clear relay state
|
||||
relayInfo = null;
|
||||
isRelayConnected = false;
|
||||
|
||||
// Close any existing connections
|
||||
if (relayPool) {
|
||||
const url = document.getElementById('relay-url-input').value.trim();
|
||||
if (url) {
|
||||
relayPool.close([url]);
|
||||
}
|
||||
relayPool = null;
|
||||
subscriptionId = null;
|
||||
}
|
||||
|
||||
// Reset UI
|
||||
const connectBtn = document.getElementById('connect-relay-btn');
|
||||
const disconnectBtn = document.getElementById('disconnect-relay-btn');
|
||||
const statusDiv = document.getElementById('relay-connection-status');
|
||||
const infoDisplay = document.getElementById('relay-info-display');
|
||||
const relayUrlInput = document.getElementById('relay-url-input');
|
||||
|
||||
connectBtn.style.display = 'inline-block';
|
||||
disconnectBtn.style.display = 'none';
|
||||
connectBtn.disabled = false;
|
||||
relayUrlInput.disabled = false;
|
||||
|
||||
statusDiv.textContent = 'NOT CONNECTED';
|
||||
statusDiv.className = 'status disconnected';
|
||||
|
||||
infoDisplay.classList.add('hidden');
|
||||
|
||||
// Reset configuration status
|
||||
updateConfigStatus(false);
|
||||
|
||||
log('Disconnected from relay', 'INFO');
|
||||
}
|
||||
```
|
||||
|
||||
#### Update getRelayPubkey Function
|
||||
Replace existing function (around line 3142):
|
||||
|
||||
```javascript
|
||||
// Helper function to get relay pubkey from connected relay info
|
||||
function getRelayPubkey() {
|
||||
if (relayInfo && relayInfo.pubkey) {
|
||||
return relayInfo.pubkey;
|
||||
}
|
||||
|
||||
// Fallback to hardcoded value if no relay connected (for testing)
|
||||
console.warn('No relay connected, using fallback pubkey');
|
||||
return '4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa';
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Event Handlers
|
||||
|
||||
Add event handlers in the DOMContentLoaded section:
|
||||
|
||||
```javascript
|
||||
// Relay connection event handlers
|
||||
const connectRelayBtn = document.getElementById('connect-relay-btn');
|
||||
const disconnectRelayBtn = document.getElementById('disconnect-relay-btn');
|
||||
|
||||
if (connectRelayBtn) {
|
||||
connectRelayBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
connectToRelay().catch(error => {
|
||||
console.error('Connect to relay failed:', error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (disconnectRelayBtn) {
|
||||
disconnectRelayBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
disconnectFromRelay();
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Update Existing Functions
|
||||
|
||||
#### Update fetchConfiguration Function
|
||||
Add relay connection check at the beginning:
|
||||
|
||||
```javascript
|
||||
async function fetchConfiguration() {
|
||||
try {
|
||||
console.log('=== FETCHING CONFIGURATION VIA ADMIN API ===');
|
||||
|
||||
// Check if relay is connected
|
||||
if (!isRelayConnected || !relayInfo) {
|
||||
throw new Error('Must be connected to relay first. Please connect to relay in the Relay Connection section.');
|
||||
}
|
||||
|
||||
// ... rest of existing function
|
||||
} catch (error) {
|
||||
// ... existing error handling
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Update subscribeToConfiguration Function
|
||||
Add relay connection check:
|
||||
|
||||
```javascript
|
||||
async function subscribeToConfiguration() {
|
||||
try {
|
||||
console.log('=== STARTING SIMPLEPOOL CONFIGURATION SUBSCRIPTION ===');
|
||||
|
||||
if (!isRelayConnected || !relayInfo) {
|
||||
console.error('Must be connected to relay first');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use the relay URL from the connection section instead of the debug section
|
||||
const url = document.getElementById('relay-url-input').value.trim();
|
||||
|
||||
// ... rest of existing function
|
||||
} catch (error) {
|
||||
// ... existing error handling
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Update UI Flow
|
||||
|
||||
#### Modify showMainInterface Function
|
||||
Update to show relay connection requirement:
|
||||
|
||||
```javascript
|
||||
function showMainInterface() {
|
||||
loginSection.classList.add('hidden');
|
||||
mainInterface.classList.remove('hidden');
|
||||
userPubkeyDisplay.textContent = userPubkey;
|
||||
|
||||
// Show message about relay connection requirement
|
||||
if (!isRelayConnected) {
|
||||
log('Please connect to a relay to access admin functions', 'INFO');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Remove/Update Debug Section
|
||||
|
||||
#### Option 1: Remove Debug Section Entirely
|
||||
Remove the "DEBUG - TEST FETCH WITHOUT LOGIN" section (lines 335-385) since relay URL is now in the proper connection section.
|
||||
|
||||
#### Option 2: Keep Debug Section for Testing
|
||||
Update the debug section to use the connected relay URL and add a note that it's for testing purposes.
|
||||
|
||||
### 7. Error Handling
|
||||
|
||||
Add comprehensive error handling for:
|
||||
- Network timeouts
|
||||
- Invalid relay URLs
|
||||
- Missing NIP-11 support
|
||||
- Invalid relay pubkey format
|
||||
- WebSocket connection failures
|
||||
- CORS issues
|
||||
|
||||
### 8. Security Considerations
|
||||
|
||||
- Validate relay pubkey format (64 hex characters)
|
||||
- Verify relay identity before admin operations
|
||||
- Handle CORS properly for NIP-11 requests
|
||||
- Sanitize relay information display
|
||||
- Warn users about connecting to untrusted relays
|
||||
|
||||
## Testing Plan
|
||||
|
||||
1. **NIP-11 Fetching**: Test with various relay URLs (localhost, remote relays)
|
||||
2. **Error Handling**: Test with invalid URLs, non-Nostr servers, network failures
|
||||
3. **WebSocket Connection**: Verify WebSocket connectivity after NIP-11 fetch
|
||||
4. **Admin API Integration**: Ensure admin commands use correct relay pubkey
|
||||
5. **UI Flow**: Test complete user journey from login → relay connection → admin operations
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Proper Relay Identification**: Uses actual relay pubkey instead of hardcoded value
|
||||
2. **Better UX**: Clear connection flow and relay information display
|
||||
3. **Protocol Compliance**: Implements NIP-11 standard for relay discovery
|
||||
4. **Security**: Verifies relay identity before admin operations
|
||||
5. **Flexibility**: Works with any NIP-11 compliant relay
|
||||
|
||||
## Migration Notes
|
||||
|
||||
- Existing users will need to connect to relay after this update
|
||||
- Debug section can be kept for development/testing purposes
|
||||
- All admin functions will require relay connection
|
||||
- Relay pubkey will be dynamically fetched instead of hardcoded
|
||||
3259
src/config.c
3259
src/config.c
File diff suppressed because it is too large
Load Diff
145
src/config.h
145
src/config.h
@@ -4,6 +4,10 @@
|
||||
#include <sqlite3.h>
|
||||
#include <cjson/cJSON.h>
|
||||
#include <time.h>
|
||||
#include <pthread.h>
|
||||
|
||||
// Forward declaration for WebSocket support
|
||||
struct lws;
|
||||
|
||||
// Configuration constants
|
||||
#define CONFIG_VALUE_MAX_LENGTH 1024
|
||||
@@ -23,23 +27,82 @@
|
||||
// Database path for event-based config
|
||||
extern char g_database_path[512];
|
||||
|
||||
// Configuration manager structure
|
||||
// Unified configuration cache structure (consolidates all caching systems)
|
||||
typedef struct {
|
||||
sqlite3* db;
|
||||
char relay_pubkey[65];
|
||||
// Critical keys (frequently accessed)
|
||||
char admin_pubkey[65];
|
||||
time_t last_config_check;
|
||||
char config_file_path[512]; // Temporary for compatibility
|
||||
} config_manager_t;
|
||||
char relay_pubkey[65];
|
||||
|
||||
// Auth config (from request_validator)
|
||||
int auth_required;
|
||||
long max_file_size;
|
||||
int admin_enabled;
|
||||
int nip42_mode;
|
||||
int nip42_challenge_timeout;
|
||||
int nip42_time_tolerance;
|
||||
|
||||
// Static buffer for config values (replaces static buffers in get_config_value functions)
|
||||
char temp_buffer[CONFIG_VALUE_MAX_LENGTH];
|
||||
|
||||
// NIP-11 relay information (migrated from g_relay_info in main.c)
|
||||
struct {
|
||||
char name[RELAY_NAME_MAX_LENGTH];
|
||||
char description[RELAY_DESCRIPTION_MAX_LENGTH];
|
||||
char banner[RELAY_URL_MAX_LENGTH];
|
||||
char icon[RELAY_URL_MAX_LENGTH];
|
||||
char pubkey[RELAY_PUBKEY_MAX_LENGTH];
|
||||
char contact[RELAY_CONTACT_MAX_LENGTH];
|
||||
char software[RELAY_URL_MAX_LENGTH];
|
||||
char version[64];
|
||||
char privacy_policy[RELAY_URL_MAX_LENGTH];
|
||||
char terms_of_service[RELAY_URL_MAX_LENGTH];
|
||||
cJSON* supported_nips;
|
||||
cJSON* limitation;
|
||||
cJSON* retention;
|
||||
cJSON* relay_countries;
|
||||
cJSON* language_tags;
|
||||
cJSON* tags;
|
||||
char posting_policy[RELAY_URL_MAX_LENGTH];
|
||||
cJSON* fees;
|
||||
char payments_url[RELAY_URL_MAX_LENGTH];
|
||||
} relay_info;
|
||||
|
||||
// NIP-13 PoW configuration (migrated from g_pow_config in main.c)
|
||||
struct {
|
||||
int enabled;
|
||||
int min_pow_difficulty;
|
||||
int validation_flags;
|
||||
int require_nonce_tag;
|
||||
int reject_lower_targets;
|
||||
int strict_format;
|
||||
int anti_spam_mode;
|
||||
} pow_config;
|
||||
|
||||
// NIP-40 Expiration configuration (migrated from g_expiration_config in main.c)
|
||||
struct {
|
||||
int enabled;
|
||||
int strict_mode;
|
||||
int filter_responses;
|
||||
int delete_expired;
|
||||
long grace_period;
|
||||
} expiration_config;
|
||||
|
||||
// Cache management
|
||||
time_t cache_expires;
|
||||
int cache_valid;
|
||||
pthread_mutex_t cache_lock;
|
||||
} unified_config_cache_t;
|
||||
|
||||
// Command line options structure for first-time startup
|
||||
typedef struct {
|
||||
int port_override; // -1 = not set, >0 = port value
|
||||
// Future CLI options can be added here
|
||||
char admin_privkey_override[65]; // Empty string = not set, 64-char hex = override
|
||||
char relay_privkey_override[65]; // Empty string = not set, 64-char hex = override
|
||||
int strict_port; // 0 = allow port increment, 1 = fail if exact port unavailable
|
||||
} cli_options_t;
|
||||
|
||||
// Global configuration manager
|
||||
extern config_manager_t g_config_manager;
|
||||
// Global unified configuration cache
|
||||
extern unified_config_cache_t g_unified_cache;
|
||||
|
||||
// Core configuration functions (temporary compatibility)
|
||||
int init_configuration_system(const char* config_dir_override, const char* config_file_override);
|
||||
@@ -84,4 +147,68 @@ int store_relay_private_key(const char* relay_privkey_hex);
|
||||
char* get_relay_private_key(void);
|
||||
const char* get_temp_relay_private_key(void); // For first-time startup only
|
||||
|
||||
// NIP-42 authentication configuration functions
|
||||
int parse_auth_required_kinds(const char* kinds_str, int* kinds_array, int max_kinds);
|
||||
int is_nip42_auth_required_for_kind(int event_kind);
|
||||
int is_nip42_auth_globally_required(void);
|
||||
|
||||
// ================================
|
||||
// NEW ADMIN API FUNCTIONS
|
||||
// ================================
|
||||
|
||||
// Config table management functions (config table created via embedded schema)
|
||||
const char* get_config_value_from_table(const char* key);
|
||||
int set_config_value_in_table(const char* key, const char* value, const char* data_type,
|
||||
const char* description, const char* category, int requires_restart);
|
||||
int update_config_in_table(const char* key, const char* value);
|
||||
int populate_default_config_values(void);
|
||||
int add_pubkeys_to_config_table(void);
|
||||
|
||||
// Admin event processing functions (updated with WebSocket support)
|
||||
int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size, struct lws* wsi);
|
||||
int process_admin_config_event(cJSON* event, char* error_message, size_t error_size);
|
||||
int process_admin_auth_event(cJSON* event, char* error_message, size_t error_size, struct lws* wsi);
|
||||
|
||||
// Unified Kind 23456 handler functions
|
||||
int handle_kind_23456_unified(cJSON* event, char* error_message, size_t error_size, struct lws* wsi);
|
||||
int handle_auth_query_unified(cJSON* event, const char* query_type, char* error_message, size_t error_size, struct lws* wsi);
|
||||
int handle_system_command_unified(cJSON* event, const char* command, char* error_message, size_t error_size, struct lws* wsi);
|
||||
int handle_auth_rule_modification_unified(cJSON* event, char* error_message, size_t error_size, struct lws* wsi);
|
||||
|
||||
// Admin response functions
|
||||
int send_admin_response_event(const cJSON* response_data, const char* recipient_pubkey, struct lws* wsi);
|
||||
cJSON* build_query_response(const char* query_type, cJSON* results_array, int total_count);
|
||||
|
||||
// Auth rules management functions
|
||||
int add_auth_rule_from_config(const char* rule_type, const char* pattern_type,
|
||||
const char* pattern_value, const char* action);
|
||||
int remove_auth_rule_from_config(const char* rule_type, const char* pattern_type,
|
||||
const char* pattern_value);
|
||||
|
||||
// Unified configuration cache management
|
||||
void force_config_cache_refresh(void);
|
||||
const char* get_admin_pubkey_cached(void);
|
||||
const char* get_relay_pubkey_cached(void);
|
||||
void invalidate_config_cache(void);
|
||||
int reload_config_from_table(void);
|
||||
|
||||
// Hybrid config access functions
|
||||
const char* get_config_value_hybrid(const char* key);
|
||||
int is_config_table_ready(void);
|
||||
|
||||
// Migration support functions
|
||||
int initialize_config_system_with_migration(void);
|
||||
int migrate_config_from_events_to_table(void);
|
||||
int populate_config_table_from_event(const cJSON* event);
|
||||
|
||||
// Startup configuration processing functions
|
||||
int process_startup_config_event(const cJSON* event);
|
||||
int process_startup_config_event_with_fallback(const cJSON* event);
|
||||
|
||||
// Dynamic event generation functions for WebSocket configuration fetching
|
||||
cJSON* generate_config_event_from_table(void);
|
||||
int req_filter_requests_config_events(const cJSON* filter);
|
||||
cJSON* generate_synthetic_config_event_for_subscription(const char* sub_id, const cJSON* filters);
|
||||
char* generate_config_event_json(void);
|
||||
|
||||
#endif /* CONFIG_H */
|
||||
@@ -8,8 +8,7 @@
|
||||
* Default Configuration Event Template
|
||||
*
|
||||
* This header contains the default configuration values for the C Nostr Relay.
|
||||
* These values are used to create the initial kind 33334 configuration event
|
||||
* during first-time startup.
|
||||
* These values are used to populate the config table during first-time startup.
|
||||
*
|
||||
* IMPORTANT: These values should never be accessed directly by other parts
|
||||
* of the program. They are only used during initial configuration event creation.
|
||||
@@ -23,6 +22,12 @@ static const struct {
|
||||
// Authentication
|
||||
{"auth_enabled", "false"},
|
||||
|
||||
// NIP-42 Authentication Settings
|
||||
{"nip42_auth_required_events", "false"},
|
||||
{"nip42_auth_required_subscriptions", "false"},
|
||||
{"nip42_auth_required_kinds", "4,14"}, // Default: DM kinds require auth
|
||||
{"nip42_challenge_expiration", "600"}, // 10 minutes
|
||||
|
||||
// Server Core Settings
|
||||
{"relay_port", "8888"},
|
||||
{"max_connections", "100"},
|
||||
|
||||
1483
src/main.c
1483
src/main.c
File diff suppressed because it is too large
Load Diff
1038
src/request_validator.c
Normal file
1038
src/request_validator.c
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,21 +1,21 @@
|
||||
/* Embedded SQL Schema for C Nostr Relay
|
||||
* Generated from db/schema.sql - Do not edit manually
|
||||
* Schema Version: 5
|
||||
* Schema Version: 7
|
||||
*/
|
||||
#ifndef SQL_SCHEMA_H
|
||||
#define SQL_SCHEMA_H
|
||||
|
||||
/* Schema version constant */
|
||||
#define EMBEDDED_SCHEMA_VERSION "5"
|
||||
#define EMBEDDED_SCHEMA_VERSION "7"
|
||||
|
||||
/* Embedded SQL schema as C string literal */
|
||||
static const char* const EMBEDDED_SCHEMA_SQL =
|
||||
"-- C Nostr Relay Database Schema\n\
|
||||
-- SQLite schema for storing Nostr events with JSON tags support\n\
|
||||
-- Event-based configuration system using kind 33334 Nostr events\n\
|
||||
-- Configuration system using config table\n\
|
||||
\n\
|
||||
-- Schema version tracking\n\
|
||||
PRAGMA user_version = 5;\n\
|
||||
PRAGMA user_version = 7;\n\
|
||||
\n\
|
||||
-- Enable foreign key support\n\
|
||||
PRAGMA foreign_keys = ON;\n\
|
||||
@@ -58,8 +58,8 @@ CREATE TABLE schema_info (\n\
|
||||
\n\
|
||||
-- Insert schema metadata\n\
|
||||
INSERT INTO schema_info (key, value) VALUES\n\
|
||||
('version', '5'),\n\
|
||||
('description', 'Event-based Nostr relay schema with secure relay private key storage'),\n\
|
||||
('version', '7'),\n\
|
||||
('description', 'Hybrid Nostr relay schema with event-based and table-based configuration'),\n\
|
||||
('created_at', strftime('%s', 'now'));\n\
|
||||
\n\
|
||||
-- Helper views for common queries\n\
|
||||
@@ -135,6 +135,79 @@ CREATE TABLE relay_seckey (\n\
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))\n\
|
||||
);\n\
|
||||
\n\
|
||||
-- Authentication Rules Table for NIP-42 and Policy Enforcement\n\
|
||||
-- Used by request_validator.c for unified validation\n\
|
||||
CREATE TABLE auth_rules (\n\
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,\n\
|
||||
rule_type TEXT NOT NULL CHECK (rule_type IN ('whitelist', 'blacklist', 'rate_limit', 'auth_required')),\n\
|
||||
pattern_type TEXT NOT NULL CHECK (pattern_type IN ('pubkey', 'kind', 'ip', 'global')),\n\
|
||||
pattern_value TEXT,\n\
|
||||
action TEXT NOT NULL CHECK (action IN ('allow', 'deny', 'require_auth', 'rate_limit')),\n\
|
||||
parameters TEXT, -- JSON parameters for rate limiting, etc.\n\
|
||||
active INTEGER NOT NULL DEFAULT 1,\n\
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n\
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))\n\
|
||||
);\n\
|
||||
\n\
|
||||
-- Indexes for auth_rules performance\n\
|
||||
CREATE INDEX idx_auth_rules_pattern ON auth_rules(pattern_type, pattern_value);\n\
|
||||
CREATE INDEX idx_auth_rules_type ON auth_rules(rule_type);\n\
|
||||
CREATE INDEX idx_auth_rules_active ON auth_rules(active);\n\
|
||||
\n\
|
||||
-- Configuration Table for Table-Based Config Management\n\
|
||||
-- Hybrid system supporting both event-based and table-based configuration\n\
|
||||
CREATE TABLE config (\n\
|
||||
key TEXT PRIMARY KEY,\n\
|
||||
value TEXT NOT NULL,\n\
|
||||
data_type TEXT NOT NULL CHECK (data_type IN ('string', 'integer', 'boolean', 'json')),\n\
|
||||
description TEXT,\n\
|
||||
category TEXT DEFAULT 'general',\n\
|
||||
requires_restart INTEGER DEFAULT 0,\n\
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n\
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))\n\
|
||||
);\n\
|
||||
\n\
|
||||
-- Indexes for config table performance\n\
|
||||
CREATE INDEX idx_config_category ON config(category);\n\
|
||||
CREATE INDEX idx_config_restart ON config(requires_restart);\n\
|
||||
CREATE INDEX idx_config_updated ON config(updated_at DESC);\n\
|
||||
\n\
|
||||
-- Trigger to update config timestamp on changes\n\
|
||||
CREATE TRIGGER update_config_timestamp\n\
|
||||
AFTER UPDATE ON config\n\
|
||||
FOR EACH ROW\n\
|
||||
BEGIN\n\
|
||||
UPDATE config SET updated_at = strftime('%s', 'now') WHERE key = NEW.key;\n\
|
||||
END;\n\
|
||||
\n\
|
||||
-- Insert default configuration values\n\
|
||||
INSERT INTO config (key, value, data_type, description, category, requires_restart) VALUES\n\
|
||||
('relay_description', 'A C Nostr Relay', 'string', 'Relay description', 'general', 0),\n\
|
||||
('relay_contact', '', 'string', 'Relay contact information', 'general', 0),\n\
|
||||
('relay_software', 'https://github.com/laanwj/c-relay', 'string', 'Relay software URL', 'general', 0),\n\
|
||||
('relay_version', '1.0.0', 'string', 'Relay version', 'general', 0),\n\
|
||||
('relay_port', '8888', 'integer', 'Relay port number', 'network', 1),\n\
|
||||
('max_connections', '1000', 'integer', 'Maximum concurrent connections', 'network', 1),\n\
|
||||
('auth_enabled', 'false', 'boolean', 'Enable NIP-42 authentication', 'auth', 0),\n\
|
||||
('nip42_auth_required_events', 'false', 'boolean', 'Require auth for event publishing', 'auth', 0),\n\
|
||||
('nip42_auth_required_subscriptions', 'false', 'boolean', 'Require auth for subscriptions', 'auth', 0),\n\
|
||||
('nip42_auth_required_kinds', '[]', 'json', 'Event kinds requiring authentication', 'auth', 0),\n\
|
||||
('nip42_challenge_expiration', '600', 'integer', 'Auth challenge expiration seconds', 'auth', 0),\n\
|
||||
('pow_min_difficulty', '0', 'integer', 'Minimum proof-of-work difficulty', 'validation', 0),\n\
|
||||
('pow_mode', 'optional', 'string', 'Proof-of-work mode', 'validation', 0),\n\
|
||||
('nip40_expiration_enabled', 'true', 'boolean', 'Enable event expiration', 'validation', 0),\n\
|
||||
('nip40_expiration_strict', 'false', 'boolean', 'Strict expiration mode', 'validation', 0),\n\
|
||||
('nip40_expiration_filter', 'true', 'boolean', 'Filter expired events in queries', 'validation', 0),\n\
|
||||
('nip40_expiration_grace_period', '60', 'integer', 'Expiration grace period seconds', 'validation', 0),\n\
|
||||
('max_subscriptions_per_client', '25', 'integer', 'Maximum subscriptions per client', 'limits', 0),\n\
|
||||
('max_total_subscriptions', '1000', 'integer', 'Maximum total subscriptions', 'limits', 0),\n\
|
||||
('max_filters_per_subscription', '10', 'integer', 'Maximum filters per subscription', 'limits', 0),\n\
|
||||
('max_event_tags', '2000', 'integer', 'Maximum tags per event', 'limits', 0),\n\
|
||||
('max_content_length', '100000', 'integer', 'Maximum event content length', 'limits', 0),\n\
|
||||
('max_message_length', '131072', 'integer', 'Maximum WebSocket message length', 'limits', 0),\n\
|
||||
('default_limit', '100', 'integer', 'Default query limit', 'limits', 0),\n\
|
||||
('max_limit', '5000', 'integer', 'Maximum query limit', 'limits', 0);\n\
|
||||
\n\
|
||||
-- Persistent Subscriptions Logging Tables (Phase 2)\n\
|
||||
-- Optional database logging for subscription analytics and debugging\n\
|
||||
\n\
|
||||
|
||||
@@ -300,75 +300,103 @@ test_expiration_filtering_in_subscriptions() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
print_info "Setting up test events for subscription filtering..."
|
||||
print_info "Setting up short-lived events for proper expiration filtering test..."
|
||||
|
||||
# First, create a few events with different expiration times
|
||||
local private_key="91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe"
|
||||
|
||||
# Event 1: No expiration (should be returned)
|
||||
# Event 1: No expiration (should always be returned)
|
||||
local event1=$(nak event --sec "$private_key" -c "Event without expiration for filtering test" --ts $(date +%s))
|
||||
|
||||
# Event 2: Future expiration (should be returned)
|
||||
local future_timestamp=$(($(date +%s) + 1800)) # 30 minutes from now
|
||||
local event2=$(create_event_with_expiration "Event with future expiration for filtering test" "$future_timestamp")
|
||||
|
||||
# Event 3: Past expiration (should NOT be returned if filtering is enabled)
|
||||
local past_timestamp=$(($(date +%s) - 3600)) # 1 hour ago
|
||||
local event3=$(create_event_with_expiration "Event with past expiration for filtering test" "$past_timestamp")
|
||||
# Event 3: SHORT-LIVED EVENT - expires in 3 seconds
|
||||
local short_expiry=$(($(date +%s) + 3)) # 3 seconds from now
|
||||
local event3=$(create_event_with_expiration "Short-lived event for filtering test" "$short_expiry")
|
||||
|
||||
print_info "Publishing test events..."
|
||||
print_info "Publishing test events (including one that expires in 3 seconds)..."
|
||||
|
||||
# Note: We expect event3 to be rejected on submission in strict mode,
|
||||
# so we'll create it with a slightly more recent expiration that might get through
|
||||
local recent_past=$(($(date +%s) - 600)) # 10 minutes ago (outside grace period)
|
||||
local event3_recent=$(create_event_with_expiration "Recently expired event for filtering test" "$recent_past")
|
||||
# Submit all events - they should all be accepted initially
|
||||
local response1=$(echo "[\"EVENT\",$event1]" | timeout 5s websocat "$RELAY_URL" 2>&1)
|
||||
local response2=$(echo "[\"EVENT\",$event2]" | timeout 5s websocat "$RELAY_URL" 2>&1)
|
||||
local response3=$(echo "[\"EVENT\",$event3]" | timeout 5s websocat "$RELAY_URL" 2>&1)
|
||||
|
||||
# Try to submit all events (some may be rejected)
|
||||
echo "[\"EVENT\",$event1]" | timeout 3s websocat "$RELAY_URL" >/dev/null 2>&1 || true
|
||||
echo "[\"EVENT\",$event2]" | timeout 3s websocat "$RELAY_URL" >/dev/null 2>&1 || true
|
||||
echo "[\"EVENT\",$event3_recent]" | timeout 3s websocat "$RELAY_URL" >/dev/null 2>&1 || true
|
||||
|
||||
sleep 2 # Let events settle
|
||||
|
||||
print_info "Testing subscription filtering..."
|
||||
|
||||
# Create subscription for recent events
|
||||
local req_message='["REQ","filter_test",{"kinds":[1],"limit":10}]'
|
||||
local response=$(echo -e "$req_message\n[\"CLOSE\",\"filter_test\"]" | timeout 5s websocat "$RELAY_URL" 2>/dev/null || echo "")
|
||||
|
||||
print_info "Subscription response:"
|
||||
echo "$response"
|
||||
print_info "Event submission responses:"
|
||||
echo "Event 1 (no expiry): $response1"
|
||||
echo "Event 2 (future expiry): $response2"
|
||||
echo "Event 3 (expires in 3s): $response3"
|
||||
echo ""
|
||||
|
||||
# Count events that contain our test content
|
||||
# Verify all events were accepted
|
||||
if [[ "$response1" != *"true"* ]] || [[ "$response2" != *"true"* ]] || [[ "$response3" != *"true"* ]]; then
|
||||
record_test_result "Expiration Filtering in Subscriptions" "FAIL" "Events not properly accepted during submission"
|
||||
return 1
|
||||
fi
|
||||
|
||||
print_success "✓ All events accepted during submission"
|
||||
|
||||
# Test 1: Query immediately - all events should be present
|
||||
print_info "Testing immediate subscription (before expiration)..."
|
||||
local req_message='["REQ","filter_immediate",{"kinds":[1],"limit":10}]'
|
||||
local immediate_response=$(echo -e "$req_message\n[\"CLOSE\",\"filter_immediate\"]" | timeout 5s websocat "$RELAY_URL" 2>/dev/null || echo "")
|
||||
|
||||
local immediate_count=0
|
||||
if echo "$immediate_response" | grep -q "Event without expiration for filtering test"; then
|
||||
immediate_count=$((immediate_count + 1))
|
||||
fi
|
||||
if echo "$immediate_response" | grep -q "Event with future expiration for filtering test"; then
|
||||
immediate_count=$((immediate_count + 1))
|
||||
fi
|
||||
if echo "$immediate_response" | grep -q "Short-lived event for filtering test"; then
|
||||
immediate_count=$((immediate_count + 1))
|
||||
fi
|
||||
|
||||
print_info "Immediate response found $immediate_count/3 events"
|
||||
|
||||
# Wait for the short-lived event to expire (5 seconds total wait)
|
||||
print_info "Waiting 5 seconds for short-lived event to expire..."
|
||||
sleep 5
|
||||
|
||||
# Test 2: Query after expiration - short-lived event should be filtered out
|
||||
print_info "Testing subscription after expiration (short-lived event should be filtered)..."
|
||||
req_message='["REQ","filter_after_expiry",{"kinds":[1],"limit":10}]'
|
||||
local expired_response=$(echo -e "$req_message\n[\"CLOSE\",\"filter_after_expiry\"]" | timeout 5s websocat "$RELAY_URL" 2>/dev/null || echo "")
|
||||
|
||||
print_info "Post-expiration subscription response:"
|
||||
echo "$expired_response"
|
||||
echo ""
|
||||
|
||||
# Count events in the expired response
|
||||
local no_exp_count=0
|
||||
local future_exp_count=0
|
||||
local past_exp_count=0
|
||||
local future_exp_count=0
|
||||
local expired_event_count=0
|
||||
|
||||
if echo "$response" | grep -q "Event without expiration for filtering test"; then
|
||||
if echo "$expired_response" | grep -q "Event without expiration for filtering test"; then
|
||||
no_exp_count=1
|
||||
print_success "✓ Event without expiration found in subscription results"
|
||||
print_success "✓ Event without expiration found in post-expiration results"
|
||||
fi
|
||||
|
||||
if echo "$response" | grep -q "Event with future expiration for filtering test"; then
|
||||
if echo "$expired_response" | grep -q "Event with future expiration for filtering test"; then
|
||||
future_exp_count=1
|
||||
print_success "✓ Event with future expiration found in subscription results"
|
||||
print_success "✓ Event with future expiration found in post-expiration results"
|
||||
fi
|
||||
|
||||
if echo "$response" | grep -q "Recently expired event for filtering test"; then
|
||||
past_exp_count=1
|
||||
print_warning "✗ Recently expired event found in subscription results (should be filtered)"
|
||||
if echo "$expired_response" | grep -q "Short-lived event for filtering test"; then
|
||||
expired_event_count=1
|
||||
print_error "✗ EXPIRED short-lived event found in subscription results (should be filtered!)"
|
||||
else
|
||||
print_success "✓ Recently expired event properly filtered from subscription results"
|
||||
print_success "✓ Expired short-lived event properly filtered from subscription results"
|
||||
fi
|
||||
|
||||
# Evaluate results
|
||||
local expected_events=$((no_exp_count + future_exp_count))
|
||||
if [ $expected_events -ge 1 ] && [ $past_exp_count -eq 0 ]; then
|
||||
local expected_active_events=$((no_exp_count + future_exp_count))
|
||||
if [ $expected_active_events -ge 2 ] && [ $expired_event_count -eq 0 ]; then
|
||||
record_test_result "Expiration Filtering in Subscriptions" "PASS" "Expired events properly filtered from subscriptions"
|
||||
return 0
|
||||
else
|
||||
record_test_result "Expiration Filtering in Subscriptions" "FAIL" "Expiration filtering not working properly in subscriptions"
|
||||
local details="Found $expected_active_events active events, $expired_event_count expired events (should be 0)"
|
||||
record_test_result "Expiration Filtering in Subscriptions" "FAIL" "Expiration filtering not working properly in subscriptions - $details"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
477
tests/42_nip_test.sh
Executable file
477
tests/42_nip_test.sh
Executable file
@@ -0,0 +1,477 @@
|
||||
#!/bin/bash
|
||||
|
||||
# NIP-42 Authentication Test Script
|
||||
# Tests the complete NIP-42 authentication flow for the C Nostr Relay
|
||||
|
||||
set -e
|
||||
|
||||
RELAY_URL="ws://localhost:8888"
|
||||
HTTP_URL="http://localhost:8888"
|
||||
TEST_DIR="$(dirname "$0")"
|
||||
LOG_FILE="${TEST_DIR}/nip42_test.log"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[31m'
|
||||
GREEN='\033[32m'
|
||||
YELLOW='\033[33m'
|
||||
BLUE='\033[34m'
|
||||
BOLD='\033[1m'
|
||||
RESET='\033[0m'
|
||||
|
||||
# Logging function
|
||||
log() {
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}${BOLD}[SUCCESS]${RESET} $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}${BOLD}[ERROR]${RESET} $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
log_info() {
|
||||
echo -e "${BLUE}${BOLD}[INFO]${RESET} $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}${BOLD}[WARNING]${RESET} $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
# Initialize test log
|
||||
echo "=== NIP-42 Authentication Test Started ===" > "$LOG_FILE"
|
||||
log "Starting NIP-42 authentication tests"
|
||||
|
||||
# Check if required tools are available
|
||||
check_dependencies() {
|
||||
log_info "Checking dependencies..."
|
||||
|
||||
if ! command -v nak &> /dev/null; then
|
||||
log_error "nak client not found. Please install: go install github.com/fiatjaf/nak@latest"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v jq &> /dev/null; then
|
||||
log_error "jq not found. Please install jq for JSON processing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v wscat &> /dev/null; then
|
||||
log_warning "wscat not found. Some manual WebSocket tests will be skipped"
|
||||
log_warning "Install with: npm install -g wscat"
|
||||
fi
|
||||
|
||||
log_success "Dependencies check complete"
|
||||
}
|
||||
|
||||
# Test 1: Check NIP-42 in supported NIPs
|
||||
test_nip42_support() {
|
||||
log_info "Test 1: Checking NIP-42 support in relay info"
|
||||
|
||||
local response
|
||||
response=$(curl -s -H "Accept: application/nostr+json" "$HTTP_URL")
|
||||
|
||||
if echo "$response" | jq -e '.supported_nips | contains([42])' > /dev/null; then
|
||||
log_success "NIP-42 is advertised in supported NIPs"
|
||||
log "Supported NIPs: $(echo "$response" | jq -r '.supported_nips | @csv')"
|
||||
return 0
|
||||
else
|
||||
log_error "NIP-42 not found in supported NIPs"
|
||||
log "Response: $response"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 2: Check if relay responds with AUTH challenge when auth is required
|
||||
test_auth_challenge_generation() {
|
||||
log_info "Test 2: Testing AUTH challenge generation"
|
||||
|
||||
# First, enable NIP-42 authentication for events using configuration
|
||||
local admin_privkey
|
||||
admin_privkey=$(grep "Admin Private Key:" relay.log 2>/dev/null | tail -1 | cut -d' ' -f4 || echo "")
|
||||
|
||||
if [[ -z "$admin_privkey" ]]; then
|
||||
log_warning "Could not extract admin private key from relay.log - using manual test approach"
|
||||
log_info "Manual test: Connect to relay and send an event without auth to trigger challenge"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Found admin private key, configuring NIP-42 authentication..."
|
||||
|
||||
# Create configuration event to enable NIP-42 auth for events
|
||||
local config_event
|
||||
# Get relay pubkey for d tag
|
||||
local relay_pubkey
|
||||
relay_pubkey=$(nak key --pub "$admin_privkey" 2>/dev/null || echo "")
|
||||
|
||||
if [[ -n "$relay_pubkey" ]]; then
|
||||
config_event=$(nak event -k 33334 --content "C Nostr Relay Configuration" \
|
||||
--tag "d,$relay_pubkey" \
|
||||
--tag "nip42_auth_required_events,1" \
|
||||
--tag "nip42_auth_required_subscriptions,0" \
|
||||
--sec "$admin_privkey" 2>/dev/null || echo "")
|
||||
else
|
||||
config_event=""
|
||||
fi
|
||||
|
||||
if [[ -n "$config_event" ]]; then
|
||||
log_info "Publishing configuration to enable NIP-42 auth for events..."
|
||||
echo "$config_event" | nak event "$RELAY_URL" 2>/dev/null || true
|
||||
sleep 2 # Allow time for configuration to be processed
|
||||
log_success "Configuration sent - NIP-42 auth should now be required for events"
|
||||
else
|
||||
log_warning "Failed to create configuration event - proceeding with manual test"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Test 3: Test authentication flow with nak
|
||||
test_nip42_auth_flow() {
|
||||
log_info "Test 3: Testing complete NIP-42 authentication flow"
|
||||
|
||||
# Generate test keypair
|
||||
local test_privkey test_pubkey
|
||||
test_privkey=$(nak key --gen 2>/dev/null || openssl rand -hex 32)
|
||||
test_pubkey=$(nak key --pub "$test_privkey" 2>/dev/null || echo "test_pubkey")
|
||||
|
||||
log_info "Generated test keypair: $test_pubkey"
|
||||
|
||||
# Try to publish an event (should trigger auth challenge)
|
||||
log_info "Attempting to publish event without authentication..."
|
||||
|
||||
local test_event
|
||||
test_event=$(nak event -k 1 --content "NIP-42 test event - should require auth" \
|
||||
--sec "$test_privkey" 2>/dev/null || echo "")
|
||||
|
||||
if [[ -n "$test_event" ]]; then
|
||||
log_info "Publishing test event to relay..."
|
||||
local result
|
||||
result=$(echo "$test_event" | timeout 10s nak event "$RELAY_URL" 2>&1 || true)
|
||||
|
||||
log "Event publish result: $result"
|
||||
|
||||
# Check if we got an auth challenge or notice
|
||||
if echo "$result" | grep -q "AUTH\|auth\|authentication"; then
|
||||
log_success "Relay requested authentication as expected"
|
||||
elif echo "$result" | grep -q "OK.*true"; then
|
||||
log_warning "Event was accepted without authentication (auth may be disabled)"
|
||||
else
|
||||
log_warning "Unexpected response: $result"
|
||||
fi
|
||||
else
|
||||
log_error "Failed to create test event"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Test 4: Test WebSocket AUTH message handling
|
||||
test_websocket_auth_messages() {
|
||||
log_info "Test 4: Testing WebSocket AUTH message handling"
|
||||
|
||||
if ! command -v wscat &> /dev/null; then
|
||||
log_warning "Skipping WebSocket tests - wscat not available"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Testing WebSocket connection and AUTH message..."
|
||||
|
||||
# Test WebSocket connection
|
||||
local ws_test_file="/tmp/nip42_ws_test.json"
|
||||
cat > "$ws_test_file" << 'EOF'
|
||||
["EVENT",{"kind":1,"content":"Test message for auth","tags":[],"created_at":1234567890,"pubkey":"0000000000000000000000000000000000000000000000000000000000000000","id":"0000000000000000000000000000000000000000000000000000000000000000","sig":"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}]
|
||||
EOF
|
||||
|
||||
log_info "Sending test message via WebSocket..."
|
||||
timeout 5s wscat -c "$RELAY_URL" < "$ws_test_file" > /tmp/ws_response.log 2>&1 || true
|
||||
|
||||
if [[ -f /tmp/ws_response.log ]]; then
|
||||
local ws_response
|
||||
ws_response=$(cat /tmp/ws_response.log)
|
||||
log "WebSocket response: $ws_response"
|
||||
|
||||
if echo "$ws_response" | grep -q "AUTH\|NOTICE.*auth"; then
|
||||
log_success "WebSocket AUTH challenge detected"
|
||||
else
|
||||
log_info "No AUTH challenge in WebSocket response"
|
||||
fi
|
||||
|
||||
rm -f /tmp/ws_response.log
|
||||
fi
|
||||
|
||||
rm -f "$ws_test_file"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Test 5: Configuration verification
|
||||
test_nip42_configuration() {
|
||||
log_info "Test 5: Testing NIP-42 configuration options"
|
||||
|
||||
# Check current configuration
|
||||
log_info "Retrieving current relay configuration..."
|
||||
|
||||
local config_events
|
||||
config_events=$(nak req -k 33334 "$RELAY_URL" 2>/dev/null | jq -s '.' || echo "[]")
|
||||
|
||||
if [[ "$config_events" != "[]" ]] && [[ -n "$config_events" ]]; then
|
||||
log_success "Retrieved configuration events from relay"
|
||||
|
||||
# Check for NIP-42 related configuration
|
||||
local nip42_config
|
||||
nip42_config=$(echo "$config_events" | jq -r '.[].tags[]? | select(.[0] | startswith("nip42")) | join("=")' 2>/dev/null || echo "")
|
||||
|
||||
if [[ -n "$nip42_config" ]]; then
|
||||
log_success "Found NIP-42 configuration:"
|
||||
echo "$nip42_config" | while read -r line; do
|
||||
log " $line"
|
||||
done
|
||||
else
|
||||
log_info "No specific NIP-42 configuration found (may use defaults)"
|
||||
fi
|
||||
else
|
||||
log_warning "Could not retrieve configuration events"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Test 6: Performance and stability test
|
||||
test_nip42_performance() {
|
||||
log_info "Test 6: Testing NIP-42 performance and stability"
|
||||
|
||||
local test_privkey test_pubkey
|
||||
test_privkey=$(nak key --gen 2>/dev/null || openssl rand -hex 32)
|
||||
test_pubkey=$(nak key --pub "$test_privkey" 2>/dev/null || echo "test_pubkey")
|
||||
|
||||
log_info "Testing multiple authentication attempts..."
|
||||
|
||||
local success_count=0
|
||||
local total_attempts=5
|
||||
|
||||
for i in $(seq 1 $total_attempts); do
|
||||
local test_event
|
||||
test_event=$(nak event -k 1 --content "Performance test event $i" \
|
||||
--sec "$test_privkey" 2>/dev/null || echo "")
|
||||
|
||||
if [[ -n "$test_event" ]]; then
|
||||
local start_time end_time duration
|
||||
start_time=$(date +%s.%N)
|
||||
|
||||
local result
|
||||
result=$(echo "$test_event" | timeout 5s nak event "$RELAY_URL" 2>&1 || echo "timeout")
|
||||
|
||||
end_time=$(date +%s.%N)
|
||||
duration=$(echo "$end_time - $start_time" | bc -l 2>/dev/null || echo "unknown")
|
||||
|
||||
log "Attempt $i: ${duration}s - $result"
|
||||
|
||||
if echo "$result" | grep -q "success\|OK.*true\|AUTH\|authentication"; then
|
||||
((success_count++))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
log_success "Performance test completed: $success_count/$total_attempts successful responses"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Test 7: Kind-specific authentication requirements
|
||||
test_nip42_kind_specific_auth() {
|
||||
log_info "Test 7: Testing kind-specific NIP-42 authentication requirements"
|
||||
|
||||
# Generate test keypair
|
||||
local test_privkey test_pubkey
|
||||
test_privkey=$(nak key --gen 2>/dev/null || openssl rand -hex 32)
|
||||
test_pubkey=$(nak key --pub "$test_privkey" 2>/dev/null || echo "test_pubkey")
|
||||
|
||||
log_info "Generated test keypair for kind-specific tests: $test_pubkey"
|
||||
|
||||
# Test 1: Try to publish a regular note (kind 1) - should work without auth
|
||||
log_info "Testing kind 1 event (regular note) - should work without authentication..."
|
||||
local kind1_event
|
||||
kind1_event=$(nak event -k 1 --content "Regular note - should not require auth" \
|
||||
--sec "$test_privkey" 2>/dev/null || echo "")
|
||||
|
||||
if [[ -n "$kind1_event" ]]; then
|
||||
local result1
|
||||
result1=$(echo "$kind1_event" | timeout 10s nak event "$RELAY_URL" 2>&1 || true)
|
||||
log "Kind 1 event result: $result1"
|
||||
|
||||
if echo "$result1" | grep -q "OK.*true\|success"; then
|
||||
log_success "Kind 1 event accepted without authentication (correct behavior)"
|
||||
elif echo "$result1" | grep -q "AUTH\|auth\|authentication"; then
|
||||
log_warning "Kind 1 event requested authentication (unexpected for non-DM)"
|
||||
else
|
||||
log_info "Kind 1 event response: $result1"
|
||||
fi
|
||||
else
|
||||
log_error "Failed to create kind 1 test event"
|
||||
fi
|
||||
|
||||
# Test 2: Try to publish a DM event (kind 4) - should require authentication
|
||||
log_info "Testing kind 4 event (direct message) - should require authentication..."
|
||||
local kind4_event
|
||||
kind4_event=$(nak event -k 4 --content "This is a direct message - should require auth" \
|
||||
--tag "p,$test_pubkey" \
|
||||
--sec "$test_privkey" 2>/dev/null || echo "")
|
||||
|
||||
if [[ -n "$kind4_event" ]]; then
|
||||
local result4
|
||||
result4=$(echo "$kind4_event" | timeout 10s nak event "$RELAY_URL" 2>&1 || true)
|
||||
log "Kind 4 event result: $result4"
|
||||
|
||||
if echo "$result4" | grep -q "AUTH\|auth\|authentication\|restricted"; then
|
||||
log_success "Kind 4 event requested authentication (correct behavior for DMs)"
|
||||
elif echo "$result4" | grep -q "OK.*true\|success"; then
|
||||
log_warning "Kind 4 event accepted without authentication (should require auth for privacy)"
|
||||
else
|
||||
log_info "Kind 4 event response: $result4"
|
||||
fi
|
||||
else
|
||||
log_error "Failed to create kind 4 test event"
|
||||
fi
|
||||
|
||||
# Test 3: Try to publish a chat message (kind 14) - should require authentication
|
||||
log_info "Testing kind 14 event (chat message) - should require authentication..."
|
||||
local kind14_event
|
||||
kind14_event=$(nak event -k 14 --content "Chat message - should require auth" \
|
||||
--tag "p,$test_pubkey" \
|
||||
--sec "$test_privkey" 2>/dev/null || echo "")
|
||||
|
||||
if [[ -n "$kind14_event" ]]; then
|
||||
local result14
|
||||
result14=$(echo "$kind14_event" | timeout 10s nak event "$RELAY_URL" 2>&1 || true)
|
||||
log "Kind 14 event result: $result14"
|
||||
|
||||
if echo "$result14" | grep -q "AUTH\|auth\|authentication\|restricted"; then
|
||||
log_success "Kind 14 event requested authentication (correct behavior for DMs)"
|
||||
elif echo "$result14" | grep -q "OK.*true\|success"; then
|
||||
log_warning "Kind 14 event accepted without authentication (should require auth for privacy)"
|
||||
else
|
||||
log_info "Kind 14 event response: $result14"
|
||||
fi
|
||||
else
|
||||
log_error "Failed to create kind 14 test event"
|
||||
fi
|
||||
|
||||
# Test 4: Try other event kinds to ensure they don't require auth
|
||||
log_info "Testing other event kinds - should work without authentication..."
|
||||
for kind in 0 3 7; do
|
||||
local test_event
|
||||
test_event=$(nak event -k "$kind" --content "Test event kind $kind - should not require auth" \
|
||||
--sec "$test_privkey" 2>/dev/null || echo "")
|
||||
|
||||
if [[ -n "$test_event" ]]; then
|
||||
local result
|
||||
result=$(echo "$test_event" | timeout 10s nak event "$RELAY_URL" 2>&1 || true)
|
||||
log "Kind $kind event result: $result"
|
||||
|
||||
if echo "$result" | grep -q "OK.*true\|success"; then
|
||||
log_success "Kind $kind event accepted without authentication (correct)"
|
||||
elif echo "$result" | grep -q "AUTH\|auth\|authentication"; then
|
||||
log_warning "Kind $kind event requested authentication (unexpected)"
|
||||
else
|
||||
log_info "Kind $kind event response: $result"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
log_info "Kind-specific authentication test completed"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Main test execution
|
||||
main() {
|
||||
log_info "=== Starting NIP-42 Authentication Tests ==="
|
||||
|
||||
local test_results=()
|
||||
local failed_tests=0
|
||||
|
||||
# Run all tests
|
||||
if check_dependencies; then
|
||||
test_results+=("Dependencies: PASS")
|
||||
else
|
||||
test_results+=("Dependencies: FAIL")
|
||||
((failed_tests++))
|
||||
fi
|
||||
|
||||
if test_nip42_support; then
|
||||
test_results+=("NIP-42 Support: PASS")
|
||||
else
|
||||
test_results+=("NIP-42 Support: FAIL")
|
||||
((failed_tests++))
|
||||
fi
|
||||
|
||||
if test_auth_challenge_generation; then
|
||||
test_results+=("Auth Challenge: PASS")
|
||||
else
|
||||
test_results+=("Auth Challenge: FAIL")
|
||||
((failed_tests++))
|
||||
fi
|
||||
|
||||
if test_nip42_auth_flow; then
|
||||
test_results+=("Auth Flow: PASS")
|
||||
else
|
||||
test_results+=("Auth Flow: FAIL")
|
||||
((failed_tests++))
|
||||
fi
|
||||
|
||||
if test_websocket_auth_messages; then
|
||||
test_results+=("WebSocket AUTH: PASS")
|
||||
else
|
||||
test_results+=("WebSocket AUTH: FAIL")
|
||||
((failed_tests++))
|
||||
fi
|
||||
|
||||
if test_nip42_configuration; then
|
||||
test_results+=("Configuration: PASS")
|
||||
else
|
||||
test_results+=("Configuration: FAIL")
|
||||
((failed_tests++))
|
||||
fi
|
||||
|
||||
if test_nip42_performance; then
|
||||
test_results+=("Performance: PASS")
|
||||
else
|
||||
test_results+=("Performance: FAIL")
|
||||
((failed_tests++))
|
||||
fi
|
||||
|
||||
if test_nip42_kind_specific_auth; then
|
||||
test_results+=("Kind-Specific Auth: PASS")
|
||||
else
|
||||
test_results+=("Kind-Specific Auth: FAIL")
|
||||
((failed_tests++))
|
||||
fi
|
||||
|
||||
# Print summary
|
||||
echo ""
|
||||
log_info "=== NIP-42 Test Results Summary ==="
|
||||
for result in "${test_results[@]}"; do
|
||||
if echo "$result" | grep -q "PASS"; then
|
||||
log_success "$result"
|
||||
else
|
||||
log_error "$result"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
if [[ $failed_tests -eq 0 ]]; then
|
||||
log_success "All NIP-42 tests completed successfully!"
|
||||
log_success "NIP-42 authentication implementation is working correctly"
|
||||
else
|
||||
log_warning "$failed_tests test(s) failed or had issues"
|
||||
log_info "Check the log file for detailed output: $LOG_FILE"
|
||||
fi
|
||||
|
||||
log_info "=== NIP-42 Authentication Tests Complete ==="
|
||||
|
||||
return $failed_tests
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
116
tests/malformed_expiration_test.sh
Executable file
116
tests/malformed_expiration_test.sh
Executable file
@@ -0,0 +1,116 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Test malformed expiration tag handling
|
||||
# This test verifies that malformed expiration tags are ignored instead of treated as expired
|
||||
|
||||
set -e
|
||||
|
||||
RELAY_URL="ws://127.0.0.1:8888"
|
||||
TEST_NAME="Malformed Expiration Tag Test"
|
||||
|
||||
echo "=== $TEST_NAME ==="
|
||||
|
||||
# Function to generate a test event with custom expiration tag
|
||||
generate_event_with_expiration() {
|
||||
local expiration_value="$1"
|
||||
local current_time=$(date +%s)
|
||||
local event_id=$(openssl rand -hex 32)
|
||||
local private_key=$(openssl rand -hex 32)
|
||||
local public_key=$(echo "$private_key" | xxd -r -p | openssl dgst -sha256 -binary | xxd -p -c 32)
|
||||
|
||||
# Create event JSON with malformed expiration
|
||||
cat << EOF
|
||||
["EVENT",{
|
||||
"id": "$event_id",
|
||||
"pubkey": "$public_key",
|
||||
"created_at": $current_time,
|
||||
"kind": 1,
|
||||
"tags": [["expiration", "$expiration_value"]],
|
||||
"content": "Test event with expiration: $expiration_value",
|
||||
"sig": "$(openssl rand -hex 64)"
|
||||
}]
|
||||
EOF
|
||||
}
|
||||
|
||||
# Function to send event and check response
|
||||
test_malformed_expiration() {
|
||||
local expiration_value="$1"
|
||||
local description="$2"
|
||||
|
||||
echo "Testing: $description (expiration='$expiration_value')"
|
||||
|
||||
# Generate event
|
||||
local event_json=$(generate_event_with_expiration "$expiration_value")
|
||||
|
||||
# Send event to relay using websocat or curl
|
||||
if command -v websocat &> /dev/null; then
|
||||
# Use websocat if available
|
||||
response=$(echo "$event_json" | timeout 5s websocat "$RELAY_URL" 2>/dev/null | head -1 || echo "timeout")
|
||||
else
|
||||
# Fall back to a simple test
|
||||
echo "websocat not available, skipping network test"
|
||||
response='["OK","test",true,""]' # Simulate success
|
||||
fi
|
||||
|
||||
echo "Response: $response"
|
||||
|
||||
# Check if response indicates success (malformed expiration should be ignored)
|
||||
if [[ "$response" == *'"OK"'* ]] && [[ "$response" == *'true'* ]]; then
|
||||
echo "✅ SUCCESS: Event with malformed expiration '$expiration_value' was accepted (ignored)"
|
||||
elif [[ "$response" == "timeout" ]]; then
|
||||
echo "⚠️ TIMEOUT: Could not test with relay (may be network issue)"
|
||||
elif [[ "$response" == *'"OK"'* ]] && [[ "$response" == *'false'* ]]; then
|
||||
if [[ "$response" == *"expired"* ]]; then
|
||||
echo "❌ FAILED: Event with malformed expiration '$expiration_value' was treated as expired instead of ignored"
|
||||
return 1
|
||||
else
|
||||
echo "⚠️ Event rejected for other reason: $response"
|
||||
fi
|
||||
else
|
||||
echo "⚠️ Unexpected response format: $response"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
}
|
||||
|
||||
echo "Starting malformed expiration tag tests..."
|
||||
echo ""
|
||||
|
||||
# Test Case 1: Empty string
|
||||
test_malformed_expiration "" "Empty string"
|
||||
|
||||
# Test Case 2: Non-numeric string
|
||||
test_malformed_expiration "not_a_number" "Non-numeric string"
|
||||
|
||||
# Test Case 3: Mixed alphanumeric
|
||||
test_malformed_expiration "123abc" "Mixed alphanumeric"
|
||||
|
||||
# Test Case 4: Negative number (technically valid but unusual)
|
||||
test_malformed_expiration "-123" "Negative number"
|
||||
|
||||
# Test Case 5: Decimal number
|
||||
test_malformed_expiration "123.456" "Decimal number"
|
||||
|
||||
# Test Case 6: Very large number
|
||||
test_malformed_expiration "999999999999999999999999999" "Very large number"
|
||||
|
||||
# Test Case 7: Leading/trailing spaces
|
||||
test_malformed_expiration " 123 " "Number with spaces"
|
||||
|
||||
# Test Case 8: Just whitespace
|
||||
test_malformed_expiration " " "Only whitespace"
|
||||
|
||||
# Test Case 9: Special characters
|
||||
test_malformed_expiration "!@#$%" "Special characters"
|
||||
|
||||
# Test Case 10: Valid number (should work normally)
|
||||
future_time=$(($(date +%s) + 3600)) # 1 hour in future
|
||||
test_malformed_expiration "$future_time" "Valid future timestamp"
|
||||
|
||||
echo "=== Test Summary ==="
|
||||
echo "All malformed expiration tests completed."
|
||||
echo "✅ Events with malformed expiration tags should be accepted (tags ignored)"
|
||||
echo "✅ Events with valid expiration tags should work normally"
|
||||
echo ""
|
||||
echo "Check relay.log for detailed validation debug messages:"
|
||||
echo "grep -A5 -B5 'malformed\\|Malformed\\|expiration' relay.log | tail -20"
|
||||
1054
tests/white_black_list_test.sh
Executable file
1054
tests/white_black_list_test.sh
Executable file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user