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/
|
dev-config/
|
||||||
db/
|
db/
|
||||||
copy_executable_local.sh
|
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
|
BUILD_DIR = build
|
||||||
|
|
||||||
# Source files
|
# 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
|
NOSTR_CORE_LIB = nostr_core_lib/libnostr_core_x64.a
|
||||||
|
|
||||||
# Architecture detection
|
# 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.
|
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
|
## 📜 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-20: Command Results
|
||||||
- [x] NIP-33: Parameterized Replaceable Events
|
- [x] NIP-33: Parameterized Replaceable Events
|
||||||
- [x] NIP-40: Expiration Timestamp
|
- [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-45: Counting results
|
||||||
- [ ] NIP-50: Keywords filter
|
- [ ] NIP-50: Keywords filter
|
||||||
- [ ] NIP-70: Protected Events
|
- [ ] 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
|
# Parse command line arguments
|
||||||
PRESERVE_DATABASE=false
|
PRESERVE_DATABASE=false
|
||||||
HELP=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
|
while [[ $# -gt 0 ]]; do
|
||||||
case $1 in
|
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
|
PRESERVE_DATABASE=true
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
|
--test-keys|-t)
|
||||||
|
USE_TEST_KEYS=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
--help|-h)
|
--help|-h)
|
||||||
HELP=true
|
HELP=true
|
||||||
shift
|
shift
|
||||||
@@ -27,23 +83,53 @@ while [[ $# -gt 0 ]]; do
|
|||||||
esac
|
esac
|
||||||
done
|
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
|
# Show help
|
||||||
if [ "$HELP" = true ]; then
|
if [ "$HELP" = true ]; then
|
||||||
echo "Usage: $0 [OPTIONS]"
|
echo "Usage: $0 [OPTIONS]"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Options:"
|
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 " --help, -h Show this help message"
|
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 ""
|
||||||
echo "Event-Based Configuration:"
|
echo "Event-Based Configuration:"
|
||||||
echo " This relay now uses event-based configuration stored directly in the database."
|
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 " 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 ""
|
||||||
echo "Examples:"
|
echo "Examples:"
|
||||||
echo " $0 # Fresh start with new keys (default)"
|
echo " $0 # Fresh start with random keys"
|
||||||
echo " $0 -p # Preserve existing database and 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 ""
|
||||||
|
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 "Default behavior: Deletes existing database files to start fresh with new keys"
|
||||||
echo " for development purposes"
|
echo " for development purposes"
|
||||||
exit 0
|
exit 0
|
||||||
@@ -51,15 +137,22 @@ fi
|
|||||||
|
|
||||||
# Handle database file cleanup for fresh start
|
# Handle database file cleanup for fresh start
|
||||||
if [ "$PRESERVE_DATABASE" = false ]; then
|
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..."
|
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"
|
echo "✓ Database files removed - will generate new keys and database"
|
||||||
else
|
else
|
||||||
echo "No existing database found - will generate fresh setup"
|
echo "No existing database found - will generate fresh setup"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo "Preserving existing database files as requested"
|
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
|
fi
|
||||||
|
|
||||||
# Clean up legacy files that are no longer used
|
# 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..."
|
echo "Building project..."
|
||||||
make clean all
|
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
|
# Check if build was successful
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo "ERROR: Build failed. Cannot restart relay."
|
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 "Starting relay server..."
|
||||||
echo "Debug: Current processes: $(ps aux | grep 'c_relay_' | grep -v grep || echo 'None')"
|
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)
|
# Build command line arguments for relay binary
|
||||||
$BINARY_PATH > relay.log 2>&1 &
|
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=$!
|
RELAY_PID=$!
|
||||||
|
# Change back to original directory
|
||||||
|
cd ..
|
||||||
|
|
||||||
echo "Started with PID: $RELAY_PID"
|
echo "Started with PID: $RELAY_PID"
|
||||||
|
|
||||||
@@ -142,7 +275,34 @@ sleep 3
|
|||||||
if ps -p "$RELAY_PID" >/dev/null 2>&1; then
|
if ps -p "$RELAY_PID" >/dev/null 2>&1; then
|
||||||
echo "Relay started successfully!"
|
echo "Relay started successfully!"
|
||||||
echo "PID: $RELAY_PID"
|
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 "Log file: relay.log"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
|||||||
774
src/config.c
774
src/config.c
@@ -1,3 +1,4 @@
|
|||||||
|
#define _GNU_SOURCE
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "default_config_event.h"
|
#include "default_config_event.h"
|
||||||
#include "../nostr_core_lib/nostr_core/nostr_core.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)
|
// Cache for initial configuration event (before database is initialized)
|
||||||
static cJSON* g_pending_config_event = NULL;
|
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
|
// UTILITY FUNCTIONS
|
||||||
// ================================
|
// ================================
|
||||||
|
|
||||||
char** find_existing_nrdb_files(void) {
|
char** find_existing_db_files(void) {
|
||||||
DIR *dir;
|
DIR *dir;
|
||||||
struct dirent *entry;
|
struct dirent *entry;
|
||||||
char **files = NULL;
|
char **files = NULL;
|
||||||
@@ -44,9 +48,9 @@ char** find_existing_nrdb_files(void) {
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count .nrdb files
|
// Count .db files
|
||||||
while ((entry = readdir(dir)) != NULL) {
|
while ((entry = readdir(dir)) != NULL) {
|
||||||
if (strstr(entry->d_name, ".nrdb") != NULL) {
|
if (strstr(entry->d_name, ".db") != NULL) {
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,7 +71,7 @@ char** find_existing_nrdb_files(void) {
|
|||||||
// Store filenames
|
// Store filenames
|
||||||
int i = 0;
|
int i = 0;
|
||||||
while ((entry = readdir(dir)) != NULL && i < count) {
|
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);
|
files[i] = malloc(strlen(entry->d_name) + 1);
|
||||||
if (files[i]) {
|
if (files[i]) {
|
||||||
strcpy(files[i], entry->d_name);
|
strcpy(files[i], entry->d_name);
|
||||||
@@ -84,8 +88,8 @@ char** find_existing_nrdb_files(void) {
|
|||||||
char* extract_pubkey_from_filename(const char* filename) {
|
char* extract_pubkey_from_filename(const char* filename) {
|
||||||
if (!filename) return NULL;
|
if (!filename) return NULL;
|
||||||
|
|
||||||
// Find .nrdb extension
|
// Find .db extension
|
||||||
const char* dot = strstr(filename, ".nrdb");
|
const char* dot = strstr(filename, ".db");
|
||||||
if (!dot) return NULL;
|
if (!dot) return NULL;
|
||||||
|
|
||||||
// Calculate pubkey length
|
// Calculate pubkey length
|
||||||
@@ -107,10 +111,10 @@ char* get_database_name_from_relay_pubkey(const char* relay_pubkey) {
|
|||||||
return NULL;
|
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;
|
if (!db_name) return NULL;
|
||||||
|
|
||||||
sprintf(db_name, "%s.nrdb", relay_pubkey);
|
sprintf(db_name, "%s.db", relay_pubkey);
|
||||||
return db_name;
|
return db_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,12 +341,89 @@ int get_config_bool(const char* key, int default_value) {
|
|||||||
return 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
|
// FIRST-TIME STARTUP FUNCTIONS
|
||||||
// ================================
|
// ================================
|
||||||
|
|
||||||
int is_first_time_startup(void) {
|
int is_first_time_startup(void) {
|
||||||
char** existing_files = find_existing_nrdb_files();
|
char** existing_files = find_existing_db_files();
|
||||||
if (existing_files) {
|
if (existing_files) {
|
||||||
// Free the array
|
// Free the array
|
||||||
for (int i = 0; existing_files[i]; i++) {
|
for (int i = 0; existing_files[i]; i++) {
|
||||||
@@ -437,13 +518,111 @@ int generate_random_private_key_bytes(unsigned char* privkey_bytes) {
|
|||||||
return 0;
|
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
|
// DEFAULT CONFIG EVENT CREATION
|
||||||
// ================================
|
// ================================
|
||||||
|
|
||||||
cJSON* create_default_config_event(const unsigned char* admin_privkey_bytes,
|
cJSON* create_default_config_event(const unsigned char* admin_privkey_bytes,
|
||||||
const char* relay_privkey_hex,
|
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) {
|
if (!admin_privkey_bytes || !relay_privkey_hex || !relay_pubkey_hex) {
|
||||||
log_error("Invalid parameters for creating default config event");
|
log_error("Invalid parameters for creating default config event");
|
||||||
return NULL;
|
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(relay_pubkey_tag, cJSON_CreateString(relay_pubkey_hex));
|
||||||
cJSON_AddItemToArray(tags, relay_pubkey_tag);
|
cJSON_AddItemToArray(tags, relay_pubkey_tag);
|
||||||
|
|
||||||
cJSON* relay_privkey_tag = cJSON_CreateArray();
|
// Note: relay_privkey is now stored securely in relay_seckey table
|
||||||
cJSON_AddItemToArray(relay_privkey_tag, cJSON_CreateString("relay_privkey"));
|
// It is no longer included in the public configuration event
|
||||||
cJSON_AddItemToArray(relay_privkey_tag, cJSON_CreateString(relay_privkey_hex));
|
|
||||||
cJSON_AddItemToArray(tags, relay_privkey_tag);
|
|
||||||
|
|
||||||
// Add all default configuration values
|
// Add all default configuration values with command line overrides
|
||||||
for (size_t i = 0; i < DEFAULT_CONFIG_COUNT; i++) {
|
for (size_t i = 0; i < DEFAULT_CONFIG_COUNT; i++) {
|
||||||
cJSON* tag = cJSON_CreateArray();
|
cJSON* tag = cJSON_CreateArray();
|
||||||
cJSON_AddItemToArray(tag, cJSON_CreateString(DEFAULT_CONFIG_VALUES[i].key));
|
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);
|
cJSON_AddItemToArray(tags, tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -516,18 +710,38 @@ cJSON* create_default_config_event(const unsigned char* admin_privkey_bytes,
|
|||||||
// IMPLEMENTED FUNCTIONS
|
// 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...");
|
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];
|
unsigned char admin_privkey_bytes[32];
|
||||||
char admin_privkey[65], admin_pubkey[65];
|
char admin_privkey[65], admin_pubkey[65];
|
||||||
|
|
||||||
if (generate_random_private_key_bytes(admin_privkey_bytes) != 0) {
|
if (cli_options && strlen(cli_options->admin_privkey_override) == 64) {
|
||||||
log_error("Failed to generate admin private key");
|
// Use provided admin private key
|
||||||
return -1;
|
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);
|
||||||
}
|
}
|
||||||
nostr_bytes_to_hex(admin_privkey_bytes, 32, admin_privkey);
|
|
||||||
|
|
||||||
unsigned char admin_pubkey_bytes[32];
|
unsigned char admin_pubkey_bytes[32];
|
||||||
if (nostr_ec_public_key_from_private_key(admin_privkey_bytes, admin_pubkey_bytes) != NOSTR_SUCCESS) {
|
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);
|
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];
|
unsigned char relay_privkey_bytes[32];
|
||||||
char relay_privkey[65], relay_pubkey[65];
|
char relay_privkey[65], relay_pubkey[65];
|
||||||
|
|
||||||
if (generate_random_private_key_bytes(relay_privkey_bytes) != 0) {
|
if (cli_options && strlen(cli_options->relay_privkey_override) == 64) {
|
||||||
log_error("Failed to generate relay private key");
|
// Use provided relay private key
|
||||||
return -1;
|
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);
|
||||||
}
|
}
|
||||||
nostr_bytes_to_hex(relay_privkey_bytes, 32, relay_privkey);
|
|
||||||
|
|
||||||
unsigned char relay_pubkey_bytes[32];
|
unsigned char relay_pubkey_bytes[32];
|
||||||
if (nostr_ec_public_key_from_private_key(relay_privkey_bytes, relay_pubkey_bytes) != NOSTR_SUCCESS) {
|
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;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Create initial configuration event using defaults
|
// 5. Store relay private key in temporary storage for later secure storage
|
||||||
cJSON* config_event = create_default_config_event(admin_privkey_bytes, relay_privkey, relay_pubkey);
|
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) {
|
if (!config_event) {
|
||||||
log_error("Failed to create default configuration event");
|
log_error("Failed to create default configuration event");
|
||||||
return -1;
|
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) {
|
if (store_config_event_in_database(config_event) == 0) {
|
||||||
log_success("Initial configuration event stored successfully");
|
log_success("Initial configuration event stored successfully");
|
||||||
} else {
|
} else {
|
||||||
@@ -584,23 +823,22 @@ int first_time_startup_sequence(void) {
|
|||||||
g_pending_config_event = cJSON_Duplicate(config_event, 1);
|
g_pending_config_event = cJSON_Duplicate(config_event, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. Cache the current config
|
// 8. Cache the current config
|
||||||
if (g_current_config) {
|
if (g_current_config) {
|
||||||
cJSON_Delete(g_current_config);
|
cJSON_Delete(g_current_config);
|
||||||
}
|
}
|
||||||
g_current_config = cJSON_Duplicate(config_event, 1);
|
g_current_config = cJSON_Duplicate(config_event, 1);
|
||||||
|
|
||||||
// 8. Clean up
|
// 9. Clean up
|
||||||
cJSON_Delete(config_event);
|
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("=================================================================\n");
|
printf("=================================================================\n");
|
||||||
printf("IMPORTANT: SAVE THIS ADMIN PRIVATE KEY SECURELY!\n");
|
printf("IMPORTANT: SAVE THIS ADMIN PRIVATE KEY SECURELY!\n");
|
||||||
printf("=================================================================\n");
|
printf("=================================================================\n");
|
||||||
printf("Admin Private Key: %s\n", admin_privkey);
|
printf("Admin Private Key: %s\n", admin_privkey);
|
||||||
printf("Admin Public Key: %s\n", admin_pubkey);
|
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("Relay Public Key: %s\n", relay_pubkey);
|
||||||
printf("\nDatabase: %s\n", g_database_path);
|
printf("\nDatabase: %s\n", g_database_path);
|
||||||
printf("\nThis admin private key is needed to update configuration!\n");
|
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;
|
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) {
|
int process_configuration_event(const cJSON* event) {
|
||||||
if (!event) {
|
if (!event) {
|
||||||
log_error("Invalid configuration 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");
|
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
|
// Store in database
|
||||||
if (store_config_event_in_database(event) != 0) {
|
if (store_config_event_in_database(event) != 0) {
|
||||||
log_error("Failed to store configuration event");
|
log_error("Failed to store configuration event");
|
||||||
@@ -702,7 +1404,7 @@ int process_configuration_event(const cJSON* event) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
log_success("Configuration event processed successfully");
|
log_success("Configuration event processed successfully with field validation");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
21
src/config.h
21
src/config.h
@@ -32,6 +32,13 @@ typedef struct {
|
|||||||
char config_file_path[512]; // Temporary for compatibility
|
char config_file_path[512]; // Temporary for compatibility
|
||||||
} config_manager_t;
|
} 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
|
// Global configuration manager
|
||||||
extern config_manager_t g_config_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
|
// First-time startup functions
|
||||||
int is_first_time_startup(void);
|
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);
|
int startup_existing_relay(const char* relay_pubkey);
|
||||||
|
|
||||||
// Configuration application functions
|
// 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);
|
int apply_runtime_config_handlers(const cJSON* old_event, const cJSON* new_event);
|
||||||
|
|
||||||
// Utility functions
|
// Utility functions
|
||||||
char** find_existing_nrdb_files(void);
|
char** find_existing_db_files(void);
|
||||||
char* extract_pubkey_from_filename(const char* filename);
|
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 */
|
#endif /* CONFIG_H */
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
#define DEFAULT_CONFIG_EVENT_H
|
#define DEFAULT_CONFIG_EVENT_H
|
||||||
|
|
||||||
#include <cjson/cJSON.h>
|
#include <cjson/cJSON.h>
|
||||||
|
#include "config.h" // For cli_options_t definition
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Default Configuration Event Template
|
* Default Configuration Event Template
|
||||||
@@ -22,6 +23,12 @@ static const struct {
|
|||||||
// Authentication
|
// Authentication
|
||||||
{"auth_enabled", "false"},
|
{"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
|
// Server Core Settings
|
||||||
{"relay_port", "8888"},
|
{"relay_port", "8888"},
|
||||||
{"max_connections", "100"},
|
{"max_connections", "100"},
|
||||||
@@ -61,8 +68,9 @@ static const struct {
|
|||||||
#define DEFAULT_CONFIG_COUNT (sizeof(DEFAULT_CONFIG_VALUES) / sizeof(DEFAULT_CONFIG_VALUES[0]))
|
#define DEFAULT_CONFIG_COUNT (sizeof(DEFAULT_CONFIG_VALUES) / sizeof(DEFAULT_CONFIG_VALUES[0]))
|
||||||
|
|
||||||
// Function to create default configuration event
|
// Function to create default configuration event
|
||||||
cJSON* create_default_config_event(const unsigned char* admin_privkey_bytes,
|
cJSON* create_default_config_event(const unsigned char* admin_privkey_bytes,
|
||||||
const char* relay_privkey_hex,
|
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 */
|
#endif /* DEFAULT_CONFIG_EVENT_H */
|
||||||
877
src/main.c
877
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
|
/* Embedded SQL Schema for C Nostr Relay
|
||||||
* Generated from db/schema.sql - Do not edit manually
|
* Generated from db/schema.sql - Do not edit manually
|
||||||
* Schema Version: 4
|
* Schema Version: 6
|
||||||
*/
|
*/
|
||||||
#ifndef SQL_SCHEMA_H
|
#ifndef SQL_SCHEMA_H
|
||||||
#define SQL_SCHEMA_H
|
#define SQL_SCHEMA_H
|
||||||
|
|
||||||
/* Schema version constant */
|
/* Schema version constant */
|
||||||
#define EMBEDDED_SCHEMA_VERSION "4"
|
#define EMBEDDED_SCHEMA_VERSION "6"
|
||||||
|
|
||||||
/* Embedded SQL schema as C string literal */
|
/* Embedded SQL schema as C string literal */
|
||||||
static const char* const EMBEDDED_SCHEMA_SQL =
|
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\
|
-- Event-based configuration system using kind 33334 Nostr events\n\
|
||||||
\n\
|
\n\
|
||||||
-- Schema version tracking\n\
|
-- Schema version tracking\n\
|
||||||
PRAGMA user_version = 4;\n\
|
PRAGMA user_version = 6;\n\
|
||||||
\n\
|
\n\
|
||||||
-- Enable foreign key support\n\
|
-- Enable foreign key support\n\
|
||||||
PRAGMA foreign_keys = ON;\n\
|
PRAGMA foreign_keys = ON;\n\
|
||||||
@@ -58,8 +58,8 @@ CREATE TABLE schema_info (\n\
|
|||||||
\n\
|
\n\
|
||||||
-- Insert schema metadata\n\
|
-- Insert schema metadata\n\
|
||||||
INSERT INTO schema_info (key, value) VALUES\n\
|
INSERT INTO schema_info (key, value) VALUES\n\
|
||||||
('version', '4'),\n\
|
('version', '6'),\n\
|
||||||
('description', 'Event-based Nostr relay schema with kind 33334 configuration events'),\n\
|
('description', 'Event-based Nostr relay schema with secure relay private key storage'),\n\
|
||||||
('created_at', strftime('%s', 'now'));\n\
|
('created_at', strftime('%s', 'now'));\n\
|
||||||
\n\
|
\n\
|
||||||
-- Helper views for common queries\n\
|
-- Helper views for common queries\n\
|
||||||
@@ -128,6 +128,32 @@ BEGIN\n\
|
|||||||
AND id != NEW.id;\n\
|
AND id != NEW.id;\n\
|
||||||
END;\n\
|
END;\n\
|
||||||
\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\
|
-- Persistent Subscriptions Logging Tables (Phase 2)\n\
|
||||||
-- Optional database logging for subscription analytics and debugging\n\
|
-- Optional database logging for subscription analytics and debugging\n\
|
||||||
\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
|
return 0
|
||||||
fi
|
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"
|
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))
|
local event1=$(nak event --sec "$private_key" -c "Event without expiration for filtering test" --ts $(date +%s))
|
||||||
|
|
||||||
# Event 2: Future expiration (should be returned)
|
# Event 2: Future expiration (should be returned)
|
||||||
local future_timestamp=$(($(date +%s) + 1800)) # 30 minutes from now
|
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")
|
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)
|
# Event 3: SHORT-LIVED EVENT - expires in 3 seconds
|
||||||
local past_timestamp=$(($(date +%s) - 3600)) # 1 hour ago
|
local short_expiry=$(($(date +%s) + 3)) # 3 seconds from now
|
||||||
local event3=$(create_event_with_expiration "Event with past expiration for filtering test" "$past_timestamp")
|
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,
|
# Submit all events - they should all be accepted initially
|
||||||
# so we'll create it with a slightly more recent expiration that might get through
|
local response1=$(echo "[\"EVENT\",$event1]" | timeout 5s websocat "$RELAY_URL" 2>&1)
|
||||||
local recent_past=$(($(date +%s) - 600)) # 10 minutes ago (outside grace period)
|
local response2=$(echo "[\"EVENT\",$event2]" | timeout 5s websocat "$RELAY_URL" 2>&1)
|
||||||
local event3_recent=$(create_event_with_expiration "Recently expired event for filtering test" "$recent_past")
|
local response3=$(echo "[\"EVENT\",$event3]" | timeout 5s websocat "$RELAY_URL" 2>&1)
|
||||||
|
|
||||||
# Try to submit all events (some may be rejected)
|
print_info "Event submission responses:"
|
||||||
echo "[\"EVENT\",$event1]" | timeout 3s websocat "$RELAY_URL" >/dev/null 2>&1 || true
|
echo "Event 1 (no expiry): $response1"
|
||||||
echo "[\"EVENT\",$event2]" | timeout 3s websocat "$RELAY_URL" >/dev/null 2>&1 || true
|
echo "Event 2 (future expiry): $response2"
|
||||||
echo "[\"EVENT\",$event3_recent]" | timeout 3s websocat "$RELAY_URL" >/dev/null 2>&1 || true
|
echo "Event 3 (expires in 3s): $response3"
|
||||||
|
|
||||||
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"
|
|
||||||
echo ""
|
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 no_exp_count=0
|
||||||
local future_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
|
no_exp_count=1
|
||||||
print_success "✓ Event without expiration found in subscription results"
|
print_success "✓ Event without expiration found in post-expiration results"
|
||||||
fi
|
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
|
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
|
fi
|
||||||
|
|
||||||
if echo "$response" | grep -q "Recently expired event for filtering test"; then
|
if echo "$expired_response" | grep -q "Short-lived event for filtering test"; then
|
||||||
past_exp_count=1
|
expired_event_count=1
|
||||||
print_warning "✗ Recently expired event found in subscription results (should be filtered)"
|
print_error "✗ EXPIRED short-lived event found in subscription results (should be filtered!)"
|
||||||
else
|
else
|
||||||
print_success "✓ Recently expired event properly filtered from subscription results"
|
print_success "✓ Expired short-lived event properly filtered from subscription results"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Evaluate results
|
# Evaluate results
|
||||||
local expected_events=$((no_exp_count + future_exp_count))
|
local expected_active_events=$((no_exp_count + future_exp_count))
|
||||||
if [ $expected_events -ge 1 ] && [ $past_exp_count -eq 0 ]; then
|
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"
|
record_test_result "Expiration Filtering in Subscriptions" "PASS" "Expired events properly filtered from subscriptions"
|
||||||
return 0
|
return 0
|
||||||
else
|
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
|
return 1
|
||||||
fi
|
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