Compare commits

...

5 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
15 changed files with 4408 additions and 1696 deletions

3
.gitignore vendored
View File

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

View File

@@ -27,7 +27,7 @@
## Critical Integration Issues ## Critical Integration Issues
### Event-Based Configuration System ### 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 - Admin private key shown **only once** on first startup
- Configuration changes require cryptographically signed events - Configuration changes require cryptographically signed events
- Database path determined by generated relay pubkey - Database path determined by generated relay pubkey
@@ -35,7 +35,7 @@
### First-Time Startup Sequence ### First-Time Startup Sequence
1. Relay generates admin keypair and relay keypair 1. Relay generates admin keypair and relay keypair
2. Creates database file with relay pubkey as filename 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 4. **CRITICAL**: Admin private key displayed once and never stored on disk
### Port Management ### Port Management
@@ -48,20 +48,30 @@
- Schema version 4 with JSON tag storage - Schema version 4 with JSON tag storage
- **Critical**: Event expiration filtering done at application level, not SQL level - **Critical**: Event expiration filtering done at application level, not SQL level
### Configuration Event Structure ### Admin API Event Structure
```json ```json
{ {
"kind": 33334, "kind": 23456,
"content": "C Nostr Relay Configuration", "content": "base64_nip44_encrypted_command_array",
"tags": [ "tags": [
["d", "<relay_pubkey>"], ["p", "<relay_pubkey>"]
["relay_description", "value"],
["max_subscriptions_per_client", "25"],
["pow_min_difficulty", "16"]
] ]
} }
``` ```
**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 ### Process Management
```bash ```bash
# Kill existing relay processes # Kill existing relay processes

View File

@@ -1,513 +0,0 @@
# Implementation Plan: Enhanced Admin Event API Structure
## Current Issue
The current admin event routing at [`main.c:3248-3268`](src/main.c:3248) has a security vulnerability:
```c
if (event_kind == 23455 || event_kind == 23456) {
// Admin event processing
int admin_result = process_admin_event_in_config(event, admin_error, sizeof(admin_error), wsi);
} else {
// Regular event storage and broadcasting
}
```
**Problem**: Any event with these kinds gets routed to admin processing, regardless of authorization. This allows unauthorized users to send admin events that could be processed as legitimate admin commands.
**Note**: Event kinds 33334 and 33335 are no longer used and have been removed from the admin event routing.
## Required Security Enhancement
Admin events must be validated for proper authorization BEFORE routing to admin processing:
1. **Relay Public Key Check**: Event must have a `p` tag equal to the relay's public key
2. **Admin Signature Check**: Event must be signed by an authorized admin private key
3. **Fallback to Regular Processing**: If authorization fails, treat as regular event (not admin event)
## Implementation Plan
### Phase 1: Add Admin Authorization Validation
#### 1.1 Create Consolidated Admin Authorization Function
**Location**: [`src/main.c`](src/main.c) or [`src/config.c`](src/config.c)
```c
/**
* Consolidated admin event authorization validator
* Implements defense-in-depth security for admin events
*
* @param event - The event to validate for admin authorization
* @param error_message - Buffer for detailed error messages
* @param error_size - Size of error message buffer
* @return 0 if authorized, -1 if unauthorized, -2 if validation error
*/
int is_authorized_admin_event(cJSON* event, char* error_message, size_t error_size) {
if (!event) {
snprintf(error_message, error_size, "admin_auth: null event");
return -2;
}
// Extract event components
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
cJSON* tags_obj = cJSON_GetObjectItem(event, "tags");
if (!kind_obj || !pubkey_obj || !tags_obj) {
snprintf(error_message, error_size, "admin_auth: missing required fields");
return -2;
}
// Validation Layer 1: Kind Check
int event_kind = (int)cJSON_GetNumberValue(kind_obj);
if (event_kind != 23455 && event_kind != 23456) {
snprintf(error_message, error_size, "admin_auth: not an admin event kind");
return -1;
}
// Validation Layer 2: Relay Targeting Check
const char* relay_pubkey = get_config_value("relay_pubkey");
if (!relay_pubkey) {
snprintf(error_message, error_size, "admin_auth: relay pubkey not configured");
return -2;
}
// Check for 'p' tag targeting this relay
int has_relay_target = 0;
if (cJSON_IsArray(tags_obj)) {
cJSON* tag = NULL;
cJSON_ArrayForEach(tag, tags_obj) {
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
cJSON* tag_value = cJSON_GetArrayItem(tag, 1);
if (cJSON_IsString(tag_name) && cJSON_IsString(tag_value)) {
const char* name = cJSON_GetStringValue(tag_name);
const char* value = cJSON_GetStringValue(tag_value);
if (strcmp(name, "p") == 0 && strcmp(value, relay_pubkey) == 0) {
has_relay_target = 1;
break;
}
}
}
}
}
if (!has_relay_target) {
// Admin event for different relay - not unauthorized, just not for us
snprintf(error_message, error_size, "admin_auth: admin event for different relay");
return -1;
}
// Validation Layer 3: Admin Signature Check (only if targeting this relay)
const char* event_pubkey = cJSON_GetStringValue(pubkey_obj);
if (!event_pubkey) {
snprintf(error_message, error_size, "admin_auth: invalid pubkey format");
return -2;
}
const char* admin_pubkey = get_config_value("admin_pubkey");
if (!admin_pubkey || strcmp(event_pubkey, admin_pubkey) != 0) {
// This is the ONLY case where we log as "Unauthorized admin event attempt"
// because it's targeting THIS relay but from wrong admin
snprintf(error_message, error_size, "admin_auth: unauthorized admin for this relay");
log_warning("SECURITY: Unauthorized admin event attempt for this relay");
return -1;
}
// All validation layers passed
log_info("ADMIN: Admin event authorized");
return 0;
}
```
#### 1.2 Update Event Routing Logic
**Location**: [`main.c:3248`](src/main.c:3248)
```c
// Current problematic code:
if (event_kind == 23455 || event_kind == 23456) {
// Admin event processing
int admin_result = process_admin_event_in_config(event, admin_error, sizeof(admin_error), wsi);
} else {
// Regular event storage and broadcasting
}
// Enhanced secure code with consolidated authorization:
if (result == 0) {
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
if (kind_obj && cJSON_IsNumber(kind_obj)) {
int event_kind = (int)cJSON_GetNumberValue(kind_obj);
// Check if this is an admin event
if (event_kind == 23455 || event_kind == 23456) {
// Use consolidated authorization check
char auth_error[512] = {0};
int auth_result = is_authorized_admin_event(event, auth_error, sizeof(auth_error));
if (auth_result == 0) {
// Authorized admin event - process through admin API
char admin_error[512] = {0};
int admin_result = process_admin_event_in_config(event, admin_error, sizeof(admin_error), wsi);
if (admin_result != 0) {
result = -1;
strncpy(error_message, admin_error, sizeof(error_message) - 1);
}
// Admin events are NOT broadcast to subscriptions
} else {
// Unauthorized admin event - treat as regular event
log_warning("Unauthorized admin event treated as regular event");
if (store_event(event) != 0) {
result = -1;
strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1);
} else {
broadcast_event_to_subscriptions(event);
}
}
} else {
// Regular event - normal processing
if (store_event(event) != 0) {
result = -1;
strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1);
} else {
broadcast_event_to_subscriptions(event);
}
}
}
}
```
### Phase 2: Enhanced Admin Event Processing
#### 2.1 Admin Event Validation in Config System
**Location**: [`src/config.c`](src/config.c) - [`process_admin_event_in_config()`](src/config.c:2065)
Add additional validation within the admin processing function:
```c
int process_admin_event_in_config(cJSON* event, char* error_buffer, size_t error_buffer_size, struct lws* wsi) {
// Double-check authorization (defense in depth)
if (!is_authorized_admin_event(event)) {
snprintf(error_buffer, error_buffer_size, "unauthorized: not a valid admin event");
return -1;
}
// Continue with existing admin event processing...
// ... rest of function unchanged
}
```
#### 2.2 Logging and Monitoring
Add comprehensive logging for admin event attempts:
```c
// In the routing logic - enhanced logging
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
int event_kind = kind_obj ? cJSON_GetNumberValue(kind_obj) : -1;
const char* event_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : "unknown";
if (is_authorized_admin_event(event)) {
char log_msg[256];
snprintf(log_msg, sizeof(log_msg),
"ADMIN EVENT: Authorized admin event (kind=%d) from pubkey=%.16s...",
event_kind, event_pubkey);
log_info(log_msg);
} else if (event_kind == 23455 || event_kind == 23456) {
// This catches unauthorized admin event attempts
char log_msg[256];
snprintf(log_msg, sizeof(log_msg),
"SECURITY: Unauthorized admin event attempt (kind=%d) from pubkey=%.16s...",
event_kind, event_pubkey);
log_warning(log_msg);
}
```
## Phase 3: Unified Output Flow Architecture
### 3.1 Current Output Flow Analysis
After analyzing both [`main.c`](src/main.c) and [`config.c`](src/config.c), the **admin event responses already flow through the standard WebSocket output pipeline**. This is the correct architecture and requires no changes.
#### Standard WebSocket Output Pipeline
**Regular Events** ([`main.c:2978-2996`](src/main.c:2978)):
```c
// Database query responses
unsigned char* buf = malloc(LWS_PRE + msg_len);
memcpy(buf + LWS_PRE, msg_str, msg_len);
lws_write(wsi, buf + LWS_PRE, msg_len, LWS_WRITE_TEXT);
free(buf);
```
**OK Responses** ([`main.c:3342-3375`](src/main.c:3342)):
```c
// Event processing results: ["OK", event_id, success_boolean, message]
unsigned char *buf = malloc(LWS_PRE + response_len);
memcpy(buf + LWS_PRE, response_str, response_len);
lws_write(wsi, buf + LWS_PRE, response_len, LWS_WRITE_TEXT);
free(buf);
```
#### Admin Event Output Pipeline (Already Unified)
**Admin Responses** ([`config.c:2363-2414`](src/config.c:2363)):
```c
// Admin query responses use IDENTICAL pattern
int send_websocket_response_data(struct lws* wsi, cJSON* response_data) {
unsigned char* buf = malloc(LWS_PRE + response_len);
memcpy(buf + LWS_PRE, response_str, response_len);
// Same lws_write() call as regular events
int result = lws_write(wsi, buf + LWS_PRE, response_len, LWS_WRITE_TEXT);
free(buf);
return result;
}
```
### 3.2 Unified Output Flow Confirmation
**Admin responses already use the same WebSocket transmission mechanism as regular events**
**Both admin and regular events use identical buffer allocation patterns**
**Both admin and regular events use the same [`lws_write()`](src/config.c:2393) function**
**Both admin and regular events follow the same cleanup patterns**
### 3.3 Output Flow Integration Points
The admin event processing in [`config.c:2436`](src/config.c:2436) already integrates correctly with the unified output system:
1. **Admin Query Processing** ([`config.c:2568-2583`](src/config.c:2568)):
- Auth queries return structured JSON via [`send_websocket_response_data()`](src/config.c:2571)
- System commands return status data via [`send_websocket_response_data()`](src/config.c:2631)
2. **Response Format Consistency**:
- Admin responses use standard JSON format
- Regular events use standard Nostr event format
- Both transmitted through same WebSocket pipeline
3. **Error Handling Consistency**:
- Admin errors returned via same WebSocket connection
- Regular event errors returned via OK messages
- Both use identical transmission mechanism
### 3.4 Key Architectural Benefits
**No Changes Required**: The output flow is already unified and correctly implemented.
**Security Separation**: Admin events are processed separately but responses flow through the same secure WebSocket channel.
**Performance Consistency**: Both admin and regular responses use the same optimized transmission path.
**Maintenance Simplicity**: Single WebSocket output pipeline reduces complexity and potential bugs.
### 3.5 Admin Event Flow Summary
```
Admin Event Input → Authorization Check → Admin Processing → Unified WebSocket Output
Regular Event Input → Validation → Storage + Broadcast → Unified WebSocket Output
```
Both flows converge at the **Unified WebSocket Output** stage, which is already correctly implemented.
## Phase 4: Integration Points for Secure Admin Event Routing
### 4.1 Configuration System Integration
**Required Configuration Values**:
- `admin_pubkey` - Public key of authorized administrator
- `relay_pubkey` - Public key of this relay instance
**Integration Points**:
1. [`get_config_value()`](src/config.c) - Used by authorization function
2. [`get_relay_pubkey_cached()`](src/config.c) - Used for relay targeting validation
3. Configuration loading during startup - Must ensure admin/relay pubkeys are available
### 4.3 Forward Declarations Required
**Location**: [`src/main.c`](src/main.c) - Add near other forward declarations (around line 230)
```c
// Forward declarations for enhanced admin event authorization
int is_authorized_admin_event(cJSON* event, char* error_message, size_t error_size);
```
### 4.4 Error Handling Integration
**Enhanced Error Response System**:
```c
// In main.c event processing - enhanced error handling for admin events
if (auth_result != 0) {
// Admin authorization failed - send detailed OK response
cJSON* event_id = cJSON_GetObjectItem(event, "id");
if (event_id && cJSON_IsString(event_id)) {
cJSON* response = cJSON_CreateArray();
cJSON_AddItemToArray(response, cJSON_CreateString("OK"));
cJSON_AddItemToArray(response, cJSON_CreateString(cJSON_GetStringValue(event_id)));
cJSON_AddItemToArray(response, cJSON_CreateBool(0)); // Failed
cJSON_AddItemToArray(response, cJSON_CreateString(auth_error));
// Send via standard WebSocket output pipeline
char *response_str = cJSON_Print(response);
if (response_str) {
size_t response_len = strlen(response_str);
unsigned char *buf = malloc(LWS_PRE + response_len);
if (buf) {
memcpy(buf + LWS_PRE, response_str, response_len);
lws_write(wsi, buf + LWS_PRE, response_len, LWS_WRITE_TEXT);
free(buf);
}
free(response_str);
}
cJSON_Delete(response);
}
}
```
### 4.5 Logging Integration Points
**Console Logging**: Uses existing [`log_warning()`](src/main.c:993), [`log_info()`](src/main.c:972) functions
**Security Event Categories**:
- Admin authorization success logged via `log_info()`
- Admin authorization failures logged via `log_warning()`
- Admin event processing logged via existing admin logging
## Phase 5: Detailed Function Specifications
### 5.1 Core Authorization Function
**Function**: `is_authorized_admin_event()`
**Location**: [`src/main.c`](src/main.c) or [`src/config.c`](src/config.c)
**Dependencies**:
- `get_config_value()` for admin/relay pubkeys
- `log_warning()` and `log_info()` for logging
- `cJSON` library for event parsing
**Return Values**:
- `0` - Event is authorized for admin processing
- `-1` - Event is unauthorized (treat as regular event)
- `-2` - Validation error (malformed event)
**Error Handling**: Detailed error messages in provided buffer for client feedback
### 5.2 Enhanced Event Routing
**Location**: [`main.c:3248-3340`](src/main.c:3248)
**Integration**: Replaces existing admin event routing logic
**Dependencies**:
- `is_authorized_admin_event()` for authorization
- `process_admin_event_in_config()` for admin processing
- `store_event()` and `broadcast_event_to_subscriptions()` for regular events
**Security Features**:
- Graceful degradation for unauthorized admin events
- Comprehensive logging of authorization attempts
- No broadcast of admin events to subscriptions
- Detailed error responses for failed authorization
### 5.4 Defense-in-Depth Validation
**Primary Validation**: In main event routing logic
**Secondary Validation**: In `process_admin_event_in_config()` function
**Tertiary Validation**: In individual admin command handlers
**Validation Layers**:
1. **Kind Check** - Must be admin event kind (23455/23456)
2. **Relay Targeting Check** - Must have 'p' tag with this relay's pubkey
3. **Admin Signature Check** - Must be signed by authorized admin (only if targeting this relay)
4. **Processing Check** - Additional validation in admin handlers
**Security Logic**:
- If no 'p' tag for this relay → Admin event for different relay (not unauthorized)
- If 'p' tag for this relay + wrong admin signature → "Unauthorized admin event attempt"
## Phase 6: Event Flow Documentation
### 6.1 Complete Event Processing Flow
```
┌─────────────────┐
│ WebSocket Input │
└─────────┬───────┘
┌─────────────────┐
│ Unified │
│ Validation │ ← nostr_validate_unified_request()
└─────────┬───────┘
┌─────────────────┐
│ Kind-Based │
│ Routing Check │ ← Check if kind 23455/23456
└─────────┬───────┘
┌────▼────┐
│ Admin? │
└────┬────┘
┌─────▼─────┐ ┌─────────────┐
│ YES │ │ NO │
│ │ │ │
▼ │ ▼ │
┌─────────────┐ │ ┌─────────────┐ │
│ Admin │ │ │ Regular │ │
│ Authorization│ │ │ Event │ │
│ Check │ │ │ Processing │ │
└─────┬───────┘ │ └─────┬───────┘ │
│ │ │ │
┌────▼────┐ │ ▼ │
│Authorized?│ │ ┌─────────────┐ │
└────┬────┘ │ │ store_event()│ │
│ │ │ + │ │
┌─────▼─────┐ │ │ broadcast() │ │
│ YES NO │ │ └─────┬───────┘ │
│ │ │ │ │ │ │
│ ▼ ▼ │ │ ▼ │
│┌─────┐┌───┴┐ │ ┌─────────────┐ │
││Admin││Treat│ │ │ WebSocket │ │
││API ││as │ │ │ OK Response │ │
││ ││Reg │ │ └─────────────┘ │
│└──┬──┘└───┬┘ │ │
│ │ │ │ │
│ ▼ │ │ │
│┌─────────┐│ │ │
││WebSocket││ │ │
││Response ││ │ │
│└─────────┘│ │ │
└───────────┴───┘ │
│ │
└───────────────────────────┘
┌─────────────┐
│ Unified │
│ WebSocket │
│ Output │
└─────────────┘
```
### 6.2 Security Decision Points
1. **Event Kind Check** - Identifies potential admin events
2. **Authorization Validation** - Three-layer security check
3. **Routing Decision** - Admin API vs Regular processing
4. **Response Generation** - Unified output pipeline
5. **Audit Logging** - Security event tracking
### 6.3 Error Handling Paths
**Validation Errors**: Return detailed error messages via OK response
**Authorization Failures**: Log security event + treat as regular event
**Processing Errors**: Return admin-specific error responses
**System Errors**: Fallback to standard error handling
This completes the comprehensive implementation plan for the enhanced admin event API structure with unified output flow architecture.

View File

@@ -24,7 +24,7 @@ Do NOT modify the formatting, add emojis, or change the text. Keep the simple fo
## 🔧 Administrator API ## 🔧 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 **tag-based parameters** for simplicity and compatibility. 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 ### Authentication
@@ -32,7 +32,7 @@ All admin commands require signing with the admin private key displayed during f
### Event Structure ### Event Structure
All admin commands use the same unified event structure with tag-based parameters: All admin commands use the same unified event structure with NIP-44 encrypted content:
**Admin Command Event:** **Admin Command Event:**
```json ```json
@@ -41,14 +41,16 @@ All admin commands use the same unified event structure with tag-based parameter
"pubkey": "admin_public_key", "pubkey": "admin_public_key",
"created_at": 1234567890, "created_at": 1234567890,
"kind": 23456, "kind": 23456,
"content": "<nip44 encrypted command>", "content": "AqHBUgcM7dXFYLQuDVzGwMST1G8jtWYyVvYxXhVGEu4nAb4LVw...",
"tags": [ "tags": [
["p", "relay_public_key"], ["p", "relay_public_key"]
], ],
"sig": "event_signature" "sig": "event_signature"
} }
``` ```
The `content` field contains a NIP-44 encrypted JSON array representing the command.
**Admin Response Event:** **Admin Response Event:**
```json ```json
["EVENT", "temp_sub_id", { ["EVENT", "temp_sub_id", {
@@ -56,7 +58,7 @@ All admin commands use the same unified event structure with tag-based parameter
"pubkey": "relay_public_key", "pubkey": "relay_public_key",
"created_at": 1234567890, "created_at": 1234567890,
"kind": 23457, "kind": 23457,
"content": "<nip44 encrypted response>", "content": "BpKCVhfN8eYtRmPqSvWxZnMkL2gHjUiOp3rTyEwQaS5dFg...",
"tags": [ "tags": [
["p", "admin_public_key"] ["p", "admin_public_key"]
], ],
@@ -64,18 +66,21 @@ All admin commands use the same unified event structure with tag-based parameter
}] }]
``` ```
The `content` field contains a NIP-44 encrypted JSON response object.
### Admin Commands ### Admin Commands
All commands are sent as nip44 encrypted content. The following table lists all available commands: All commands are sent as NIP-44 encrypted JSON arrays in the event content. The following table lists all available commands:
| Command Type | Tag Format | Description | | Command Type | Command Format | Description |
|--------------|------------|-------------| |--------------|----------------|-------------|
| **Configuration Management** | | **Configuration Management** |
| `config_update` | `["relay_description", "My Relay"]` | Update relay configuration parameters | | `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", "list_all_keys"]` | List all available configuration keys | | `config_query` | `["config_query", "all"]` | Query all configuration parameters |
| **Auth Rules Management** | | **Auth Rules Management** |
| `auth_add_blacklist` | `["blacklist", "pubkey", "abc123..."]` | Add pubkey to blacklist | | `auth_add_blacklist` | `["blacklist", "pubkey", "abc123..."]` | Add pubkey to blacklist |
| `auth_add_whitelist` | `["whitelist", "pubkey", "def456..."]` | Add pubkey to whitelist | | `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_all` | `["auth_query", "all"]` | Query all auth rules |
| `auth_query_type` | `["auth_query", "whitelist"]` | Query specific rule type | | `auth_query_type` | `["auth_query", "whitelist"]` | Query specific rule type |
| `auth_query_pattern` | `["auth_query", "pattern", "abc123..."]` | Query specific pattern | | `auth_query_pattern` | `["auth_query", "pattern", "abc123..."]` | Query specific pattern |
@@ -116,7 +121,7 @@ All admin commands return **signed EVENT responses** via WebSocket following sta
"pubkey": "relay_public_key", "pubkey": "relay_public_key",
"created_at": 1234567890, "created_at": 1234567890,
"kind": 23457, "kind": 23457,
"content": "nip44 encrypted:{\"status\": \"success\", \"message\": \"Operation completed successfully\"}", "content": "nip44 encrypted:{\"query_type\": \"config_update\", \"status\": \"success\", \"message\": \"Operation completed successfully\", \"timestamp\": 1234567890}",
"tags": [ "tags": [
["p", "admin_public_key"] ["p", "admin_public_key"]
], ],
@@ -131,7 +136,7 @@ All admin commands return **signed EVENT responses** via WebSocket following sta
"pubkey": "relay_public_key", "pubkey": "relay_public_key",
"created_at": 1234567890, "created_at": 1234567890,
"kind": 23457, "kind": 23457,
"content": "nip44 encrypted:{\"status\": \"error\", \"message\": \"Error: invalid configuration value\"}", "content": "nip44 encrypted:{\"query_type\": \"config_update\", \"status\": \"error\", \"error\": \"invalid configuration value\", \"timestamp\": 1234567890}",
"tags": [ "tags": [
["p", "admin_public_key"] ["p", "admin_public_key"]
], ],
@@ -146,7 +151,7 @@ All admin commands return **signed EVENT responses** via WebSocket following sta
"pubkey": "relay_public_key", "pubkey": "relay_public_key",
"created_at": 1234567890, "created_at": 1234567890,
"kind": 23457, "kind": 23457,
"content": "nip44 encrypted:{\"query_type\": \"auth_rules\", \"total_results\": 2, \"data\": [{\"rule_type\": \"blacklist\", \"pattern_type\": \"pubkey\", \"pattern_value\": \"abc123...\", \"action\": \"deny\"}]}", "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": [ "tags": [
["p", "admin_public_key"] ["p", "admin_public_key"]
], ],
@@ -161,7 +166,7 @@ All admin commands return **signed EVENT responses** via WebSocket following sta
"pubkey": "relay_public_key", "pubkey": "relay_public_key",
"created_at": 1234567890, "created_at": 1234567890,
"kind": 23457, "kind": 23457,
"content": "nip44 encrypted:{\"query_type\": \"config_keys\", \"config_keys\": [\"auth_enabled\", \"max_connections\"], \"descriptions\": {\"auth_enabled\": \"Enable whitelist/blacklist rules\"}}", "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": [ "tags": [
["p", "admin_public_key"] ["p", "admin_public_key"]
], ],
@@ -169,3 +174,32 @@ All admin commands return **signed EVENT responses** via WebSocket following sta
}] }]
``` ```
**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

View File

@@ -36,122 +36,70 @@ CREATE TABLE auth_rules (
#### Admin API Commands (via WebSocket with admin private key) #### Admin API Commands (via WebSocket with admin private key)
**Kind 23455: Configuration Management (Ephemeral)** **Kind 23456: Unified Admin API (Ephemeral)**
- Update relay settings, limits, authentication policies - Configuration management: Update relay settings, limits, authentication policies
- **Standard Mode**: Commands in tags `["config_key", "config_value"]`
- **Encrypted Mode**: Commands NIP-44 encrypted in content `{"encrypted_tags": "..."}`
- Content: Descriptive text or encrypted payload
- Security: Optional NIP-44 encryption for sensitive operations
**Kind 23456: Auth Rules & System Management (Ephemeral)**
- Auth rules: Add/remove/query whitelist/blacklist rules - Auth rules: Add/remove/query whitelist/blacklist rules
- System commands: clear rules, status, cache management - System commands: clear rules, status, cache management
- **Standard Mode**: Commands in tags - **Unified Format**: All commands use NIP-44 encrypted content with `["p", "relay_pubkey"]` tags
- Rule format: `["rule_type", "pattern_type", "pattern_value"]` - **Command Types**:
- Query format: `["auth_query", "filter"]` - Configuration: `["config_key", "config_value"]`
- System format: `["system_command", "command_name"]` - Auth rules: `["rule_type", "pattern_type", "pattern_value"]`
- **Encrypted Mode**: Commands NIP-44 encrypted in content `{"encrypted_tags": "..."}` - Queries: `["auth_query", "filter"]` or `["system_command", "command_name"]`
- Content: Action description + optional encrypted payload - **Security**: All admin commands use NIP-44 encryption for privacy and security
- Security: Optional NIP-44 encryption for sensitive operations
#### Configuration Query Commands (using Kind 23455) #### Configuration Commands (using Kind 23456)
1. **List All Configuration Keys (Standard)**: 1. **Update Configuration**:
```json
{
"kind": 23455,
"content": "Discovery query",
"tags": [["config_query", "list_all_keys"]]
}
```
2. **List All Configuration Keys (Encrypted)**:
```json
{
"kind": 23455,
"content": "{\"query\":\"list_config_keys\",\"encrypted_tags\":\"nip44_encrypted_payload\"}",
"tags": []
}
```
*Encrypted payload contains:* `[["config_query", "list_all_keys"]]`
3. **Get Current Configuration (Standard)**:
```json
{
"kind": 23455,
"content": "Config query",
"tags": [["config_query", "get_current_config"]]
}
```
4. **Get Current Configuration (Encrypted)**:
```json
{
"kind": 23455,
"content": "{\"query\":\"get_config\",\"encrypted_tags\":\"nip44_encrypted_payload\"}",
"tags": []
}
```
*Encrypted payload contains:* `[["config_query", "get_current_config"]]`
#### System Management Commands (using Kind 23456)
1. **Clear All Auth Rules (Standard)**:
```json ```json
{ {
"kind": 23456, "kind": 23456,
"content": "{\"action\":\"clear_all\"}", "content": "base64_nip44_encrypted_command_array",
"tags": [["system_command", "clear_all_auth_rules"]] "tags": [["p", "relay_pubkey"]]
} }
``` ```
*Encrypted content contains:* `["relay_description", "My Relay"]`
2. **Clear All Auth Rules (Encrypted)**: 2. **Query System Status**:
```json ```json
{ {
"kind": 23456, "kind": 23456,
"content": "{\"action\":\"clear_all\",\"encrypted_tags\":\"nip44_encrypted_payload\"}", "content": "base64_nip44_encrypted_command_array",
"tags": [] "tags": [["p", "relay_pubkey"]]
} }
``` ```
*Encrypted payload contains:* `[["system_command", "clear_all_auth_rules"]]` *Encrypted content contains:* `["system_command", "system_status"]`
3. **Query All Auth Rules (Standard)**: #### Auth Rules and System Commands (using Kind 23456)
1. **Clear All Auth Rules**:
```json ```json
{ {
"kind": 23456, "kind": 23456,
"content": "{\"query\":\"list_auth_rules\"}", "content": "base64_nip44_encrypted_command_array",
"tags": [["auth_query", "all"]] "tags": [["p", "relay_pubkey"]]
} }
``` ```
*Encrypted content contains:* `["system_command", "clear_all_auth_rules"]`
4. **Query All Auth Rules (Encrypted)**: 2. **Query All Auth Rules**:
```json ```json
{ {
"kind": 23456, "kind": 23456,
"content": "{\"query\":\"list_auth_rules\",\"encrypted_tags\":\"nip44_encrypted_payload\"}", "content": "base64_nip44_encrypted_command_array",
"tags": [] "tags": [["p", "relay_pubkey"]]
} }
``` ```
*Encrypted payload contains:* `[["auth_query", "all"]]` *Encrypted content contains:* `["auth_query", "all"]`
5. **Add Blacklist Rule (Standard)**: 3. **Add Blacklist Rule**:
```json ```json
{ {
"kind": 23456, "kind": 23456,
"content": "{\"action\":\"add\"}", "content": "base64_nip44_encrypted_command_array",
"tags": [["blacklist", "pubkey", "deadbeef1234abcd..."]] "tags": [["p", "relay_pubkey"]]
} }
``` ```
*Encrypted content contains:* `["blacklist", "pubkey", "deadbeef1234abcd..."]`
6. **Add Blacklist Rule (Encrypted)**:
```json
{
"kind": 23456,
"content": "{\"action\":\"add\",\"encrypted_tags\":\"nip44_encrypted_payload\"}",
"tags": []
}
```
*Encrypted payload contains:* `[["blacklist", "pubkey", "deadbeef1234abcd..."]]`
### Phase 2: Auth Rules Schema Alignment ### Phase 2: Auth Rules Schema Alignment
@@ -181,12 +129,12 @@ Would require changing schema, migration scripts, and storage logic.
#### High Priority (Critical for blacklist functionality): #### High Priority (Critical for blacklist functionality):
1. Fix request_validator.c schema mismatch 1. Fix request_validator.c schema mismatch
2. Ensure auth_required configuration is enabled 2. Ensure auth_required configuration is enabled
3. Update tests to use ephemeral event kinds (23455/23456) 3. Update tests to use unified ephemeral event kind (23456)
4. Test blacklist enforcement 4. Test blacklist enforcement
#### Medium Priority (Enhanced Admin Features): #### Medium Priority (Enhanced Admin Features):
1. **Implement NIP-44 Encryption Support**: 1. **Implement NIP-44 Encryption Support**:
- Detect empty tags array for Kind 23455/23456 events - Detect NIP-44 encrypted content for Kind 23456 events
- Parse `encrypted_tags` field from content JSON - Parse `encrypted_tags` field from content JSON
- Decrypt using admin privkey and relay pubkey - Decrypt using admin privkey and relay pubkey
- Process decrypted tags as normal commands - Process decrypted tags as normal commands
@@ -218,45 +166,20 @@ Would require changing schema, migration scripts, and storage logic.
## Authentication ## Authentication
All admin commands require signing with the admin private key generated during first startup. All admin commands require signing with the admin private key generated during first startup.
## Configuration Management (Kind 23455 - Ephemeral) ## Unified Admin API (Kind 23456 - Ephemeral)
Update relay configuration parameters or query available settings. Update relay configuration parameters or query available settings.
**Configuration Update Event:** **Configuration Update Event:**
```json ```json
{ {
"kind": 23455, "kind": 23456,
"content": "Configuration update", "content": "base64_nip44_encrypted_command_array",
"tags": [ "tags": [["p", "relay_pubkey"]]
["config_key1", "config_value1"],
["config_key2", "config_value2"]
]
} }
``` ```
*Encrypted content contains:* `["relay_description", "My Relay Description"]`
**List Available Config Keys:** **Auth Rules Management:**
```json
{
"kind": 23455,
"content": "{\"query\":\"list_config_keys\",\"description\":\"Get editable config keys\"}",
"tags": [
["config_query", "list_all_keys"]
]
}
```
**Get Current Configuration:**
```json
{
"kind": 23455,
"content": "{\"query\":\"get_config\",\"description\":\"Get current config values\"}",
"tags": [
["config_query", "get_current_config"]
]
}
```
## Auth Rules Management (Kind 23456 - Ephemeral)
Manage whitelist and blacklist rules.
**Add Rule Event:** **Add Rule Event:**
```json ```json
@@ -364,7 +287,7 @@ All admin commands return JSON responses via WebSocket:
### Enable Authentication & Add Blacklist ### Enable Authentication & Add Blacklist
```bash ```bash
# 1. Enable auth system # 1. Enable auth system
nak event -k 23455 --content "Enable authentication" \ nak event -k 23456 --content "base64_nip44_encrypted_command" \
-t "auth_enabled=true" \ -t "auth_enabled=true" \
--sec $ADMIN_PRIVKEY | nak event ws://localhost:8888 --sec $ADMIN_PRIVKEY | nak event ws://localhost:8888
@@ -389,18 +312,18 @@ nak event -k 23456 --content '{"action":"clear_all","description":"Clear all rul
### Configuration Query Response ### Configuration Query Response
```json ```json
["EVENT", "subscription_id", { ["EVENT", "subscription_id", {
"kind": 23455, "kind": 23457,
"content": "{\"config_keys\": [\"auth_enabled\", \"max_connections\"], \"descriptions\": {\"auth_enabled\": \"Enable whitelist/blacklist rules\"}}", "content": "base64_nip44_encrypted_response",
"tags": [["response_type", "config_keys_list"]] "tags": [["p", "admin_pubkey"]]
}] }]
``` ```
### Current Config Response ### Current Config Response
```json ```json
["EVENT", "subscription_id", { ["EVENT", "subscription_id", {
"kind": 23455, "kind": 23457,
"content": "{\"current_config\": {\"auth_enabled\": \"true\", \"max_connections\": \"1000\"}}", "content": "base64_nip44_encrypted_response",
"tags": [["response_type", "current_config"]] "tags": [["p", "admin_pubkey"]]
}] }]
``` ```
@@ -427,7 +350,7 @@ nak event -k 23456 --content '{"action":"clear_all","description":"Clear all rul
1. **Document API** (this file) ✅ 1. **Document API** (this file) ✅
2. **Update to ephemeral event kinds** ✅ 2. **Update to ephemeral event kinds** ✅
3. **Fix request_validator.c** schema mismatch 3. **Fix request_validator.c** schema mismatch
4. **Update tests** to use Kind 23455/23456 4. **Update tests** to use unified Kind 23456
5. **Add auth rule query functionality** 5. **Add auth rule query functionality**
6. **Add configuration discovery feature** 6. **Add configuration discovery feature**
7. **Test blacklist functionality** 7. **Test blacklist functionality**
@@ -449,8 +372,8 @@ This plan addresses the immediate blacklist issue while establishing a comprehen
```c ```c
// In admin event processing function // In admin event processing function
bool is_encrypted_command(struct nostr_event *event) { bool is_encrypted_command(struct nostr_event *event) {
// Check if Kind 23455 or 23456 with empty tags // Check if Kind 23456 with NIP-44 encrypted content
if ((event->kind == 23455 || event->kind == 23456) && if (event->kind == 23456 &&
event->tags_count == 0) { event->tags_count == 0) {
return true; return true;
} }
@@ -483,7 +406,7 @@ cJSON *decrypt_admin_tags(struct nostr_event *event) {
``` ```
### Admin Event Processing Flow ### Admin Event Processing Flow
1. **Receive Event**: Kind 23455/23456 with admin signature 1. **Receive Event**: Kind 23456 with admin signature
2. **Check Mode**: Empty tags = encrypted, populated tags = standard 2. **Check Mode**: Empty tags = encrypted, populated tags = standard
3. **Decrypt if Needed**: Extract and decrypt `encrypted_tags` from content 3. **Decrypt if Needed**: Extract and decrypt `encrypted_tags` from content
4. **Process Commands**: Use decrypted/standard tags for command processing 4. **Process Commands**: Use decrypted/standard tags for command processing
@@ -510,7 +433,7 @@ char* nip44_decrypt(const char* ciphertext, const char* recipient_privkey, const
#### Phase 1: Core Infrastructure (Complete) #### Phase 1: Core Infrastructure (Complete)
- [x] Event-based admin authentication system - [x] Event-based admin authentication system
- [x] Kind 23455/23456 (Configuration/Auth Rules) processing - [x] Kind 23456 (Unified Admin API) processing
- [x] Basic configuration parameter updates - [x] Basic configuration parameter updates
- [x] Auth rule add/remove/clear functionality - [x] Auth rule add/remove/clear functionality
- [x] Updated to ephemeral event kinds - [x] Updated to ephemeral event kinds

View File

@@ -282,14 +282,14 @@ cd build
# Start relay in background and capture its PID # Start relay in background and capture its PID
if [ "$USE_TEST_KEYS" = true ]; then if [ "$USE_TEST_KEYS" = true ]; then
echo "Using deterministic test keys for development..." 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 elif [ -n "$RELAY_ARGS" ]; then
echo "Starting relay with custom configuration..." 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 else
# No command line arguments needed for random key generation # No command line arguments needed for random key generation
echo "Starting relay with 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 fi
RELAY_PID=$! RELAY_PID=$!
# Change back to original directory # 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 @@
285781 1182553

File diff suppressed because it is too large Load Diff

View File

@@ -98,6 +98,7 @@ typedef struct {
int port_override; // -1 = not set, >0 = port value int port_override; // -1 = not set, >0 = port value
char admin_privkey_override[65]; // Empty string = not set, 64-char hex = override 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 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; } cli_options_t;
// Global unified configuration cache // Global unified configuration cache
@@ -172,10 +173,10 @@ int process_admin_auth_event(cJSON* event, char* error_message, size_t error_siz
int handle_kind_23456_unified(cJSON* event, char* error_message, size_t error_size, struct lws* wsi); 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_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_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); int handle_auth_rule_modification_unified(cJSON* event, char* error_message, size_t error_size, struct lws* wsi);
// WebSocket response functions // Admin response functions
int send_websocket_response_data(cJSON* event, cJSON* response_data, struct lws* wsi); 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); cJSON* build_query_response(const char* query_type, cJSON* results_array, int total_count);
// Auth rules management functions // Auth rules management functions

View File

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

View File

@@ -224,10 +224,7 @@ int handle_event_message(cJSON* event, char* error_message, size_t error_size);
// Forward declaration for unified validation // Forward declaration for unified validation
int nostr_validate_unified_request(const char* json_string, size_t json_length); int nostr_validate_unified_request(const char* json_string, size_t json_length);
// Forward declaration for configuration event handling (kind 33334) // Forward declaration for admin event processing (kind 23456)
int handle_configuration_event(cJSON* event, char* error_message, size_t error_size);
// 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, struct lws* wsi); int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size, struct lws* wsi);
// Forward declaration for enhanced admin event authorization // Forward declaration for enhanced admin event authorization
@@ -3035,7 +3032,7 @@ int is_authorized_admin_event(cJSON* event, char* error_buffer, size_t error_buf
} }
int event_kind = kind_json->valueint; int event_kind = kind_json->valueint;
if (event_kind != 33334 && event_kind != 33335 && event_kind != 23455 && event_kind != 23456) { if (event_kind != 23456) {
snprintf(error_buffer, error_buffer_size, "Event kind %d is not an admin event type", event_kind); snprintf(error_buffer, error_buffer_size, "Event kind %d is not an admin event type", event_kind);
return -1; return -1;
} }
@@ -3203,11 +3200,18 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
memcpy(message, in, len); memcpy(message, in, len);
message[len] = '\0'; message[len] = '\0';
log_info("Received WebSocket message"); // Parse JSON message (this is the normal program flow)
// Parse JSON message
cJSON* json = cJSON_Parse(message); cJSON* json = cJSON_Parse(message);
if (json && cJSON_IsArray(json)) { 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 // Get message type
cJSON* type = cJSON_GetArrayItem(json, 0); cJSON* type = cJSON_GetArrayItem(json, 0);
if (type && cJSON_IsString(type)) { if (type && cJSON_IsString(type)) {
@@ -3349,7 +3353,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
// Cleanup event JSON string // Cleanup event JSON string
free(event_json_str); free(event_json_str);
// Check for admin events (kinds 33334, 33335, 23455, and 23456) and intercept them // Check for admin events (kind 23456) and intercept them
if (result == 0) { if (result == 0) {
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind"); cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
if (kind_obj && cJSON_IsNumber(kind_obj)) { if (kind_obj && cJSON_IsNumber(kind_obj)) {
@@ -3357,8 +3361,8 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
log_info("DEBUG ADMIN: Checking if admin event processing is needed"); log_info("DEBUG ADMIN: Checking if admin event processing is needed");
// Log reception of Kind 23455 and 23456 events // Log reception of Kind 23456 events
if (event_kind == 23455 || event_kind == 23456) { if (event_kind == 23456) {
char* event_json_debug = cJSON_Print(event); char* event_json_debug = cJSON_Print(event);
char debug_received_msg[1024]; char debug_received_msg[1024];
snprintf(debug_received_msg, sizeof(debug_received_msg), snprintf(debug_received_msg, sizeof(debug_received_msg),
@@ -3371,7 +3375,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
} }
} }
if (event_kind == 33334 || event_kind == 33335 || event_kind == 23455 || event_kind == 23456) { if (event_kind == 23456) {
// Enhanced admin event security - check authorization first // Enhanced admin event security - check authorization first
log_info("DEBUG ADMIN: Admin event detected, checking authorization"); log_info("DEBUG ADMIN: Admin event detected, checking authorization");
@@ -3403,8 +3407,8 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
"DEBUG ADMIN: process_admin_event_in_config returned %d", admin_result); "DEBUG ADMIN: process_admin_event_in_config returned %d", admin_result);
log_info(debug_admin_msg); log_info(debug_admin_msg);
// Log results for Kind 23455 and 23456 events // Log results for Kind 23456 events
if (event_kind == 23455 || event_kind == 23456) { if (event_kind == 23456) {
if (admin_result == 0) { if (admin_result == 0) {
char success_result_msg[256]; char success_result_msg[256];
snprintf(success_result_msg, sizeof(success_result_msg), snprintf(success_result_msg, sizeof(success_result_msg),
@@ -3666,6 +3670,7 @@ int check_port_available(int port) {
int sockfd; int sockfd;
struct sockaddr_in addr; struct sockaddr_in addr;
int result; int result;
int reuse = 1;
// Create a socket // Create a socket
sockfd = socket(AF_INET, SOCK_STREAM, 0); sockfd = socket(AF_INET, SOCK_STREAM, 0);
@@ -3673,6 +3678,13 @@ int check_port_available(int port) {
return 0; // Cannot create socket, assume port unavailable 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 // Set up the address structure
memset(&addr, 0, sizeof(addr)); memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET; addr.sin_family = AF_INET;
@@ -3690,7 +3702,7 @@ int check_port_available(int port) {
} }
// Start libwebsockets-based WebSocket Nostr relay server // 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; struct lws_context_creation_info info;
log_info("Starting libwebsockets-based Nostr relay server..."); log_info("Starting libwebsockets-based Nostr relay server...");
@@ -3700,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 configured_port = (port_override > 0) ? port_override : get_config_int("relay_port", DEFAULT_PORT);
int actual_port = configured_port; int actual_port = configured_port;
int port_attempts = 0; int port_attempts = 0;
const int max_port_attempts = 5; const int max_port_attempts = 10; // Increased from 5 to 10
// Minimal libwebsockets configuration // Minimal libwebsockets configuration
info.protocols = protocols; info.protocols = protocols;
@@ -3719,8 +3731,8 @@ int start_websocket_relay(int port_override) {
// Max payload size for Nostr events // Max payload size for Nostr events
info.max_http_header_data = 4096; info.max_http_header_data = 4096;
// Find an available port with pre-checking // Find an available port with pre-checking (or fail immediately in strict mode)
while (port_attempts < max_port_attempts) { while (port_attempts < (strict_port ? 1 : max_port_attempts)) {
char attempt_msg[256]; char attempt_msg[256];
snprintf(attempt_msg, sizeof(attempt_msg), "Checking port availability: %d", actual_port); snprintf(attempt_msg, sizeof(attempt_msg), "Checking port availability: %d", actual_port);
log_info(attempt_msg); log_info(attempt_msg);
@@ -3728,7 +3740,13 @@ int start_websocket_relay(int port_override) {
// Pre-check if port is available // Pre-check if port is available
if (!check_port_available(actual_port)) { if (!check_port_available(actual_port)) {
port_attempts++; 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]; char retry_msg[256];
snprintf(retry_msg, sizeof(retry_msg), "Port %d is in use, trying port %d (attempt %d/%d)", 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); actual_port, actual_port + 1, port_attempts + 1, max_port_attempts);
@@ -3767,7 +3785,13 @@ int start_websocket_relay(int port_override) {
log_warning(lws_error_msg); log_warning(lws_error_msg);
port_attempts++; 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++; actual_port++;
continue; continue;
} }
@@ -3833,6 +3857,7 @@ void print_usage(const char* program_name) {
printf(" -p, --port PORT Override relay port (first-time startup only)\n"); 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(" -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(" -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("\n");
printf("Configuration:\n"); printf("Configuration:\n");
printf(" This relay uses event-based configuration stored in the database.\n"); printf(" This relay uses event-based configuration stored in the database.\n");
@@ -3841,10 +3866,16 @@ void print_usage(const char* program_name) {
printf(" After initial setup, all configuration is managed via database events.\n"); printf(" After initial setup, all configuration is managed via database events.\n");
printf(" Database file: <relay_pubkey>.db (created automatically)\n"); printf(" Database file: <relay_pubkey>.db (created automatically)\n");
printf("\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("Examples:\n");
printf(" %s # Start relay (auto-configure on first run)\n", program_name); 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 -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 --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 --help # Show this help\n", program_name);
printf(" %s --version # Show version info\n", program_name); printf(" %s --version # Show version info\n", program_name);
printf("\n"); printf("\n");
@@ -3863,7 +3894,8 @@ int main(int argc, char* argv[]) {
cli_options_t cli_options = { cli_options_t cli_options = {
.port_override = -1, // -1 = not set .port_override = -1, // -1 = not set
.admin_privkey_override = {0}, // Empty string = 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 // Parse command line arguments
@@ -3958,6 +3990,10 @@ int main(int argc, char* argv[]) {
i++; // Skip the key argument i++; // Skip the key argument
log_info("Relay private key override specified"); 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 { } else {
log_error("Unknown argument. Use --help for usage information."); log_error("Unknown argument. Use --help for usage information.");
print_usage(argv[0]); print_usage(argv[0]);
@@ -4179,7 +4215,7 @@ int main(int argc, char* argv[]) {
log_info("Starting relay server..."); log_info("Starting relay server...");
// Start WebSocket Nostr relay server (port from configuration) // 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
cleanup_relay_info(); cleanup_relay_info();

View File

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

View File

@@ -34,12 +34,9 @@ RELAY_URL="ws://${RELAY_HOST}:${RELAY_PORT}"
TIMEOUT=5 TIMEOUT=5
TEMP_DIR="/tmp/c_relay_test_$$" TEMP_DIR="/tmp/c_relay_test_$$"
# WebSocket connection state # WebSocket connection state (simplified - no persistent connections)
WS_PID="" # These variables are kept for compatibility but not used
WS_INPUT_FIFO=""
WS_OUTPUT_FIFO=""
WS_CONNECTED=0 WS_CONNECTED=0
WS_RESPONSE_LOG=""
# Color codes for output # Color codes for output
RED='\033[0;31m' RED='\033[0;31m'
@@ -120,24 +117,99 @@ generate_test_keypair() {
echo "$pubkey" > "$pubkey_file" 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 # Export for use in calling functions
eval "${name}_PRIVKEY=\"$privkey\"" eval "${name}_PRIVKEY=\"$privkey\""
eval "${name}_PUBKEY=\"$pubkey\"" eval "${name}_PUBKEY=\"$pubkey\""
} }
# Send WebSocket message and capture response # NIP-44 encryption helper function using nak
encrypt_nip44_content() {
local content="$1"
local sender_privkey="$2"
local receiver_pubkey="$3"
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() { send_websocket_message() {
local message="$1" local message="$1"
local expected_response="$2" local timeout="${2:-$TIMEOUT}"
local timeout="${3:-$TIMEOUT}"
# Use websocat to send message and capture response (following pattern from tests/1_nip_test.sh) # Use websocat to send message and capture response (following pattern from tests/1_nip_test.sh)
local response="" local response=""
if command -v websocat &> /dev/null; then if command -v websocat &> /dev/null; then
# Capture output from websocat (following working pattern from 1_nip_test.sh) response=$(printf '%s\n' "$message" | timeout "$timeout" websocat "$RELAY_URL" 2>&1 || echo "Connection failed")
response=$(echo "$message" | timeout "$timeout" websocat "$RELAY_URL" 2>&1 || echo "Connection failed")
# Check if connection failed # Check if connection failed
if [[ "$response" == *"Connection failed"* ]]; then if [[ "$response" == *"Connection failed"* ]]; then
@@ -154,143 +226,137 @@ send_websocket_message() {
echo "$response" echo "$response"
} }
# ======================================================================= # Send admin event and capture response (simplified pattern from 1_nip_test.sh)
# PERSISTENT WEBSOCKET CONNECTION MANAGEMENT send_admin_event() {
# ======================================================================= local event_json="$1"
local description="$2"
# Open persistent WebSocket connection local timeout_seconds="${3:-10}"
open_websocket_connection() {
log_info "Opening persistent WebSocket connection to $RELAY_URL..."
# Create unique named pipes for this test session log_info "Sending admin event: $description"
WS_INPUT_FIFO="${TEMP_DIR}/ws_input_$$"
WS_OUTPUT_FIFO="${TEMP_DIR}/ws_output_$$"
WS_RESPONSE_LOG="${TEMP_DIR}/ws_responses_$$"
# Create named pipes # Create EVENT message using jq to properly handle special characters
mkfifo "$WS_INPUT_FIFO" "$WS_OUTPUT_FIFO" local event_message
event_message=$(jq -n --argjson event "$event_json" '["EVENT", $event]')
# Start websocat in background with bidirectional pipes # Validate that the event message is valid UTF-8 (temporarily disabled for debugging)
# Input: we write to WS_INPUT_FIFO, websocat reads and sends to relay # if ! echo "$event_message" | iconv -f utf-8 -t utf-8 >/dev/null 2>&1; then
# Output: websocat receives from relay and writes to WS_OUTPUT_FIFO # log_error "Event message contains invalid UTF-8 characters"
websocat "$RELAY_URL" < "$WS_INPUT_FIFO" > "$WS_OUTPUT_FIFO" & # return 1
WS_PID=$! # fi
# Start background response logger # Use websocat to send event and capture OK response
tail -f "$WS_OUTPUT_FIFO" >> "$WS_RESPONSE_LOG" & local response=""
local logger_pid=$! if command -v websocat &> /dev/null; then
log_info "Sending event using websocat..."
# Keep input pipe open by redirecting from /dev/null in background
exec {ws_fd}> "$WS_INPUT_FIFO" # Debug: Show what we're sending
log_info "DEBUG: Event message being sent: $event_message"
# Test connection with a simple REQ message
sleep 1 # Write to temporary file to avoid shell interpretation issues
echo '["REQ","test_conn",{}]' >&${ws_fd} local temp_file="${TEMP_DIR}/event_message_$$"
printf '%s\n' "$event_message" > "$temp_file"
# Wait for response to confirm connection
local connection_timeout=5 # Send via websocat using file input with delay to receive response
local start_time=$(date +%s) response=$(timeout "$timeout_seconds" sh -c "cat '$temp_file'; sleep 0.5" | websocat "$RELAY_URL" 2>&1)
local websocat_exit_code=$?
while [ $(($(date +%s) - start_time)) -lt $connection_timeout ]; do
if [ -s "$WS_RESPONSE_LOG" ]; then # Clean up temp file
WS_CONNECTED=1 rm -f "$temp_file"
log_success "Persistent WebSocket connection established"
log_info "WebSocket PID: $WS_PID" log_info "DEBUG: Websocat exit code: $websocat_exit_code"
return 0 log_info "DEBUG: Websocat response: $response"
fi
sleep 0.1 # Check for specific websocat errors
done if [[ "$response" == *"UTF-8 failure"* ]]; then
log_error "UTF-8 encoding error in event data for $description"
# Connection failed log_error "Event message: $event_message"
log_error "Failed to establish persistent WebSocket connection" return 1
close_websocket_connection elif [[ "$response" == *"Connection failed"* ]] || [[ "$response" == *"Connection refused"* ]] || [[ "$response" == *"timeout"* ]]; then
return 1 log_error "Failed to connect to relay for $description"
} return 1
elif [[ "$response" == *"error running"* ]]; then
# Close persistent WebSocket connection log_error "Websocat error for $description: $response"
close_websocket_connection() { return 1
log_info "Closing persistent WebSocket connection..." elif [ $websocat_exit_code -eq 0 ]; then
log_info "Event sent successfully via websocat"
if [ -n "$WS_PID" ] && kill -0 "$WS_PID" 2>/dev/null; then else
# Close input pipe first log_warning "Websocat returned exit code $websocat_exit_code"
if [ -n "${ws_fd}" ]; then
exec {ws_fd}>&-
fi fi
# Send close frame and terminate websocat else
kill "$WS_PID" 2>/dev/null log_error "websocat not found - required for WebSocket testing"
wait "$WS_PID" 2>/dev/null
fi
# Kill any remaining background processes
pkill -f "tail -f.*$WS_OUTPUT_FIFO" 2>/dev/null || true
# Clean up pipes
[ -p "$WS_INPUT_FIFO" ] && rm -f "$WS_INPUT_FIFO"
[ -p "$WS_OUTPUT_FIFO" ] && rm -f "$WS_OUTPUT_FIFO"
WS_PID=""
WS_CONNECTED=0
log_info "WebSocket connection closed"
}
# Send event through persistent WebSocket connection
send_websocket_event() {
local event_json="$1"
local timeout_seconds="${2:-10}"
if [ "$WS_CONNECTED" != "1" ]; then
log_error "WebSocket connection not established"
return 1 return 1
fi fi
# Clear previous responses echo "$response"
> "$WS_RESPONSE_LOG"
# Create EVENT message
local event_message="[\"EVENT\",$event_json]"
# Send through persistent connection
echo "$event_message" >&${ws_fd}
# Wait for OK response
local start_time=$(date +%s)
while [ $(($(date +%s) - start_time)) -lt $timeout_seconds ]; do
if grep -q '"OK"' "$WS_RESPONSE_LOG" 2>/dev/null; then
local response=$(tail -1 "$WS_RESPONSE_LOG")
echo "$response"
return 0
fi
sleep 0.1
done
log_error "Timeout waiting for WebSocket response"
return 1
} }
# Wait for query response data from relay # Send admin query and wait for encrypted response
wait_for_query_response() { send_admin_query() {
local timeout_seconds="${1:-10}" local event_json="$1"
local start_time=$(date +%s) local description="$2"
local timeout_seconds="${3:-15}"
log_info "Waiting for query response data..." log_info "Sending admin query: $description"
# Clear any OK responses and wait for JSON data # Create EVENT message using jq to properly handle special characters
sleep 0.5 # Brief delay to ensure OK response is processed first local event_message
event_message=$(jq -n --argjson event "$event_json" '["EVENT", $event]')
while [ $(($(date +%s) - start_time)) -lt $timeout_seconds ]; do # For queries, we need to also send a REQ to get the response
# Look for JSON response with query data (not just OK responses) local sub_id="admin_query_$(date +%s)"
if grep -q '"query_type"' "$WS_RESPONSE_LOG" 2>/dev/null; then local req_message="[\"REQ\",\"$sub_id\",{\"kinds\":[23456],\"authors\":[\"$RELAY_PUBKEY\"],\"#p\":[\"$ADMIN_PUBKEY\"]}]"
local response=$(grep '"query_type"' "$WS_RESPONSE_LOG" | tail -1) local close_message="[\"CLOSE\",\"$sub_id\"]"
echo "$response"
return 0 # 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 fi
sleep 0.1
done # 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
log_error "Timeout waiting for query response data" # Return the raw response if no encrypted content found
return 1 echo "$response"
} }
# Create and send auth rule event # Create and send auth rule event
@@ -301,15 +367,26 @@ send_auth_rule_event() {
local pattern_value="$4" # actual pubkey or hash value local pattern_value="$4" # actual pubkey or hash value
local description="$5" # optional description 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 with correct tag format for the actual implementation # Create command array according to README.md API specification
# Server expects tags like ["whitelist", "pubkey", "abc123..."] or ["blacklist", "pubkey", "def456..."] # Format: ["blacklist", "pubkey", "abc123..."] or ["whitelist", "pubkey", "def456..."]
# Using Kind 23456 (ephemeral auth rules management) with proper relay targeting 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 local event_json
event_json=$(nak event -k 23456 --content "" \ event_json=$(nak event -k 23456 --content "$encrypted_content" \
-t "p=$RELAY_PUBKEY" \ -t "p=$RELAY_PUBKEY" \
-t "$rule_type=$pattern_type=$pattern_value" \
--sec "$ADMIN_PRIVKEY" 2>/dev/null) --sec "$ADMIN_PRIVKEY" 2>/dev/null)
if [ $? -ne 0 ] || [ -z "$event_json" ]; then if [ $? -ne 0 ] || [ -z "$event_json" ]; then
@@ -317,38 +394,23 @@ send_auth_rule_event() {
return 1 return 1
fi fi
# Send the event through persistent WebSocket connection log_info "DEBUG: Created event JSON: $event_json"
# Send the event using simplified WebSocket pattern
log_info "Publishing auth rule event to relay..." log_info "Publishing auth rule event to relay..."
local result local result
if [ "$WS_CONNECTED" = "1" ]; then result=$(send_admin_event "$event_json" "auth rule $action")
result=$(send_websocket_event "$event_json") local exit_code=$?
local exit_code=$?
log_info "Auth rule event result: $result"
log_info "Auth rule event result: $result"
# Check if response indicates success
# Check if response indicates success if [ $exit_code -eq 0 ] && echo "$result" | grep -q -i '"OK".*true'; then
if [ $exit_code -eq 0 ] && echo "$result" | grep -q -i '"OK".*true'; then log_success "Auth rule $action successful"
log_success "Auth rule $action successful" return 0
return 0
else
log_error "Auth rule $action failed: $result (exit code: $exit_code)"
return 1
fi
else else
# Fallback to one-shot connection if persistent connection not available log_error "Auth rule $action failed: $result (exit code: $exit_code)"
result=$(echo "$event_json" | timeout 10s nak event "$RELAY_URL" 2>&1) return 1
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
log_success "Auth rule $action successful"
return 0
else
log_error "Auth rule $action failed: $result (exit code: $exit_code)"
return 1
fi
fi fi
} }
@@ -356,12 +418,27 @@ send_auth_rule_event() {
clear_all_auth_rules() { clear_all_auth_rules() {
log_info "Clearing all existing 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 # Create system command event to clear all auth rules
# Using Kind 23456 (ephemeral auth rules management) with proper relay targeting # Using Kind 23456 (admin commands) with proper relay targeting and encrypted content
local event_json local event_json
event_json=$(nak event -k 23456 --content "" \ event_json=$(nak event -k 23456 --content "$encrypted_content" \
-t "p=$RELAY_PUBKEY" \ -t "p=$RELAY_PUBKEY" \
-t "system_command=clear_all_auth_rules" \
--sec "$ADMIN_PRIVKEY" 2>/dev/null) --sec "$ADMIN_PRIVKEY" 2>/dev/null)
if [ $? -ne 0 ] || [ -z "$event_json" ]; then if [ $? -ne 0 ] || [ -z "$event_json" ]; then
@@ -369,38 +446,23 @@ clear_all_auth_rules() {
return 1 return 1
fi fi
# Send the event through persistent WebSocket connection log_info "DEBUG: Created event JSON: $event_json"
# Send the event using simplified WebSocket pattern
log_info "Sending clear all auth rules command..." log_info "Sending clear all auth rules command..."
local result local result
if [ "$WS_CONNECTED" = "1" ]; then result=$(send_admin_event "$event_json" "clear all auth rules")
result=$(send_websocket_event "$event_json") local exit_code=$?
local exit_code=$?
log_info "Clear auth rules result: $result"
log_info "Clear auth rules result: $result"
# Check if response indicates success
# Check if response indicates success if [ $exit_code -eq 0 ] && echo "$result" | grep -q -i '"OK".*true'; then
if [ $exit_code -eq 0 ] && echo "$result" | grep -q -i '"OK".*true'; then log_success "All auth rules cleared successfully"
log_success "All auth rules cleared successfully" return 0
return 0
else
log_error "Failed to clear auth rules: $result (exit code: $exit_code)"
return 1
fi
else else
# Fallback to one-shot connection if persistent connection not available log_error "Failed to clear auth rules: $result (exit code: $exit_code)"
result=$(echo "$event_json" | timeout 10s nak event "$RELAY_URL" 2>&1) return 1
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 "success\|OK.*true\|published"; 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
fi fi
} }
@@ -414,7 +476,7 @@ test_event_publishing() {
log_info "Testing event publishing: $description" log_info "Testing event publishing: $description"
# Create a simple test event (kind 1 - text note) using nak like NIP-42 test # 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 local test_event
test_event=$(nak event -k 1 --content "$test_content" --sec "$test_privkey" 2>/dev/null) test_event=$(nak event -k 1 --content "$test_content" --sec "$test_privkey" 2>/dev/null)
@@ -426,7 +488,7 @@ test_event_publishing() {
# Send the event using nak directly (more reliable than websocat) # Send the event using nak directly (more reliable than websocat)
log_info "Publishing test event to relay..." log_info "Publishing test event to relay..."
local result 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=$? local exit_code=$?
log_info "Event publishing result: $result" log_info "Event publishing result: $result"
@@ -506,11 +568,22 @@ test_admin_authentication() {
log "Test $TESTS_RUN: Admin Authentication" log "Test $TESTS_RUN: Admin Authentication"
# Create a simple configuration event to test admin authentication # Create a simple configuration event to test admin authentication
# Using Kind 23456 (admin commands) with proper relay targeting # 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 local config_event
config_event=$(nak event -k 23456 --content "" \ config_event=$(nak event -k 23456 --content "$encrypted_content" \
-t "p=$RELAY_PUBKEY" \ -t "p=$RELAY_PUBKEY" \
-t "system_command=system_status" \
--sec "$ADMIN_PRIVKEY" 2>/dev/null) --sec "$ADMIN_PRIVKEY" 2>/dev/null)
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
@@ -518,15 +591,17 @@ test_admin_authentication() {
return return
fi fi
# Send admin event # Send admin event using the proper admin event function
local message="[\"EVENT\",$config_event]"
local response local response
response=$(send_websocket_message "$message" "OK" 10) response=$(send_admin_event "$config_event" "admin authentication test")
local exit_code=$?
if echo "$response" | grep -q '"OK".*true'; then log_info "Admin authentication result: $response"
if [ $exit_code -eq 0 ] && echo "$response" | grep -q '"OK".*true'; then
pass_test "Admin authentication successful" pass_test "Admin authentication successful"
else else
fail_test "Admin authentication failed: $response" fail_test "Admin authentication failed: $response (exit code: $exit_code)"
fi fi
} }
@@ -548,10 +623,22 @@ test_auth_rules_storage_query() {
# Query all auth rules using admin query # Query all auth rules using admin query
log_info "Querying all auth rules..." 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 local query_event
query_event=$(nak event -k 23456 --content "" \ query_event=$(nak event -k 23456 --content "$encrypted_content" \
-t "p=$RELAY_PUBKEY" \ -t "p=$RELAY_PUBKEY" \
-t "auth_query=all" \
--sec "$ADMIN_PRIVKEY" 2>/dev/null) --sec "$ADMIN_PRIVKEY" 2>/dev/null)
if [ $? -ne 0 ] || [ -z "$query_event" ]; then if [ $? -ne 0 ] || [ -z "$query_event" ]; then
@@ -559,23 +646,23 @@ test_auth_rules_storage_query() {
return return
fi fi
# Send the query event # Send the query event using simplified WebSocket pattern
log_info "Sending auth query to relay..." log_info "Sending auth query to relay..."
local query_result local decrypted_response
query_result=$(echo "$query_event" | timeout 10s nak event "$RELAY_URL" 2>&1) decrypted_response=$(send_admin_query "$query_event" "auth rules query")
local exit_code=$? local exit_code=$?
log_info "Auth query result: $query_result" if [ $exit_code -eq 0 ] && [ -n "$decrypted_response" ]; then
log_info "Decrypted query response: $decrypted_response"
# Check if we got a response and if it contains our test rule
if [ $exit_code -eq 0 ]; then # Check if the decrypted response contains our test rule
if echo "$query_result" | grep -q "$TEST1_PUBKEY"; then if echo "$decrypted_response" | grep -q "$TEST1_PUBKEY"; then
pass_test "Auth rule storage and query working - found test rule in query results" pass_test "Auth rule storage and query working - found test rule in decrypted query results"
else else
fail_test "Auth rule not found in query results - rule may not have been stored" fail_test "Auth rule not found in decrypted query results - rule may not have been stored"
fi fi
else else
fail_test "Auth query failed: $query_result" fail_test "Failed to receive or decrypt auth query response"
fi fi
else else
fail_test "Failed to add auth rule for storage test" fail_test "Failed to add auth rule for storage test"
@@ -747,14 +834,14 @@ test_hash_blacklist() {
return return
fi 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 # Add the event ID to hash blacklist
if send_auth_rule_event "add" "blacklist" "hash" "$event_id" "Test hash blacklist"; then 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 # Try to publish the same event using nak - should be blocked
log_info "Attempting to publish blacklisted event..." log_info "Attempting to publish blacklisted event..."
local result 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=$? local exit_code=$?
if [ $exit_code -ne 0 ] || echo "$result" | grep -q -i "blocked\|denied\|rejected\|blacklist"; then if [ $exit_code -ne 0 ] || echo "$result" | grep -q -i "blocked\|denied\|rejected\|blacklist"; then
@@ -786,7 +873,7 @@ test_websocket_behavior() {
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
local message="[\"EVENT\",$test_event]" local message="[\"EVENT\",$test_event]"
local response local response
response=$(send_websocket_message "$message" "OK" 5) response=$(send_websocket_message "$message" 5)
if echo "$response" | grep -q '"OK"'; then if echo "$response" | grep -q '"OK"'; then
rapid_success_count=$((rapid_success_count + 1)) rapid_success_count=$((rapid_success_count + 1))
@@ -884,7 +971,7 @@ run_all_tests() {
clear_all_auth_rules clear_all_auth_rules
test_admin_authentication test_admin_authentication
test_auth_rules_storage_query # test_auth_rules_storage_query
# test_basic_whitelist # test_basic_whitelist
# test_basic_blacklist # test_basic_blacklist
# test_rule_removal # test_rule_removal
@@ -941,7 +1028,7 @@ main() {
echo "" echo ""
# Check if relay is running - using websocat like the working tests # Check if relay is running - using websocat like the working tests
if ! echo '["REQ","connection_test",{}]' | timeout 5 websocat "$RELAY_URL" >/dev/null 2>&1; then 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 "Cannot connect to relay at $RELAY_URL"
log_error "Please ensure the C-Relay server is running in test mode" log_error "Please ensure the C-Relay server is running in test mode"
exit 1 exit 1