v0.3.16 - Admin system getting better

This commit is contained in:
Your Name
2025-09-30 05:32:23 -04:00
parent 6fd3e531c3
commit 6dac231040
10 changed files with 2528 additions and 1053 deletions

3
.gitignore vendored
View File

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

View File

@@ -48,20 +48,30 @@
- Schema version 4 with JSON tag storage
- **Critical**: Event expiration filtering done at application level, not SQL level
### Configuration Event Structure
### Admin API Event Structure
```json
{
"kind": 23455,
"content": "C Nostr Relay Configuration",
"kind": 23456,
"content": "base64_nip44_encrypted_command_array",
"tags": [
["d", "<relay_pubkey>"],
["relay_description", "value"],
["max_subscriptions_per_client", "25"],
["pow_min_difficulty", "16"]
["p", "<relay_pubkey>"]
]
}
```
**Configuration Commands** (encrypted in content):
- `["relay_description", "My Relay"]`
- `["max_subscriptions_per_client", "25"]`
- `["pow_min_difficulty", "16"]`
**Auth Rule Commands** (encrypted in content):
- `["blacklist", "pubkey", "hex_pubkey_value"]`
- `["whitelist", "pubkey", "hex_pubkey_value"]`
**Query Commands** (encrypted in content):
- `["auth_query", "all"]`
- `["system_command", "system_status"]`
### Process Management
```bash
# Kill existing relay processes

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
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
@@ -32,7 +32,7 @@ All admin commands require signing with the admin private key displayed during f
### 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:**
```json
@@ -41,14 +41,16 @@ All admin commands use the same unified event structure with tag-based parameter
"pubkey": "admin_public_key",
"created_at": 1234567890,
"kind": 23456,
"content": "<nip44 encrypted command>",
"content": "AqHBUgcM7dXFYLQuDVzGwMST1G8jtWYyVvYxXhVGEu4nAb4LVw...",
"tags": [
["p", "relay_public_key"],
["p", "relay_public_key"]
],
"sig": "event_signature"
}
```
The `content` field contains a NIP-44 encrypted JSON array representing the command.
**Admin Response Event:**
```json
["EVENT", "temp_sub_id", {
@@ -56,7 +58,7 @@ All admin commands use the same unified event structure with tag-based parameter
"pubkey": "relay_public_key",
"created_at": 1234567890,
"kind": 23457,
"content": "<nip44 encrypted response>",
"content": "BpKCVhfN8eYtRmPqSvWxZnMkL2gHjUiOp3rTyEwQaS5dFg...",
"tags": [
["p", "admin_public_key"]
],
@@ -64,15 +66,17 @@ 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
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** |
| `config_update` | `["relay_description", "My Relay"]` | Update relay configuration parameters |
| `config_query` | `["config_query", "list_all_keys"]` | List all available configuration keys |
| `config_update` | `["config_update", [{"key": "auth_enabled", "value": "true", "data_type": "boolean", "category": "auth"}, {"key": "relay_description", "value": "My Relay", "data_type": "string", "category": "relay"}, ...]]` | Update relay configuration parameters (supports multiple updates) |
| `config_query` | `["config_query", "all"]` | Query all configuration parameters |
| **Auth Rules Management** |
| `auth_add_blacklist` | `["blacklist", "pubkey", "abc123..."]` | Add pubkey to blacklist |
| `auth_add_whitelist` | `["whitelist", "pubkey", "def456..."]` | Add pubkey to whitelist |
@@ -117,7 +121,7 @@ All admin commands return **signed EVENT responses** via WebSocket following sta
"pubkey": "relay_public_key",
"created_at": 1234567890,
"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": [
["p", "admin_public_key"]
],
@@ -132,7 +136,7 @@ All admin commands return **signed EVENT responses** via WebSocket following sta
"pubkey": "relay_public_key",
"created_at": 1234567890,
"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": [
["p", "admin_public_key"]
],
@@ -147,7 +151,7 @@ All admin commands return **signed EVENT responses** via WebSocket following sta
"pubkey": "relay_public_key",
"created_at": 1234567890,
"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": [
["p", "admin_public_key"]
],
@@ -162,7 +166,7 @@ All admin commands return **signed EVENT responses** via WebSocket following sta
"pubkey": "relay_public_key",
"created_at": 1234567890,
"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": [
["p", "admin_public_key"]
],
@@ -170,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)
**Kind 23455: Configuration Management (Ephemeral)**
- 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)**
**Kind 23456: Unified Admin API (Ephemeral)**
- Configuration management: Update relay settings, limits, authentication policies
- Auth rules: Add/remove/query whitelist/blacklist rules
- System commands: clear rules, status, cache management
- **Standard Mode**: Commands in tags
- Rule format: `["rule_type", "pattern_type", "pattern_value"]`
- Query format: `["auth_query", "filter"]`
- System format: `["system_command", "command_name"]`
- **Encrypted Mode**: Commands NIP-44 encrypted in content `{"encrypted_tags": "..."}`
- Content: Action description + optional encrypted payload
- Security: Optional NIP-44 encryption for sensitive operations
- **Unified Format**: All commands use NIP-44 encrypted content with `["p", "relay_pubkey"]` tags
- **Command Types**:
- Configuration: `["config_key", "config_value"]`
- Auth rules: `["rule_type", "pattern_type", "pattern_value"]`
- Queries: `["auth_query", "filter"]` or `["system_command", "command_name"]`
- **Security**: All admin commands use NIP-44 encryption for privacy and security
#### Configuration Query Commands (using Kind 23455)
#### Configuration Commands (using Kind 23456)
1. **List All Configuration Keys (Standard)**:
```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)**:
1. **Update Configuration**:
```json
{
"kind": 23456,
"content": "{\"action\":\"clear_all\"}",
"tags": [["system_command", "clear_all_auth_rules"]]
"content": "base64_nip44_encrypted_command_array",
"tags": [["p", "relay_pubkey"]]
}
```
*Encrypted content contains:* `["relay_description", "My Relay"]`
2. **Clear All Auth Rules (Encrypted)**:
2. **Query System Status**:
```json
{
"kind": 23456,
"content": "{\"action\":\"clear_all\",\"encrypted_tags\":\"nip44_encrypted_payload\"}",
"tags": []
"content": "base64_nip44_encrypted_command_array",
"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
{
"kind": 23456,
"content": "{\"query\":\"list_auth_rules\"}",
"tags": [["auth_query", "all"]]
"content": "base64_nip44_encrypted_command_array",
"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
{
"kind": 23456,
"content": "{\"query\":\"list_auth_rules\",\"encrypted_tags\":\"nip44_encrypted_payload\"}",
"tags": []
"content": "base64_nip44_encrypted_command_array",
"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
{
"kind": 23456,
"content": "{\"action\":\"add\"}",
"tags": [["blacklist", "pubkey", "deadbeef1234abcd..."]]
"content": "base64_nip44_encrypted_command_array",
"tags": [["p", "relay_pubkey"]]
}
```
6. **Add Blacklist Rule (Encrypted)**:
```json
{
"kind": 23456,
"content": "{\"action\":\"add\",\"encrypted_tags\":\"nip44_encrypted_payload\"}",
"tags": []
}
```
*Encrypted payload contains:* `[["blacklist", "pubkey", "deadbeef1234abcd..."]]`
*Encrypted content contains:* `["blacklist", "pubkey", "deadbeef1234abcd..."]`
### 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):
1. Fix request_validator.c schema mismatch
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
#### Medium Priority (Enhanced Admin Features):
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
- Decrypt using admin privkey and relay pubkey
- Process decrypted tags as normal commands
@@ -218,45 +166,20 @@ Would require changing schema, migration scripts, and storage logic.
## Authentication
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.
**Configuration Update Event:**
```json
{
"kind": 23455,
"content": "Configuration update",
"tags": [
["config_key1", "config_value1"],
["config_key2", "config_value2"]
]
"kind": 23456,
"content": "base64_nip44_encrypted_command_array",
"tags": [["p", "relay_pubkey"]]
}
```
*Encrypted content contains:* `["relay_description", "My Relay Description"]`
**List Available Config Keys:**
```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.
**Auth Rules Management:**
**Add Rule Event:**
```json
@@ -364,7 +287,7 @@ All admin commands return JSON responses via WebSocket:
### Enable Authentication & Add Blacklist
```bash
# 1. Enable auth system
nak event -k 23455 --content "Enable authentication" \
nak event -k 23456 --content "base64_nip44_encrypted_command" \
-t "auth_enabled=true" \
--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
```json
["EVENT", "subscription_id", {
"kind": 23455,
"content": "{\"config_keys\": [\"auth_enabled\", \"max_connections\"], \"descriptions\": {\"auth_enabled\": \"Enable whitelist/blacklist rules\"}}",
"tags": [["response_type", "config_keys_list"]]
"kind": 23457,
"content": "base64_nip44_encrypted_response",
"tags": [["p", "admin_pubkey"]]
}]
```
### Current Config Response
```json
["EVENT", "subscription_id", {
"kind": 23455,
"content": "{\"current_config\": {\"auth_enabled\": \"true\", \"max_connections\": \"1000\"}}",
"tags": [["response_type", "current_config"]]
"kind": 23457,
"content": "base64_nip44_encrypted_response",
"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) ✅
2. **Update to ephemeral event kinds** ✅
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**
6. **Add configuration discovery feature**
7. **Test blacklist functionality**
@@ -449,8 +372,8 @@ This plan addresses the immediate blacklist issue while establishing a comprehen
```c
// In admin event processing function
bool is_encrypted_command(struct nostr_event *event) {
// Check if Kind 23455 or 23456 with empty tags
if ((event->kind == 23455 || event->kind == 23456) &&
// Check if Kind 23456 with NIP-44 encrypted content
if (event->kind == 23456 &&
event->tags_count == 0) {
return true;
}
@@ -483,7 +406,7 @@ cJSON *decrypt_admin_tags(struct nostr_event *event) {
```
### 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
3. **Decrypt if Needed**: Extract and decrypt `encrypted_tags` from content
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)
- [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] Auth rule add/remove/clear functionality
- [x] Updated to ephemeral event kinds

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 @@
659207
1182553

View File

@@ -72,11 +72,14 @@ int remove_auth_rule_from_config(const char* rule_type, const char* pattern_type
int is_config_table_ready(void);
int migrate_config_from_events_to_table(void);
int populate_config_table_from_event(const cJSON* event);
int handle_config_query_unified(cJSON* event, const char* query_type, char* error_message, size_t error_size, struct lws* wsi);
int handle_config_set_unified(cJSON* event, const char* config_key, const char* config_value, char* error_message, size_t error_size, struct lws* wsi);
// Forward declarations for tag parsing utilities
const char* get_first_tag_name(cJSON* event);
const char* get_tag_value(cJSON* event, const char* tag_name, int value_index);
int parse_auth_query_parameters(cJSON* event, char** query_type, char** pattern_value);
int handle_config_update_unified(cJSON* event, char* error_message, size_t error_size, struct lws* wsi);
// Current configuration cache
@@ -380,48 +383,9 @@ cJSON* load_config_event_from_database(const char* relay_pubkey) {
return NULL;
}
const char* sql;
sqlite3_stmt* stmt;
int rc;
// Configuration is now managed through config table, not events
log_info("Configuration events are no longer stored in events table");
return NULL;
cJSON* event = NULL;
if (sqlite3_step(stmt) == SQLITE_ROW) {
// Reconstruct the event JSON from database columns
event = cJSON_CreateObject();
if (event) {
const char* event_pubkey = (const char*)sqlite3_column_text(stmt, 1);
cJSON_AddStringToObject(event, "id", (const char*)sqlite3_column_text(stmt, 0));
cJSON_AddStringToObject(event, "pubkey", event_pubkey);
cJSON_AddNumberToObject(event, "created_at", sqlite3_column_int64(stmt, 2));
cJSON_AddNumberToObject(event, "kind", sqlite3_column_int(stmt, 3));
cJSON_AddStringToObject(event, "content", (const char*)sqlite3_column_text(stmt, 4));
cJSON_AddStringToObject(event, "sig", (const char*)sqlite3_column_text(stmt, 5));
// If we didn't have admin pubkey in cache, we should update the cache
// Note: This will be handled by the cache refresh mechanism automatically
// Parse tags JSON
const char* tags_str = (const char*)sqlite3_column_text(stmt, 6);
if (tags_str) {
cJSON* tags = cJSON_Parse(tags_str);
if (tags) {
cJSON_AddItemToObject(event, "tags", tags);
} else {
cJSON_AddItemToObject(event, "tags", cJSON_CreateArray());
}
} else {
cJSON_AddItemToObject(event, "tags", cJSON_CreateArray());
}
}
}
sqlite3_finalize(stmt);
return event;
}
// ================================
@@ -920,7 +884,7 @@ cJSON* create_default_config_event(const unsigned char* admin_privkey_bytes,
// Create and sign event using nostr_core_lib
cJSON* event = nostr_create_and_sign_event(
23455, // kind
33334, // kind
"C Nostr Relay Configuration", // content
tags, // tags
admin_privkey_bytes, // private key bytes for signing
@@ -1597,7 +1561,7 @@ int process_configuration_event(const cJSON* event) {
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
if (!kind_obj || (cJSON_GetNumberValue(kind_obj) != 23455 && cJSON_GetNumberValue(kind_obj) != 23456)) {
if (!kind_obj || cJSON_GetNumberValue(kind_obj) != 33334) {
log_error("Invalid event kind for configuration");
return -1;
}
@@ -2047,7 +2011,7 @@ int add_pubkeys_to_config_table(void) {
// Forward declaration for admin authorization function from main.c
extern int is_authorized_admin_event(cJSON* event);
// Process admin events (updated for new Kind 23455/23456)
// Process admin events (updated for Kind 23456)
int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size, struct lws* wsi) {
log_info("DEBUG: Entering process_admin_event_in_config()");
@@ -2098,9 +2062,6 @@ int process_admin_event_in_config(cJSON* event, char* error_message, size_t erro
// Route to appropriate handler based on kind
log_info("DEBUG: Routing to kind-specific handler");
switch (kind) {
case 23455: // New ephemeral configuration management
log_info("DEBUG: Routing to process_admin_config_event (kind 23455)");
return process_admin_config_event(event, error_message, error_size);
case 23456: // New ephemeral auth rules management
log_info("DEBUG: Routing to process_admin_auth_event (kind 23456)");
return process_admin_auth_event(event, error_message, error_size, wsi);
@@ -2112,7 +2073,7 @@ int process_admin_event_in_config(cJSON* event, char* error_message, size_t erro
}
}
// Handle Kind 23455 configuration management events
// Handle legacy Kind 33334 configuration management events
int process_admin_config_event(cJSON* event, char* error_message, size_t error_size) {
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
int kind = kind_obj ? (int)cJSON_GetNumberValue(kind_obj) : 0;
@@ -2143,9 +2104,8 @@ int process_admin_config_event(cJSON* event, char* error_message, size_t error_s
if (strcmp(tag_key, "config_query") == 0) {
printf(" Config Query: %s\n", tag_val);
// For now, config queries are not implemented in the unified handler
// They would need to be added to handle_kind_23455_unified similar to auth queries
snprintf(error_message, error_size, "config queries not yet implemented in unified handler");
// Config queries are not implemented for legacy kind 33334
snprintf(error_message, error_size, "config queries not supported for legacy kind 33334");
return -1;
}
}
@@ -2592,6 +2552,38 @@ int send_admin_response_event(const cJSON* response_data, const char* recipient_
// ================================
// Map query command types to proper response types for frontend routing
static const char* map_auth_query_type_to_response(const char* query_type) {
if (!query_type) return "auth_rules_unknown";
if (strcmp(query_type, "all") == 0) {
return "auth_rules_all";
} else if (strcmp(query_type, "whitelist") == 0) {
return "auth_rules_whitelist";
} else if (strcmp(query_type, "blacklist") == 0) {
return "auth_rules_blacklist";
} else if (strcmp(query_type, "pattern") == 0) {
return "auth_rules_pattern";
} else {
return "auth_rules_unknown";
}
}
// Map config query command types to proper response types for frontend routing
static const char* map_config_query_type_to_response(const char* query_type) {
if (!query_type) return "config_unknown";
if (strcmp(query_type, "all") == 0) {
return "config_all";
} else if (strcmp(query_type, "category") == 0) {
return "config_category";
} else if (strcmp(query_type, "key") == 0) {
return "config_key";
} else {
return "config_unknown";
}
}
// Build standardized query response
cJSON* build_query_response(const char* query_type, cJSON* results_array, int total_count) {
if (!query_type || !results_array) return NULL;
@@ -2826,6 +2818,33 @@ int handle_kind_23456_unified(cJSON* event, char* error_message, size_t error_si
printf(" Query type: %s\n", query_type);
return handle_auth_query_unified(event, query_type, error_message, error_size, wsi);
}
else if (strcmp(action_type, "config_query") == 0) {
log_info("DEBUG: Routing to config_query handler");
const char* query_type = get_tag_value(event, action_type, 1);
if (!query_type) {
log_error("DEBUG: Missing config_query type parameter");
snprintf(error_message, error_size, "invalid: missing config_query type");
return -1;
}
printf(" Query type: %s\n", query_type);
return handle_config_query_unified(event, query_type, error_message, error_size, wsi);
}
else if (strcmp(action_type, "config_set") == 0) {
log_info("DEBUG: Routing to config_set handler");
const char* config_key = get_tag_value(event, action_type, 1);
const char* config_value = get_tag_value(event, action_type, 2);
if (!config_key || !config_value) {
log_error("DEBUG: Missing config_set parameters");
snprintf(error_message, error_size, "invalid: missing config_set key or value");
return -1;
}
printf(" Key: %s, Value: %s\n", config_key, config_value);
return handle_config_set_unified(event, config_key, config_value, error_message, error_size, wsi);
}
else if (strcmp(action_type, "config_update") == 0) {
log_info("DEBUG: Routing to config_update handler");
return handle_config_update_unified(event, error_message, error_size, wsi);
}
else if (strcmp(action_type, "system_command") == 0) {
log_info("DEBUG: Routing to system_command handler");
const char* command = get_tag_value(event, action_type, 1);
@@ -2940,8 +2959,9 @@ int handle_auth_query_unified(cJSON* event, const char* query_type, char* error_
sqlite3_finalize(stmt);
// Build and send response
cJSON* response = build_query_response(query_type, results_array, rule_count);
// Build and send response with mapped query type for frontend routing
const char* mapped_query_type = map_auth_query_type_to_response(query_type);
cJSON* response = build_query_response(mapped_query_type, results_array, rule_count);
if (response) {
// Get admin pubkey from event for response
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
@@ -2958,6 +2978,7 @@ int handle_auth_query_unified(cJSON* event, const char* query_type, char* error_
if (send_admin_response_event(response, admin_pubkey, wsi) == 0) {
printf("Total results: %d\n", rule_count);
log_success("Auth query completed successfully with signed response");
printf(" Response query_type: %s (mapped from %s)\n", mapped_query_type, query_type);
cJSON_Delete(response);
cJSON_Delete(results_array);
return 0;
@@ -2970,6 +2991,219 @@ int handle_auth_query_unified(cJSON* event, const char* query_type, char* error_
return -1;
}
// Unified config query handler
int handle_config_query_unified(cJSON* event, const char* query_type, char* error_message, size_t error_size, struct lws* wsi) {
// Suppress unused parameter warning
(void)wsi;
if (!g_db) {
snprintf(error_message, error_size, "database not available");
return -1;
}
log_info("Processing unified config query");
printf(" Query type: %s\n", query_type);
const char* sql = NULL;
int use_pattern_param = 0;
char* pattern_value = NULL;
// Build appropriate SQL query based on query type
if (strcmp(query_type, "all") == 0) {
sql = "SELECT key, value, data_type, category, description FROM config ORDER BY category, key";
}
else if (strcmp(query_type, "category") == 0) {
// Get category value from tags
pattern_value = (char*)get_tag_value(event, "config_query", 2);
if (!pattern_value) {
snprintf(error_message, error_size, "invalid: category query requires category value");
return -1;
}
sql = "SELECT key, value, data_type, category, description FROM config WHERE category = ? ORDER BY key";
use_pattern_param = 1;
}
else if (strcmp(query_type, "key") == 0) {
// Get key value from tags
pattern_value = (char*)get_tag_value(event, "config_query", 2);
if (!pattern_value) {
snprintf(error_message, error_size, "invalid: key query requires key value");
return -1;
}
sql = "SELECT key, value, data_type, category, description FROM config WHERE key = ? ORDER BY key";
use_pattern_param = 1;
}
else {
snprintf(error_message, error_size, "invalid: unknown config query type '%s'", query_type);
return -1;
}
// Execute query
sqlite3_stmt* stmt;
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
snprintf(error_message, error_size, "failed to prepare config query");
return -1;
}
if (use_pattern_param && pattern_value) {
sqlite3_bind_text(stmt, 1, pattern_value, -1, SQLITE_STATIC);
}
// Build results array
cJSON* results_array = cJSON_CreateArray();
if (!results_array) {
sqlite3_finalize(stmt);
snprintf(error_message, error_size, "failed to create results array");
return -1;
}
int config_count = 0;
printf("=== Config Query Results (%s) ===\n", query_type);
while (sqlite3_step(stmt) == SQLITE_ROW) {
const char* key = (const char*)sqlite3_column_text(stmt, 0);
const char* value = (const char*)sqlite3_column_text(stmt, 1);
const char* data_type = (const char*)sqlite3_column_text(stmt, 2);
const char* category = (const char*)sqlite3_column_text(stmt, 3);
const char* description = (const char*)sqlite3_column_text(stmt, 4);
printf(" %s = %s [%s] (%s)\n",
key ? key : "",
value ? value : "",
data_type ? data_type : "string",
category ? category : "general");
// Add config item to results array
cJSON* config_obj = cJSON_CreateObject();
cJSON_AddStringToObject(config_obj, "key", key ? key : "");
cJSON_AddStringToObject(config_obj, "value", value ? value : "");
cJSON_AddStringToObject(config_obj, "data_type", data_type ? data_type : "string");
cJSON_AddStringToObject(config_obj, "category", category ? category : "general");
cJSON_AddStringToObject(config_obj, "description", description ? description : "");
cJSON_AddItemToArray(results_array, config_obj);
config_count++;
}
sqlite3_finalize(stmt);
// Build and send response with mapped query type for frontend routing
const char* mapped_query_type = map_config_query_type_to_response(query_type);
cJSON* response = build_query_response(mapped_query_type, results_array, config_count);
if (response) {
// Get admin pubkey from event for response
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
const char* admin_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : NULL;
if (!admin_pubkey) {
cJSON_Delete(response);
cJSON_Delete(results_array);
snprintf(error_message, error_size, "missing admin pubkey for response");
return -1;
}
// Send response as signed kind 23457 event
if (send_admin_response_event(response, admin_pubkey, wsi) == 0) {
printf("Total results: %d\n", config_count);
log_success("Config query completed successfully with signed response");
printf(" Response query_type: %s (mapped from %s)\n", mapped_query_type, query_type);
cJSON_Delete(response);
cJSON_Delete(results_array);
return 0;
}
cJSON_Delete(response);
}
cJSON_Delete(results_array);
snprintf(error_message, error_size, "failed to send config query response");
return -1;
}
// Unified config set handler
int handle_config_set_unified(cJSON* event, const char* config_key, const char* config_value, char* error_message, size_t error_size, struct lws* wsi) {
// Suppress unused parameter warning
(void)wsi;
if (!g_db) {
snprintf(error_message, error_size, "database not available");
return -1;
}
log_info("Processing unified config set command");
printf(" Key: %s\n", config_key);
printf(" Value: %s\n", config_value);
// Validate the configuration field before updating
char validation_error[512];
if (validate_config_field(config_key, config_value, validation_error, sizeof(validation_error)) != 0) {
log_error("Config field validation failed");
printf(" Validation error: %s\n", validation_error);
snprintf(error_message, error_size, "validation failed: %s", validation_error);
return -1;
}
// Check if the config key exists in the table
const char* check_sql = "SELECT COUNT(*) FROM config WHERE key = ?";
sqlite3_stmt* check_stmt;
int check_rc = sqlite3_prepare_v2(g_db, check_sql, -1, &check_stmt, NULL);
if (check_rc != SQLITE_OK) {
snprintf(error_message, error_size, "failed to prepare config existence check");
return -1;
}
sqlite3_bind_text(check_stmt, 1, config_key, -1, SQLITE_STATIC);
int config_exists = 0;
if (sqlite3_step(check_stmt) == SQLITE_ROW) {
config_exists = sqlite3_column_int(check_stmt, 0) > 0;
}
sqlite3_finalize(check_stmt);
if (!config_exists) {
snprintf(error_message, error_size, "error: configuration key '%s' not found", config_key);
return -1;
}
// Update the configuration value
if (update_config_in_table(config_key, config_value) != 0) {
snprintf(error_message, error_size, "failed to update configuration in database");
return -1;
}
// Invalidate cache to ensure fresh reads
invalidate_config_cache();
// Build response
cJSON* response = cJSON_CreateObject();
cJSON_AddStringToObject(response, "command", "config_set");
cJSON_AddStringToObject(response, "key", config_key);
cJSON_AddStringToObject(response, "value", config_value);
cJSON_AddStringToObject(response, "status", "success");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
printf("Updated config: %s = %s\n", config_key, config_value);
// Get admin pubkey from event for response
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
const char* admin_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : NULL;
if (!admin_pubkey) {
cJSON_Delete(response);
snprintf(error_message, error_size, "missing admin pubkey for response");
return -1;
}
// Send response as signed kind 23457 event
if (send_admin_response_event(response, admin_pubkey, wsi) == 0) {
log_success("Config set command completed successfully with signed response");
cJSON_Delete(response);
return 0;
}
cJSON_Delete(response);
snprintf(error_message, error_size, "failed to send config set response");
return -1;
}
// Unified system command handler
int handle_system_command_unified(cJSON* event, const char* command, char* error_message, size_t error_size, struct lws* wsi) {
// Suppress unused parameter warning
@@ -3298,6 +3532,340 @@ int handle_auth_rule_modification_unified(cJSON* event, char* error_message, siz
return -1;
}
}
// Unified config update handler - handles multiple config objects in single atomic command
int handle_config_update_unified(cJSON* event, char* error_message, size_t error_size, struct lws* wsi) {
// Suppress unused parameter warning
(void)wsi;
if (!g_db) {
snprintf(error_message, error_size, "database not available");
return -1;
}
log_info("Processing unified config update command");
// Extract config objects array from synthetic tags created by NIP-44 decryption
// The decryption process creates synthetic tags like: ["config_update", [config_objects]]
cJSON* tags_obj = cJSON_GetObjectItem(event, "tags");
if (!tags_obj || !cJSON_IsArray(tags_obj)) {
snprintf(error_message, error_size, "invalid: config update event must have tags");
return -1;
}
// Find the config_update tag with config objects array
cJSON* config_objects_array = NULL;
cJSON* tag = NULL;
cJSON_ArrayForEach(tag, tags_obj) {
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
continue;
}
cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
if (!tag_name || !cJSON_IsString(tag_name)) {
continue;
}
if (strcmp(cJSON_GetStringValue(tag_name), "config_update") == 0) {
// Found config_update tag, get the config objects array
cJSON* config_array_item = cJSON_GetArrayItem(tag, 1);
if (config_array_item) {
// The config objects should be in a JSON string format in the tag
if (cJSON_IsString(config_array_item)) {
// Parse the JSON string to get the actual array
const char* config_json = cJSON_GetStringValue(config_array_item);
config_objects_array = cJSON_Parse(config_json);
} else if (cJSON_IsArray(config_array_item)) {
// Direct array reference
config_objects_array = cJSON_Duplicate(config_array_item, 1);
}
}
break;
}
}
if (!config_objects_array || !cJSON_IsArray(config_objects_array)) {
snprintf(error_message, error_size, "invalid: config_update command requires config objects array");
return -1;
}
int config_count = cJSON_GetArraySize(config_objects_array);
log_info("Config update command contains config objects");
printf(" Config objects count: %d\n", config_count);
if (config_count == 0) {
cJSON_Delete(config_objects_array);
snprintf(error_message, error_size, "invalid: config_update command requires at least one config object");
return -1;
}
// Begin transaction for atomic config updates
int rc = sqlite3_exec(g_db, "BEGIN IMMEDIATE TRANSACTION", NULL, NULL, NULL);
if (rc != SQLITE_OK) {
cJSON_Delete(config_objects_array);
snprintf(error_message, error_size, "failed to begin config update transaction");
return -1;
}
int updates_applied = 0;
int validation_errors = 0;
char first_validation_error[512] = {0}; // Track first specific validation error
char first_error_field[128] = {0}; // Track which field failed first
cJSON* processed_configs = cJSON_CreateArray();
if (!processed_configs) {
sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL);
cJSON_Delete(config_objects_array);
snprintf(error_message, error_size, "failed to create response array");
return -1;
}
// Process each config object in the array
cJSON* config_obj = NULL;
cJSON_ArrayForEach(config_obj, config_objects_array) {
if (!cJSON_IsObject(config_obj)) {
log_warning("Skipping non-object item in config objects array");
continue;
}
// Extract required fields from config object
cJSON* key_obj = cJSON_GetObjectItem(config_obj, "key");
cJSON* value_obj = cJSON_GetObjectItem(config_obj, "value");
cJSON* data_type_obj = cJSON_GetObjectItem(config_obj, "data_type");
cJSON* category_obj = cJSON_GetObjectItem(config_obj, "category");
if (!key_obj || !cJSON_IsString(key_obj) ||
!value_obj || !cJSON_IsString(value_obj)) {
log_error("Config object missing required key or value fields");
validation_errors++;
continue;
}
const char* key = cJSON_GetStringValue(key_obj);
const char* value = cJSON_GetStringValue(value_obj);
const char* data_type = data_type_obj && cJSON_IsString(data_type_obj) ?
cJSON_GetStringValue(data_type_obj) : "string";
const char* category = category_obj && cJSON_IsString(category_obj) ?
cJSON_GetStringValue(category_obj) : "general";
log_info("Processing config object");
printf(" Key: %s\n", key);
printf(" Value: %s\n", value);
printf(" Data type: %s\n", data_type);
printf(" Category: %s\n", category);
// Validate the configuration field before updating
char validation_error[512];
if (validate_config_field(key, value, validation_error, sizeof(validation_error)) != 0) {
log_error("Config field validation failed");
printf(" Validation error: %s\n", validation_error);
validation_errors++;
// Capture first validation error for enhanced error message
if (validation_errors == 1) {
strncpy(first_validation_error, validation_error, sizeof(first_validation_error) - 1);
first_validation_error[sizeof(first_validation_error) - 1] = '\0';
strncpy(first_error_field, key, sizeof(first_error_field) - 1);
first_error_field[sizeof(first_error_field) - 1] = '\0';
}
// Add failed config to response array
cJSON* failed_config = cJSON_CreateObject();
cJSON_AddStringToObject(failed_config, "key", key);
cJSON_AddStringToObject(failed_config, "value", value);
cJSON_AddStringToObject(failed_config, "data_type", data_type);
cJSON_AddStringToObject(failed_config, "category", category);
cJSON_AddStringToObject(failed_config, "status", "validation_failed");
cJSON_AddStringToObject(failed_config, "error", validation_error);
cJSON_AddItemToArray(processed_configs, failed_config);
continue;
}
// Check if the config key exists in the table
const char* check_sql = "SELECT COUNT(*) FROM config WHERE key = ?";
sqlite3_stmt* check_stmt;
int check_rc = sqlite3_prepare_v2(g_db, check_sql, -1, &check_stmt, NULL);
if (check_rc != SQLITE_OK) {
log_error("Failed to prepare config existence check");
validation_errors++;
continue;
}
sqlite3_bind_text(check_stmt, 1, key, -1, SQLITE_STATIC);
int config_exists = 0;
if (sqlite3_step(check_stmt) == SQLITE_ROW) {
config_exists = sqlite3_column_int(check_stmt, 0) > 0;
}
sqlite3_finalize(check_stmt);
if (!config_exists) {
log_error("Configuration key not found");
printf(" Key not found: %s\n", key);
validation_errors++;
// Add failed config to response array
cJSON* failed_config = cJSON_CreateObject();
cJSON_AddStringToObject(failed_config, "key", key);
cJSON_AddStringToObject(failed_config, "value", value);
cJSON_AddStringToObject(failed_config, "data_type", data_type);
cJSON_AddStringToObject(failed_config, "category", category);
cJSON_AddStringToObject(failed_config, "status", "key_not_found");
cJSON_AddStringToObject(failed_config, "error", "configuration key not found in database");
cJSON_AddItemToArray(processed_configs, failed_config);
continue;
}
// Update the configuration value in the table
if (update_config_in_table(key, value) == 0) {
updates_applied++;
// Add successful config to response array
cJSON* success_config = cJSON_CreateObject();
cJSON_AddStringToObject(success_config, "key", key);
cJSON_AddStringToObject(success_config, "value", value);
cJSON_AddStringToObject(success_config, "data_type", data_type);
cJSON_AddStringToObject(success_config, "category", category);
cJSON_AddStringToObject(success_config, "status", "updated");
cJSON_AddItemToArray(processed_configs, success_config);
log_success("Config field updated successfully");
printf(" Updated: %s = %s\n", key, value);
} else {
log_error("Failed to update config field in database");
printf(" Failed to update: %s = %s\n", key, value);
validation_errors++;
// Add failed config to response array
cJSON* failed_config = cJSON_CreateObject();
cJSON_AddStringToObject(failed_config, "key", key);
cJSON_AddStringToObject(failed_config, "value", value);
cJSON_AddStringToObject(failed_config, "data_type", data_type);
cJSON_AddStringToObject(failed_config, "category", category);
cJSON_AddStringToObject(failed_config, "status", "database_error");
cJSON_AddStringToObject(failed_config, "error", "failed to update configuration in database");
cJSON_AddItemToArray(processed_configs, failed_config);
}
}
// Clean up config objects array
cJSON_Delete(config_objects_array);
// Determine transaction outcome
if (updates_applied > 0 && validation_errors == 0) {
// All updates successful
sqlite3_exec(g_db, "COMMIT", NULL, NULL, NULL);
invalidate_config_cache();
char success_msg[256];
snprintf(success_msg, sizeof(success_msg), "Applied %d configuration updates successfully", updates_applied);
log_success(success_msg);
} else if (updates_applied > 0 && validation_errors > 0) {
// Partial success - rollback for atomic behavior
sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL);
char error_msg[256];
snprintf(error_msg, sizeof(error_msg), "Config update failed: %d validation errors (atomic rollback)", validation_errors);
log_error(error_msg);
// Build error response with validation details
cJSON* error_response = cJSON_CreateObject();
cJSON_AddStringToObject(error_response, "query_type", "config_update");
cJSON_AddStringToObject(error_response, "status", "error");
// Create enhanced error message with specific validation details
char enhanced_error_message[1024];
if (strlen(first_validation_error) > 0 && strlen(first_error_field) > 0) {
snprintf(enhanced_error_message, sizeof(enhanced_error_message),
"field validation failed: %s - %s",
first_error_field, first_validation_error);
} else {
snprintf(enhanced_error_message, sizeof(enhanced_error_message),
"field validation failed: atomic rollback performed");
}
cJSON_AddStringToObject(error_response, "error", enhanced_error_message);
cJSON_AddNumberToObject(error_response, "validation_errors", validation_errors);
cJSON_AddNumberToObject(error_response, "timestamp", (double)time(NULL));
cJSON_AddItemToObject(error_response, "data", processed_configs);
// Get admin pubkey from event for error response
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
const char* admin_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : NULL;
if (admin_pubkey) {
// Send error response as signed kind 23457 event
if (send_admin_response_event(error_response, admin_pubkey, wsi) == 0) {
log_info("Config update validation error response sent successfully");
cJSON_Delete(error_response);
return 0; // Return success after sending error response
}
}
cJSON_Delete(error_response);
snprintf(error_message, error_size, "validation failed: %d errors, atomic rollback performed", validation_errors);
return -1;
} else {
// No updates applied
sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL);
// Build error response for no valid updates
cJSON* error_response = cJSON_CreateObject();
cJSON_AddStringToObject(error_response, "query_type", "config_update");
cJSON_AddStringToObject(error_response, "status", "error");
cJSON_AddStringToObject(error_response, "error", "no valid configuration updates found");
cJSON_AddNumberToObject(error_response, "timestamp", (double)time(NULL));
cJSON_AddItemToObject(error_response, "data", processed_configs);
// Get admin pubkey from event for error response
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
const char* admin_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : NULL;
if (admin_pubkey) {
// Send error response as signed kind 23457 event
if (send_admin_response_event(error_response, admin_pubkey, wsi) == 0) {
log_info("Config update 'no valid updates' error response sent successfully");
cJSON_Delete(error_response);
return 0; // Return success after sending error response
}
}
cJSON_Delete(error_response);
snprintf(error_message, error_size, "no valid configuration updates found");
return -1;
}
// Build response with query_type for frontend routing
cJSON* response = cJSON_CreateObject();
cJSON_AddStringToObject(response, "query_type", "config_update");
cJSON_AddStringToObject(response, "command", "config_update");
cJSON_AddNumberToObject(response, "configs_processed", updates_applied);
cJSON_AddNumberToObject(response, "total_configs", config_count);
cJSON_AddStringToObject(response, "status", "success");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
cJSON_AddItemToObject(response, "processed_configs", processed_configs);
printf("Config update completed: %d/%d configs updated successfully\n", updates_applied, config_count);
// Get admin pubkey from event for response
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
const char* admin_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : NULL;
if (!admin_pubkey) {
cJSON_Delete(response);
snprintf(error_message, error_size, "missing admin pubkey for response");
return -1;
}
// Send response as signed kind 23457 event
if (send_admin_response_event(response, admin_pubkey, wsi) == 0) {
log_success("Config update command completed successfully with signed response");
printf(" Response query_type: config_update\n");
cJSON_Delete(response);
return 0;
}
cJSON_Delete(response);
snprintf(error_message, error_size, "failed to send config update response");
return -1;
}
@@ -3580,7 +4148,7 @@ int process_startup_config_event(const cJSON* event) {
// Validate event structure first
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
if (!kind_obj || cJSON_GetNumberValue(kind_obj) != 23455) {
if (!kind_obj || cJSON_GetNumberValue(kind_obj) != 33334) {
log_error("Invalid event kind for startup configuration");
return -1;
}
@@ -3709,7 +4277,7 @@ cJSON* generate_config_event_from_table(void) {
cJSON_AddStringToObject(event, "id", "synthetic_config_event_id");
cJSON_AddStringToObject(event, "pubkey", relay_pubkey); // Use relay pubkey as event author
cJSON_AddNumberToObject(event, "created_at", (double)time(NULL));
cJSON_AddNumberToObject(event, "kind", 23455);
cJSON_AddNumberToObject(event, "kind", 33334);
cJSON_AddStringToObject(event, "content", "C Nostr Relay Configuration");
cJSON_AddStringToObject(event, "sig", "synthetic_signature");
@@ -3790,7 +4358,7 @@ int req_filter_requests_config_events(const cJSON* filter) {
cJSON* kind_item = NULL;
cJSON_ArrayForEach(kind_item, kinds) {
int kind_val = (int)cJSON_GetNumberValue(kind_item);
if (cJSON_IsNumber(kind_item) && (kind_val == 23455 || kind_val == 23456)) {
if (cJSON_IsNumber(kind_item) && kind_val == 33334) {
return 1;
}
}

View File

@@ -224,7 +224,7 @@ int handle_event_message(cJSON* event, char* error_message, size_t error_size);
// Forward declaration for unified validation
int nostr_validate_unified_request(const char* json_string, size_t json_length);
// Forward declaration for admin event processing (kinds 23455 and 23456)
// Forward declaration for admin event processing (kind 23456)
int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size, struct lws* wsi);
// Forward declaration for enhanced admin event authorization
@@ -3032,7 +3032,7 @@ int is_authorized_admin_event(cJSON* event, char* error_buffer, size_t error_buf
}
int event_kind = kind_json->valueint;
if (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);
return -1;
}
@@ -3353,7 +3353,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
// Cleanup event JSON string
free(event_json_str);
// Check for admin events (kinds 23455 and 23456) and intercept them
// Check for admin events (kind 23456) and intercept them
if (result == 0) {
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
if (kind_obj && cJSON_IsNumber(kind_obj)) {
@@ -3361,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 reception of Kind 23455 and 23456 events
if (event_kind == 23455 || event_kind == 23456) {
// Log reception of Kind 23456 events
if (event_kind == 23456) {
char* event_json_debug = cJSON_Print(event);
char debug_received_msg[1024];
snprintf(debug_received_msg, sizeof(debug_received_msg),
@@ -3375,7 +3375,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
}
}
if (event_kind == 23455 || event_kind == 23456) {
if (event_kind == 23456) {
// Enhanced admin event security - check authorization first
log_info("DEBUG ADMIN: Admin event detected, checking authorization");
@@ -3407,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);
log_info(debug_admin_msg);
// Log results for Kind 23455 and 23456 events
if (event_kind == 23455 || event_kind == 23456) {
// Log results for Kind 23456 events
if (event_kind == 23456) {
if (admin_result == 0) {
char success_result_msg[256];
snprintf(success_result_msg, sizeof(success_result_msg),
@@ -3670,6 +3670,7 @@ int check_port_available(int port) {
int sockfd;
struct sockaddr_in addr;
int result;
int reuse = 1;
// Create a socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
@@ -3677,6 +3678,13 @@ int check_port_available(int port) {
return 0; // Cannot create socket, assume port unavailable
}
// Set SO_REUSEADDR to allow binding to ports in TIME_WAIT state
// This matches libwebsockets behavior and prevents false unavailability
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) {
close(sockfd);
return 0; // Failed to set socket option
}
// Set up the address structure
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;