Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d66b8bf1d | ||
|
|
f3d6afead1 | ||
|
|
1690b58c67 | ||
|
|
2e8eda5c67 | ||
|
|
74a4dc2533 | ||
|
|
be7ae2b580 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,3 +8,4 @@ src/version.h
|
||||
dev-config/
|
||||
db/
|
||||
copy_executable_local.sh
|
||||
nostr_login_lite/
|
||||
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.]
|
||||
5
.roo/commands/push.md
Normal file
5
.roo/commands/push.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
description: "Brief description of what this command does"
|
||||
---
|
||||
|
||||
Run build_and_push.sh, and supply a good git commit 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.
|
||||
142
AGENTS.md
Normal file
142
AGENTS.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# 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 as kind 33334 Nostr events
|
||||
- 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 as kind 33334 event
|
||||
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
|
||||
|
||||
### Configuration Event Structure
|
||||
```json
|
||||
{
|
||||
"kind": 33334,
|
||||
"content": "C Nostr Relay Configuration",
|
||||
"tags": [
|
||||
["d", "<relay_pubkey>"],
|
||||
["relay_description", "value"],
|
||||
["max_subscriptions_per_client", "25"],
|
||||
["pow_min_difficulty", "16"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
320
README.md
320
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,9 @@ 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
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Relay won't start**
|
||||
```bash
|
||||
# Check for port conflicts
|
||||
netstat -tln | grep 8888
|
||||
|
||||
# Check permissions
|
||||
ls -la build/c_relay_x86
|
||||
|
||||
# Check dependencies
|
||||
ldd build/c_relay_x86
|
||||
```
|
||||
|
||||
**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
|
||||
|
||||
**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
|
||||
```
|
||||
|
||||
**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
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||
|
||||
## 🤝 Support
|
||||
|
||||
- **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
|
||||
|
||||
## 🚀 Future Roadmap
|
||||
|
||||
- [ ] Docker containerization
|
||||
- [ ] NIP-42 authentication support
|
||||
- [ ] Advanced analytics dashboard
|
||||
- [ ] Clustering support for high availability
|
||||
- [ ] Performance optimizations
|
||||
- [ ] Additional NIP implementations
|
||||
|
||||
---
|
||||
|
||||
**The C Nostr Relay represents the future of Nostr infrastructure - zero configuration, event-based management, and cryptographically secure administration.**
|
||||
|
||||
1194
api/index.html
Normal file
1194
api/index.html
Normal file
File diff suppressed because it is too large
Load Diff
3122
api/nostr-lite.js
Normal file
3122
api/nostr-lite.js
Normal file
File diff suppressed because it is too large
Load Diff
6860
api/nostr.bundle.js
Normal file
6860
api/nostr.bundle.js
Normal file
File diff suppressed because it is too large
Load Diff
BIN
c-relay-x86_64
BIN
c-relay-x86_64
Binary file not shown.
@@ -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
|
||||
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,23 +83,53 @@ 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 " -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."
|
||||
echo " On first startup, keys are automatically generated and printed once."
|
||||
echo " Database file: <relay_pubkey>.nrdb (created automatically)"
|
||||
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
|
||||
@@ -51,15 +137,22 @@ fi
|
||||
|
||||
# Handle database file cleanup for fresh start
|
||||
if [ "$PRESERVE_DATABASE" = false ]; then
|
||||
if ls *.nrdb >/dev/null 2>&1; then
|
||||
if ls *.db >/dev/null 2>&1 || ls build/*.db >/dev/null 2>&1; then
|
||||
echo "Removing existing database files to trigger fresh key generation..."
|
||||
rm -f *.nrdb
|
||||
rm -f *.db build/*.db
|
||||
echo "✓ Database files removed - will generate new keys and database"
|
||||
else
|
||||
echo "No existing database found - will generate fresh setup"
|
||||
fi
|
||||
else
|
||||
echo "Preserving existing database files as requested"
|
||||
# Back up database files before clean build
|
||||
if ls build/*.db >/dev/null 2>&1; then
|
||||
echo "Backing up existing database files..."
|
||||
mkdir -p /tmp/relay_backup_$$
|
||||
cp build/*.db* /tmp/relay_backup_$$/ 2>/dev/null || true
|
||||
echo "Database files backed up to temporary location"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Clean up legacy files that are no longer used
|
||||
@@ -70,6 +163,14 @@ rm -f db/c_nostr_relay.db* 2>/dev/null
|
||||
echo "Building project..."
|
||||
make clean all
|
||||
|
||||
# Restore database files if preserving
|
||||
if [ "$PRESERVE_DATABASE" = true ] && [ -d "/tmp/relay_backup_$$" ]; then
|
||||
echo "Restoring preserved database files..."
|
||||
cp /tmp/relay_backup_$$/*.db* build/ 2>/dev/null || true
|
||||
rm -rf /tmp/relay_backup_$$
|
||||
echo "Database files restored to build directory"
|
||||
fi
|
||||
|
||||
# Check if build was successful
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "ERROR: Build failed. Cannot restart relay."
|
||||
@@ -129,9 +230,41 @@ 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')"
|
||||
|
||||
# Start relay in background and capture its PID (no command line arguments needed)
|
||||
$BINARY_PATH > relay.log 2>&1 &
|
||||
# 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
|
||||
if [ "$USE_TEST_KEYS" = true ]; then
|
||||
echo "Using deterministic test keys for development..."
|
||||
./$(basename $BINARY_PATH) -a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -r 1111111111111111111111111111111111111111111111111111111111111111 > ../relay.log 2>&1 &
|
||||
elif [ -n "$RELAY_ARGS" ]; then
|
||||
echo "Starting relay with custom configuration..."
|
||||
./$(basename $BINARY_PATH) $RELAY_ARGS > ../relay.log 2>&1 &
|
||||
else
|
||||
# No command line arguments needed for random key generation
|
||||
echo "Starting relay with random key generation..."
|
||||
./$(basename $BINARY_PATH) > ../relay.log 2>&1 &
|
||||
fi
|
||||
RELAY_PID=$!
|
||||
# Change back to original directory
|
||||
cd ..
|
||||
|
||||
echo "Started with PID: $RELAY_PID"
|
||||
|
||||
@@ -142,7 +275,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 ""
|
||||
|
||||
|
||||
756
src/config.c
756
src/config.c
@@ -1,3 +1,4 @@
|
||||
#define _GNU_SOURCE
|
||||
#include "config.h"
|
||||
#include "default_config_event.h"
|
||||
#include "../nostr_core_lib/nostr_core/nostr_core.h"
|
||||
@@ -29,11 +30,14 @@ static cJSON* g_current_config = NULL;
|
||||
// Cache for initial configuration event (before database is initialized)
|
||||
static cJSON* g_pending_config_event = NULL;
|
||||
|
||||
// Temporary storage for relay private key during first-time setup
|
||||
static char g_temp_relay_privkey[65] = {0};
|
||||
|
||||
// ================================
|
||||
// UTILITY FUNCTIONS
|
||||
// ================================
|
||||
|
||||
char** find_existing_nrdb_files(void) {
|
||||
char** find_existing_db_files(void) {
|
||||
DIR *dir;
|
||||
struct dirent *entry;
|
||||
char **files = NULL;
|
||||
@@ -44,9 +48,9 @@ char** find_existing_nrdb_files(void) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Count .nrdb files
|
||||
// Count .db files
|
||||
while ((entry = readdir(dir)) != NULL) {
|
||||
if (strstr(entry->d_name, ".nrdb") != NULL) {
|
||||
if (strstr(entry->d_name, ".db") != NULL) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
@@ -67,7 +71,7 @@ char** find_existing_nrdb_files(void) {
|
||||
// Store filenames
|
||||
int i = 0;
|
||||
while ((entry = readdir(dir)) != NULL && i < count) {
|
||||
if (strstr(entry->d_name, ".nrdb") != NULL) {
|
||||
if (strstr(entry->d_name, ".db") != NULL) {
|
||||
files[i] = malloc(strlen(entry->d_name) + 1);
|
||||
if (files[i]) {
|
||||
strcpy(files[i], entry->d_name);
|
||||
@@ -84,8 +88,8 @@ char** find_existing_nrdb_files(void) {
|
||||
char* extract_pubkey_from_filename(const char* filename) {
|
||||
if (!filename) return NULL;
|
||||
|
||||
// Find .nrdb extension
|
||||
const char* dot = strstr(filename, ".nrdb");
|
||||
// Find .db extension
|
||||
const char* dot = strstr(filename, ".db");
|
||||
if (!dot) return NULL;
|
||||
|
||||
// Calculate pubkey length
|
||||
@@ -107,10 +111,10 @@ char* get_database_name_from_relay_pubkey(const char* relay_pubkey) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char* db_name = malloc(strlen(relay_pubkey) + 6); // +6 for ".nrdb\0"
|
||||
char* db_name = malloc(strlen(relay_pubkey) + 4); // +4 for ".db\0"
|
||||
if (!db_name) return NULL;
|
||||
|
||||
sprintf(db_name, "%s.nrdb", relay_pubkey);
|
||||
sprintf(db_name, "%s.db", relay_pubkey);
|
||||
return db_name;
|
||||
}
|
||||
|
||||
@@ -337,12 +341,89 @@ int get_config_bool(const char* key, int default_value) {
|
||||
return default_value;
|
||||
}
|
||||
|
||||
// ================================
|
||||
// NIP-42 KIND-SPECIFIC AUTHENTICATION
|
||||
// ================================
|
||||
|
||||
// Parse comma-separated kind list into array of integers
|
||||
// Returns number of kinds parsed, or -1 on error
|
||||
int parse_auth_required_kinds(const char* kinds_str, int* kinds_array, int max_kinds) {
|
||||
if (!kinds_str || !kinds_array || max_kinds <= 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Empty string means no kinds require auth
|
||||
if (strlen(kinds_str) == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
char* str_copy = strdup(kinds_str);
|
||||
if (!str_copy) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
char* token = strtok(str_copy, ",");
|
||||
|
||||
while (token && count < max_kinds) {
|
||||
// Trim whitespace
|
||||
while (*token == ' ' || *token == '\t') token++;
|
||||
char* end = token + strlen(token) - 1;
|
||||
while (end > token && (*end == ' ' || *end == '\t')) end--;
|
||||
*(end + 1) = '\0';
|
||||
|
||||
// Convert to integer
|
||||
char* endptr;
|
||||
long kind = strtol(token, &endptr, 10);
|
||||
|
||||
if (endptr != token && *endptr == '\0' && kind >= 0 && kind <= 65535) {
|
||||
kinds_array[count] = (int)kind;
|
||||
count++;
|
||||
}
|
||||
|
||||
token = strtok(NULL, ",");
|
||||
}
|
||||
|
||||
free(str_copy);
|
||||
return count;
|
||||
}
|
||||
|
||||
// Check if a specific event kind requires NIP-42 authentication
|
||||
int is_nip42_auth_required_for_kind(int event_kind) {
|
||||
const char* kinds_str = get_config_value("nip42_auth_required_kinds");
|
||||
if (!kinds_str) {
|
||||
return 0; // No authentication required if setting is missing
|
||||
}
|
||||
|
||||
// Parse the kinds list
|
||||
int required_kinds[100]; // Support up to 100 different kinds
|
||||
int count = parse_auth_required_kinds(kinds_str, required_kinds, 100);
|
||||
|
||||
if (count < 0) {
|
||||
return 0; // Parse error, default to no auth required
|
||||
}
|
||||
|
||||
// Check if event_kind is in the list
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (required_kinds[i] == event_kind) {
|
||||
return 1; // Authentication required for this kind
|
||||
}
|
||||
}
|
||||
|
||||
return 0; // Authentication not required for this kind
|
||||
}
|
||||
|
||||
// Get NIP-42 global authentication requirement
|
||||
int is_nip42_auth_globally_required(void) {
|
||||
return get_config_bool("nip42_auth_required", 0);
|
||||
}
|
||||
|
||||
// ================================
|
||||
// FIRST-TIME STARTUP FUNCTIONS
|
||||
// ================================
|
||||
|
||||
int is_first_time_startup(void) {
|
||||
char** existing_files = find_existing_nrdb_files();
|
||||
char** existing_files = find_existing_db_files();
|
||||
if (existing_files) {
|
||||
// Free the array
|
||||
for (int i = 0; existing_files[i]; i++) {
|
||||
@@ -437,13 +518,111 @@ int generate_random_private_key_bytes(unsigned char* privkey_bytes) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ================================
|
||||
// SECURE RELAY PRIVATE KEY STORAGE
|
||||
// ================================
|
||||
|
||||
int store_relay_private_key(const char* relay_privkey_hex) {
|
||||
if (!relay_privkey_hex) {
|
||||
log_error("Invalid relay private key for storage");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Validate private key format (must be 64 hex characters)
|
||||
if (strlen(relay_privkey_hex) != 64) {
|
||||
log_error("Invalid relay private key length (must be 64 hex characters)");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Validate hex format
|
||||
for (int i = 0; i < 64; i++) {
|
||||
char c = relay_privkey_hex[i];
|
||||
if (!((c >= '0' && c <= '9') ||
|
||||
(c >= 'a' && c <= 'f') ||
|
||||
(c >= 'A' && c <= 'F'))) {
|
||||
log_error("Invalid relay private key format (must be hex characters only)");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!g_db) {
|
||||
log_error("Database not available for relay private key storage");
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char* sql = "INSERT OR REPLACE INTO relay_seckey (private_key_hex) VALUES (?)";
|
||||
sqlite3_stmt* stmt;
|
||||
|
||||
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
log_error("Failed to prepare relay private key storage query");
|
||||
return -1;
|
||||
}
|
||||
|
||||
sqlite3_bind_text(stmt, 1, relay_privkey_hex, -1, SQLITE_STATIC);
|
||||
|
||||
rc = sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
if (rc == SQLITE_DONE) {
|
||||
log_success("Relay private key stored securely in database");
|
||||
return 0;
|
||||
} else {
|
||||
log_error("Failed to store relay private key in database");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
char* get_relay_private_key(void) {
|
||||
if (!g_db) {
|
||||
log_error("Database not available for relay private key retrieval");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char* sql = "SELECT private_key_hex FROM relay_seckey";
|
||||
sqlite3_stmt* stmt;
|
||||
|
||||
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
log_error("Failed to prepare relay private key retrieval query");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char* private_key = NULL;
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
const char* key_from_db = (const char*)sqlite3_column_text(stmt, 0);
|
||||
if (key_from_db && strlen(key_from_db) == 64) {
|
||||
private_key = malloc(65); // 64 chars + null terminator
|
||||
if (private_key) {
|
||||
strcpy(private_key, key_from_db);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
if (!private_key) {
|
||||
log_error("Relay private key not found in secure storage");
|
||||
}
|
||||
|
||||
return private_key;
|
||||
}
|
||||
|
||||
const char* get_temp_relay_private_key(void) {
|
||||
if (strlen(g_temp_relay_privkey) == 64) {
|
||||
return g_temp_relay_privkey;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// ================================
|
||||
// DEFAULT CONFIG EVENT CREATION
|
||||
// ================================
|
||||
|
||||
cJSON* create_default_config_event(const unsigned char* admin_privkey_bytes,
|
||||
const char* relay_privkey_hex,
|
||||
const char* relay_pubkey_hex) {
|
||||
const char* relay_pubkey_hex,
|
||||
const cli_options_t* cli_options) {
|
||||
if (!admin_privkey_bytes || !relay_privkey_hex || !relay_pubkey_hex) {
|
||||
log_error("Invalid parameters for creating default config event");
|
||||
return NULL;
|
||||
@@ -470,16 +649,31 @@ cJSON* create_default_config_event(const unsigned char* admin_privkey_bytes,
|
||||
cJSON_AddItemToArray(relay_pubkey_tag, cJSON_CreateString(relay_pubkey_hex));
|
||||
cJSON_AddItemToArray(tags, relay_pubkey_tag);
|
||||
|
||||
cJSON* relay_privkey_tag = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(relay_privkey_tag, cJSON_CreateString("relay_privkey"));
|
||||
cJSON_AddItemToArray(relay_privkey_tag, cJSON_CreateString(relay_privkey_hex));
|
||||
cJSON_AddItemToArray(tags, relay_privkey_tag);
|
||||
// Note: relay_privkey is now stored securely in relay_seckey table
|
||||
// It is no longer included in the public configuration event
|
||||
|
||||
// Add all default configuration values
|
||||
// Add all default configuration values with command line overrides
|
||||
for (size_t i = 0; i < DEFAULT_CONFIG_COUNT; i++) {
|
||||
cJSON* tag = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(tag, cJSON_CreateString(DEFAULT_CONFIG_VALUES[i].key));
|
||||
cJSON_AddItemToArray(tag, cJSON_CreateString(DEFAULT_CONFIG_VALUES[i].value));
|
||||
|
||||
// Check for command line overrides
|
||||
const char* value = DEFAULT_CONFIG_VALUES[i].value;
|
||||
if (cli_options) {
|
||||
// Override relay_port if specified on command line
|
||||
if (cli_options->port_override > 0 && strcmp(DEFAULT_CONFIG_VALUES[i].key, "relay_port") == 0) {
|
||||
char port_str[16];
|
||||
snprintf(port_str, sizeof(port_str), "%d", cli_options->port_override);
|
||||
cJSON_AddItemToArray(tag, cJSON_CreateString(port_str));
|
||||
log_info("Using command line port override in configuration event");
|
||||
printf(" Port: %d (overriding default %s)\n", cli_options->port_override, DEFAULT_CONFIG_VALUES[i].value);
|
||||
} else {
|
||||
cJSON_AddItemToArray(tag, cJSON_CreateString(value));
|
||||
}
|
||||
} else {
|
||||
cJSON_AddItemToArray(tag, cJSON_CreateString(value));
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(tags, tag);
|
||||
}
|
||||
|
||||
@@ -516,18 +710,38 @@ cJSON* create_default_config_event(const unsigned char* admin_privkey_bytes,
|
||||
// IMPLEMENTED FUNCTIONS
|
||||
// ================================
|
||||
|
||||
int first_time_startup_sequence(void) {
|
||||
int first_time_startup_sequence(const cli_options_t* cli_options) {
|
||||
log_info("Starting first-time startup sequence...");
|
||||
|
||||
// 1. Generate admin keypair using /dev/urandom + nostr_core_lib
|
||||
// 1. Generate or use provided admin keypair
|
||||
unsigned char admin_privkey_bytes[32];
|
||||
char admin_privkey[65], admin_pubkey[65];
|
||||
|
||||
if (cli_options && strlen(cli_options->admin_privkey_override) == 64) {
|
||||
// Use provided admin private key
|
||||
log_info("Using provided admin private key override");
|
||||
strncpy(admin_privkey, cli_options->admin_privkey_override, sizeof(admin_privkey) - 1);
|
||||
admin_privkey[sizeof(admin_privkey) - 1] = '\0';
|
||||
|
||||
// Convert hex string to bytes
|
||||
if (nostr_hex_to_bytes(admin_privkey, admin_privkey_bytes, 32) != NOSTR_SUCCESS) {
|
||||
log_error("Failed to convert admin private key hex to bytes");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Validate the private key
|
||||
if (nostr_ec_private_key_verify(admin_privkey_bytes) != NOSTR_SUCCESS) {
|
||||
log_error("Provided admin private key is invalid");
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
// Generate random admin keypair using /dev/urandom + nostr_core_lib
|
||||
if (generate_random_private_key_bytes(admin_privkey_bytes) != 0) {
|
||||
log_error("Failed to generate admin private key");
|
||||
return -1;
|
||||
}
|
||||
nostr_bytes_to_hex(admin_privkey_bytes, 32, admin_privkey);
|
||||
}
|
||||
|
||||
unsigned char admin_pubkey_bytes[32];
|
||||
if (nostr_ec_public_key_from_private_key(admin_privkey_bytes, admin_pubkey_bytes) != NOSTR_SUCCESS) {
|
||||
@@ -536,15 +750,35 @@ int first_time_startup_sequence(void) {
|
||||
}
|
||||
nostr_bytes_to_hex(admin_pubkey_bytes, 32, admin_pubkey);
|
||||
|
||||
// 2. Generate relay keypair using /dev/urandom + nostr_core_lib
|
||||
// 2. Generate or use provided relay keypair
|
||||
unsigned char relay_privkey_bytes[32];
|
||||
char relay_privkey[65], relay_pubkey[65];
|
||||
|
||||
if (cli_options && strlen(cli_options->relay_privkey_override) == 64) {
|
||||
// Use provided relay private key
|
||||
log_info("Using provided relay private key override");
|
||||
strncpy(relay_privkey, cli_options->relay_privkey_override, sizeof(relay_privkey) - 1);
|
||||
relay_privkey[sizeof(relay_privkey) - 1] = '\0';
|
||||
|
||||
// Convert hex string to bytes
|
||||
if (nostr_hex_to_bytes(relay_privkey, relay_privkey_bytes, 32) != NOSTR_SUCCESS) {
|
||||
log_error("Failed to convert relay private key hex to bytes");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Validate the private key
|
||||
if (nostr_ec_private_key_verify(relay_privkey_bytes) != NOSTR_SUCCESS) {
|
||||
log_error("Provided relay private key is invalid");
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
// Generate random relay keypair using /dev/urandom + nostr_core_lib
|
||||
if (generate_random_private_key_bytes(relay_privkey_bytes) != 0) {
|
||||
log_error("Failed to generate relay private key");
|
||||
return -1;
|
||||
}
|
||||
nostr_bytes_to_hex(relay_privkey_bytes, 32, relay_privkey);
|
||||
}
|
||||
|
||||
unsigned char relay_pubkey_bytes[32];
|
||||
if (nostr_ec_public_key_from_private_key(relay_privkey_bytes, relay_pubkey_bytes) != NOSTR_SUCCESS) {
|
||||
@@ -565,14 +799,19 @@ int first_time_startup_sequence(void) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 5. Create initial configuration event using defaults
|
||||
cJSON* config_event = create_default_config_event(admin_privkey_bytes, relay_privkey, relay_pubkey);
|
||||
// 5. Store relay private key in temporary storage for later secure storage
|
||||
strncpy(g_temp_relay_privkey, relay_privkey, sizeof(g_temp_relay_privkey) - 1);
|
||||
g_temp_relay_privkey[sizeof(g_temp_relay_privkey) - 1] = '\0';
|
||||
log_info("Relay private key cached for secure storage after database initialization");
|
||||
|
||||
// 6. Create initial configuration event using defaults (without private key)
|
||||
cJSON* config_event = create_default_config_event(admin_privkey_bytes, relay_privkey, relay_pubkey, cli_options);
|
||||
if (!config_event) {
|
||||
log_error("Failed to create default configuration event");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 6. Try to store configuration event in database, but cache it if database isn't ready
|
||||
// 7. Try to store configuration event in database, but cache it if database isn't ready
|
||||
if (store_config_event_in_database(config_event) == 0) {
|
||||
log_success("Initial configuration event stored successfully");
|
||||
} else {
|
||||
@@ -584,23 +823,22 @@ int first_time_startup_sequence(void) {
|
||||
g_pending_config_event = cJSON_Duplicate(config_event, 1);
|
||||
}
|
||||
|
||||
// 7. Cache the current config
|
||||
// 8. Cache the current config
|
||||
if (g_current_config) {
|
||||
cJSON_Delete(g_current_config);
|
||||
}
|
||||
g_current_config = cJSON_Duplicate(config_event, 1);
|
||||
|
||||
// 8. Clean up
|
||||
// 9. Clean up
|
||||
cJSON_Delete(config_event);
|
||||
|
||||
// 9. Print admin private key for user to save
|
||||
// 10. Print admin private key for user to save
|
||||
printf("\n");
|
||||
printf("=================================================================\n");
|
||||
printf("IMPORTANT: SAVE THIS ADMIN PRIVATE KEY SECURELY!\n");
|
||||
printf("=================================================================\n");
|
||||
printf("Admin Private Key: %s\n", admin_privkey);
|
||||
printf("Admin Public Key: %s\n", admin_pubkey);
|
||||
printf("\nRelay Private Key: %s\n", relay_privkey);
|
||||
printf("Relay Public Key: %s\n", relay_pubkey);
|
||||
printf("\nDatabase: %s\n", g_database_path);
|
||||
printf("\nThis admin private key is needed to update configuration!\n");
|
||||
@@ -642,6 +880,462 @@ int startup_existing_relay(const char* relay_pubkey) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ================================
|
||||
// CONFIGURATION FIELD VALIDATION
|
||||
// ================================
|
||||
|
||||
// Validation helper functions
|
||||
static int is_valid_port(const char* port_str) {
|
||||
if (!port_str) return 0;
|
||||
|
||||
char* endptr;
|
||||
long port = strtol(port_str, &endptr, 10);
|
||||
|
||||
// Must be valid number and in valid port range
|
||||
return (endptr != port_str && *endptr == '\0' && port >= 1 && port <= 65535);
|
||||
}
|
||||
|
||||
static int is_valid_boolean(const char* bool_str) {
|
||||
if (!bool_str) return 0;
|
||||
|
||||
return (strcasecmp(bool_str, "true") == 0 ||
|
||||
strcasecmp(bool_str, "false") == 0 ||
|
||||
strcasecmp(bool_str, "yes") == 0 ||
|
||||
strcasecmp(bool_str, "no") == 0 ||
|
||||
strcasecmp(bool_str, "1") == 0 ||
|
||||
strcasecmp(bool_str, "0") == 0);
|
||||
}
|
||||
|
||||
static int is_valid_positive_integer(const char* int_str) {
|
||||
if (!int_str) return 0;
|
||||
|
||||
char* endptr;
|
||||
long val = strtol(int_str, &endptr, 10);
|
||||
|
||||
return (endptr != int_str && *endptr == '\0' && val >= 0);
|
||||
}
|
||||
|
||||
static int is_valid_non_negative_integer(const char* int_str) {
|
||||
if (!int_str) return 0;
|
||||
|
||||
char* endptr;
|
||||
long val = strtol(int_str, &endptr, 10);
|
||||
|
||||
return (endptr != int_str && *endptr == '\0' && val >= 0);
|
||||
}
|
||||
|
||||
static int is_valid_string_length(const char* str, size_t max_length) {
|
||||
if (!str) return 1; // NULL strings are valid (use defaults)
|
||||
return strlen(str) <= max_length;
|
||||
}
|
||||
|
||||
static int is_valid_pow_mode(const char* mode_str) {
|
||||
if (!mode_str) return 0;
|
||||
|
||||
return (strcasecmp(mode_str, "basic") == 0 ||
|
||||
strcasecmp(mode_str, "strict") == 0 ||
|
||||
strcasecmp(mode_str, "disabled") == 0);
|
||||
}
|
||||
|
||||
static int is_valid_hex_key(const char* key_str) {
|
||||
if (!key_str) return 0;
|
||||
|
||||
// Must be exactly 64 hex characters
|
||||
if (strlen(key_str) != 64) return 0;
|
||||
|
||||
// Must contain only hex characters
|
||||
for (int i = 0; i < 64; i++) {
|
||||
char c = key_str[i];
|
||||
if (!((c >= '0' && c <= '9') ||
|
||||
(c >= 'a' && c <= 'f') ||
|
||||
(c >= 'A' && c <= 'F'))) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Main validation function for configuration fields
|
||||
static int validate_config_field(const char* key, const char* value, char* error_msg, size_t error_size) {
|
||||
if (!key || !value) {
|
||||
snprintf(error_msg, error_size, "key or value is NULL");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Port validation
|
||||
if (strcmp(key, "relay_port") == 0) {
|
||||
if (!is_valid_port(value)) {
|
||||
snprintf(error_msg, error_size, "invalid port number '%s' (must be 1-65535)", value);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Connection limits
|
||||
if (strcmp(key, "max_connections") == 0) {
|
||||
if (!is_valid_positive_integer(value)) {
|
||||
snprintf(error_msg, error_size, "invalid max_connections '%s' (must be positive integer)", value);
|
||||
return -1;
|
||||
}
|
||||
int val = atoi(value);
|
||||
if (val < 1 || val > 10000) {
|
||||
snprintf(error_msg, error_size, "max_connections '%s' out of range (1-10000)", value);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Boolean fields
|
||||
if (strcmp(key, "auth_enabled") == 0 ||
|
||||
strcmp(key, "nip40_expiration_enabled") == 0 ||
|
||||
strcmp(key, "nip40_expiration_strict") == 0 ||
|
||||
strcmp(key, "nip40_expiration_filter") == 0) {
|
||||
if (!is_valid_boolean(value)) {
|
||||
snprintf(error_msg, error_size, "invalid boolean value '%s' for %s", value, key);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// String length validation
|
||||
if (strcmp(key, "relay_description") == 0) {
|
||||
if (!is_valid_string_length(value, RELAY_DESCRIPTION_MAX_LENGTH)) {
|
||||
snprintf(error_msg, error_size, "relay_description too long (max %d characters)", RELAY_DESCRIPTION_MAX_LENGTH);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (strcmp(key, "relay_contact") == 0) {
|
||||
if (!is_valid_string_length(value, RELAY_CONTACT_MAX_LENGTH)) {
|
||||
snprintf(error_msg, error_size, "relay_contact too long (max %d characters)", RELAY_CONTACT_MAX_LENGTH);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (strcmp(key, "relay_software") == 0 ||
|
||||
strcmp(key, "relay_version") == 0) {
|
||||
if (!is_valid_string_length(value, 256)) {
|
||||
snprintf(error_msg, error_size, "%s too long (max 256 characters)", key);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// PoW difficulty validation
|
||||
if (strcmp(key, "pow_min_difficulty") == 0) {
|
||||
if (!is_valid_non_negative_integer(value)) {
|
||||
snprintf(error_msg, error_size, "invalid pow_min_difficulty '%s' (must be non-negative integer)", value);
|
||||
return -1;
|
||||
}
|
||||
int val = atoi(value);
|
||||
if (val > 32) { // 32 is practically impossible
|
||||
snprintf(error_msg, error_size, "pow_min_difficulty '%s' too high (max 32)", value);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// PoW mode validation
|
||||
if (strcmp(key, "pow_mode") == 0) {
|
||||
if (!is_valid_pow_mode(value)) {
|
||||
snprintf(error_msg, error_size, "invalid pow_mode '%s' (must be basic, strict, or disabled)", value);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Time-based validation
|
||||
if (strcmp(key, "nip40_expiration_grace_period") == 0) {
|
||||
if (!is_valid_non_negative_integer(value)) {
|
||||
snprintf(error_msg, error_size, "invalid grace period '%s' (must be non-negative integer)", value);
|
||||
return -1;
|
||||
}
|
||||
int val = atoi(value);
|
||||
if (val > 86400) { // Max 1 day
|
||||
snprintf(error_msg, error_size, "grace period '%s' too long (max 86400 seconds)", value);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Subscription limits
|
||||
if (strcmp(key, "max_subscriptions_per_client") == 0) {
|
||||
if (!is_valid_positive_integer(value)) {
|
||||
snprintf(error_msg, error_size, "invalid max_subscriptions_per_client '%s'", value);
|
||||
return -1;
|
||||
}
|
||||
int val = atoi(value);
|
||||
if (val < 1 || val > 1000) {
|
||||
snprintf(error_msg, error_size, "max_subscriptions_per_client '%s' out of range (1-1000)", value);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (strcmp(key, "max_total_subscriptions") == 0) {
|
||||
if (!is_valid_positive_integer(value)) {
|
||||
snprintf(error_msg, error_size, "invalid max_total_subscriptions '%s'", value);
|
||||
return -1;
|
||||
}
|
||||
int val = atoi(value);
|
||||
if (val < 1 || val > 100000) {
|
||||
snprintf(error_msg, error_size, "max_total_subscriptions '%s' out of range (1-100000)", value);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (strcmp(key, "max_filters_per_subscription") == 0) {
|
||||
if (!is_valid_positive_integer(value)) {
|
||||
snprintf(error_msg, error_size, "invalid max_filters_per_subscription '%s'", value);
|
||||
return -1;
|
||||
}
|
||||
int val = atoi(value);
|
||||
if (val < 1 || val > 100) {
|
||||
snprintf(error_msg, error_size, "max_filters_per_subscription '%s' out of range (1-100)", value);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Event limits
|
||||
if (strcmp(key, "max_event_tags") == 0) {
|
||||
if (!is_valid_positive_integer(value)) {
|
||||
snprintf(error_msg, error_size, "invalid max_event_tags '%s'", value);
|
||||
return -1;
|
||||
}
|
||||
int val = atoi(value);
|
||||
if (val < 1 || val > 1000) {
|
||||
snprintf(error_msg, error_size, "max_event_tags '%s' out of range (1-1000)", value);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (strcmp(key, "max_content_length") == 0) {
|
||||
if (!is_valid_positive_integer(value)) {
|
||||
snprintf(error_msg, error_size, "invalid max_content_length '%s'", value);
|
||||
return -1;
|
||||
}
|
||||
int val = atoi(value);
|
||||
if (val < 100 || val > 1048576) { // 1MB max
|
||||
snprintf(error_msg, error_size, "max_content_length '%s' out of range (100-1048576)", value);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (strcmp(key, "max_message_length") == 0) {
|
||||
if (!is_valid_positive_integer(value)) {
|
||||
snprintf(error_msg, error_size, "invalid max_message_length '%s'", value);
|
||||
return -1;
|
||||
}
|
||||
int val = atoi(value);
|
||||
if (val < 1024 || val > 1048576) { // 1KB to 1MB
|
||||
snprintf(error_msg, error_size, "max_message_length '%s' out of range (1024-1048576)", value);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Performance limits
|
||||
if (strcmp(key, "default_limit") == 0 || strcmp(key, "max_limit") == 0) {
|
||||
if (!is_valid_positive_integer(value)) {
|
||||
snprintf(error_msg, error_size, "invalid %s '%s'", key, value);
|
||||
return -1;
|
||||
}
|
||||
int val = atoi(value);
|
||||
if (val < 1 || val > 50000) {
|
||||
snprintf(error_msg, error_size, "%s '%s' out of range (1-50000)", key, value);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Key validation for relay keys
|
||||
if (strcmp(key, "relay_pubkey") == 0 || strcmp(key, "relay_privkey") == 0) {
|
||||
if (!is_valid_hex_key(value)) {
|
||||
snprintf(error_msg, error_size, "invalid %s format (must be 64-character hex)", key);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Special validation for d tag (relay identifier)
|
||||
if (strcmp(key, "d") == 0) {
|
||||
if (!is_valid_hex_key(value)) {
|
||||
snprintf(error_msg, error_size, "invalid relay identifier 'd' format (must be 64-character hex)");
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// NIP-42 Authentication fields
|
||||
if (strcmp(key, "nip42_auth_required") == 0) {
|
||||
if (!is_valid_boolean(value)) {
|
||||
snprintf(error_msg, error_size, "invalid boolean value '%s' for nip42_auth_required", value);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (strcmp(key, "nip42_auth_required_kinds") == 0) {
|
||||
// Validate comma-separated list of kind numbers
|
||||
if (!value || strlen(value) == 0) {
|
||||
return 0; // Empty list is valid
|
||||
}
|
||||
|
||||
char* value_copy = strdup(value);
|
||||
if (!value_copy) {
|
||||
snprintf(error_msg, error_size, "memory allocation failed for kind validation");
|
||||
return -1;
|
||||
}
|
||||
|
||||
char* token = strtok(value_copy, ",");
|
||||
while (token) {
|
||||
// Trim whitespace
|
||||
while (*token == ' ' || *token == '\t') token++;
|
||||
char* end = token + strlen(token) - 1;
|
||||
while (end > token && (*end == ' ' || *end == '\t')) end--;
|
||||
*(end + 1) = '\0';
|
||||
|
||||
// Validate each kind number
|
||||
if (!is_valid_non_negative_integer(token)) {
|
||||
free(value_copy);
|
||||
snprintf(error_msg, error_size, "invalid kind number '%s' in nip42_auth_required_kinds", token);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int kind = atoi(token);
|
||||
if (kind < 0 || kind > 65535) {
|
||||
free(value_copy);
|
||||
snprintf(error_msg, error_size, "kind number '%s' out of range (0-65535)", token);
|
||||
return -1;
|
||||
}
|
||||
|
||||
token = strtok(NULL, ",");
|
||||
}
|
||||
|
||||
free(value_copy);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (strcmp(key, "nip42_challenge_expiration") == 0) {
|
||||
if (!is_valid_positive_integer(value)) {
|
||||
snprintf(error_msg, error_size, "invalid nip42_challenge_expiration '%s' (must be positive integer)", value);
|
||||
return -1;
|
||||
}
|
||||
int val = atoi(value);
|
||||
if (val < 60 || val > 3600) { // 1 minute to 1 hour
|
||||
snprintf(error_msg, error_size, "nip42_challenge_expiration '%s' out of range (60-3600 seconds)", value);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (strcmp(key, "nip42_challenge_window") == 0) {
|
||||
if (!is_valid_positive_integer(value)) {
|
||||
snprintf(error_msg, error_size, "invalid nip42_challenge_window '%s' (must be positive integer)", value);
|
||||
return -1;
|
||||
}
|
||||
int val = atoi(value);
|
||||
if (val < 300 || val > 7200) { // 5 minutes to 2 hours
|
||||
snprintf(error_msg, error_size, "nip42_challenge_window '%s' out of range (300-7200 seconds)", value);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (strcmp(key, "nip42_max_auth_events") == 0) {
|
||||
if (!is_valid_positive_integer(value)) {
|
||||
snprintf(error_msg, error_size, "invalid nip42_max_auth_events '%s' (must be positive integer)", value);
|
||||
return -1;
|
||||
}
|
||||
int val = atoi(value);
|
||||
if (val < 10 || val > 10000) { // 10 to 10,000 auth events
|
||||
snprintf(error_msg, error_size, "nip42_max_auth_events '%s' out of range (10-10000)", value);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Unknown field - log warning but allow
|
||||
log_warning("Unknown configuration field");
|
||||
printf(" Field: %s = %s\n", key, value);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Validate all fields in a configuration event
|
||||
static int validate_configuration_event_fields(const cJSON* event, char* error_msg, size_t error_size) {
|
||||
if (!event) {
|
||||
snprintf(error_msg, error_size, "null configuration event");
|
||||
return -1;
|
||||
}
|
||||
|
||||
log_info("Validating configuration event fields...");
|
||||
|
||||
cJSON* tags = cJSON_GetObjectItem(event, "tags");
|
||||
if (!tags || !cJSON_IsArray(tags)) {
|
||||
snprintf(error_msg, error_size, "missing or invalid tags array");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int validated_fields = 0;
|
||||
int validation_errors = 0;
|
||||
char field_error[512];
|
||||
|
||||
cJSON* tag = NULL;
|
||||
cJSON_ArrayForEach(tag, tags) {
|
||||
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
|
||||
cJSON* tag_key = cJSON_GetArrayItem(tag, 0);
|
||||
cJSON* tag_value = cJSON_GetArrayItem(tag, 1);
|
||||
|
||||
if (tag_key && tag_value &&
|
||||
cJSON_IsString(tag_key) && cJSON_IsString(tag_value)) {
|
||||
|
||||
const char* key = cJSON_GetStringValue(tag_key);
|
||||
const char* value = cJSON_GetStringValue(tag_value);
|
||||
|
||||
if (validate_config_field(key, value, field_error, sizeof(field_error)) != 0) {
|
||||
// Safely truncate the error message if needed
|
||||
size_t prefix_len = strlen("field validation failed: ");
|
||||
size_t available_space = error_size > prefix_len ? error_size - prefix_len - 1 : 0;
|
||||
|
||||
if (available_space > 0) {
|
||||
snprintf(error_msg, error_size, "field validation failed: %.*s",
|
||||
(int)available_space, field_error);
|
||||
} else {
|
||||
strncpy(error_msg, "field validation failed", error_size - 1);
|
||||
error_msg[error_size - 1] = '\0';
|
||||
}
|
||||
|
||||
log_error("Configuration field validation failed");
|
||||
printf(" Field: %s = %s\n", key, value);
|
||||
printf(" Error: %s\n", field_error);
|
||||
validation_errors++;
|
||||
return -1; // Stop on first error
|
||||
} else {
|
||||
validated_fields++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (validation_errors > 0) {
|
||||
char summary[256];
|
||||
snprintf(summary, sizeof(summary), "%d configuration fields failed validation", validation_errors);
|
||||
log_error(summary);
|
||||
return -1;
|
||||
}
|
||||
|
||||
char success_msg[256];
|
||||
snprintf(success_msg, sizeof(success_msg), "%d configuration fields validated successfully", validated_fields);
|
||||
log_success(success_msg);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int process_configuration_event(const cJSON* event) {
|
||||
if (!event) {
|
||||
log_error("Invalid configuration event");
|
||||
@@ -690,6 +1384,14 @@ int process_configuration_event(const cJSON* event) {
|
||||
|
||||
log_success("Configuration event structure and signature validated successfully");
|
||||
|
||||
// NEW: Validate configuration field values
|
||||
char validation_error[512];
|
||||
if (validate_configuration_event_fields(event, validation_error, sizeof(validation_error)) != 0) {
|
||||
log_error("Configuration field validation failed");
|
||||
printf(" Validation error: %s\n", validation_error);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Store in database
|
||||
if (store_config_event_in_database(event) != 0) {
|
||||
log_error("Failed to store configuration event");
|
||||
@@ -702,7 +1404,7 @@ int process_configuration_event(const cJSON* event) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
log_success("Configuration event processed successfully");
|
||||
log_success("Configuration event processed successfully with field validation");
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
21
src/config.h
21
src/config.h
@@ -32,6 +32,13 @@ typedef struct {
|
||||
char config_file_path[512]; // Temporary for compatibility
|
||||
} config_manager_t;
|
||||
|
||||
// Command line options structure for first-time startup
|
||||
typedef struct {
|
||||
int port_override; // -1 = not set, >0 = port value
|
||||
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
|
||||
} cli_options_t;
|
||||
|
||||
// Global configuration manager
|
||||
extern config_manager_t g_config_manager;
|
||||
|
||||
@@ -62,7 +69,7 @@ int get_config_bool(const char* key, int default_value);
|
||||
|
||||
// First-time startup functions
|
||||
int is_first_time_startup(void);
|
||||
int first_time_startup_sequence(void);
|
||||
int first_time_startup_sequence(const cli_options_t* cli_options);
|
||||
int startup_existing_relay(const char* relay_pubkey);
|
||||
|
||||
// Configuration application functions
|
||||
@@ -70,7 +77,17 @@ int apply_configuration_from_event(const cJSON* event);
|
||||
int apply_runtime_config_handlers(const cJSON* old_event, const cJSON* new_event);
|
||||
|
||||
// Utility functions
|
||||
char** find_existing_nrdb_files(void);
|
||||
char** find_existing_db_files(void);
|
||||
char* extract_pubkey_from_filename(const char* filename);
|
||||
|
||||
// Secure relay private key storage functions
|
||||
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);
|
||||
|
||||
#endif /* CONFIG_H */
|
||||
@@ -2,6 +2,7 @@
|
||||
#define DEFAULT_CONFIG_EVENT_H
|
||||
|
||||
#include <cjson/cJSON.h>
|
||||
#include "config.h" // For cli_options_t definition
|
||||
|
||||
/*
|
||||
* Default Configuration Event Template
|
||||
@@ -22,6 +23,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"},
|
||||
@@ -63,6 +70,7 @@ static const struct {
|
||||
// Function to create default configuration event
|
||||
cJSON* create_default_config_event(const unsigned char* admin_privkey_bytes,
|
||||
const char* relay_privkey_hex,
|
||||
const char* relay_pubkey_hex);
|
||||
const char* relay_pubkey_hex,
|
||||
const cli_options_t* cli_options);
|
||||
|
||||
#endif /* DEFAULT_CONFIG_EVENT_H */
|
||||
871
src/main.c
871
src/main.c
File diff suppressed because it is too large
Load Diff
1174
src/request_validator.c
Normal file
1174
src/request_validator.c
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,12 @@
|
||||
/* Embedded SQL Schema for C Nostr Relay
|
||||
* Generated from db/schema.sql - Do not edit manually
|
||||
* Schema Version: 4
|
||||
* Schema Version: 6
|
||||
*/
|
||||
#ifndef SQL_SCHEMA_H
|
||||
#define SQL_SCHEMA_H
|
||||
|
||||
/* Schema version constant */
|
||||
#define EMBEDDED_SCHEMA_VERSION "4"
|
||||
#define EMBEDDED_SCHEMA_VERSION "6"
|
||||
|
||||
/* Embedded SQL schema as C string literal */
|
||||
static const char* const EMBEDDED_SCHEMA_SQL =
|
||||
@@ -15,7 +15,7 @@ static const char* const EMBEDDED_SCHEMA_SQL =
|
||||
-- Event-based configuration system using kind 33334 Nostr events\n\
|
||||
\n\
|
||||
-- Schema version tracking\n\
|
||||
PRAGMA user_version = 4;\n\
|
||||
PRAGMA user_version = 6;\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', '4'),\n\
|
||||
('description', 'Event-based Nostr relay schema with kind 33334 configuration events'),\n\
|
||||
('version', '6'),\n\
|
||||
('description', 'Event-based Nostr relay schema with secure relay private key storage'),\n\
|
||||
('created_at', strftime('%s', 'now'));\n\
|
||||
\n\
|
||||
-- Helper views for common queries\n\
|
||||
@@ -128,6 +128,32 @@ BEGIN\n\
|
||||
AND id != NEW.id;\n\
|
||||
END;\n\
|
||||
\n\
|
||||
-- Relay Private Key Secure Storage\n\
|
||||
-- Stores the relay's private key separately from public configuration\n\
|
||||
CREATE TABLE relay_seckey (\n\
|
||||
private_key_hex TEXT NOT NULL CHECK (length(private_key_hex) = 64),\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\
|
||||
-- Persistent Subscriptions Logging Tables (Phase 2)\n\
|
||||
-- Optional database logging for subscription analytics and debugging\n\
|
||||
\n\
|
||||
|
||||
191
test_relay.js
Normal file
191
test_relay.js
Normal file
@@ -0,0 +1,191 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Import the nostr-tools bundle
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { TextEncoder, TextDecoder } = require('util');
|
||||
|
||||
// Load nostr.bundle.js
|
||||
const bundlePath = path.join(__dirname, 'api', 'nostr.bundle.js');
|
||||
if (!fs.existsSync(bundlePath)) {
|
||||
console.error('nostr.bundle.js not found at:', bundlePath);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Read and eval the bundle to get NostrTools
|
||||
const bundleCode = fs.readFileSync(bundlePath, 'utf8');
|
||||
const vm = require('vm');
|
||||
|
||||
// Create a more complete browser-like context
|
||||
const context = {
|
||||
window: {},
|
||||
global: {},
|
||||
console: console,
|
||||
setTimeout: setTimeout,
|
||||
setInterval: setInterval,
|
||||
clearTimeout: clearTimeout,
|
||||
clearInterval: clearInterval,
|
||||
Buffer: Buffer,
|
||||
process: process,
|
||||
require: require,
|
||||
module: module,
|
||||
exports: exports,
|
||||
__dirname: __dirname,
|
||||
__filename: __filename,
|
||||
TextEncoder: TextEncoder,
|
||||
TextDecoder: TextDecoder,
|
||||
crypto: require('crypto'),
|
||||
atob: (str) => Buffer.from(str, 'base64').toString('binary'),
|
||||
btoa: (str) => Buffer.from(str, 'binary').toString('base64'),
|
||||
fetch: require('https').get // Basic polyfill, might need adjustment
|
||||
};
|
||||
|
||||
// Add common browser globals to window
|
||||
context.window.TextEncoder = TextEncoder;
|
||||
context.window.TextDecoder = TextDecoder;
|
||||
context.window.crypto = context.crypto;
|
||||
context.window.atob = context.atob;
|
||||
context.window.btoa = context.btoa;
|
||||
context.window.console = console;
|
||||
context.window.setTimeout = setTimeout;
|
||||
context.window.setInterval = setInterval;
|
||||
context.window.clearTimeout = clearTimeout;
|
||||
context.window.clearInterval = clearInterval;
|
||||
|
||||
// Execute bundle in context
|
||||
vm.createContext(context);
|
||||
try {
|
||||
vm.runInContext(bundleCode, context);
|
||||
} catch (error) {
|
||||
console.error('Error loading nostr bundle:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Debug what's available in the context
|
||||
console.log('Bundle loaded, checking available objects...');
|
||||
console.log('context.window keys:', Object.keys(context.window));
|
||||
console.log('context.global keys:', Object.keys(context.global));
|
||||
|
||||
// Try different ways to access NostrTools
|
||||
let NostrTools = context.window.NostrTools || context.NostrTools || context.global.NostrTools;
|
||||
|
||||
// If still not found, look for other possible exports
|
||||
if (!NostrTools) {
|
||||
console.log('Looking for alternative exports...');
|
||||
|
||||
// Check if it's under a different name
|
||||
const windowKeys = Object.keys(context.window);
|
||||
const possibleExports = windowKeys.filter(key =>
|
||||
key.toLowerCase().includes('nostr') ||
|
||||
key.toLowerCase().includes('tools') ||
|
||||
typeof context.window[key] === 'object'
|
||||
);
|
||||
|
||||
console.log('Possible nostr-related exports:', possibleExports);
|
||||
|
||||
// Try the first one that looks promising
|
||||
if (possibleExports.length > 0) {
|
||||
NostrTools = context.window[possibleExports[0]];
|
||||
console.log(`Trying ${possibleExports[0]}:`, typeof NostrTools);
|
||||
}
|
||||
}
|
||||
|
||||
if (!NostrTools) {
|
||||
console.error('NostrTools not found in bundle');
|
||||
console.error('Bundle might not be compatible with Node.js or needs different loading approach');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('NostrTools loaded successfully');
|
||||
console.log('Available methods:', Object.keys(NostrTools));
|
||||
|
||||
async function testRelay() {
|
||||
const relayUrl = 'ws://127.0.0.1:8888';
|
||||
|
||||
try {
|
||||
console.log('\n=== Testing Relay Connection ===');
|
||||
console.log('Relay URL:', relayUrl);
|
||||
|
||||
// Create SimplePool
|
||||
const pool = new NostrTools.SimplePool();
|
||||
console.log('SimplePool created');
|
||||
|
||||
// Test 1: Query for kind 1 events
|
||||
console.log('\n--- Test 1: Kind 1 Events ---');
|
||||
const kind1Events = await pool.querySync([relayUrl], {
|
||||
kinds: [1],
|
||||
limit: 5
|
||||
});
|
||||
|
||||
console.log(`Found ${kind1Events.length} kind 1 events`);
|
||||
kind1Events.forEach((event, index) => {
|
||||
console.log(`Event ${index + 1}:`, {
|
||||
id: event.id,
|
||||
kind: event.kind,
|
||||
pubkey: event.pubkey.substring(0, 16) + '...',
|
||||
created_at: new Date(event.created_at * 1000).toISOString(),
|
||||
content: event.content.substring(0, 50) + (event.content.length > 50 ? '...' : '')
|
||||
});
|
||||
});
|
||||
|
||||
// Test 2: Query for kind 33334 events (configuration)
|
||||
console.log('\n--- Test 2: Kind 33334 Events (Configuration) ---');
|
||||
const configEvents = await pool.querySync([relayUrl], {
|
||||
kinds: [33334],
|
||||
limit: 10
|
||||
});
|
||||
|
||||
console.log(`Found ${configEvents.length} kind 33334 events`);
|
||||
configEvents.forEach((event, index) => {
|
||||
console.log(`Config Event ${index + 1}:`, {
|
||||
id: event.id,
|
||||
kind: event.kind,
|
||||
pubkey: event.pubkey.substring(0, 16) + '...',
|
||||
created_at: new Date(event.created_at * 1000).toISOString(),
|
||||
tags: event.tags.length,
|
||||
content: event.content
|
||||
});
|
||||
|
||||
// Show some tags
|
||||
if (event.tags.length > 0) {
|
||||
console.log(' Sample tags:');
|
||||
event.tags.slice(0, 5).forEach(tag => {
|
||||
console.log(` ${tag[0]}: ${tag[1] || ''}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Test 3: Query for any events
|
||||
console.log('\n--- Test 3: Any Events (limit 3) ---');
|
||||
const anyEvents = await pool.querySync([relayUrl], {
|
||||
limit: 3
|
||||
});
|
||||
|
||||
console.log(`Found ${anyEvents.length} total events`);
|
||||
anyEvents.forEach((event, index) => {
|
||||
console.log(`Event ${index + 1}:`, {
|
||||
id: event.id,
|
||||
kind: event.kind,
|
||||
pubkey: event.pubkey.substring(0, 16) + '...',
|
||||
created_at: new Date(event.created_at * 1000).toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
// Clean up
|
||||
pool.close([relayUrl]);
|
||||
console.log('\n=== Test Complete ===');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Relay test failed:', error.message);
|
||||
console.error('Stack:', error.stack);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the test
|
||||
testRelay().then(() => {
|
||||
console.log('Test finished');
|
||||
process.exit(0);
|
||||
}).catch((error) => {
|
||||
console.error('Test failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -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 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"
|
||||
93
tests/nip42_test.log
Normal file
93
tests/nip42_test.log
Normal file
@@ -0,0 +1,93 @@
|
||||
=== NIP-42 Authentication Test Started ===
|
||||
2025-09-13 08:48:02 - Starting NIP-42 authentication tests
|
||||
[34m[1m[INFO][0m === Starting NIP-42 Authentication Tests ===
|
||||
[34m[1m[INFO][0m Checking dependencies...
|
||||
[32m[1m[SUCCESS][0m Dependencies check complete
|
||||
[34m[1m[INFO][0m Test 1: Checking NIP-42 support in relay info
|
||||
[32m[1m[SUCCESS][0m NIP-42 is advertised in supported NIPs
|
||||
2025-09-13 08:48:02 - Supported NIPs: 1,9,11,13,15,20,40,42
|
||||
[34m[1m[INFO][0m Test 2: Testing AUTH challenge generation
|
||||
[34m[1m[INFO][0m Found admin private key, configuring NIP-42 authentication...
|
||||
[33m[1m[WARNING][0m Failed to create configuration event - proceeding with manual test
|
||||
[34m[1m[INFO][0m Test 3: Testing complete NIP-42 authentication flow
|
||||
[34m[1m[INFO][0m Generated test keypair: test_pubkey
|
||||
[34m[1m[INFO][0m Attempting to publish event without authentication...
|
||||
[34m[1m[INFO][0m Publishing test event to relay...
|
||||
2025-09-13 08:48:03 - Event publish result: connecting to ws://localhost:8888... ok.
|
||||
{"kind":1,"id":"c42a8cbdd1cc6ea3e7fd060919c57386aef0c35da272ba2fa34b45f80934cfca","pubkey":"d0111448b3bd0da6aa699b92163f684291bb43bc213aa54a2ee726c2acde76e8","created_at":1757767683,"tags":[],"content":"NIP-42 test event - should require auth","sig":"d2a2c7efc00e06d8d8582fa05b2ec8cb96979525770dff9ef36a91df6d53807c86115581de2d6058d7d64eebe3b7d7404cc03dbb2ad1e91d140283703c2dec53"}
|
||||
publishing to ws://localhost:8888... success.
|
||||
[32m[1m[SUCCESS][0m Relay requested authentication as expected
|
||||
[34m[1m[INFO][0m Test 4: Testing WebSocket AUTH message handling
|
||||
[34m[1m[INFO][0m Testing WebSocket connection and AUTH message...
|
||||
[34m[1m[INFO][0m Sending test message via WebSocket...
|
||||
2025-09-13 08:48:03 - WebSocket response:
|
||||
[34m[1m[INFO][0m No AUTH challenge in WebSocket response
|
||||
[34m[1m[INFO][0m Test 5: Testing NIP-42 configuration options
|
||||
[34m[1m[INFO][0m Retrieving current relay configuration...
|
||||
[32m[1m[SUCCESS][0m Retrieved configuration events from relay
|
||||
[32m[1m[SUCCESS][0m Found NIP-42 configuration:
|
||||
2025-09-13 08:48:04 - nip42_auth_required_events=false
|
||||
2025-09-13 08:48:04 - nip42_auth_required_subscriptions=false
|
||||
2025-09-13 08:48:04 - nip42_auth_required_kinds=4,14
|
||||
2025-09-13 08:48:04 - nip42_challenge_expiration=600
|
||||
[34m[1m[INFO][0m Test 6: Testing NIP-42 performance and stability
|
||||
[34m[1m[INFO][0m Testing multiple authentication attempts...
|
||||
2025-09-13 08:48:05 - Attempt 1: .271641300s - connecting to ws://localhost:8888... ok.
|
||||
{"kind":1,"id":"916049dbd6835443e8fd553bd12a37ef03060a01fedb099b414ea2cc18b597eb","pubkey":"b383f405d81860ec9b0eebf88612093ab18dc6abd322639b19ac79969599c8c4","created_at":1757767685,"tags":[],"content":"Performance test event 1","sig":"b04e0b38bbb49e0aa3c8a69530071bb08d917c4ba12eae38045a487c43e83f6dc1389ac4640453b0492d9c991df37f71e25ef501fd48c4c11c878e6cb3fa7a84"}
|
||||
publishing to ws://localhost:8888... success.
|
||||
2025-09-13 08:48:05 - Attempt 2: .259343520s - connecting to ws://localhost:8888... ok.
|
||||
{"kind":1,"id":"e4495a56ec6f1ba2759eabbf0128aec615c53acf3e4720be7726dcd7163da703","pubkey":"b383f405d81860ec9b0eebf88612093ab18dc6abd322639b19ac79969599c8c4","created_at":1757767685,"tags":[],"content":"Performance test event 2","sig":"d1efe3f576eeded4e292ec22f2fea12296fa17ed2f87a8cd2dde0444b594ef55f7d74b680aeca11295a16397df5ccc53a938533947aece27efb965e6c643b62c"}
|
||||
publishing to ws://localhost:8888... success.
|
||||
2025-09-13 08:48:06 - Attempt 3: .221167032s - connecting to ws://localhost:8888... ok.
|
||||
{"kind":1,"id":"55035b4c95a2c93a169236c7f5f5bd627838ec13522c88cf82d8b55516560cd9","pubkey":"b383f405d81860ec9b0eebf88612093ab18dc6abd322639b19ac79969599c8c4","created_at":1757767686,"tags":[],"content":"Performance test event 3","sig":"4bd581580a5a2416e6a9af44c055333635832dbf21793517f16100f1366c73437659545a8a712dcc4623a801b9deccd372b36b658309e7102a4300c3f481facb"}
|
||||
publishing to ws://localhost:8888... success.
|
||||
2025-09-13 08:48:06 - Attempt 4: .260219496s - connecting to ws://localhost:8888... ok.
|
||||
{"kind":1,"id":"58dee587a1a0f085ff44441b3074f5ff42715088ee24e694107100df3c63ff2b","pubkey":"b383f405d81860ec9b0eebf88612093ab18dc6abd322639b19ac79969599c8c4","created_at":1757767686,"tags":[],"content":"Performance test event 4","sig":"b6174b0c56138466d3bb228ef2ced1d917f7253b76c624235fa3b661c9fa109c78ae557c4ddaf0e6232aa597608916f0dfba1c192f8b90ffb819c36ac1e4e516"}
|
||||
publishing to ws://localhost:8888... success.
|
||||
2025-09-13 08:48:07 - Attempt 5: .260125188s - connecting to ws://localhost:8888... ok.
|
||||
{"kind":1,"id":"b8069c80f98fff3780eaeb605baf1a5818c9ab05185c1776a28469d2b0b32c6a","pubkey":"b383f405d81860ec9b0eebf88612093ab18dc6abd322639b19ac79969599c8c4","created_at":1757767687,"tags":[],"content":"Performance test event 5","sig":"5130d3a0c778728747b12aae77f2516db5b055d8ec43f413a4b117fcadb6025a49b6f602307bbe758bd97557e326e8735631fd03dc45c9296509e94aa305adf2"}
|
||||
publishing to ws://localhost:8888... success.
|
||||
[32m[1m[SUCCESS][0m Performance test completed: 5/5 successful responses
|
||||
[34m[1m[INFO][0m Test 7: Testing kind-specific NIP-42 authentication requirements
|
||||
[34m[1m[INFO][0m Generated test keypair for kind-specific tests: test_pubkey
|
||||
[34m[1m[INFO][0m Testing kind 1 event (regular note) - should work without authentication...
|
||||
2025-09-13 08:48:08 - Kind 1 event result: connecting to ws://localhost:8888... ok.
|
||||
{"kind":1,"id":"f2ac02a5290db3797c0b7b38435920d5db593d333e582454d8ed32da4c141b74","pubkey":"da031504ff61656d1829f723c52f526d7591400fb9e2aecb7b4ef5aeeea66fc7","created_at":1757767688,"tags":[],"content":"Regular note - should not require auth","sig":"8e4272d9cb258fc4b140eb8e8c2e802c3e8b62e34c17c9e545d83c68dfb86ffd2cdd4a8153660b663a46906459aa67719257ac263f21d1f8a6185806e055dcfd"}
|
||||
publishing to ws://localhost:8888... success.
|
||||
[32m[1m[SUCCESS][0m Kind 1 event accepted without authentication (correct behavior)
|
||||
[34m[1m[INFO][0m Testing kind 4 event (direct message) - should require authentication...
|
||||
2025-09-13 08:48:18 - Kind 4 event result: connecting to ws://localhost:8888... ok.
|
||||
{"kind":4,"id":"935af23e2bf7efd324d86a0c82631e5ebe492edf21920ed0f548faa73a18ac1d","pubkey":"da031504ff61656d1829f723c52f526d7591400fb9e2aecb7b4ef5aeeea66fc7","created_at":1757767688,"tags":[["p,test_pubkey"]],"content":"This is a direct message - should require auth","sig":"b2b86ee394b41505ddbd787c22f4223665770d84a21dd03e74bf4e8fa879ff82dd6b1f7d6921d93f8d89787102c3dc3012e6270d66ca5b5d4b87f1a545481e76"}
|
||||
publishing to ws://localhost:8888...
|
||||
[32m[1m[SUCCESS][0m Kind 4 event requested authentication (correct behavior for DMs)
|
||||
[34m[1m[INFO][0m Testing kind 14 event (chat message) - should require authentication...
|
||||
2025-09-13 08:48:28 - Kind 14 event result: connecting to ws://localhost:8888... ok.
|
||||
{"kind":14,"id":"aeb1ac58dd465c90ce5a70c7b16e3cc32fae86c221bb2e86ca29934333604669","pubkey":"da031504ff61656d1829f723c52f526d7591400fb9e2aecb7b4ef5aeeea66fc7","created_at":1757767698,"tags":[["p,test_pubkey"]],"content":"Chat message - should require auth","sig":"24e23737e6684e4ef01c08d72304e6f235ce75875b94b37460065f9ead986438435585818ba104e7f78f14345406b5d03605c925042e9c06fed8c99369cd8694"}
|
||||
publishing to ws://localhost:8888...
|
||||
[32m[1m[SUCCESS][0m Kind 14 event requested authentication (correct behavior for DMs)
|
||||
[34m[1m[INFO][0m Testing other event kinds - should work without authentication...
|
||||
2025-09-13 08:48:29 - Kind 0 event result: connecting to ws://localhost:8888... ok.
|
||||
{"kind":0,"id":"3b2cc834dd874ebbe07c2da9e41c07b3f0c61a57b4d6b7299c2243dbad29f2ca","pubkey":"da031504ff61656d1829f723c52f526d7591400fb9e2aecb7b4ef5aeeea66fc7","created_at":1757767709,"tags":[],"content":"Test event kind 0 - should not require auth","sig":"4f2016fde84d72cf5a5aa4c0ec5de677ef06c7971ca2dd756b02a94c47604fae1c67254703a2df3d17b13fee2d9c45661b76086f29ac93820a4c062fc52dea74"}
|
||||
publishing to ws://localhost:8888... success.
|
||||
[32m[1m[SUCCESS][0m Kind 0 event accepted without authentication (correct)
|
||||
2025-09-13 08:48:29 - Kind 3 event result: connecting to ws://localhost:8888... ok.
|
||||
{"kind":3,"id":"6e1ea0b1cbf342feea030fa39226c316e730c5d333fa8333495748afd386ec80","pubkey":"da031504ff61656d1829f723c52f526d7591400fb9e2aecb7b4ef5aeeea66fc7","created_at":1757767709,"tags":[],"content":"Test event kind 3 - should not require auth","sig":"e5f66c5f022497f8888f003a8bfbb5e807a2520d314c80889548efa267f9d6de28d5ee7b0588cc8660f2963ab44e530c8a74d71a227148e5a6843fcef4de2197"}
|
||||
publishing to ws://localhost:8888... success.
|
||||
[32m[1m[SUCCESS][0m Kind 3 event accepted without authentication (correct)
|
||||
2025-09-13 08:48:30 - Kind 7 event result: connecting to ws://localhost:8888... ok.
|
||||
{"kind":7,"id":"a64466b9899cad257313e2dced357fd3f87f40bd7e13e29372689aae7c718919","pubkey":"da031504ff61656d1829f723c52f526d7591400fb9e2aecb7b4ef5aeeea66fc7","created_at":1757767710,"tags":[],"content":"Test event kind 7 - should not require auth","sig":"78d18bcb0c2b11b4e2b74bcdfb140564b4563945e983014a279977356e50b57f3c5a262fa55de26dbd4c8d8b9f5beafbe21af869be64079f54a712284f03d9ac"}
|
||||
publishing to ws://localhost:8888... success.
|
||||
[32m[1m[SUCCESS][0m Kind 7 event accepted without authentication (correct)
|
||||
[34m[1m[INFO][0m Kind-specific authentication test completed
|
||||
[34m[1m[INFO][0m === NIP-42 Test Results Summary ===
|
||||
[32m[1m[SUCCESS][0m Dependencies: PASS
|
||||
[32m[1m[SUCCESS][0m NIP-42 Support: PASS
|
||||
[32m[1m[SUCCESS][0m Auth Challenge: PASS
|
||||
[32m[1m[SUCCESS][0m Auth Flow: PASS
|
||||
[32m[1m[SUCCESS][0m WebSocket AUTH: PASS
|
||||
[32m[1m[SUCCESS][0m Configuration: PASS
|
||||
[32m[1m[SUCCESS][0m Performance: PASS
|
||||
[32m[1m[SUCCESS][0m Kind-Specific Auth: PASS
|
||||
[32m[1m[SUCCESS][0m All NIP-42 tests completed successfully!
|
||||
[32m[1m[SUCCESS][0m NIP-42 authentication implementation is working correctly
|
||||
[34m[1m[INFO][0m === NIP-42 Authentication Tests Complete ===
|
||||
Reference in New Issue
Block a user