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