Compare commits

..

7 Commits

Author SHA1 Message Date
Your Name
6dac231040 v0.3.16 - Admin system getting better 2025-09-30 05:32:23 -04:00
Your Name
6fd3e531c3 v0.3.15 - How can administration take so long 2025-09-27 15:50:42 -04:00
Your Name
c1c05991cf v0.3.14 - I think the admin api is finally working 2025-09-27 14:08:45 -04:00
Your Name
ab378e14d1 v0.3.13 - Working on admin system 2025-09-27 13:32:21 -04:00
Your Name
c0f9bf9ef5 v0.3.12 - Working through auth still 2025-09-25 17:33:38 -04:00
Your Name
bc6a7b3f20 Working on API 2025-09-25 16:35:16 -04:00
Your Name
036b0823b9 v0.3.11 - Working on admin api 2025-09-25 11:25:50 -04:00
16 changed files with 5834 additions and 1052 deletions

3
.gitignore vendored
View File

@@ -8,4 +8,5 @@ src/version.h
dev-config/
db/
copy_executable_local.sh
nostr_login_lite/
nostr_login_lite/
style_guide/

View File

@@ -27,7 +27,7 @@
## Critical Integration Issues
### Event-Based Configuration System
- **No traditional config files** - all configuration stored as kind 33334 Nostr events
- **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
@@ -35,7 +35,7 @@
### First-Time Startup Sequence
1. Relay generates admin keypair and relay keypair
2. Creates database file with relay pubkey as filename
3. Stores default configuration as kind 33334 event
3. Stores default configuration in config table
4. **CRITICAL**: Admin private key displayed once and never stored on disk
### Port Management
@@ -48,20 +48,30 @@
- Schema version 4 with JSON tag storage
- **Critical**: Event expiration filtering done at application level, not SQL level
### Configuration Event Structure
### Admin API Event Structure
```json
{
"kind": 33334,
"content": "C Nostr Relay Configuration",
"kind": 23456,
"content": "base64_nip44_encrypted_command_array",
"tags": [
["d", "<relay_pubkey>"],
["relay_description", "value"],
["max_subscriptions_per_client", "25"],
["pow_min_difficulty", "16"]
["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

180
README.md
View File

@@ -22,4 +22,184 @@ Do NOT modify the formatting, add emojis, or change the text. Keep the simple fo
- [ ] NIP-50: Keywords filter
- [ ] NIP-70: Protected Events
## 🔧 Administrator API
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.
### Authentication
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.
### Event Structure
All admin commands use the same unified event structure with NIP-44 encrypted content:
**Admin Command Event:**
```json
{
"id": "event_id",
"pubkey": "admin_public_key",
"created_at": 1234567890,
"kind": 23456,
"content": "AqHBUgcM7dXFYLQuDVzGwMST1G8jtWYyVvYxXhVGEu4nAb4LVw...",
"tags": [
["p", "relay_public_key"]
],
"sig": "event_signature"
}
```
The `content` field contains a NIP-44 encrypted JSON array representing the command.
**Admin Response Event:**
```json
["EVENT", "temp_sub_id", {
"id": "response_event_id",
"pubkey": "relay_public_key",
"created_at": 1234567890,
"kind": 23457,
"content": "BpKCVhfN8eYtRmPqSvWxZnMkL2gHjUiOp3rTyEwQaS5dFg...",
"tags": [
["p", "admin_public_key"]
],
"sig": "response_event_signature"
}]
```
The `content` field contains a NIP-44 encrypted JSON response object.
### Admin Commands
All commands are sent as NIP-44 encrypted JSON arrays in the event content. The following table lists all available commands:
| 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 |
### Available Configuration Keys
**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
**Authentication & Access Control:**
- `auth_enabled`: Enable whitelist/blacklist auth rules (`true`/`false`)
- `nip42_auth_required`: Enable NIP-42 cryptographic authentication (`true`/`false`)
- `nip42_auth_required_kinds`: Event kinds requiring NIP-42 auth (comma-separated)
- `nip42_challenge_timeout`: NIP-42 challenge expiration seconds
**Proof of Work & Validation:**
- `pow_min_difficulty`: Minimum proof-of-work difficulty
- `nip40_expiration_enabled`: Enable event expiration (`true`/`false`)
### 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"
}]
```

File diff suppressed because it is too large Load Diff

460
docs/admin_api_plan.md Normal file
View 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.

View File

@@ -198,25 +198,54 @@ fi
echo "Build successful. Proceeding with relay restart..."
# Kill existing relay if running
# Kill existing relay if running - start aggressive immediately
echo "Stopping any existing relay servers..."
pkill -f "c_relay_" 2>/dev/null
sleep 2 # Give time for shutdown
# Check if port is still bound
if lsof -i :8888 >/dev/null 2>&1; then
echo "Port 8888 still in use, force killing..."
fuser -k 8888/tcp 2>/dev/null || echo "No process on port 8888"
# Get all relay processes and kill them immediately with -9
RELAY_PIDS=$(pgrep -f "c_relay_" || echo "")
if [ -n "$RELAY_PIDS" ]; then
echo "Force killing relay processes immediately: $RELAY_PIDS"
kill -9 $RELAY_PIDS 2>/dev/null
else
echo "No existing relay processes found"
fi
# Get any remaining processes
REMAINING_PIDS=$(pgrep -f "c_relay_" || echo "")
if [ -n "$REMAINING_PIDS" ]; then
echo "Force killing remaining processes: $REMAINING_PIDS"
kill -9 $REMAINING_PIDS 2>/dev/null
# Ensure port 8888 is completely free with retry loop
echo "Ensuring port 8888 is available..."
for attempt in {1..15}; do
if ! lsof -i :8888 >/dev/null 2>&1; then
echo "Port 8888 is now free"
break
fi
echo "Attempt $attempt: Port 8888 still in use, force killing..."
# Kill anything using port 8888
fuser -k 8888/tcp 2>/dev/null || true
# Double-check for any remaining relay processes
REMAINING_PIDS=$(pgrep -f "c_relay_" || echo "")
if [ -n "$REMAINING_PIDS" ]; then
echo "Killing remaining relay processes: $REMAINING_PIDS"
kill -9 $REMAINING_PIDS 2>/dev/null || true
fi
sleep 2
if [ $attempt -eq 15 ]; then
echo "ERROR: Could not free port 8888 after 15 attempts"
echo "Current processes using port:"
lsof -i :8888 2>/dev/null || echo "No process details available"
echo "You may need to manually kill processes or reboot"
exit 1
fi
done
# Final safety check - ensure no relay processes remain
FINAL_PIDS=$(pgrep -f "c_relay_" || echo "")
if [ -n "$FINAL_PIDS" ]; then
echo "Final cleanup: killing processes $FINAL_PIDS"
kill -9 $FINAL_PIDS 2>/dev/null || true
sleep 1
else
echo "No existing relay found"
fi
# Clean up PID file
@@ -253,14 +282,14 @@ cd build
# Start relay in background and capture its PID
if [ "$USE_TEST_KEYS" = true ]; then
echo "Using deterministic test keys for development..."
./$(basename $BINARY_PATH) -a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -r 1111111111111111111111111111111111111111111111111111111111111111 > ../relay.log 2>&1 &
./$(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 > ../relay.log 2>&1 &
./$(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) > ../relay.log 2>&1 &
./$(basename $BINARY_PATH) --strict-port > ../relay.log 2>&1 &
fi
RELAY_PID=$!
# Change back to original directory

View 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

View File

@@ -1 +1 @@
1950163
1182553

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,9 @@
#include <time.h>
#include <pthread.h>
// Forward declaration for WebSocket support
struct lws;
// Configuration constants
#define CONFIG_VALUE_MAX_LENGTH 1024
#define RELAY_NAME_MAX_LENGTH 256
@@ -95,6 +98,7 @@ 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
@@ -160,10 +164,20 @@ 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
int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size);
// 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);
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,

View File

@@ -8,8 +8,7 @@
* Default Configuration Event Template
*
* This header contains the default configuration values for the C Nostr Relay.
* These values are used to create the initial kind 33334 configuration event
* during first-time startup.
* These values are used to populate the config table during first-time startup.
*
* IMPORTANT: These values should never be accessed directly by other parts
* of the program. They are only used during initial configuration event creation.

View File

@@ -224,11 +224,11 @@ int handle_event_message(cJSON* event, char* error_message, size_t error_size);
// Forward declaration for unified validation
int nostr_validate_unified_request(const char* json_string, size_t json_length);
// Forward declaration for configuration event handling (kind 33334)
int handle_configuration_event(cJSON* event, char* error_message, size_t error_size);
// Forward declaration for admin event processing (kind 23456)
int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size, struct lws* wsi);
// Forward declaration for admin event processing (kinds 33334 and 33335)
int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size);
// Forward declaration for enhanced admin event authorization
int is_authorized_admin_event(cJSON* event, char* error_message, size_t error_size);
// Forward declaration for NOTICE message support
void send_notice_message(struct lws* wsi, const char* message);
@@ -972,28 +972,28 @@ static void get_timestamp_string(char* buffer, size_t buffer_size) {
void log_info(const char* message) {
char timestamp[32];
get_timestamp_string(timestamp, sizeof(timestamp));
printf("[%s] " BLUE "[INFO]" RESET " %s\n", timestamp, message);
printf("[%s] [INFO] %s\n", timestamp, message);
fflush(stdout);
}
void log_success(const char* message) {
char timestamp[32];
get_timestamp_string(timestamp, sizeof(timestamp));
printf("[%s] " GREEN "[SUCCESS]" RESET " %s\n", timestamp, message);
printf("[%s] [SUCCESS] %s\n", timestamp, message);
fflush(stdout);
}
void log_error(const char* message) {
char timestamp[32];
get_timestamp_string(timestamp, sizeof(timestamp));
printf("[%s] " RED "[ERROR]" RESET " %s\n", timestamp, message);
printf("[%s] [ERROR] %s\n", timestamp, message);
fflush(stdout);
}
void log_warning(const char* message) {
char timestamp[32];
get_timestamp_string(timestamp, sizeof(timestamp));
printf("[%s] " YELLOW "[WARNING]" RESET " %s\n", timestamp, message);
printf("[%s] [WARNING] %s\n", timestamp, message);
fflush(stdout);
}
@@ -3009,6 +3009,109 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
return events_sent;
}
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
// ADMIN EVENT AUTHORIZATION
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
// Enhanced admin event authorization function
int is_authorized_admin_event(cJSON* event, char* error_buffer, size_t error_buffer_size) {
if (!event || !error_buffer) {
if (error_buffer && error_buffer_size > 0) {
snprintf(error_buffer, error_buffer_size, "Invalid parameters for admin authorization");
}
return -1;
}
// Step 1: Verify event kind is admin type
cJSON *kind_json = cJSON_GetObjectItem(event, "kind");
if (!kind_json || !cJSON_IsNumber(kind_json)) {
snprintf(error_buffer, error_buffer_size, "Missing or invalid event kind");
return -1;
}
int event_kind = kind_json->valueint;
if (event_kind != 23456) {
snprintf(error_buffer, error_buffer_size, "Event kind %d is not an admin event type", event_kind);
return -1;
}
// Step 2: Check if event targets this relay (look for 'p' tag with our relay pubkey)
cJSON *tags = cJSON_GetObjectItem(event, "tags");
if (!tags || !cJSON_IsArray(tags)) {
// No tags array - treat as regular event for different relay
log_info("Admin event has no tags array - treating as event for different relay");
snprintf(error_buffer, error_buffer_size, "Admin event not targeting this relay (no tags)");
return -1;
}
int targets_this_relay = 0;
cJSON *tag;
cJSON_ArrayForEach(tag, tags) {
if (cJSON_IsArray(tag)) {
cJSON *tag_name = cJSON_GetArrayItem(tag, 0);
cJSON *tag_value = cJSON_GetArrayItem(tag, 1);
if (tag_name && cJSON_IsString(tag_name) &&
tag_value && cJSON_IsString(tag_value) &&
strcmp(tag_name->valuestring, "p") == 0) {
// Compare with our relay pubkey
const char* relay_pubkey = get_config_value("relay_pubkey");
if (relay_pubkey && strcmp(tag_value->valuestring, relay_pubkey) == 0) {
targets_this_relay = 1;
break;
}
}
}
}
if (!targets_this_relay) {
// Admin event for different relay - not an error, just not for us
log_info("Admin event targets different relay - treating as regular event");
snprintf(error_buffer, error_buffer_size, "Admin event not targeting this relay");
return -1;
}
// Step 3: Verify admin signature authorization
cJSON *pubkey_json = cJSON_GetObjectItem(event, "pubkey");
if (!pubkey_json || !cJSON_IsString(pubkey_json)) {
log_warning("Unauthorized admin event attempt: missing or invalid pubkey");
snprintf(error_buffer, error_buffer_size, "Unauthorized admin event attempt: missing pubkey");
return -1;
}
// Get admin pubkey from configuration
const char* admin_pubkey = get_config_value("admin_pubkey");
if (!admin_pubkey || strlen(admin_pubkey) == 0) {
log_warning("Unauthorized admin event attempt: no admin pubkey configured");
snprintf(error_buffer, error_buffer_size, "Unauthorized admin event attempt: no admin configured");
return -1;
}
// Compare pubkeys
if (strcmp(pubkey_json->valuestring, admin_pubkey) != 0) {
log_warning("Unauthorized admin event attempt: pubkey mismatch");
char warning_msg[256];
snprintf(warning_msg, sizeof(warning_msg),
"Unauthorized admin event attempt from pubkey: %.32s...", pubkey_json->valuestring);
log_warning(warning_msg);
snprintf(error_buffer, error_buffer_size, "Unauthorized admin event attempt: invalid admin pubkey");
return -1;
}
// Step 4: Verify event signature
if (nostr_verify_event_signature(event) != 0) {
log_warning("Unauthorized admin event attempt: invalid signature");
snprintf(error_buffer, error_buffer_size, "Unauthorized admin event attempt: signature verification failed");
return -1;
}
// All checks passed - authorized admin event
log_info("Admin event authorization successful");
return 0;
}
@@ -3097,11 +3200,18 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
memcpy(message, in, len);
message[len] = '\0';
log_info("Received WebSocket message");
// Parse JSON message
// Parse JSON message (this is the normal program flow)
cJSON* json = cJSON_Parse(message);
if (json && cJSON_IsArray(json)) {
// Log the complete parsed JSON message once
char* complete_message = cJSON_Print(json);
if (complete_message) {
char debug_msg[2048];
snprintf(debug_msg, sizeof(debug_msg),
"Received complete WebSocket message: %s", complete_message);
log_info(debug_msg);
free(complete_message);
}
// Get message type
cJSON* type = cJSON_GetArrayItem(json, 0);
if (type && cJSON_IsString(type)) {
@@ -3243,7 +3353,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
// Cleanup event JSON string
free(event_json_str);
// Check for admin events (kinds 33334 and 33335) and intercept them
// Check for admin events (kind 23456) and intercept them
if (result == 0) {
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
if (kind_obj && cJSON_IsNumber(kind_obj)) {
@@ -3251,33 +3361,83 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
log_info("DEBUG ADMIN: Checking if admin event processing is needed");
if (event_kind == 33334 || event_kind == 33335) {
// This is an admin event - process it through the admin API instead of normal storage
log_info("DEBUG ADMIN: Admin event detected, processing through admin API");
// Log reception of Kind 23456 events
if (event_kind == 23456) {
char* event_json_debug = cJSON_Print(event);
char debug_received_msg[1024];
snprintf(debug_received_msg, sizeof(debug_received_msg),
"RECEIVED Kind %d event: %s", event_kind,
event_json_debug ? event_json_debug : "Failed to serialize");
log_info(debug_received_msg);
char admin_error[512] = {0};
int admin_result = process_admin_event_in_config(event, admin_error, sizeof(admin_error));
if (event_json_debug) {
free(event_json_debug);
}
}
if (event_kind == 23456) {
// Enhanced admin event security - check authorization first
log_info("DEBUG ADMIN: Admin event detected, checking authorization");
char debug_admin_msg[256];
snprintf(debug_admin_msg, sizeof(debug_admin_msg),
"DEBUG ADMIN: process_admin_event_in_config returned %d", admin_result);
log_info(debug_admin_msg);
char auth_error[512] = {0};
int auth_result = is_authorized_admin_event(event, auth_error, sizeof(auth_error));
if (admin_result != 0) {
log_error("DEBUG ADMIN: Failed to process admin event through admin API");
if (auth_result != 0) {
// Authorization failed - log and reject
log_warning("DEBUG ADMIN: Admin event authorization failed");
result = -1;
size_t error_len = strlen(admin_error);
size_t error_len = strlen(auth_error);
size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1;
memcpy(error_message, admin_error, copy_len);
memcpy(error_message, auth_error, copy_len);
error_message[copy_len] = '\0';
char debug_admin_error_msg[600];
snprintf(debug_admin_error_msg, sizeof(debug_admin_error_msg),
"DEBUG ADMIN ERROR: %.400s", admin_error);
log_error(debug_admin_error_msg);
char debug_auth_error_msg[600];
snprintf(debug_auth_error_msg, sizeof(debug_auth_error_msg),
"DEBUG ADMIN AUTH ERROR: %.400s", auth_error);
log_warning(debug_auth_error_msg);
} else {
log_success("DEBUG ADMIN: Admin event processed successfully through admin API");
// Admin events are processed by the admin API, not broadcast to subscriptions
// Authorization successful - process through admin API
log_info("DEBUG ADMIN: Admin event authorized, processing through admin API");
char admin_error[512] = {0};
int admin_result = process_admin_event_in_config(event, admin_error, sizeof(admin_error), wsi);
char debug_admin_msg[256];
snprintf(debug_admin_msg, sizeof(debug_admin_msg),
"DEBUG ADMIN: process_admin_event_in_config returned %d", admin_result);
log_info(debug_admin_msg);
// Log results for Kind 23456 events
if (event_kind == 23456) {
if (admin_result == 0) {
char success_result_msg[256];
snprintf(success_result_msg, sizeof(success_result_msg),
"SUCCESS: Kind %d event processed successfully", event_kind);
log_success(success_result_msg);
} else {
char error_result_msg[512];
snprintf(error_result_msg, sizeof(error_result_msg),
"ERROR: Kind %d event processing failed: %s", event_kind, admin_error);
log_error(error_result_msg);
}
}
if (admin_result != 0) {
log_error("DEBUG ADMIN: Failed to process admin event through admin API");
result = -1;
size_t error_len = strlen(admin_error);
size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1;
memcpy(error_message, admin_error, copy_len);
error_message[copy_len] = '\0';
char debug_admin_error_msg[600];
snprintf(debug_admin_error_msg, sizeof(debug_admin_error_msg),
"DEBUG ADMIN ERROR: %.400s", admin_error);
log_error(debug_admin_error_msg);
} else {
log_success("DEBUG ADMIN: Admin event processed successfully through admin API");
// Admin events are processed by the admin API, not broadcast to subscriptions
}
}
} else {
// Regular event - store in database and broadcast
@@ -3510,6 +3670,7 @@ int check_port_available(int port) {
int sockfd;
struct sockaddr_in addr;
int result;
int reuse = 1;
// Create a socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
@@ -3517,6 +3678,13 @@ int check_port_available(int port) {
return 0; // Cannot create socket, assume port unavailable
}
// Set SO_REUSEADDR to allow binding to ports in TIME_WAIT state
// This matches libwebsockets behavior and prevents false unavailability
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) {
close(sockfd);
return 0; // Failed to set socket option
}
// Set up the address structure
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
@@ -3534,7 +3702,7 @@ int check_port_available(int port) {
}
// Start libwebsockets-based WebSocket Nostr relay server
int start_websocket_relay(int port_override) {
int start_websocket_relay(int port_override, int strict_port) {
struct lws_context_creation_info info;
log_info("Starting libwebsockets-based Nostr relay server...");
@@ -3544,7 +3712,7 @@ int start_websocket_relay(int port_override) {
int configured_port = (port_override > 0) ? port_override : get_config_int("relay_port", DEFAULT_PORT);
int actual_port = configured_port;
int port_attempts = 0;
const int max_port_attempts = 5;
const int max_port_attempts = 10; // Increased from 5 to 10
// Minimal libwebsockets configuration
info.protocols = protocols;
@@ -3563,8 +3731,8 @@ int start_websocket_relay(int port_override) {
// Max payload size for Nostr events
info.max_http_header_data = 4096;
// Find an available port with pre-checking
while (port_attempts < max_port_attempts) {
// Find an available port with pre-checking (or fail immediately in strict mode)
while (port_attempts < (strict_port ? 1 : max_port_attempts)) {
char attempt_msg[256];
snprintf(attempt_msg, sizeof(attempt_msg), "Checking port availability: %d", actual_port);
log_info(attempt_msg);
@@ -3572,7 +3740,13 @@ int start_websocket_relay(int port_override) {
// Pre-check if port is available
if (!check_port_available(actual_port)) {
port_attempts++;
if (port_attempts < max_port_attempts) {
if (strict_port) {
char error_msg[256];
snprintf(error_msg, sizeof(error_msg),
"Strict port mode: port %d is not available", actual_port);
log_error(error_msg);
return -1;
} else if (port_attempts < max_port_attempts) {
char retry_msg[256];
snprintf(retry_msg, sizeof(retry_msg), "Port %d is in use, trying port %d (attempt %d/%d)",
actual_port, actual_port + 1, port_attempts + 1, max_port_attempts);
@@ -3611,7 +3785,13 @@ int start_websocket_relay(int port_override) {
log_warning(lws_error_msg);
port_attempts++;
if (port_attempts < max_port_attempts) {
if (strict_port) {
char error_msg[256];
snprintf(error_msg, sizeof(error_msg),
"Strict port mode: failed to bind to port %d", actual_port);
log_error(error_msg);
break;
} else if (port_attempts < max_port_attempts) {
actual_port++;
continue;
}
@@ -3677,6 +3857,7 @@ void print_usage(const char* program_name) {
printf(" -p, --port PORT Override relay port (first-time startup only)\n");
printf(" -a, --admin-privkey HEX Override admin private key (64-char hex)\n");
printf(" -r, --relay-privkey HEX Override relay private key (64-char hex)\n");
printf(" --strict-port Fail if exact port is unavailable (no port increment)\n");
printf("\n");
printf("Configuration:\n");
printf(" This relay uses event-based configuration stored in the database.\n");
@@ -3685,10 +3866,16 @@ void print_usage(const char* program_name) {
printf(" After initial setup, all configuration is managed via database events.\n");
printf(" Database file: <relay_pubkey>.db (created automatically)\n");
printf("\n");
printf("Port Binding:\n");
printf(" Default: Try up to 10 consecutive ports if requested port is busy\n");
printf(" --strict-port: Fail immediately if exact requested port is unavailable\n");
printf("\n");
printf("Examples:\n");
printf(" %s # Start relay (auto-configure on first run)\n", program_name);
printf(" %s -p 8080 # First-time setup with port 8080\n", program_name);
printf(" %s --port 9000 # First-time setup with port 9000\n", program_name);
printf(" %s --strict-port # Fail if default port 8888 is unavailable\n", program_name);
printf(" %s -p 8080 --strict-port # Fail if port 8080 is unavailable\n", program_name);
printf(" %s --help # Show this help\n", program_name);
printf(" %s --version # Show version info\n", program_name);
printf("\n");
@@ -3707,7 +3894,8 @@ int main(int argc, char* argv[]) {
cli_options_t cli_options = {
.port_override = -1, // -1 = not set
.admin_privkey_override = {0}, // Empty string = not set
.relay_privkey_override = {0} // Empty string = not set
.relay_privkey_override = {0}, // Empty string = not set
.strict_port = 0 // 0 = allow port increment (default)
};
// Parse command line arguments
@@ -3802,6 +3990,10 @@ int main(int argc, char* argv[]) {
i++; // Skip the key argument
log_info("Relay private key override specified");
} else if (strcmp(argv[i], "--strict-port") == 0) {
// Strict port mode option
cli_options.strict_port = 1;
log_info("Strict port mode enabled - will fail if exact port is unavailable");
} else {
log_error("Unknown argument. Use --help for usage information.");
print_usage(argv[0]);
@@ -4023,7 +4215,7 @@ int main(int argc, char* argv[]) {
log_info("Starting relay server...");
// Start WebSocket Nostr relay server (port from configuration)
int result = start_websocket_relay(-1); // Let config system determine port
int result = start_websocket_relay(-1, cli_options.strict_port); // Let config system determine port, pass strict_port flag
// Cleanup
cleanup_relay_info();

View File

@@ -629,28 +629,26 @@ static int check_database_auth_rules(const char *pubkey, const char *operation,
// Step 1: Check pubkey blacklist (highest priority)
const char *blacklist_sql =
"SELECT rule_type, description FROM auth_rules WHERE rule_type = "
"'pubkey_blacklist' AND rule_target = ? AND operation = ? AND enabled = "
"1 ORDER BY priority LIMIT 1";
"SELECT rule_type, action FROM auth_rules WHERE rule_type = "
"'blacklist' AND pattern_type = 'pubkey' AND pattern_value = ? LIMIT 1";
rc = sqlite3_prepare_v2(db, blacklist_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, pubkey, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, operation ? operation : "", -1, SQLITE_STATIC);
if (sqlite3_step(stmt) == SQLITE_ROW) {
const char *description = (const char *)sqlite3_column_text(stmt, 1);
const char *action = (const char *)sqlite3_column_text(stmt, 1);
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 1 FAILED - "
"Pubkey blacklisted\n");
char blacklist_msg[256];
sprintf(blacklist_msg,
"VALIDATOR_DEBUG: RULES ENGINE - Blacklist rule matched: %s\n",
description ? description : "Unknown");
"VALIDATOR_DEBUG: RULES ENGINE - Blacklist rule matched: action=%s\n",
action ? action : "deny");
validator_debug_log(blacklist_msg);
// Set specific violation details for status code mapping
strcpy(g_last_rule_violation.violation_type, "pubkey_blacklist");
sprintf(g_last_rule_violation.reason, "%s: Public key blacklisted",
description ? description : "TEST_PUBKEY_BLACKLIST");
sprintf(g_last_rule_violation.reason, "Public key blacklisted: %s",
action ? action : "PUBKEY_BLACKLIST");
sqlite3_finalize(stmt);
sqlite3_close(db);
@@ -664,29 +662,27 @@ static int check_database_auth_rules(const char *pubkey, const char *operation,
// Step 2: Check hash blacklist
if (resource_hash) {
const char *hash_blacklist_sql =
"SELECT rule_type, description FROM auth_rules WHERE rule_type = "
"'hash_blacklist' AND rule_target = ? AND operation = ? AND enabled = "
"1 ORDER BY priority LIMIT 1";
"SELECT rule_type, action FROM auth_rules WHERE rule_type = "
"'blacklist' AND pattern_type = 'hash' AND pattern_value = ? LIMIT 1";
rc = sqlite3_prepare_v2(db, hash_blacklist_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, resource_hash, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, operation ? operation : "", -1, SQLITE_STATIC);
if (sqlite3_step(stmt) == SQLITE_ROW) {
const char *description = (const char *)sqlite3_column_text(stmt, 1);
const char *action = (const char *)sqlite3_column_text(stmt, 1);
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 2 FAILED - "
"Hash blacklisted\n");
char hash_blacklist_msg[256];
sprintf(
hash_blacklist_msg,
"VALIDATOR_DEBUG: RULES ENGINE - Hash blacklist rule matched: %s\n",
description ? description : "Unknown");
"VALIDATOR_DEBUG: RULES ENGINE - Hash blacklist rule matched: action=%s\n",
action ? action : "deny");
validator_debug_log(hash_blacklist_msg);
// Set specific violation details for status code mapping
strcpy(g_last_rule_violation.violation_type, "hash_blacklist");
sprintf(g_last_rule_violation.reason, "%s: File hash blacklisted",
description ? description : "TEST_HASH_BLACKLIST");
sprintf(g_last_rule_violation.reason, "File hash blacklisted: %s",
action ? action : "HASH_BLACKLIST");
sqlite3_finalize(stmt);
sqlite3_close(db);
@@ -703,22 +699,20 @@ static int check_database_auth_rules(const char *pubkey, const char *operation,
// Step 3: Check pubkey whitelist
const char *whitelist_sql =
"SELECT rule_type, description FROM auth_rules WHERE rule_type = "
"'pubkey_whitelist' AND rule_target = ? AND operation = ? AND enabled = "
"1 ORDER BY priority LIMIT 1";
"SELECT rule_type, action FROM auth_rules WHERE rule_type = "
"'whitelist' AND pattern_type = 'pubkey' AND pattern_value = ? LIMIT 1";
rc = sqlite3_prepare_v2(db, whitelist_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, pubkey, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, operation ? operation : "", -1, SQLITE_STATIC);
if (sqlite3_step(stmt) == SQLITE_ROW) {
const char *description = (const char *)sqlite3_column_text(stmt, 1);
const char *action = (const char *)sqlite3_column_text(stmt, 1);
validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 3 PASSED - "
"Pubkey whitelisted\n");
char whitelist_msg[256];
sprintf(whitelist_msg,
"VALIDATOR_DEBUG: RULES ENGINE - Whitelist rule matched: %s\n",
description ? description : "Unknown");
"VALIDATOR_DEBUG: RULES ENGINE - Whitelist rule matched: action=%s\n",
action ? action : "allow");
validator_debug_log(whitelist_msg);
sqlite3_finalize(stmt);
sqlite3_close(db);
@@ -731,12 +725,10 @@ static int check_database_auth_rules(const char *pubkey, const char *operation,
// Step 4: Check if any whitelist rules exist - if yes, deny by default
const char *whitelist_exists_sql =
"SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'pubkey_whitelist' "
"AND operation = ? AND enabled = 1 LIMIT 1";
"SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'whitelist' "
"AND pattern_type = 'pubkey' LIMIT 1";
rc = sqlite3_prepare_v2(db, whitelist_exists_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, operation ? operation : "", -1, SQLITE_STATIC);
if (sqlite3_step(stmt) == SQLITE_ROW) {
int whitelist_count = sqlite3_column_int(stmt, 0);
if (whitelist_count > 0) {

View File

@@ -12,7 +12,7 @@
static const char* const EMBEDDED_SCHEMA_SQL =
"-- C Nostr Relay Database Schema\n\
-- SQLite schema for storing Nostr events with JSON tags support\n\
-- Event-based configuration system using kind 33334 Nostr events\n\
-- Configuration system using config table\n\
\n\
-- Schema version tracking\n\
PRAGMA user_version = 7;\n\

View File

@@ -20,21 +20,24 @@ set -e # Exit on any error
# CONFIGURATION
# =======================================================================
# Test mode credentials (provided by user)
# Test mode credentials (from current relay startup)
ADMIN_PRIVKEY="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
ADMIN_PUBKEY="6a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3"
RELAY_PUBKEY="4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"
# Server configuration
RELAY_HOST="localhost"
RELAY_HOST="127.0.0.1"
RELAY_PORT="8888"
RELAY_URL="ws://${RELAY_HOST}:${RELAY_PORT}"
# Test configuration
TIMEOUT=5
LOG_FILE="whitelist_blacklist_test.log"
TEMP_DIR="/tmp/c_relay_test_$$"
# WebSocket connection state (simplified - no persistent connections)
# These variables are kept for compatibility but not used
WS_CONNECTED=0
# Color codes for output
RED='\033[0;31m'
GREEN='\033[0;32m'
@@ -53,23 +56,23 @@ TESTS_FAILED=0
# =======================================================================
log() {
echo -e "${BLUE}[$(date '+%H:%M:%S')]${RESET} $1" | tee -a "$LOG_FILE"
echo -e "${BLUE}[$(date '+%H:%M:%S')]${RESET} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${RESET} $1" | tee -a "$LOG_FILE"
echo -e "${GREEN}[SUCCESS]${RESET} $1"
}
log_error() {
echo -e "${RED}[ERROR]${RESET} $1" | tee -a "$LOG_FILE"
echo -e "${RED}[ERROR]${RESET} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${RESET} $1" | tee -a "$LOG_FILE"
echo -e "${YELLOW}[WARNING]${RESET} $1"
}
log_info() {
echo -e "${BLUE}[INFO]${RESET} $1" | tee -a "$LOG_FILE"
echo -e "${BLUE}[INFO]${RESET} $1"
}
increment_test() {
@@ -79,11 +82,15 @@ increment_test() {
pass_test() {
TESTS_PASSED=$((TESTS_PASSED + 1))
log_success "Test $TESTS_RUN: PASSED - $1"
echo ""
echo ""
}
fail_test() {
TESTS_FAILED=$((TESTS_FAILED + 1))
log_error "Test $TESTS_RUN: FAILED - $1"
echo ""
echo ""
}
# Generate test keypairs
@@ -110,30 +117,245 @@ generate_test_keypair() {
echo "$pubkey" > "$pubkey_file"
log_info "Generated keypair for $name: pubkey=${pubkey:0:16}..."
log_info "Generated keypair for $name: pubkey=$pubkey"
# Export for use in calling functions
eval "${name}_PRIVKEY=\"$privkey\""
eval "${name}_PUBKEY=\"$pubkey\""
}
# Send WebSocket message and capture response
send_websocket_message() {
local message="$1"
local expected_response="$2"
local timeout="${3:-$TIMEOUT}"
# NIP-44 encryption helper function using nak
encrypt_nip44_content() {
local content="$1"
local sender_privkey="$2"
local receiver_pubkey="$3"
log_info "Sending WebSocket message: ${message:0:100}..."
# Use wscat to send message and capture response
local response=""
if command -v wscat &> /dev/null; then
response=$(echo "$message" | timeout "$timeout" wscat -c "$RELAY_URL" 2>/dev/null | head -1)
else
log_error "wscat not found - required for WebSocket testing"
if [ -z "$content" ] || [ -z "$sender_privkey" ] || [ -z "$receiver_pubkey" ]; then
log_error "encrypt_nip44_content: missing required parameters"
return 1
fi
log_info "DEBUG: About to encrypt content: '$content'" >&2
log_info "DEBUG: Sender privkey: $sender_privkey" >&2
log_info "DEBUG: Receiver pubkey: $receiver_pubkey" >&2
# Use nak to perform NIP-44 encryption with correct syntax:
# nak encrypt --recipient-pubkey <pubkey> --sec <private_key> [plaintext]
local encrypted_content
encrypted_content=$(nak encrypt --recipient-pubkey "$receiver_pubkey" --sec "$sender_privkey" "$content" 2>/dev/null)
if [ $? -ne 0 ] || [ -z "$encrypted_content" ]; then
log_error "Failed to encrypt content with NIP-44"
log_error "Content: $content"
log_error "Sender privkey: $sender_privkey"
log_error "Receiver pubkey: $receiver_pubkey"
return 1
fi
# Validate that encrypted content is valid base64 and doesn't contain problematic characters
if ! echo "$encrypted_content" | grep -q '^[A-Za-z0-9+/]*=*$'; then
log_error "Encrypted content contains invalid characters for JSON: $encrypted_content"
return 1
fi
# Check if encrypted content is valid UTF-8/base64
if ! echo "$encrypted_content" | base64 -d >/dev/null 2>&1; then
log_warning "Encrypted content may not be valid base64: $encrypted_content"
fi
log_info "DEBUG: Encrypted content: $encrypted_content" >&2
log_info "Successfully encrypted content with NIP-44" >&2
echo "$encrypted_content"
return 0
}
# NIP-44 decryption helper function using nak
decrypt_nip44_content() {
local encrypted_content="$1"
local receiver_privkey="$2"
local sender_pubkey="$3"
if [ -z "$encrypted_content" ] || [ -z "$receiver_privkey" ] || [ -z "$sender_pubkey" ]; then
log_error "decrypt_nip44_content: missing required parameters"
return 1
fi
log_info "DEBUG: Decrypting content: $encrypted_content"
# Use nak to perform NIP-44 decryption with correct syntax:
# nak decrypt --sender-pubkey <pubkey> --sec <private_key> [encrypted_content]
local decrypted_content
decrypted_content=$(nak decrypt --sender-pubkey "$sender_pubkey" --sec "$receiver_privkey" "$encrypted_content" 2>/dev/null)
if [ $? -ne 0 ] || [ -z "$decrypted_content" ]; then
log_error "Failed to decrypt content with NIP-44"
log_error "Encrypted content: $encrypted_content"
log_error "Receiver privkey: $receiver_privkey"
log_error "Sender pubkey: $sender_pubkey"
return 1
fi
log_info "DEBUG: Decrypted content: $decrypted_content"
log_info "Successfully decrypted content with NIP-44"
echo "$decrypted_content"
return 0
}
# Send WebSocket message and capture response (simplified pattern from 1_nip_test.sh)
send_websocket_message() {
local message="$1"
local timeout="${2:-$TIMEOUT}"
# Use websocat to send message and capture response (following pattern from tests/1_nip_test.sh)
local response=""
if command -v websocat &> /dev/null; then
response=$(printf '%s\n' "$message" | timeout "$timeout" websocat "$RELAY_URL" 2>&1 || echo "Connection failed")
# Check if connection failed
if [[ "$response" == *"Connection failed"* ]]; then
log_error "Failed to connect to relay"
return 1
fi
else
log_error "websocat not found - required for WebSocket testing"
log_error "Please install websocat for WebSocket communication"
return 1
fi
echo "$response"
}
# Send admin event and capture response (simplified pattern from 1_nip_test.sh)
send_admin_event() {
local event_json="$1"
local description="$2"
local timeout_seconds="${3:-10}"
log_info "Sending admin event: $description"
# Create EVENT message using jq to properly handle special characters
local event_message
event_message=$(jq -n --argjson event "$event_json" '["EVENT", $event]')
# Validate that the event message is valid UTF-8 (temporarily disabled for debugging)
# if ! echo "$event_message" | iconv -f utf-8 -t utf-8 >/dev/null 2>&1; then
# log_error "Event message contains invalid UTF-8 characters"
# return 1
# fi
# Use websocat to send event and capture OK response
local response=""
if command -v websocat &> /dev/null; then
log_info "Sending event using websocat..."
# Debug: Show what we're sending
log_info "DEBUG: Event message being sent: $event_message"
# Write to temporary file to avoid shell interpretation issues
local temp_file="${TEMP_DIR}/event_message_$$"
printf '%s\n' "$event_message" > "$temp_file"
# Send via websocat using file input with delay to receive response
response=$(timeout "$timeout_seconds" sh -c "cat '$temp_file'; sleep 0.5" | websocat "$RELAY_URL" 2>&1)
local websocat_exit_code=$?
# Clean up temp file
rm -f "$temp_file"
log_info "DEBUG: Websocat exit code: $websocat_exit_code"
log_info "DEBUG: Websocat response: $response"
# Check for specific websocat errors
if [[ "$response" == *"UTF-8 failure"* ]]; then
log_error "UTF-8 encoding error in event data for $description"
log_error "Event message: $event_message"
return 1
elif [[ "$response" == *"Connection failed"* ]] || [[ "$response" == *"Connection refused"* ]] || [[ "$response" == *"timeout"* ]]; then
log_error "Failed to connect to relay for $description"
return 1
elif [[ "$response" == *"error running"* ]]; then
log_error "Websocat error for $description: $response"
return 1
elif [ $websocat_exit_code -eq 0 ]; then
log_info "Event sent successfully via websocat"
else
log_warning "Websocat returned exit code $websocat_exit_code"
fi
else
log_error "websocat not found - required for WebSocket testing"
return 1
fi
echo "$response"
}
# Send admin query and wait for encrypted response
send_admin_query() {
local event_json="$1"
local description="$2"
local timeout_seconds="${3:-15}"
log_info "Sending admin query: $description"
# Create EVENT message using jq to properly handle special characters
local event_message
event_message=$(jq -n --argjson event "$event_json" '["EVENT", $event]')
# For queries, we need to also send a REQ to get the response
local sub_id="admin_query_$(date +%s)"
local req_message="[\"REQ\",\"$sub_id\",{\"kinds\":[23456],\"authors\":[\"$RELAY_PUBKEY\"],\"#p\":[\"$ADMIN_PUBKEY\"]}]"
local close_message="[\"CLOSE\",\"$sub_id\"]"
# Send query event and subscription in sequence
local response=""
if command -v websocat &> /dev/null; then
response=$(printf '%s\n%s\n%s\n' "$event_message" "$req_message" "$close_message" | timeout "$timeout_seconds" websocat "$RELAY_URL" 2>&1 || echo "Connection failed")
# Check if connection failed
if [[ "$response" == *"Connection failed"* ]]; then
log_error "Failed to connect to relay for $description"
return 1
fi
# Look for EVENT responses that might contain encrypted query data
local event_response=$(echo "$response" | grep '"EVENT"' | tail -1)
if [ -n "$event_response" ]; then
# Extract the event JSON from the EVENT message
local event_json=$(echo "$event_response" | jq -r '.[2]' 2>/dev/null)
if [ -n "$event_json" ] && [ "$event_json" != "null" ]; then
# Check if this is a kind 23456 response event
local event_kind=$(echo "$event_json" | jq -r '.kind' 2>/dev/null)
if [ "$event_kind" = "23456" ]; then
# Extract encrypted content and decrypt it
local encrypted_content=$(echo "$event_json" | jq -r '.content' 2>/dev/null)
if [ -n "$encrypted_content" ] && [ "$encrypted_content" != "null" ]; then
# Decrypt the response using NIP-44
local decrypted_content
decrypted_content=$(decrypt_nip44_content "$encrypted_content" "$ADMIN_PRIVKEY" "$RELAY_PUBKEY")
if [ $? -eq 0 ] && [ -n "$decrypted_content" ]; then
log_info "Successfully decrypted query response"
echo "$decrypted_content"
return 0
else
log_warning "Failed to decrypt query response content"
fi
fi
fi
fi
fi
else
log_error "websocat not found - required for WebSocket testing"
return 1
fi
# Return the raw response if no encrypted content found
echo "$response"
}
@@ -145,15 +367,26 @@ send_auth_rule_event() {
local pattern_value="$4" # actual pubkey or hash value
local description="$5" # optional description
log_info "Creating auth rule event: $action $rule_type $pattern_type ${pattern_value:0:16}..."
log_info "Creating auth rule event: $action $rule_type $pattern_type $pattern_value"
# Create the auth rule event using nak - match the working NIP-42 pattern
# Create command array according to README.md API specification
# Format: ["blacklist", "pubkey", "abc123..."] or ["whitelist", "pubkey", "def456..."]
local command_array="[\"$rule_type\", \"$pattern_type\", \"$pattern_value\"]"
# Encrypt the command content using NIP-44
local encrypted_content
encrypted_content=$(encrypt_nip44_content "$command_array" "$ADMIN_PRIVKEY" "$RELAY_PUBKEY")
if [ $? -ne 0 ] || [ -z "$encrypted_content" ]; then
log_error "Failed to encrypt auth rule command content"
return 1
fi
# Create the auth rule event using nak with NIP-44 encrypted content
# Using Kind 23456 (admin commands) with proper relay targeting and encrypted content
local event_json
event_json=$(nak event -k 33335 --content "{\"action\":\"$action\",\"description\":\"$description\"}" \
-t "d=$RELAY_PUBKEY" \
-t "$rule_type=$pattern_type" \
-t "pattern=$pattern_value" \
-t "action=$action" \
event_json=$(nak event -k 23456 --content "$encrypted_content" \
-t "p=$RELAY_PUBKEY" \
--sec "$ADMIN_PRIVKEY" 2>/dev/null)
if [ $? -ne 0 ] || [ -z "$event_json" ]; then
@@ -161,16 +394,18 @@ send_auth_rule_event() {
return 1
fi
# Send the event using nak directly to relay (more reliable than wscat)
log_info "DEBUG: Created event JSON: $event_json"
# Send the event using simplified WebSocket pattern
log_info "Publishing auth rule event to relay..."
local result
result=$(echo "$event_json" | timeout 10s nak event "$RELAY_URL" 2>&1)
result=$(send_admin_event "$event_json" "auth rule $action")
local exit_code=$?
log_info "Auth rule event result: $result"
# Check if response indicates success
if [ $exit_code -eq 0 ] && echo "$result" | grep -q -i "success\|OK.*true\|published"; then
if [ $exit_code -eq 0 ] && echo "$result" | grep -q -i '"OK".*true'; then
log_success "Auth rule $action successful"
return 0
else
@@ -179,6 +414,58 @@ send_auth_rule_event() {
fi
}
# Clear all auth rules using the new system command functionality
clear_all_auth_rules() {
log_info "Clearing all existing auth rules..."
# Create command array according to README.md API specification
# Format: ["system_command", "clear_all_auth_rules"]
local command_array="[\"system_command\", \"clear_all_auth_rules\"]"
log_info "DEBUG: Command array: $command_array"
# Encrypt the command content using NIP-44
local encrypted_content
encrypted_content=$(encrypt_nip44_content "$command_array" "$ADMIN_PRIVKEY" "$RELAY_PUBKEY")
if [ $? -ne 0 ] || [ -z "$encrypted_content" ]; then
log_error "Failed to encrypt system command content"
return 1
fi
log_info "DEBUG: Encrypted content: $encrypted_content"
# Create system command event to clear all auth rules
# Using Kind 23456 (admin commands) with proper relay targeting and encrypted content
local event_json
event_json=$(nak event -k 23456 --content "$encrypted_content" \
-t "p=$RELAY_PUBKEY" \
--sec "$ADMIN_PRIVKEY" 2>/dev/null)
if [ $? -ne 0 ] || [ -z "$event_json" ]; then
log_error "Failed to create clear auth rules event with nak"
return 1
fi
log_info "DEBUG: Created event JSON: $event_json"
# Send the event using simplified WebSocket pattern
log_info "Sending clear all auth rules command..."
local result
result=$(send_admin_event "$event_json" "clear all auth rules")
local exit_code=$?
log_info "Clear auth rules result: $result"
# Check if response indicates success
if [ $exit_code -eq 0 ] && echo "$result" | grep -q -i '"OK".*true'; then
log_success "All auth rules cleared successfully"
return 0
else
log_error "Failed to clear auth rules: $result (exit code: $exit_code)"
return 1
fi
}
# Test event publishing with a specific key
test_event_publishing() {
local test_privkey="$1"
@@ -189,7 +476,7 @@ test_event_publishing() {
log_info "Testing event publishing: $description"
# Create a simple test event (kind 1 - text note) using nak like NIP-42 test
local test_content="Test message from ${test_pubkey:0:16}... at $(date)"
local test_content="Test message from $test_pubkey at $(date)"
local test_event
test_event=$(nak event -k 1 --content "$test_content" --sec "$test_privkey" 2>/dev/null)
@@ -198,10 +485,10 @@ test_event_publishing() {
return 1
fi
# Send the event using nak directly (more reliable than wscat)
# Send the event using nak directly (more reliable than websocat)
log_info "Publishing test event to relay..."
local result
result=$(echo "$test_event" | timeout 10s nak event "$RELAY_URL" 2>&1)
result=$(printf '%s\n' "$test_event" | timeout 10s nak event "$RELAY_URL" 2>&1)
local exit_code=$?
log_info "Event publishing result: $result"
@@ -236,9 +523,6 @@ setup_test_environment() {
# Create temporary directory
mkdir -p "$TEMP_DIR"
# Clear log file
echo "=== C-Relay Whitelist/Blacklist Test Started at $(date) ===" > "$LOG_FILE"
# Check if required tools are available - like NIP-42 test
log_info "Checking dependencies..."
@@ -258,9 +542,10 @@ setup_test_environment() {
exit 1
fi
if ! command -v wscat &> /dev/null; then
log_warning "wscat not found. Some WebSocket tests may be limited"
log_warning "Install with: npm install -g wscat"
if ! command -v websocat &> /dev/null; then
log_error "websocat not found - required for WebSocket testing"
log_error "Please install websocat for WebSocket communication"
exit 1
fi
log_success "Dependencies check complete"
@@ -283,11 +568,22 @@ test_admin_authentication() {
log "Test $TESTS_RUN: Admin Authentication"
# Create a simple configuration event to test admin authentication
local content="Testing admin authentication"
# Using Kind 23456 (admin commands) with NIP-44 encrypted content
# Format: ["system_command", "system_status"]
local command_array="[\"system_command\", \"system_status\"]"
# Encrypt the command content using NIP-44
local encrypted_content
encrypted_content=$(encrypt_nip44_content "$command_array" "$ADMIN_PRIVKEY" "$RELAY_PUBKEY")
if [ $? -ne 0 ] || [ -z "$encrypted_content" ]; then
fail_test "Failed to encrypt admin authentication test content"
return
fi
local config_event
config_event=$(nak event -k 33334 --content "$content" \
-t "d=$RELAY_PUBKEY" \
-t "test_auth=true" \
config_event=$(nak event -k 23456 --content "$encrypted_content" \
-t "p=$RELAY_PUBKEY" \
--sec "$ADMIN_PRIVKEY" 2>/dev/null)
if [ $? -ne 0 ]; then
@@ -295,37 +591,92 @@ test_admin_authentication() {
return
fi
# DEBUG: Print the full event that will be sent
log_info "=== DEBUG: Full admin event being sent ==="
echo "$config_event" | jq . 2>/dev/null || echo "$config_event"
log_info "=== END DEBUG EVENT ==="
# Send admin event
local message="[\"EVENT\",$config_event]"
log_info "=== DEBUG: Full WebSocket message ==="
echo "$message"
log_info "=== END DEBUG MESSAGE ==="
# Send admin event using the proper admin event function
local response
response=$(send_websocket_message "$message" "OK" 10)
response=$(send_admin_event "$config_event" "admin authentication test")
local exit_code=$?
# DEBUG: Print the full response from server
log_info "=== DEBUG: Full server response ==="
echo "$response"
log_info "=== END DEBUG RESPONSE ==="
log_info "Admin authentication result: $response"
if echo "$response" | grep -q '"OK".*true'; then
if [ $exit_code -eq 0 ] && echo "$response" | grep -q '"OK".*true'; then
pass_test "Admin authentication successful"
else
fail_test "Admin authentication failed: $response"
fail_test "Admin authentication failed: $response (exit code: $exit_code)"
fi
}
# Test 2: Basic Whitelist Functionality
# Test 2: Auth Rules Storage and Query Test
test_auth_rules_storage_query() {
increment_test
log "Test $TESTS_RUN: Auth Rules Storage and Query Test"
# Clear all existing rules to start fresh
clear_all_auth_rules
# Add a simple blacklist rule
log_info "Adding test blacklist rule..."
if send_auth_rule_event "add" "blacklist" "pubkey" "$TEST1_PUBKEY" "Test storage blacklist entry"; then
log_success "Auth rule added successfully"
# Wait a moment for rule to be processed
sleep 1
# Query all auth rules using admin query
log_info "Querying all auth rules..."
# Create command array according to README.md API specification
# Format: ["auth_query", "all"]
local command_array="[\"auth_query\", \"all\"]"
# Encrypt the command content using NIP-44
local encrypted_content
encrypted_content=$(encrypt_nip44_content "$command_array" "$ADMIN_PRIVKEY" "$RELAY_PUBKEY")
if [ $? -ne 0 ] || [ -z "$encrypted_content" ]; then
fail_test "Failed to encrypt auth query content"
return
fi
local query_event
query_event=$(nak event -k 23456 --content "$encrypted_content" \
-t "p=$RELAY_PUBKEY" \
--sec "$ADMIN_PRIVKEY" 2>/dev/null)
if [ $? -ne 0 ] || [ -z "$query_event" ]; then
fail_test "Failed to create auth query event"
return
fi
# Send the query event using simplified WebSocket pattern
log_info "Sending auth query to relay..."
local decrypted_response
decrypted_response=$(send_admin_query "$query_event" "auth rules query")
local exit_code=$?
if [ $exit_code -eq 0 ] && [ -n "$decrypted_response" ]; then
log_info "Decrypted query response: $decrypted_response"
# Check if the decrypted response contains our test rule
if echo "$decrypted_response" | grep -q "$TEST1_PUBKEY"; then
pass_test "Auth rule storage and query working - found test rule in decrypted query results"
else
fail_test "Auth rule not found in decrypted query results - rule may not have been stored"
fi
else
fail_test "Failed to receive or decrypt auth query response"
fi
else
fail_test "Failed to add auth rule for storage test"
fi
}
# Test 3: Basic Whitelist Functionality
test_basic_whitelist() {
increment_test
log "Test $TESTS_RUN: Basic Whitelist Functionality"
# Clear all existing rules to start fresh
clear_all_auth_rules
# Add TEST1 pubkey to whitelist
if send_auth_rule_event "add" "whitelist" "pubkey" "$TEST1_PUBKEY" "Test whitelist entry"; then
# Test that whitelisted pubkey can publish
@@ -339,11 +690,14 @@ test_basic_whitelist() {
fi
}
# Test 3: Basic Blacklist Functionality
# Test 4: Basic Blacklist Functionality
test_basic_blacklist() {
increment_test
log "Test $TESTS_RUN: Basic Blacklist Functionality"
# Clear all existing rules to start fresh
clear_all_auth_rules
# Add TEST2 pubkey to blacklist
if send_auth_rule_event "add" "blacklist" "pubkey" "$TEST2_PUBKEY" "Test blacklist entry"; then
# Test that blacklisted pubkey cannot publish
@@ -357,11 +711,20 @@ test_basic_blacklist() {
fi
}
# Test 4: Rule Removal
# Test 5: Rule Removal
test_rule_removal() {
increment_test
log "Test $TESTS_RUN: Rule Removal"
# Clear all existing rules to start fresh
clear_all_auth_rules
# First add TEST2 to blacklist to test removal
if ! send_auth_rule_event "add" "blacklist" "pubkey" "$TEST2_PUBKEY" "Test blacklist for removal"; then
fail_test "Failed to add pubkey to blacklist for removal test"
return
fi
# Remove TEST2 from blacklist
if send_auth_rule_event "remove" "blacklist" "pubkey" "$TEST2_PUBKEY" "Remove test blacklist entry"; then
# Test that previously blacklisted pubkey can now publish
@@ -375,11 +738,14 @@ test_rule_removal() {
fi
}
# Test 5: Multiple Users Scenario
# Test 6: Multiple Users Scenario
test_multiple_users() {
increment_test
log "Test $TESTS_RUN: Multiple Users Scenario"
# Clear all existing rules to start fresh
clear_all_auth_rules
# Add TEST1 to whitelist and TEST3 to blacklist
local success_count=0
@@ -408,11 +774,14 @@ test_multiple_users() {
fi
}
# Test 6: Priority Testing (Blacklist vs Whitelist)
# Test 7: Priority Testing (Blacklist vs Whitelist)
test_priority_rules() {
increment_test
log "Test $TESTS_RUN: Priority Rules Testing"
# Clear all existing rules to start fresh
clear_all_auth_rules
# Add same pubkey to both whitelist and blacklist
local setup_success=0
@@ -438,11 +807,14 @@ test_priority_rules() {
fi
}
# Test 7: Hash-based Blacklist
# Test 8: Hash-based Blacklist
test_hash_blacklist() {
increment_test
log "Test $TESTS_RUN: Hash-based Blacklist"
# Clear all existing rules to start fresh
clear_all_auth_rules
# Create a test event to get its hash
local test_content="Content to be blacklisted by hash"
local test_event
@@ -462,14 +834,14 @@ test_hash_blacklist() {
return
fi
log_info "Testing hash blacklist with event ID: ${event_id:0:16}..."
log_info "Testing hash blacklist with event ID: $event_id"
# Add the event ID to hash blacklist
if send_auth_rule_event "add" "blacklist" "hash" "$event_id" "Test hash blacklist"; then
# Try to publish the same event using nak - should be blocked
log_info "Attempting to publish blacklisted event..."
local result
result=$(echo "$test_event" | timeout 10s nak event "$RELAY_URL" 2>&1)
result=$(printf '%s\n' "$test_event" | timeout 10s nak event "$RELAY_URL" 2>&1)
local exit_code=$?
if [ $exit_code -ne 0 ] || echo "$result" | grep -q -i "blocked\|denied\|rejected\|blacklist"; then
@@ -482,11 +854,14 @@ test_hash_blacklist() {
fi
}
# Test 8: WebSocket Connection Behavior
# Test 9: WebSocket Connection Behavior
test_websocket_behavior() {
increment_test
log "Test $TESTS_RUN: WebSocket Connection Behavior"
# Clear all existing rules to start fresh
clear_all_auth_rules
# Test that the WebSocket connection handles multiple rapid requests
local rapid_success_count=0
@@ -498,7 +873,7 @@ test_websocket_behavior() {
if [ $? -eq 0 ]; then
local message="[\"EVENT\",$test_event]"
local response
response=$(send_websocket_message "$message" "OK" 5)
response=$(send_websocket_message "$message" 5)
if echo "$response" | grep -q '"OK"'; then
rapid_success_count=$((rapid_success_count + 1))
@@ -516,11 +891,14 @@ test_websocket_behavior() {
fi
}
# Test 9: Rule Persistence Verification
# Test 10: Rule Persistence Verification
test_rule_persistence() {
increment_test
log "Test $TESTS_RUN: Rule Persistence Verification"
# Clear all existing rules to start fresh
clear_all_auth_rules
# Add a rule, then verify it persists by testing enforcement
if send_auth_rule_event "add" "blacklist" "pubkey" "$TEST3_PUBKEY" "Persistence test blacklist"; then
# Wait a moment for rule to be processed
@@ -546,7 +924,7 @@ test_rule_persistence() {
fi
}
# Test 10: Cleanup and Final Verification
# Test 11: Cleanup and Final Verification
test_cleanup_verification() {
increment_test
log "Test $TESTS_RUN: Cleanup and Final Verification"
@@ -589,10 +967,11 @@ run_all_tests() {
# Setup
setup_test_environment
# Run only test 1 for debugging admin authentication
test_admin_authentication
clear_all_auth_rules
# Comment out other tests for now to focus on debugging
test_admin_authentication
# test_auth_rules_storage_query
# test_basic_whitelist
# test_basic_blacklist
# test_rule_removal
@@ -648,8 +1027,8 @@ main() {
echo -e "${BLUE}===============================================${RESET}"
echo ""
# Check if relay is running - use the same method we verified manually
if ! echo '["REQ","connection_test",{}]' | timeout 5 wscat -c "$RELAY_URL" >/dev/null 2>&1; then
# Check if relay is running - using websocat like the working tests
if ! printf '%s\n' '["REQ","connection_test",{}]' | timeout 5 websocat "$RELAY_URL" >/dev/null 2>&1; then
log_error "Cannot connect to relay at $RELAY_URL"
log_error "Please ensure the C-Relay server is running in test mode"
exit 1
@@ -661,12 +1040,10 @@ main() {
if run_all_tests; then
echo ""
log_success "All whitelist/blacklist tests completed successfully!"
echo -e "Test log saved to: ${YELLOW}$LOG_FILE${RESET}"
exit 0
else
echo ""
log_error "Some tests failed. Check the log for details."
echo -e "Test log saved to: ${YELLOW}$LOG_FILE${RESET}"
log_error "Some tests failed."
exit 1
fi
}

View File

@@ -1,21 +0,0 @@
=== C-Relay Whitelist/Blacklist Test Started at Tue Sep 23 11:20:40 AM EDT 2025 ===
[INFO] Checking dependencies...
[SUCCESS] Dependencies check complete
[INFO] Generated keypair for TEST1: pubkey=eab7cac03049d07f...
[INFO] Generated keypair for TEST2: pubkey=4e07a99f656d5301...
[INFO] Generated keypair for TEST3: pubkey=bf48b836426805cb...
[SUCCESS] Test environment setup complete
[11:20:41] Test 1: Admin Authentication
[INFO] === DEBUG: Full admin event being sent ===
[INFO] === END DEBUG EVENT ===
[INFO] === DEBUG: Full WebSocket message ===
[INFO] === END DEBUG MESSAGE ===
[INFO] Sending WebSocket message: ["EVENT",{"kind":33334,"id":"ce73fa326eb558505742770eb927a50edc16a69512089939f76da90c7ca5291f","pubk...
[INFO] === DEBUG: Full server response ===
[INFO] === END DEBUG RESPONSE ===
[ERROR] Test 1: FAILED - Admin authentication failed: [INFO] Sending WebSocket message: ["EVENT",{"kind":33334,"id":"ce73fa326eb558505742770eb927a50edc16a69512089939f76da90c7ca5291f","pubk...
[ERROR] 1 out of 1 tests failed.
[ERROR] Some tests failed. Check the log for details.
[11:20:42] Cleaning up test environment...
[INFO] Temporary directory removed: /tmp/c_relay_test_1773069
[11:20:42] Test cleanup completed.