diff --git a/IMPLEMENT_API.md b/IMPLEMENT_API.md new file mode 100644 index 0000000..f89d712 --- /dev/null +++ b/IMPLEMENT_API.md @@ -0,0 +1,513 @@ +# 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. diff --git a/README.md b/README.md index 6250b4f..62e46a2 100644 --- a/README.md +++ b/README.md @@ -24,55 +24,66 @@ 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. Admin commands use ephemeral kinds 23455 and 23456 with **optional NIP-44 encryption** for enhanced security. +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. ### Authentication All admin commands require signing with the admin private key displayed during first-time startup. **Save this key securely** - it cannot be recovered and is needed for all administrative operations. -### Security Options +### Event Structure -**Standard Mode (Plaintext):** Commands sent in tags as normal -**Encrypted Mode (NIP-44):** Commands encrypted in content field, no tags used +All admin commands use the same unified event structure with tag-based parameters: -### Kind 23455: Configuration Management (Ephemeral) -Update relay configuration parameters or query available settings. - -**Configuration Update:** +**Admin Command Event:** ```json { - "kind": 23455, - "content": "", + "id": "event_id", + "pubkey": "admin_public_key", + "created_at": 1234567890, + "kind": 23456, + "content": "", "tags": [ - ["config_key1", "config_value1"], - ["config_key2", "config_value2"] - ] + ["p", "relay_public_key"], + ], + "sig": "event_signature" } ``` -**Query Available Config Keys:** +**Admin Response Event:** ```json -{ - "kind": 23455, - "content": "", +["EVENT", "temp_sub_id", { + "id": "response_event_id", + "pubkey": "relay_public_key", + "created_at": 1234567890, + "kind": 23457, + "content": "", "tags": [ - ["config_query", "list_all_keys"] - ] -} + ["p", "admin_public_key"] + ], + "sig": "response_event_signature" +}] ``` -**Get Current Configuration:** -```json -{ - "kind": 23455, - "content": "", - "tags": [ - ["config_query", "get_current_config"] - ] -} -``` +### Admin Commands -**Available Configuration Keys:** +All commands are sent as nip44 encrypted content. The following table lists all available commands: + +| Command Type | Tag 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 | +| **Auth Rules Management** | +| `auth_add_blacklist` | `["blacklist", "pubkey", "abc123..."]` | Add pubkey to blacklist | +| `auth_add_whitelist` | `["whitelist", "pubkey", "def456..."]` | Add pubkey to whitelist | +| `auth_query_all` | `["auth_query", "all"]` | Query all auth rules | +| `auth_query_type` | `["auth_query", "whitelist"]` | Query specific rule type | +| `auth_query_pattern` | `["auth_query", "pattern", "abc123..."]` | Query specific pattern | +| **System Commands** | +| `system_clear_auth` | `["system_command", "clear_all_auth_rules"]` | Clear all auth rules | +| `system_status` | `["system_command", "system_status"]` | Get system status | + +### Available Configuration Keys **Basic Relay Settings:** - `relay_description`: Relay description text @@ -92,259 +103,69 @@ Update relay configuration parameters or query available settings. - `pow_min_difficulty`: Minimum proof-of-work difficulty - `nip40_expiration_enabled`: Enable event expiration (`true`/`false`) -### Kind 23456: Auth Rules & System Management (Ephemeral) -Manage whitelist/blacklist rules and system administration commands. - -**Add Blacklist Rule:** -```json -{ - "kind": 23456, - "content": "", - "tags": [ - ["blacklist", "pubkey", "deadbeef1234abcd..."] - ] -} -``` - -**Add Whitelist Rule:** -```json -{ - "kind": 23456, - "content": "", - "tags": [ - ["whitelist", "pubkey", "cafebabe5678efgh..."] - ] -} -``` - -**Remove Rule:** -```json -{ - "kind": 23456, - "content": "", - "tags": [ - ["blacklist", "pubkey", "deadbeef1234abcd..."] - ] -} -``` - -**Query Auth Rules:** -```json -{ - "kind": 23456, - "content": "", - "tags": [ - ["auth_query", "all"] - ] -} -``` - -**Query Specific Rule Type:** -```json -{ - "kind": 23456, - "content": "", - "tags": [ - ["auth_query", "whitelist"] - ] -} -``` - -**Query Specific Pattern:** -```json -{ - "kind": 23456, - "content": "", - "tags": [ - ["auth_query", "pattern", "deadbeef1234abcd..."] - ] -} -``` - -**Clear All Auth Rules:** -```json -{ - "kind": 23456, - "content": "", - "tags": [ - ["system_command", "clear_all_auth_rules"] - ] -} -``` - ### Response Format -All admin commands return JSON responses via WebSocket: + +All admin commands return **signed EVENT responses** via WebSocket following standard Nostr protocol. Responses use JSON content with structured data. + +#### Response Examples **Success Response:** ```json -["OK", "event_id", true, "Operation completed successfully"] +["EVENT", "temp_sub_id", { + "id": "response_event_id", + "pubkey": "relay_public_key", + "created_at": 1234567890, + "kind": 23457, + "content": "nip44 encrypted:{\"status\": \"success\", \"message\": \"Operation completed successfully\"}", + "tags": [ + ["p", "admin_public_key"] + ], + "sig": "response_event_signature" +}] ``` **Error Response:** ```json -["OK", "event_id", false, "Error: invalid configuration value"] -``` - -### Command Examples - -**Using `nak` CLI tool:** - -```bash -# Set environment variables -ADMIN_PRIVKEY="your_admin_private_key_here" -RELAY_PUBKEY="your_relay_public_key_here" -RELAY_URL="ws://localhost:8888" - -# List all available configuration keys -nak event -k 23455 --content "Discovery query" \ - -t "config_query=list_all_keys" \ - --sec $ADMIN_PRIVKEY | nak event $RELAY_URL - -# Enable whitelist/blacklist auth rules -nak event -k 23455 --content "Enable auth rules" \ - -t "auth_enabled=true" \ - --sec $ADMIN_PRIVKEY | nak event $RELAY_URL - -# Add user to blacklist -nak event -k 23456 --content "Block spam user" \ - -t "blacklist=pubkey;$SPAM_USER_PUBKEY" \ - --sec $ADMIN_PRIVKEY | nak event $RELAY_URL - -# Add user to whitelist -nak event -k 23456 --content "Allow trusted user" \ - -t "whitelist=pubkey;$TRUSTED_USER_PUBKEY" \ - --sec $ADMIN_PRIVKEY | nak event $RELAY_URL - -# Query all current auth rules -nak event -k 23456 --content "Get all auth rules" \ - -t "auth_query=all" \ - --sec $ADMIN_PRIVKEY | nak event $RELAY_URL - -# Query only whitelist rules -nak event -k 23456 --content "Get whitelist rules" \ - -t "auth_query=whitelist" \ - --sec $ADMIN_PRIVKEY | nak event $RELAY_URL - -# Query only blacklist rules -nak event -k 23456 --content "Get blacklist rules" \ - -t "auth_query=blacklist" \ - --sec $ADMIN_PRIVKEY | nak event $RELAY_URL - -# Check if specific pattern exists -nak event -k 23456 --content "Check user status" \ - -t "auth_query=pattern;$CHECK_USER_PUBKEY" \ - --sec $ADMIN_PRIVKEY | nak event $RELAY_URL - -# Clear all auth rules (for testing) -nak event -k 23456 --content "Clear all rules" \ - -t "system_command=clear_all_auth_rules" \ - --sec $ADMIN_PRIVKEY | nak event $RELAY_URL - -# Update relay description -nak event -k 23455 --content "Update description" \ - -t "relay_description=My awesome relay" \ - --sec $ADMIN_PRIVKEY | nak event $RELAY_URL - -# Set connection limits -nak event -k 23455 --content "Update limits" \ - -t "max_connections=500" \ - -t "max_subscriptions_per_client=10" \ - --sec $ADMIN_PRIVKEY | nak event $RELAY_URL -``` - -**For encrypted commands (NIP-44), use empty tags and encrypted content:** -```bash -# Enable auth rules (encrypted) -ENCRYPTED_TAGS=$(echo '[["auth_enabled","true"]]' | nip44_encrypt_with_relay_pubkey) -nak event -k 23455 --content "{\"encrypted_tags\":\"$ENCRYPTED_TAGS\"}" \ - --sec $ADMIN_PRIVKEY | nak event $RELAY_URL - -# Add user to blacklist (encrypted) -ENCRYPTED_TAGS=$(echo '[["blacklist","pubkey","'$SPAM_USER_PUBKEY'"]]' | nip44_encrypt_with_relay_pubkey) -nak event -k 23456 --content "{\"encrypted_tags\":\"$ENCRYPTED_TAGS\"}" \ - --sec $ADMIN_PRIVKEY | nak event $RELAY_URL -``` - -### Authentication Systems - -C-Relay supports **two independent authentication systems**: - -#### 1. Auth Rules (Whitelist/Blacklist) -- **Config Key**: `auth_enabled=true` -- **Purpose**: Block or allow specific pubkeys from publishing events -- **Method**: Database-driven rules enforcement -- **Use Case**: Spam prevention, content moderation - -#### 2. NIP-42 Cryptographic Authentication -- **Config Key**: `nip42_auth_required=true` -- **Purpose**: Require cryptographic proof of pubkey ownership -- **Method**: Challenge-response authentication protocol -- **Use Case**: Verified identity, premium features - -**Important**: These systems can be used independently or together: -- `auth_enabled=true` + `nip42_auth_required=false`: Only whitelist/blacklist rules -- `auth_enabled=false` + `nip42_auth_required=true`: Only NIP-42 auth challenges -- Both `true`: Users must pass NIP-42 auth AND not be blacklisted - -### Security Considerations - -- **Private Key Protection**: Admin private key grants full relay control -- **Network Access**: Admin commands should only be sent over secure connections -- **Backup**: Keep secure backups of admin private key -- **Rotation**: Consider implementing admin key rotation for production deployments - -### Troubleshooting - -**Common Issues:** -- `auth-required: not authorized admin`: Verify you're using the correct admin private key -- `invalid: missing or invalid kind`: Ensure event kind is 23455 or 23456 -- `error: failed to process configuration event`: Check configuration key/value validity -- `no valid auth rules found`: Verify tag format uses semicolon syntax for multi-element tags - -**Debug Commands:** -```bash -# Check if relay is accepting connections -echo '["REQ","test",{}]' | websocat ws://localhost:8888 - -# Test admin authentication -nak event -k 23455 --content "Test auth" \ - -t "test_config=true" \ - --sec $ADMIN_PRIVKEY | nak event ws://localhost:8888 - -# Discover available configuration keys -nak event -k 23455 --content "Discovery query" \ - -t "config_query=list_all_keys" \ - --sec $ADMIN_PRIVKEY | nak event ws://localhost:8888 - -# Query current auth rules -nak event -k 23456 --content "Get auth rules" \ - -t "auth_query=all" \ - --sec $ADMIN_PRIVKEY | nak event ws://localhost:8888 -``` - -### Configuration Discovery - -Before making changes, admins can query the relay to discover all available configuration options: - -```bash -# Get list of all editable configuration keys with descriptions -nak event -k 23455 --content '{"query":"list_config_keys","description":"Discovery"}' \ - -t "config_query=list_all_keys" \ - --sec $ADMIN_PRIVKEY | nak event $RELAY_URL - -# Get current values of all configuration parameters -nak event -k 23455 --content '{"query":"get_config","description":"Current state"}' \ - -t "config_query=get_current_config" \ - --sec $ADMIN_PRIVKEY | nak event $RELAY_URL -``` - -**Expected Response Format:** -```json -["EVENT", "subscription_id", { - "kind": 23455, - "content": "{\"config_keys\": [\"auth_enabled\", \"max_connections\", ...], \"descriptions\": {...}}", - "tags": [["response_type", "config_keys_list"]] +["EVENT", "temp_sub_id", { + "id": "response_event_id", + "pubkey": "relay_public_key", + "created_at": 1234567890, + "kind": 23457, + "content": "nip44 encrypted:{\"status\": \"error\", \"message\": \"Error: invalid configuration value\"}", + "tags": [ + ["p", "admin_public_key"] + ], + "sig": "response_event_signature" }] ``` +**Auth Rules Query Response:** +```json +["EVENT", "temp_sub_id", { + "id": "response_event_id", + "pubkey": "relay_public_key", + "created_at": 1234567890, + "kind": 23457, + "content": "nip44 encrypted:{\"query_type\": \"auth_rules\", \"total_results\": 2, \"data\": [{\"rule_type\": \"blacklist\", \"pattern_type\": \"pubkey\", \"pattern_value\": \"abc123...\", \"action\": \"deny\"}]}", + "tags": [ + ["p", "admin_public_key"] + ], + "sig": "response_event_signature" +}] +``` + +**Configuration Query Response:** +```json +["EVENT", "temp_sub_id", { + "id": "response_event_id", + "pubkey": "relay_public_key", + "created_at": 1234567890, + "kind": 23457, + "content": "nip44 encrypted:{\"query_type\": \"config_keys\", \"config_keys\": [\"auth_enabled\", \"max_connections\"], \"descriptions\": {\"auth_enabled\": \"Enable whitelist/blacklist rules\"}}", + "tags": [ + ["p", "admin_public_key"] + ], + "sig": "response_event_signature" +}] +``` diff --git a/make_and_restart_relay.sh b/make_and_restart_relay.sh index 57b4463..9763fb3 100755 --- a/make_and_restart_relay.sh +++ b/make_and_restart_relay.sh @@ -198,25 +198,54 @@ fi echo "Build successful. Proceeding with relay restart..." -# Kill existing relay if running +# Kill existing relay if running - start aggressive immediately echo "Stopping any existing relay servers..." -pkill -f "c_relay_" 2>/dev/null -sleep 2 # Give time for shutdown -# Check if port is still bound -if lsof -i :8888 >/dev/null 2>&1; then - echo "Port 8888 still in use, force killing..." - fuser -k 8888/tcp 2>/dev/null || echo "No process on port 8888" +# Get all relay processes and kill them immediately with -9 +RELAY_PIDS=$(pgrep -f "c_relay_" || echo "") +if [ -n "$RELAY_PIDS" ]; then + echo "Force killing relay processes immediately: $RELAY_PIDS" + kill -9 $RELAY_PIDS 2>/dev/null +else + echo "No existing relay processes found" fi -# Get any remaining processes -REMAINING_PIDS=$(pgrep -f "c_relay_" || echo "") -if [ -n "$REMAINING_PIDS" ]; then - echo "Force killing remaining processes: $REMAINING_PIDS" - kill -9 $REMAINING_PIDS 2>/dev/null +# Ensure port 8888 is completely free with retry loop +echo "Ensuring port 8888 is available..." +for attempt in {1..15}; do + if ! lsof -i :8888 >/dev/null 2>&1; then + echo "Port 8888 is now free" + break + fi + + echo "Attempt $attempt: Port 8888 still in use, force killing..." + # Kill anything using port 8888 + fuser -k 8888/tcp 2>/dev/null || true + + # Double-check for any remaining relay processes + REMAINING_PIDS=$(pgrep -f "c_relay_" || echo "") + if [ -n "$REMAINING_PIDS" ]; then + echo "Killing remaining relay processes: $REMAINING_PIDS" + kill -9 $REMAINING_PIDS 2>/dev/null || true + fi + + sleep 2 + + if [ $attempt -eq 15 ]; then + echo "ERROR: Could not free port 8888 after 15 attempts" + echo "Current processes using port:" + lsof -i :8888 2>/dev/null || echo "No process details available" + echo "You may need to manually kill processes or reboot" + exit 1 + fi +done + +# Final safety check - ensure no relay processes remain +FINAL_PIDS=$(pgrep -f "c_relay_" || echo "") +if [ -n "$FINAL_PIDS" ]; then + echo "Final cleanup: killing processes $FINAL_PIDS" + kill -9 $FINAL_PIDS 2>/dev/null || true sleep 1 -else - echo "No existing relay found" fi # Clean up PID file diff --git a/relay.pid b/relay.pid index 9bb7b91..0143497 100644 --- a/relay.pid +++ b/relay.pid @@ -1 +1 @@ -238618 +285781 diff --git a/src/config.c b/src/config.c index 565a922..ef43ed7 100644 --- a/src/config.c +++ b/src/config.c @@ -10,6 +10,7 @@ #include #include #include +#include // External database connection (from main.c) extern sqlite3* g_db; @@ -63,7 +64,6 @@ extern void log_error(const char* message); // Forward declarations for new admin API functions int populate_default_config_values(void); int process_admin_config_event(cJSON* event, char* error_message, size_t error_size); -int process_admin_auth_event(cJSON* event, char* error_message, size_t error_size); void invalidate_config_cache(void); int add_auth_rule_from_config(const char* rule_type, const char* pattern_type, const char* pattern_value, const char* action); @@ -73,15 +73,11 @@ int is_config_table_ready(void); int migrate_config_from_events_to_table(void); int populate_config_table_from_event(const cJSON* event); -// Forward declarations for admin API query handlers -int handle_config_list_keys_query(cJSON* event, char* error_message, size_t error_size); -int handle_config_get_current_query(cJSON* event, char* error_message, size_t error_size); -int handle_auth_list_all_query(cJSON* event, char* error_message, size_t error_size); -int handle_auth_whitelist_query(cJSON* event, char* error_message, size_t error_size); -int handle_auth_blacklist_query(cJSON* event, char* error_message, size_t error_size); -int handle_auth_pattern_check_query(cJSON* event, char* error_message, size_t error_size); -int handle_clear_all_auth_rules_command(cJSON* event, char* error_message, size_t error_size); -int handle_system_status_query(cJSON* event, char* error_message, size_t error_size); +// 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); + // Current configuration cache static cJSON* g_current_config = NULL; @@ -2065,40 +2061,47 @@ int add_pubkeys_to_config_table(void) { // ADMIN EVENT PROCESSING FUNCTIONS // ================================ +// 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) -int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size) { +int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size, struct lws* wsi) { cJSON* kind_obj = cJSON_GetObjectItem(event, "kind"); if (!kind_obj || !cJSON_IsNumber(kind_obj)) { snprintf(error_message, error_size, "invalid: missing or invalid kind"); return -1; } - // Verify admin authorization - cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey"); - if (!pubkey_obj || !cJSON_IsString(pubkey_obj)) { - snprintf(error_message, error_size, "invalid: missing pubkey"); - return -1; - } - - const char* event_pubkey = cJSON_GetStringValue(pubkey_obj); - const char* admin_pubkey = get_config_value("admin_pubkey"); - - if (!admin_pubkey || strcmp(event_pubkey, admin_pubkey) != 0) { + // DEFENSE-IN-DEPTH: Use comprehensive admin authorization validation + if (!is_authorized_admin_event(event)) { + // Log the unauthorized attempt for security monitoring + cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey"); + const char* event_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : "unknown"; + + char log_msg[256]; + snprintf(log_msg, sizeof(log_msg), + "Unauthorized admin event attempt in config processing - pubkey: %.16s...", + event_pubkey ? event_pubkey : "null"); + log_warning(log_msg); + snprintf(error_message, error_size, "auth-required: not authorized admin"); return -1; } + // Log successful admin authorization for audit trail + log_info("Admin event authorized successfully in config processing"); + int kind = (int)cJSON_GetNumberValue(kind_obj); switch (kind) { case 23455: // New ephemeral configuration management return process_admin_config_event(event, error_message, error_size); case 23456: // New ephemeral auth rules management - return process_admin_auth_event(event, error_message, error_size); + return process_admin_auth_event(event, error_message, error_size, wsi); case 33334: // Legacy addressable config events (backward compatibility) return process_admin_config_event(event, error_message, error_size); case 33335: // Legacy addressable auth events (backward compatibility) - return process_admin_auth_event(event, error_message, error_size); + return process_admin_auth_event(event, error_message, error_size, wsi); default: snprintf(error_message, error_size, "invalid: unsupported admin event kind %d", kind); return -1; @@ -2110,40 +2113,41 @@ int process_admin_config_event(cJSON* event, char* error_message, size_t error_s cJSON* kind_obj = cJSON_GetObjectItem(event, "kind"); int kind = kind_obj ? (int)cJSON_GetNumberValue(kind_obj) : 0; - // Parse content for action commands - cJSON* content_obj = cJSON_GetObjectItem(event, "content"); - const char* content = content_obj ? cJSON_GetStringValue(content_obj) : ""; + log_info("Processing admin configuration event"); + printf(" Kind: %d\n", kind); - // Check if this is a query command - cJSON* content_json = cJSON_Parse(content); - char action_buffer[32] = "set"; // default action - const char* action = action_buffer; - - if (content_json) { - cJSON* action_obj = cJSON_GetObjectItem(content_json, "action"); - if (action_obj && cJSON_IsString(action_obj)) { - const char* action_str = cJSON_GetStringValue(action_obj); - if (action_str) { - strncpy(action_buffer, action_str, sizeof(action_buffer) - 1); - action_buffer[sizeof(action_buffer) - 1] = '\0'; + // Parse tags to find query commands according to API specification + cJSON* tags_obj = cJSON_GetObjectItem(event, "tags"); + if (tags_obj && cJSON_IsArray(tags_obj)) { + cJSON* tag = NULL; + cJSON_ArrayForEach(tag, tags_obj) { + if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) { + continue; + } + + cJSON* tag_name = cJSON_GetArrayItem(tag, 0); + cJSON* tag_value = cJSON_GetArrayItem(tag, 1); + + if (!cJSON_IsString(tag_name) || !cJSON_IsString(tag_value)) { + continue; + } + + const char* tag_key = cJSON_GetStringValue(tag_name); + const char* tag_val = cJSON_GetStringValue(tag_value); + + // Handle config_query commands per API spec + 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"); + return -1; } } - cJSON_Delete(content_json); } - log_info("Processing admin configuration event"); - printf(" Kind: %d, Action: %s\n", kind, action); - - // Handle query commands - if (strcmp(action, "list_config_keys") == 0) { - return handle_config_list_keys_query(event, error_message, error_size); - } - if (strcmp(action, "get_current_config") == 0) { - return handle_config_get_current_query(event, error_message, error_size); - } - - // Handle configuration updates (set action) - cJSON* tags_obj = cJSON_GetObjectItem(event, "tags"); + // Handle configuration updates (set action) - parse remaining tags for config updates if (!tags_obj || !cJSON_IsArray(tags_obj)) { snprintf(error_message, error_size, "invalid: configuration event must have tags"); return -1; @@ -2159,14 +2163,14 @@ int process_admin_config_event(cJSON* event, char* error_message, size_t error_s int updates_applied = 0; // Process each tag as a configuration parameter - cJSON* tag = NULL; - cJSON_ArrayForEach(tag, tags_obj) { - if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) { + cJSON* config_tag = NULL; + cJSON_ArrayForEach(config_tag, tags_obj) { + if (!cJSON_IsArray(config_tag) || cJSON_GetArraySize(config_tag) < 2) { continue; } - cJSON* tag_name = cJSON_GetArrayItem(tag, 0); - cJSON* tag_value = cJSON_GetArrayItem(tag, 1); + cJSON* tag_name = cJSON_GetArrayItem(config_tag, 0); + cJSON* tag_value = cJSON_GetArrayItem(config_tag, 1); if (!cJSON_IsString(tag_name) || !cJSON_IsString(tag_value)) { continue; @@ -2175,6 +2179,11 @@ int process_admin_config_event(cJSON* event, char* error_message, size_t error_s const char* key = cJSON_GetStringValue(tag_name); const char* value = cJSON_GetStringValue(tag_value); + // Skip query commands and system commands - only process config updates + if (strcmp(key, "config_query") == 0) { + continue; + } + // Skip relay identifier tag (only for legacy addressable events) if (kind == 33334 && strcmp(key, "d") == 0) { continue; @@ -2203,116 +2212,27 @@ int process_admin_config_event(cJSON* event, char* error_message, size_t error_s } // Handle Kind 23456 auth rules management and legacy Kind 33335 -int process_admin_auth_event(cJSON* event, char* error_message, size_t error_size) { +int process_admin_auth_event(cJSON* event, char* error_message, size_t error_size, struct lws* wsi) { cJSON* kind_obj = cJSON_GetObjectItem(event, "kind"); int kind = kind_obj ? (int)cJSON_GetNumberValue(kind_obj) : 0; - log_info("Processing admin auth rule event"); + log_info("Processing admin auth rule event through unified handler"); + printf(" Kind: %d\n", kind); - // Parse content for action commands - cJSON* content_obj = cJSON_GetObjectItem(event, "content"); - const char* content = content_obj ? cJSON_GetStringValue(content_obj) : ""; - - cJSON* content_json = cJSON_Parse(content); - char action_buffer[32] = "add"; // default action - const char* action = action_buffer; - - if (content_json) { - cJSON* action_obj = cJSON_GetObjectItem(content_json, "action"); - if (action_obj && cJSON_IsString(action_obj)) { - const char* action_str = cJSON_GetStringValue(action_obj); - if (action_str) { - strncpy(action_buffer, action_str, sizeof(action_buffer) - 1); - action_buffer[sizeof(action_buffer) - 1] = '\0'; - } - } - cJSON_Delete(content_json); + // Route all Kind 23456 events through the unified handler + if (kind == 23456) { + return handle_kind_23456_unified(event, error_message, error_size, wsi); } - printf(" Kind: %d, Action: %s\n", kind, action); - - // Handle query commands - if (strcmp(action, "list_all") == 0) { - return handle_auth_list_all_query(event, error_message, error_size); - } - if (strcmp(action, "whitelist_only") == 0) { - return handle_auth_whitelist_query(event, error_message, error_size); - } - if (strcmp(action, "blacklist_only") == 0) { - return handle_auth_blacklist_query(event, error_message, error_size); - } - if (strcmp(action, "pattern_check") == 0) { - return handle_auth_pattern_check_query(event, error_message, error_size); - } - if (strcmp(action, "clear_all_auth_rules") == 0) { - return handle_clear_all_auth_rules_command(event, error_message, error_size); - } - if (strcmp(action, "system_status") == 0) { - return handle_system_status_query(event, error_message, error_size); + // Legacy Kind 33335 events use the unified handler as well + if (kind == 33335) { + // For legacy events, we still use the unified handler but may need special processing + // The unified handler already supports all the functionality + return handle_kind_23456_unified(event, error_message, error_size, wsi); } - // Handle auth rule updates (add/remove actions) - cJSON* tags_obj = cJSON_GetObjectItem(event, "tags"); - if (!tags_obj || !cJSON_IsArray(tags_obj)) { - snprintf(error_message, error_size, "invalid: auth rule event must have tags"); - return -1; - } - - // Begin transaction for atomic auth rule updates - int rc = sqlite3_exec(g_db, "BEGIN IMMEDIATE TRANSACTION", NULL, NULL, NULL); - if (rc != SQLITE_OK) { - snprintf(error_message, error_size, "failed to begin auth rule transaction"); - return -1; - } - - int rules_processed = 0; - - // Process each tag as an auth rule specification - cJSON* tag = NULL; - cJSON_ArrayForEach(tag, tags_obj) { - if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 3) { - continue; - } - - cJSON* rule_type_obj = cJSON_GetArrayItem(tag, 0); - cJSON* pattern_type_obj = cJSON_GetArrayItem(tag, 1); - cJSON* pattern_value_obj = cJSON_GetArrayItem(tag, 2); - - if (!cJSON_IsString(rule_type_obj) || - !cJSON_IsString(pattern_type_obj) || - !cJSON_IsString(pattern_value_obj)) { - continue; - } - - const char* rule_type = cJSON_GetStringValue(rule_type_obj); - const char* pattern_type = cJSON_GetStringValue(pattern_type_obj); - const char* pattern_value = cJSON_GetStringValue(pattern_value_obj); - - // Process the auth rule based on action - if (strcmp(action, "add") == 0) { - if (add_auth_rule_from_config(rule_type, pattern_type, pattern_value, "allow") == 0) { - rules_processed++; - } - } else if (strcmp(action, "remove") == 0) { - if (remove_auth_rule_from_config(rule_type, pattern_type, pattern_value) == 0) { - rules_processed++; - } - } - } - - if (rules_processed > 0) { - sqlite3_exec(g_db, "COMMIT", NULL, NULL, NULL); - - char success_msg[256]; - snprintf(success_msg, sizeof(success_msg), "Processed %d auth rule updates", rules_processed); - log_success(success_msg); - } else { - sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL); - snprintf(error_message, error_size, "no valid auth rules found"); - return -1; - } - - return 0; + snprintf(error_message, error_size, "invalid: unsupported auth event kind %d", kind); + return -1; } // ================================ @@ -2372,423 +2292,480 @@ int remove_auth_rule_from_config(const char* rule_type, const char* pattern_type } // ================================ -// ADMIN API QUERY HANDLERS +// UNIFIED TAG PARSING UTILITIES // ================================ -// Handle configuration list keys query -int handle_config_list_keys_query(cJSON* event, char* error_message, size_t error_size) { - (void)event; // Suppress unused parameter warning +// Get the first tag name from an event +const char* get_first_tag_name(cJSON* event) { + if (!event) return NULL; - if (!g_db) { - snprintf(error_message, error_size, "database not available"); - return -1; + cJSON* tags_obj = cJSON_GetObjectItem(event, "tags"); + if (!tags_obj || !cJSON_IsArray(tags_obj)) return NULL; + + cJSON* first_tag = cJSON_GetArrayItem(tags_obj, 0); + if (!first_tag || !cJSON_IsArray(first_tag) || cJSON_GetArraySize(first_tag) < 1) { + return NULL; } - log_info("Processing config list keys query"); + cJSON* tag_name = cJSON_GetArrayItem(first_tag, 0); + if (!tag_name || !cJSON_IsString(tag_name)) return NULL; - const char* sql = "SELECT key, data_type, category FROM config ORDER BY category, key"; - sqlite3_stmt* stmt; + return cJSON_GetStringValue(tag_name); +} + +// Get tag value at specified index +const char* get_tag_value(cJSON* event, const char* tag_name, int value_index) { + if (!event || !tag_name) return NULL; - int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); - if (rc != SQLITE_OK) { - snprintf(error_message, error_size, "failed to prepare config keys query"); - return -1; - } + cJSON* tags_obj = cJSON_GetObjectItem(event, "tags"); + if (!tags_obj || !cJSON_IsArray(tags_obj)) return NULL; - printf("=== Configuration Keys ===\n"); - int key_count = 0; - - while (sqlite3_step(stmt) == SQLITE_ROW) { - const char* key = (const char*)sqlite3_column_text(stmt, 0); - const char* data_type = (const char*)sqlite3_column_text(stmt, 1); - const char* category = (const char*)sqlite3_column_text(stmt, 2); + cJSON* tag = NULL; + cJSON_ArrayForEach(tag, tags_obj) { + if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) <= value_index) { + continue; + } - printf(" [%s] %s (%s)\n", category ? category : "general", key ? key : "", data_type ? data_type : "string"); - key_count++; + cJSON* tag_key = cJSON_GetArrayItem(tag, 0); + if (!tag_key || !cJSON_IsString(tag_key)) continue; + + if (strcmp(cJSON_GetStringValue(tag_key), tag_name) == 0) { + cJSON* tag_value = cJSON_GetArrayItem(tag, value_index); + if (tag_value && cJSON_IsString(tag_value)) { + return cJSON_GetStringValue(tag_value); + } + } + } + return NULL; +} + +// Parse auth query parameters from event tags +int parse_auth_query_parameters(cJSON* event, char** query_type, char** pattern_value) { + if (!event || !query_type) return -1; + + *query_type = NULL; + if (pattern_value) *pattern_value = NULL; + + const char* query_val = get_tag_value(event, "auth_query", 1); + if (query_val) { + *query_type = strdup(query_val); + + // For pattern queries, get the pattern value from the same tag + if (strcmp(query_val, "pattern") == 0 && pattern_value) { + const char* pattern_val = get_tag_value(event, "auth_query", 2); + if (pattern_val) { + *pattern_value = strdup(pattern_val); + } + } + return 0; + } + return -1; +} + +// ================================ +// WEBSOCKET RESPONSE SYSTEM +// ================================ + +// Send WebSocket response data back to client +int send_websocket_response_data(cJSON* event, cJSON* response_data, struct lws* wsi) { + if (!event || !response_data || !wsi) return -1; + + log_info("Sending WebSocket response data to client"); + + // Serialize response data to JSON string + char* json_string = cJSON_Print(response_data); + if (!json_string) { + log_error("Failed to serialize response data to JSON"); + return -1; } - sqlite3_finalize(stmt); + printf("WebSocket Response Data: %s\n", json_string); - printf("Total configuration keys: %d\n", key_count); - log_success("Configuration keys listed successfully"); + // Calculate buffer size needed (LWS_PRE + JSON length) + size_t json_len = strlen(json_string); + size_t buf_size = 512 + json_len; // LWS_PRE is typically ~512 bytes + + // Allocate buffer with LWS_PRE space + unsigned char* buf = malloc(buf_size); + if (!buf) { + log_error("Failed to allocate WebSocket transmission buffer"); + free(json_string); + return -1; + } + + // Copy JSON data to buffer at LWS_PRE offset + memcpy(buf + 512, json_string, json_len); // Using 512 as LWS_PRE equivalent + + // Implement actual WebSocket transmission using LibWebSockets + int write_result = lws_write(wsi, buf + LWS_PRE, json_len, LWS_WRITE_TEXT); + + if (write_result < 0) { + log_error("Failed to write WebSocket response data"); + free(buf); + free(json_string); + return -1; + } else if ((size_t)write_result != json_len) { + log_warning("Partial WebSocket write - not all data transmitted"); + printf(" Expected: %zu bytes, Written: %d bytes\n", json_len, write_result); + } else { + log_success("WebSocket response data transmitted successfully"); + printf(" JSON length: %zu bytes\n", json_len); + printf(" Bytes written: %d\n", write_result); + } + + // Clean up + free(buf); + free(json_string); return 0; } -// Handle get current configuration query -int handle_config_get_current_query(cJSON* event, char* error_message, size_t error_size) { - (void)event; // Suppress unused parameter warning +// 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; - if (!g_db) { - snprintf(error_message, error_size, "database not available"); - return -1; - } + cJSON* response = cJSON_CreateObject(); + if (!response) return NULL; - log_info("Processing get current config query"); + cJSON_AddStringToObject(response, "query_type", query_type); + cJSON_AddNumberToObject(response, "total_results", total_count); + cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL)); + cJSON_AddItemToObject(response, "data", cJSON_Duplicate(results_array, 1)); - const char* sql = "SELECT key, value, data_type, category FROM config ORDER BY category, key"; - 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 current config query"); - return -1; - } - - printf("=== Current Configuration ===\n"); - int config_count = 0; - - 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); - - printf(" [%s] %s = %s (%s)\n", - category ? category : "general", - key ? key : "", - value ? value : "", - data_type ? data_type : "string"); - config_count++; - } - - sqlite3_finalize(stmt); - - printf("Total configuration items: %d\n", config_count); - log_success("Current configuration retrieved successfully"); - - return 0; + return response; } -// Handle auth rules list all query -int handle_auth_list_all_query(cJSON* event, char* error_message, size_t error_size) { - (void)event; // Suppress unused parameter warning +// ================================ +// UNIFIED KIND 23456 HANDLER +// ================================ + +// Single unified handler for all Kind 23456 requests +int handle_kind_23456_unified(cJSON* event, char* error_message, size_t error_size, struct lws* wsi) { + if (!event) { + snprintf(error_message, error_size, "invalid: null event"); + return -1; + } + log_info("Processing Kind 23456 event through unified handler"); + + // Parse first tag to determine action type + const char* action_type = get_first_tag_name(event); + if (!action_type) { + snprintf(error_message, error_size, "invalid: missing or invalid first tag"); + return -1; + } + + printf(" Action type: %s\n", action_type); + + // Route to appropriate handler based on action type + if (strcmp(action_type, "auth_query") == 0) { + const char* query_type = get_tag_value(event, "auth_query", 1); + if (!query_type) { + snprintf(error_message, error_size, "invalid: missing auth_query type"); + return -1; + } + return handle_auth_query_unified(event, query_type, error_message, error_size, wsi); + } + else if (strcmp(action_type, "system_command") == 0) { + const char* command = get_tag_value(event, "system_command", 1); + if (!command) { + snprintf(error_message, error_size, "invalid: missing system_command type"); + return -1; + } + return handle_system_command_unified(event, command, error_message, error_size, wsi); + } + else if (strcmp(action_type, "whitelist") == 0 || strcmp(action_type, "blacklist") == 0) { + // Handle auth rule modifications (existing logic from process_admin_auth_event) + return handle_auth_rule_modification_unified(event, error_message, error_size); + } + else { + snprintf(error_message, error_size, "invalid: unknown Kind 23456 action type '%s'", action_type); + return -1; + } +} + +// Unified auth query handler +int handle_auth_query_unified(cJSON* event, const char* query_type, char* error_message, size_t error_size, struct lws* wsi) { if (!g_db) { snprintf(error_message, error_size, "database not available"); return -1; } - log_info("Processing auth rules list all query"); + log_info("Processing unified auth query"); + printf(" Query type: %s\n", query_type); - const char* sql = "SELECT rule_type, pattern_type, pattern_value, action FROM auth_rules ORDER BY rule_type, pattern_type"; - sqlite3_stmt* stmt; + const char* sql = NULL; + int use_pattern_param = 0; + char* pattern_value = NULL; - int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); - if (rc != SQLITE_OK) { - snprintf(error_message, error_size, "failed to prepare auth rules query"); + // Build appropriate SQL query based on query type + if (strcmp(query_type, "all") == 0) { + sql = "SELECT rule_type, pattern_type, pattern_value, action FROM auth_rules ORDER BY rule_type, pattern_type"; + } + else if (strcmp(query_type, "whitelist") == 0) { + sql = "SELECT rule_type, pattern_type, pattern_value, action FROM auth_rules WHERE rule_type LIKE '%whitelist%' ORDER BY pattern_type"; + } + else if (strcmp(query_type, "blacklist") == 0) { + sql = "SELECT rule_type, pattern_type, pattern_value, action FROM auth_rules WHERE rule_type LIKE '%blacklist%' ORDER BY pattern_type"; + } + else if (strcmp(query_type, "pattern") == 0) { + // Get pattern value from tags + pattern_value = (char*)get_tag_value(event, "auth_query", 2); + if (!pattern_value) { + snprintf(error_message, error_size, "invalid: pattern query requires pattern value"); + return -1; + } + sql = "SELECT rule_type, pattern_type, pattern_value, action FROM auth_rules WHERE pattern_value = ? ORDER BY rule_type, pattern_type"; + use_pattern_param = 1; + } + else { + snprintf(error_message, error_size, "invalid: unknown auth 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 auth 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; } - printf("=== All Auth Rules ===\n"); int rule_count = 0; + printf("=== Auth Query Results (%s) ===\n", query_type); while (sqlite3_step(stmt) == SQLITE_ROW) { const char* rule_type = (const char*)sqlite3_column_text(stmt, 0); const char* pattern_type = (const char*)sqlite3_column_text(stmt, 1); - const char* pattern_value = (const char*)sqlite3_column_text(stmt, 2); + const char* pattern_value_result = (const char*)sqlite3_column_text(stmt, 2); const char* action = (const char*)sqlite3_column_text(stmt, 3); printf(" %s %s:%s -> %s\n", rule_type ? rule_type : "", pattern_type ? pattern_type : "", - pattern_value ? pattern_value : "", + pattern_value_result ? pattern_value_result : "", action ? action : "allow"); + + // Add rule to results array + cJSON* rule_obj = cJSON_CreateObject(); + cJSON_AddStringToObject(rule_obj, "rule_type", rule_type ? rule_type : ""); + cJSON_AddStringToObject(rule_obj, "pattern_type", pattern_type ? pattern_type : ""); + cJSON_AddStringToObject(rule_obj, "pattern_value", pattern_value_result ? pattern_value_result : ""); + cJSON_AddStringToObject(rule_obj, "action", action ? action : "allow"); + cJSON_AddItemToArray(results_array, rule_obj); + rule_count++; } sqlite3_finalize(stmt); - printf("Total auth rules: %d\n", rule_count); - log_success("Auth rules listed successfully"); + // Build and send response + cJSON* response = build_query_response(query_type, results_array, rule_count); + if (response) { + // Send response data via WebSocket + if (send_websocket_response_data(event, response, wsi) == 0) { + printf("Total results: %d\n", rule_count); + log_success("Auth query completed successfully with WebSocket response"); + cJSON_Delete(response); + cJSON_Delete(results_array); + return 0; + } + cJSON_Delete(response); + } - return 0; + cJSON_Delete(results_array); + snprintf(error_message, error_size, "failed to send auth query response"); + return -1; } -// Handle whitelist only query -int handle_auth_whitelist_query(cJSON* event, char* error_message, size_t error_size) { - (void)event; // Suppress unused parameter warning - +// Unified system command handler +int handle_system_command_unified(cJSON* event, const char* command, char* error_message, size_t error_size, struct lws* wsi) { if (!g_db) { snprintf(error_message, error_size, "database not available"); return -1; } - log_info("Processing whitelist rules query"); + log_info("Processing unified system command"); + printf(" Command: %s\n", command); - const char* sql = "SELECT rule_type, pattern_type, pattern_value, action FROM auth_rules WHERE rule_type LIKE '%whitelist%' ORDER BY pattern_type"; - 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 whitelist query"); + if (strcmp(command, "clear_all_auth_rules") == 0) { + // Count existing rules first + const char* count_sql = "SELECT COUNT(*) FROM auth_rules"; + sqlite3_stmt* count_stmt; + + int rc = sqlite3_prepare_v2(g_db, count_sql, -1, &count_stmt, NULL); + if (rc != SQLITE_OK) { + snprintf(error_message, error_size, "failed to prepare count query"); + return -1; + } + + int rule_count = 0; + if (sqlite3_step(count_stmt) == SQLITE_ROW) { + rule_count = sqlite3_column_int(count_stmt, 0); + } + sqlite3_finalize(count_stmt); + + // Delete all auth rules + const char* delete_sql = "DELETE FROM auth_rules"; + rc = sqlite3_exec(g_db, delete_sql, NULL, NULL, NULL); + + if (rc != SQLITE_OK) { + snprintf(error_message, error_size, "failed to execute clear auth rules command"); + return -1; + } + + // Build response + cJSON* response = cJSON_CreateObject(); + cJSON_AddStringToObject(response, "command", "clear_all_auth_rules"); + cJSON_AddNumberToObject(response, "rules_cleared", rule_count); + cJSON_AddStringToObject(response, "status", "success"); + cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL)); + + printf("Cleared %d auth rules from database\n", rule_count); + + // Send response via WebSocket + if (send_websocket_response_data(event, response, wsi) == 0) { + log_success("Clear auth rules command completed successfully"); + cJSON_Delete(response); + return 0; + } + + cJSON_Delete(response); + snprintf(error_message, error_size, "failed to send clear auth rules response"); return -1; } - - printf("=== Whitelist Rules ===\n"); - int whitelist_count = 0; - - while (sqlite3_step(stmt) == SQLITE_ROW) { - const char* rule_type = (const char*)sqlite3_column_text(stmt, 0); - const char* pattern_type = (const char*)sqlite3_column_text(stmt, 1); - const char* pattern_value = (const char*)sqlite3_column_text(stmt, 2); - const char* action = (const char*)sqlite3_column_text(stmt, 3); + else if (strcmp(command, "system_status") == 0) { + // Build system status response + cJSON* response = cJSON_CreateObject(); + cJSON_AddStringToObject(response, "command", "system_status"); + cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL)); - printf(" %s %s:%s -> %s\n", - rule_type ? rule_type : "", - pattern_type ? pattern_type : "", - pattern_value ? pattern_value : "", - action ? action : "allow"); - whitelist_count++; + cJSON* status_data = cJSON_CreateObject(); + cJSON_AddStringToObject(status_data, "database", g_db ? "connected" : "not_available"); + cJSON_AddStringToObject(status_data, "cache_status", g_unified_cache.cache_valid ? "valid" : "invalid"); + + if (strlen(g_database_path) > 0) { + cJSON_AddStringToObject(status_data, "database_path", g_database_path); + } + + // Count configuration items and auth rules + if (g_db) { + sqlite3_stmt* stmt; + + // Config count + if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM config", -1, &stmt, NULL) == SQLITE_OK) { + if (sqlite3_step(stmt) == SQLITE_ROW) { + cJSON_AddNumberToObject(status_data, "config_items", sqlite3_column_int(stmt, 0)); + } + sqlite3_finalize(stmt); + } + + // Auth rules count + if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM auth_rules", -1, &stmt, NULL) == SQLITE_OK) { + if (sqlite3_step(stmt) == SQLITE_ROW) { + cJSON_AddNumberToObject(status_data, "auth_rules", sqlite3_column_int(stmt, 0)); + } + sqlite3_finalize(stmt); + } + } + + cJSON_AddItemToObject(response, "data", status_data); + + printf("=== System Status ===\n"); + printf("Database: %s\n", g_db ? "Connected" : "Not available"); + printf("Cache status: %s\n", g_unified_cache.cache_valid ? "Valid" : "Invalid"); + + // Send response via WebSocket + if (send_websocket_response_data(event, response, wsi) == 0) { + log_success("System status query completed successfully"); + cJSON_Delete(response); + return 0; + } + + cJSON_Delete(response); + snprintf(error_message, error_size, "failed to send system status response"); + return -1; + } + else { + snprintf(error_message, error_size, "invalid: unknown system command '%s'", command); + return -1; } - - sqlite3_finalize(stmt); - - printf("Total whitelist rules: %d\n", whitelist_count); - log_success("Whitelist rules listed successfully"); - - return 0; } -// Handle blacklist only query -int handle_auth_blacklist_query(cJSON* event, char* error_message, size_t error_size) { - (void)event; // Suppress unused parameter warning - - if (!g_db) { - snprintf(error_message, error_size, "database not available"); - return -1; - } - - log_info("Processing blacklist rules query"); - - const char* sql = "SELECT rule_type, pattern_type, pattern_value, action FROM auth_rules WHERE rule_type LIKE '%blacklist%' ORDER BY pattern_type"; - 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 blacklist query"); - return -1; - } - - printf("=== Blacklist Rules ===\n"); - int blacklist_count = 0; - - while (sqlite3_step(stmt) == SQLITE_ROW) { - const char* rule_type = (const char*)sqlite3_column_text(stmt, 0); - const char* pattern_type = (const char*)sqlite3_column_text(stmt, 1); - const char* pattern_value = (const char*)sqlite3_column_text(stmt, 2); - const char* action = (const char*)sqlite3_column_text(stmt, 3); - - printf(" %s %s:%s -> %s\n", - rule_type ? rule_type : "", - pattern_type ? pattern_type : "", - pattern_value ? pattern_value : "", - action ? action : "deny"); - blacklist_count++; - } - - sqlite3_finalize(stmt); - - printf("Total blacklist rules: %d\n", blacklist_count); - log_success("Blacklist rules listed successfully"); - - return 0; -} - -// Handle pattern check query -int handle_auth_pattern_check_query(cJSON* event, char* error_message, size_t error_size) { - // Parse tags to get the pattern to check +// Handle auth rule modifications (extracted from process_admin_auth_event) +int handle_auth_rule_modification_unified(cJSON* event, char* error_message, size_t error_size) { cJSON* tags_obj = cJSON_GetObjectItem(event, "tags"); if (!tags_obj || !cJSON_IsArray(tags_obj)) { - snprintf(error_message, error_size, "invalid: pattern check requires tags with pattern information"); + snprintf(error_message, error_size, "invalid: auth rule event must have tags"); return -1; } - const char* check_pattern_type = NULL; - const char* check_pattern_value = NULL; + // Begin transaction for atomic auth rule updates + int rc = sqlite3_exec(g_db, "BEGIN IMMEDIATE TRANSACTION", NULL, NULL, NULL); + if (rc != SQLITE_OK) { + snprintf(error_message, error_size, "failed to begin auth rule transaction"); + return -1; + } - // Find pattern_check tag - cJSON* tag = NULL; - cJSON_ArrayForEach(tag, tags_obj) { - if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 3) { - cJSON* tag_name = cJSON_GetArrayItem(tag, 0); - if (tag_name && cJSON_IsString(tag_name) && - strcmp(cJSON_GetStringValue(tag_name), "pattern_check") == 0) { - - cJSON* pattern_type_obj = cJSON_GetArrayItem(tag, 1); - cJSON* pattern_value_obj = cJSON_GetArrayItem(tag, 2); - - if (pattern_type_obj && cJSON_IsString(pattern_type_obj) && - pattern_value_obj && cJSON_IsString(pattern_value_obj)) { - check_pattern_type = cJSON_GetStringValue(pattern_type_obj); - check_pattern_value = cJSON_GetStringValue(pattern_value_obj); - break; - } + int rules_processed = 0; + + // Process each tag as an auth rule specification + cJSON* auth_tag = NULL; + cJSON_ArrayForEach(auth_tag, tags_obj) { + if (!cJSON_IsArray(auth_tag) || cJSON_GetArraySize(auth_tag) < 3) { + continue; + } + + cJSON* rule_type_obj = cJSON_GetArrayItem(auth_tag, 0); + cJSON* pattern_type_obj = cJSON_GetArrayItem(auth_tag, 1); + cJSON* pattern_value_obj = cJSON_GetArrayItem(auth_tag, 2); + + if (!cJSON_IsString(rule_type_obj) || + !cJSON_IsString(pattern_type_obj) || + !cJSON_IsString(pattern_value_obj)) { + continue; + } + + const char* rule_type = cJSON_GetStringValue(rule_type_obj); + const char* pattern_type = cJSON_GetStringValue(pattern_type_obj); + const char* pattern_value = cJSON_GetStringValue(pattern_value_obj); + + // Process auth rule: ["blacklist"|"whitelist", "pubkey"|"hash", "value"] + if (strcmp(rule_type, "blacklist") == 0 || strcmp(rule_type, "whitelist") == 0) { + if (add_auth_rule_from_config(rule_type, pattern_type, pattern_value, "allow") == 0) { + rules_processed++; } } } - if (!check_pattern_type || !check_pattern_value) { - snprintf(error_message, error_size, "invalid: pattern_check tag format should be [\"pattern_check\", \"pattern_type\", \"pattern_value\"]"); - return -1; - } - - if (!g_db) { - snprintf(error_message, error_size, "database not available"); - return -1; - } - - log_info("Processing pattern check query"); - printf(" Checking pattern: %s:%s\n", check_pattern_type, check_pattern_value); - - const char* sql = "SELECT rule_type, pattern_type, pattern_value, action FROM auth_rules WHERE pattern_type = ? AND pattern_value = ? ORDER BY rule_type"; - 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 pattern check query"); - return -1; - } - - sqlite3_bind_text(stmt, 1, check_pattern_type, -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 2, check_pattern_value, -1, SQLITE_STATIC); - - printf("=== Pattern Check Results ===\n"); - printf("Pattern: %s:%s\n", check_pattern_type, check_pattern_value); - int match_count = 0; - - while (sqlite3_step(stmt) == SQLITE_ROW) { - const char* rule_type = (const char*)sqlite3_column_text(stmt, 0); - const char* pattern_type = (const char*)sqlite3_column_text(stmt, 1); - const char* pattern_value = (const char*)sqlite3_column_text(stmt, 2); - const char* action = (const char*)sqlite3_column_text(stmt, 3); + if (rules_processed > 0) { + sqlite3_exec(g_db, "COMMIT", NULL, NULL, NULL); - printf(" MATCH: %s %s:%s -> %s\n", - rule_type ? rule_type : "", - pattern_type ? pattern_type : "", - pattern_value ? pattern_value : "", - action ? action : "allow"); - match_count++; - } - - sqlite3_finalize(stmt); - - if (match_count == 0) { - printf(" No matching rules found for this pattern\n"); - } - - printf("Total matches: %d\n", match_count); - log_success("Pattern check completed successfully"); - - return 0; -} - -// Handle clear all auth rules command -int handle_clear_all_auth_rules_command(cJSON* event, char* error_message, size_t error_size) { - (void)event; // Suppress unused parameter warning - - if (!g_db) { - snprintf(error_message, error_size, "database not available"); - return -1; - } - - log_info("Processing clear all auth rules command"); - - // Count existing rules first - const char* count_sql = "SELECT COUNT(*) FROM auth_rules"; - sqlite3_stmt* count_stmt; - - int rc = sqlite3_prepare_v2(g_db, count_sql, -1, &count_stmt, NULL); - if (rc != SQLITE_OK) { - snprintf(error_message, error_size, "failed to prepare count query"); - return -1; - } - - int rule_count = 0; - if (sqlite3_step(count_stmt) == SQLITE_ROW) { - rule_count = sqlite3_column_int(count_stmt, 0); - } - sqlite3_finalize(count_stmt); - - // Delete all auth rules (this operation succeeds even if table is empty) - const char* delete_sql = "DELETE FROM auth_rules"; - rc = sqlite3_exec(g_db, delete_sql, NULL, NULL, NULL); - - if (rc != SQLITE_OK) { - snprintf(error_message, error_size, "failed to execute clear auth rules command"); - return -1; - } - - // Always return success - clearing empty table is still a successful operation - if (rule_count > 0) { - printf("Cleared %d auth rules from database\n", rule_count); - log_success("All auth rules cleared successfully"); + char success_msg[256]; + snprintf(success_msg, sizeof(success_msg), "Processed %d auth rule updates", rules_processed); + log_success(success_msg); + + return 0; } else { - printf("Auth rules table was already empty - no rules to clear\n"); - log_success("Clear auth rules completed successfully (table was already empty)"); + sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL); + snprintf(error_message, error_size, "no valid auth rules found"); + return -1; } - - return 0; } -// Handle system status query -int handle_system_status_query(cJSON* event, char* error_message, size_t error_size) { - (void)event; // Suppress unused parameter warning - (void)error_message; // This command always succeeds - (void)error_size; - - log_info("Processing system status query"); - - printf("=== System Status ===\n"); - - // Database status - printf("Database: %s\n", g_db ? "Connected" : "Not available"); - if (strlen(g_database_path) > 0) { - printf("Database path: %s\n", g_database_path); - } - - // Configuration status - printf("Unified cache: %s\n", g_unified_cache.cache_valid ? "Valid" : "Invalid"); - printf("Admin pubkey: %s\n", g_unified_cache.admin_pubkey[0] ? g_unified_cache.admin_pubkey : "Not set"); - printf("Relay pubkey: %s\n", g_unified_cache.relay_pubkey[0] ? g_unified_cache.relay_pubkey : "Not set"); - - // Count configuration items - if (g_db) { - const char* config_count_sql = "SELECT COUNT(*) FROM config"; - sqlite3_stmt* stmt; - - if (sqlite3_prepare_v2(g_db, config_count_sql, -1, &stmt, NULL) == SQLITE_OK) { - if (sqlite3_step(stmt) == SQLITE_ROW) { - int config_count = sqlite3_column_int(stmt, 0); - printf("Configuration items: %d\n", config_count); - } - sqlite3_finalize(stmt); - } - - // Count auth rules - const char* auth_count_sql = "SELECT COUNT(*) FROM auth_rules"; - if (sqlite3_prepare_v2(g_db, auth_count_sql, -1, &stmt, NULL) == SQLITE_OK) { - if (sqlite3_step(stmt) == SQLITE_ROW) { - int auth_count = sqlite3_column_int(stmt, 0); - printf("Auth rules: %d\n", auth_count); - } - sqlite3_finalize(stmt); - } - } - - // Cache expiration - if (g_unified_cache.cache_expires > 0) { - time_t now = time(NULL); - long seconds_to_expire = g_unified_cache.cache_expires - now; - printf("Cache expires in: %ld seconds\n", seconds_to_expire); - } - - printf("System time: %ld\n", (long)time(NULL)); - - log_success("System status retrieved successfully"); - - return 0; -} + // ================================ // CONFIGURATION CACHE MANAGEMENT diff --git a/src/config.h b/src/config.h index 56648e7..a1625c6 100644 --- a/src/config.h +++ b/src/config.h @@ -6,6 +6,9 @@ #include #include +// Forward declaration for WebSocket support +struct lws; + // Configuration constants #define CONFIG_VALUE_MAX_LENGTH 1024 #define RELAY_NAME_MAX_LENGTH 256 @@ -160,10 +163,20 @@ int update_config_in_table(const char* key, const char* value); int populate_default_config_values(void); int add_pubkeys_to_config_table(void); -// Admin event processing functions -int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size); +// Admin event processing functions (updated with WebSocket support) +int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size, struct lws* wsi); int process_admin_config_event(cJSON* event, char* error_message, size_t error_size); -int process_admin_auth_event(cJSON* event, char* error_message, size_t error_size); +int process_admin_auth_event(cJSON* event, char* error_message, size_t error_size, struct lws* wsi); + +// Unified Kind 23456 handler functions +int handle_kind_23456_unified(cJSON* event, char* error_message, size_t error_size, struct lws* wsi); +int handle_auth_query_unified(cJSON* event, const char* query_type, char* error_message, size_t error_size, struct lws* wsi); +int handle_system_command_unified(cJSON* event, const char* command, char* error_message, size_t error_size, struct lws* wsi); +int handle_auth_rule_modification_unified(cJSON* event, char* error_message, size_t error_size); + +// WebSocket response functions +int send_websocket_response_data(cJSON* event, cJSON* response_data, struct lws* wsi); +cJSON* build_query_response(const char* query_type, cJSON* results_array, int total_count); // Auth rules management functions int add_auth_rule_from_config(const char* rule_type, const char* pattern_type, diff --git a/src/main.c b/src/main.c index 611437f..44cc50c 100644 --- a/src/main.c +++ b/src/main.c @@ -228,7 +228,10 @@ int nostr_validate_unified_request(const char* json_string, size_t json_length); 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); +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 +int is_authorized_admin_event(cJSON* event, char* error_message, size_t error_size); // Forward declaration for NOTICE message support void send_notice_message(struct lws* wsi, const char* message); @@ -3009,6 +3012,109 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru return events_sent; } +///////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////// +// ADMIN EVENT AUTHORIZATION +///////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////// + +// Enhanced admin event authorization function +int is_authorized_admin_event(cJSON* event, char* error_buffer, size_t error_buffer_size) { + if (!event || !error_buffer) { + if (error_buffer && error_buffer_size > 0) { + snprintf(error_buffer, error_buffer_size, "Invalid parameters for admin authorization"); + } + return -1; + } + + // Step 1: Verify event kind is admin type + cJSON *kind_json = cJSON_GetObjectItem(event, "kind"); + if (!kind_json || !cJSON_IsNumber(kind_json)) { + snprintf(error_buffer, error_buffer_size, "Missing or invalid event kind"); + return -1; + } + + int event_kind = kind_json->valueint; + if (event_kind != 33334 && event_kind != 33335 && event_kind != 23455 && event_kind != 23456) { + snprintf(error_buffer, error_buffer_size, "Event kind %d is not an admin event type", event_kind); + return -1; + } + + // Step 2: Check if event targets this relay (look for 'p' tag with our relay pubkey) + cJSON *tags = cJSON_GetObjectItem(event, "tags"); + if (!tags || !cJSON_IsArray(tags)) { + // No tags array - treat as regular event for different relay + log_info("Admin event has no tags array - treating as event for different relay"); + snprintf(error_buffer, error_buffer_size, "Admin event not targeting this relay (no tags)"); + return -1; + } + + int targets_this_relay = 0; + cJSON *tag; + cJSON_ArrayForEach(tag, tags) { + if (cJSON_IsArray(tag)) { + cJSON *tag_name = cJSON_GetArrayItem(tag, 0); + cJSON *tag_value = cJSON_GetArrayItem(tag, 1); + + if (tag_name && cJSON_IsString(tag_name) && + tag_value && cJSON_IsString(tag_value) && + strcmp(tag_name->valuestring, "p") == 0) { + + // Compare with our relay pubkey + const char* relay_pubkey = get_config_value("relay_pubkey"); + if (relay_pubkey && strcmp(tag_value->valuestring, relay_pubkey) == 0) { + targets_this_relay = 1; + break; + } + } + } + } + + if (!targets_this_relay) { + // Admin event for different relay - not an error, just not for us + log_info("Admin event targets different relay - treating as regular event"); + snprintf(error_buffer, error_buffer_size, "Admin event not targeting this relay"); + return -1; + } + + // Step 3: Verify admin signature authorization + cJSON *pubkey_json = cJSON_GetObjectItem(event, "pubkey"); + if (!pubkey_json || !cJSON_IsString(pubkey_json)) { + log_warning("Unauthorized admin event attempt: missing or invalid pubkey"); + snprintf(error_buffer, error_buffer_size, "Unauthorized admin event attempt: missing pubkey"); + return -1; + } + + // Get admin pubkey from configuration + const char* admin_pubkey = get_config_value("admin_pubkey"); + if (!admin_pubkey || strlen(admin_pubkey) == 0) { + log_warning("Unauthorized admin event attempt: no admin pubkey configured"); + snprintf(error_buffer, error_buffer_size, "Unauthorized admin event attempt: no admin configured"); + return -1; + } + + // Compare pubkeys + if (strcmp(pubkey_json->valuestring, admin_pubkey) != 0) { + log_warning("Unauthorized admin event attempt: pubkey mismatch"); + char warning_msg[256]; + snprintf(warning_msg, sizeof(warning_msg), + "Unauthorized admin event attempt from pubkey: %.32s...", pubkey_json->valuestring); + log_warning(warning_msg); + snprintf(error_buffer, error_buffer_size, "Unauthorized admin event attempt: invalid admin pubkey"); + return -1; + } + + // Step 4: Verify event signature + if (nostr_verify_event_signature(event) != 0) { + log_warning("Unauthorized admin event attempt: invalid signature"); + snprintf(error_buffer, error_buffer_size, "Unauthorized admin event attempt: signature verification failed"); + return -1; + } + + // All checks passed - authorized admin event + log_info("Admin event authorization successful"); + return 0; +} @@ -3266,47 +3372,68 @@ 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) { - // This is an admin event - process it through the admin API instead of normal storage - log_info("DEBUG ADMIN: Admin event detected, processing through admin API"); + // Enhanced admin event security - check authorization first + log_info("DEBUG ADMIN: Admin event detected, checking authorization"); - char admin_error[512] = {0}; - int admin_result = process_admin_event_in_config(event, admin_error, sizeof(admin_error)); + char auth_error[512] = {0}; + int auth_result = is_authorized_admin_event(event, auth_error, sizeof(auth_error)); - char debug_admin_msg[256]; - snprintf(debug_admin_msg, sizeof(debug_admin_msg), - "DEBUG ADMIN: process_admin_event_in_config returned %d", admin_result); - log_info(debug_admin_msg); - - // Log results for Kind 23455 and 23456 events - if (event_kind == 23455 || event_kind == 23456) { - if (admin_result == 0) { - char success_result_msg[256]; - snprintf(success_result_msg, sizeof(success_result_msg), - "SUCCESS: Kind %d event processed successfully", event_kind); - log_success(success_result_msg); - } else { - char error_result_msg[512]; - snprintf(error_result_msg, sizeof(error_result_msg), - "ERROR: Kind %d event processing failed: %s", event_kind, admin_error); - log_error(error_result_msg); - } - } - - if (admin_result != 0) { - log_error("DEBUG ADMIN: Failed to process admin event through admin API"); + if (auth_result != 0) { + // Authorization failed - log and reject + log_warning("DEBUG ADMIN: Admin event authorization failed"); result = -1; - size_t error_len = strlen(admin_error); + size_t error_len = strlen(auth_error); size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1; - memcpy(error_message, admin_error, copy_len); + memcpy(error_message, auth_error, copy_len); error_message[copy_len] = '\0'; - char debug_admin_error_msg[600]; - snprintf(debug_admin_error_msg, sizeof(debug_admin_error_msg), - "DEBUG ADMIN ERROR: %.400s", admin_error); - log_error(debug_admin_error_msg); + char debug_auth_error_msg[600]; + snprintf(debug_auth_error_msg, sizeof(debug_auth_error_msg), + "DEBUG ADMIN AUTH ERROR: %.400s", auth_error); + log_warning(debug_auth_error_msg); } else { - log_success("DEBUG ADMIN: Admin event processed successfully through admin API"); - // Admin events are processed by the admin API, not broadcast to subscriptions + // Authorization successful - process through admin API + log_info("DEBUG ADMIN: Admin event authorized, processing through admin API"); + + char admin_error[512] = {0}; + int admin_result = process_admin_event_in_config(event, admin_error, sizeof(admin_error), wsi); + + char debug_admin_msg[256]; + snprintf(debug_admin_msg, sizeof(debug_admin_msg), + "DEBUG ADMIN: process_admin_event_in_config returned %d", admin_result); + log_info(debug_admin_msg); + + // Log results for Kind 23455 and 23456 events + if (event_kind == 23455 || event_kind == 23456) { + if (admin_result == 0) { + char success_result_msg[256]; + snprintf(success_result_msg, sizeof(success_result_msg), + "SUCCESS: Kind %d event processed successfully", event_kind); + log_success(success_result_msg); + } else { + char error_result_msg[512]; + snprintf(error_result_msg, sizeof(error_result_msg), + "ERROR: Kind %d event processing failed: %s", event_kind, admin_error); + log_error(error_result_msg); + } + } + + if (admin_result != 0) { + log_error("DEBUG ADMIN: Failed to process admin event through admin API"); + result = -1; + size_t error_len = strlen(admin_error); + size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1; + memcpy(error_message, admin_error, copy_len); + error_message[copy_len] = '\0'; + + char debug_admin_error_msg[600]; + snprintf(debug_admin_error_msg, sizeof(debug_admin_error_msg), + "DEBUG ADMIN ERROR: %.400s", admin_error); + log_error(debug_admin_error_msg); + } else { + log_success("DEBUG ADMIN: Admin event processed successfully through admin API"); + // Admin events are processed by the admin API, not broadcast to subscriptions + } } } else { // Regular event - store in database and broadcast diff --git a/tests/white_black_list_test.sh b/tests/white_black_list_test.sh index ee7b5bd..843979a 100755 --- a/tests/white_black_list_test.sh +++ b/tests/white_black_list_test.sh @@ -34,6 +34,13 @@ RELAY_URL="ws://${RELAY_HOST}:${RELAY_PORT}" TIMEOUT=5 TEMP_DIR="/tmp/c_relay_test_$$" +# WebSocket connection state +WS_PID="" +WS_INPUT_FIFO="" +WS_OUTPUT_FIFO="" +WS_CONNECTED=0 +WS_RESPONSE_LOG="" + # Color codes for output RED='\033[0;31m' GREEN='\033[0;32m' @@ -147,6 +154,145 @@ send_websocket_message() { echo "$response" } +# ======================================================================= +# PERSISTENT WEBSOCKET CONNECTION MANAGEMENT +# ======================================================================= + +# Open persistent WebSocket connection +open_websocket_connection() { + log_info "Opening persistent WebSocket connection to $RELAY_URL..." + + # Create unique named pipes for this test session + WS_INPUT_FIFO="${TEMP_DIR}/ws_input_$$" + WS_OUTPUT_FIFO="${TEMP_DIR}/ws_output_$$" + WS_RESPONSE_LOG="${TEMP_DIR}/ws_responses_$$" + + # Create named pipes + mkfifo "$WS_INPUT_FIFO" "$WS_OUTPUT_FIFO" + + # Start websocat in background with bidirectional pipes + # Input: we write to WS_INPUT_FIFO, websocat reads and sends to relay + # Output: websocat receives from relay and writes to WS_OUTPUT_FIFO + websocat "$RELAY_URL" < "$WS_INPUT_FIFO" > "$WS_OUTPUT_FIFO" & + WS_PID=$! + + # Start background response logger + tail -f "$WS_OUTPUT_FIFO" >> "$WS_RESPONSE_LOG" & + local logger_pid=$! + + # Keep input pipe open by redirecting from /dev/null in background + exec {ws_fd}> "$WS_INPUT_FIFO" + + # Test connection with a simple REQ message + sleep 1 + echo '["REQ","test_conn",{}]' >&${ws_fd} + + # Wait for response to confirm connection + local connection_timeout=5 + local start_time=$(date +%s) + + while [ $(($(date +%s) - start_time)) -lt $connection_timeout ]; do + if [ -s "$WS_RESPONSE_LOG" ]; then + WS_CONNECTED=1 + log_success "Persistent WebSocket connection established" + log_info "WebSocket PID: $WS_PID" + return 0 + fi + sleep 0.1 + done + + # Connection failed + log_error "Failed to establish persistent WebSocket connection" + close_websocket_connection + return 1 +} + +# Close persistent WebSocket connection +close_websocket_connection() { + log_info "Closing persistent WebSocket connection..." + + if [ -n "$WS_PID" ] && kill -0 "$WS_PID" 2>/dev/null; then + # Close input pipe first + if [ -n "${ws_fd}" ]; then + exec {ws_fd}>&- + fi + + # Send close frame and terminate websocat + kill "$WS_PID" 2>/dev/null + 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 + fi + + # Clear previous responses + > "$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 +wait_for_query_response() { + local timeout_seconds="${1:-10}" + local start_time=$(date +%s) + + log_info "Waiting for query response data..." + + # Clear any OK responses and wait for JSON data + sleep 0.5 # Brief delay to ensure OK response is processed first + + while [ $(($(date +%s) - start_time)) -lt $timeout_seconds ]; do + # Look for JSON response with query data (not just OK responses) + if grep -q '"query_type"' "$WS_RESPONSE_LOG" 2>/dev/null; then + local response=$(grep '"query_type"' "$WS_RESPONSE_LOG" | tail -1) + echo "$response" + return 0 + fi + sleep 0.1 + done + + log_error "Timeout waiting for query response data" + return 1 +} + # Create and send auth rule event send_auth_rule_event() { local action="$1" # "add" or "remove" @@ -157,12 +303,13 @@ send_auth_rule_event() { log_info "Creating auth rule event: $action $rule_type $pattern_type ${pattern_value:0:16}..." - # Create the auth rule event using nak with correct tag format - # Server expects proper key=value tags for auth rules - # Using Kind 23456 (ephemeral auth rules management) - no d tag needed + # Create the auth rule event using nak with correct tag format for the actual implementation + # Server expects tags like ["whitelist", "pubkey", "abc123..."] or ["blacklist", "pubkey", "def456..."] + # Using Kind 23456 (ephemeral auth rules management) with proper relay targeting local event_json - event_json=$(nak event -k 23456 --content "{\"action\":\"$action\",\"description\":\"$description\"}" \ - -t "$rule_type=$pattern_type" -t "pattern_value=$pattern_value" \ + event_json=$(nak event -k 23456 --content "" \ + -t "p=$RELAY_PUBKEY" \ + -t "$rule_type=$pattern_type=$pattern_value" \ --sec "$ADMIN_PRIVKEY" 2>/dev/null) if [ $? -ne 0 ] || [ -z "$event_json" ]; then @@ -170,21 +317,38 @@ send_auth_rule_event() { return 1 fi - # Send the event using nak directly to relay (more reliable than websocat) + # Send the event through persistent WebSocket connection log_info "Publishing auth rule event to relay..." local result - result=$(echo "$event_json" | timeout 10s nak event "$RELAY_URL" 2>&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 + if [ "$WS_CONNECTED" = "1" ]; then + result=$(send_websocket_event "$event_json") + 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 '"OK".*true'; then + log_success "Auth rule $action successful" + return 0 + else + log_error "Auth rule $action failed: $result (exit code: $exit_code)" + return 1 + fi else - log_error "Auth rule $action failed: $result (exit code: $exit_code)" - return 1 + # Fallback to one-shot connection if persistent connection not available + result=$(echo "$event_json" | timeout 10s nak event "$RELAY_URL" 2>&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 } @@ -193,9 +357,10 @@ clear_all_auth_rules() { log_info "Clearing all existing auth rules..." # Create system command event to clear all auth rules - # Using Kind 23456 (ephemeral auth rules management) + # Using Kind 23456 (ephemeral auth rules management) with proper relay targeting local event_json - event_json=$(nak event -k 23456 --content "{\"action\":\"clear_all\"}" \ + event_json=$(nak event -k 23456 --content "" \ + -t "p=$RELAY_PUBKEY" \ -t "system_command=clear_all_auth_rules" \ --sec "$ADMIN_PRIVKEY" 2>/dev/null) @@ -204,21 +369,38 @@ clear_all_auth_rules() { return 1 fi - # Send the event using nak directly to relay + # Send the event through persistent WebSocket connection log_info "Sending clear all auth rules command..." local result - result=$(echo "$event_json" | timeout 10s nak event "$RELAY_URL" 2>&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 + if [ "$WS_CONNECTED" = "1" ]; then + result=$(send_websocket_event "$event_json") + local exit_code=$? + + log_info "Clear auth rules result: $result" + + # Check if response indicates success + if [ $exit_code -eq 0 ] && echo "$result" | grep -q -i '"OK".*true'; then + log_success "All auth rules cleared successfully" + return 0 + else + log_error "Failed to clear auth rules: $result (exit code: $exit_code)" + return 1 + fi else - log_error "Failed to clear auth rules: $result (exit code: $exit_code)" - return 1 + # Fallback to one-shot connection if persistent connection not available + result=$(echo "$event_json" | timeout 10s nak event "$RELAY_URL" 2>&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 } @@ -324,11 +506,11 @@ test_admin_authentication() { log "Test $TESTS_RUN: Admin Authentication" # Create a simple configuration event to test admin authentication - # Using Kind 23455 (ephemeral configuration management) - no d tag needed - local content="{\"action\":\"set\",\"description\":\"Testing admin authentication\"}" + # Using Kind 23456 (admin commands) with proper relay targeting local config_event - config_event=$(nak event -k 23455 --content "$content" \ - -t "test_auth=true" \ + config_event=$(nak event -k 23456 --content "" \ + -t "p=$RELAY_PUBKEY" \ + -t "system_command=system_status" \ --sec "$ADMIN_PRIVKEY" 2>/dev/null) if [ $? -ne 0 ]; then @@ -367,8 +549,9 @@ test_auth_rules_storage_query() { # Query all auth rules using admin query log_info "Querying all auth rules..." local query_event - query_event=$(nak event -k 23456 --content "{\"action\":\"list_all\"}" \ - -t "auth_query=list_all" \ + query_event=$(nak event -k 23456 --content "" \ + -t "p=$RELAY_PUBKEY" \ + -t "auth_query=all" \ --sec "$ADMIN_PRIVKEY" 2>/dev/null) if [ $? -ne 0 ] || [ -z "$query_event" ]; then @@ -697,11 +880,11 @@ run_all_tests() { # Setup setup_test_environment - # Clear all auth rules before starting tests + clear_all_auth_rules - # test_admin_authentication - # test_auth_rules_storage_query + test_admin_authentication + test_auth_rules_storage_query # test_basic_whitelist # test_basic_blacklist # test_rule_removal diff --git a/whitelist_blacklist_test.log b/whitelist_blacklist_test.log deleted file mode 100644 index 0d11259..0000000 --- a/whitelist_blacklist_test.log +++ /dev/null @@ -1,27 +0,0 @@ -=== C-Relay Whitelist/Blacklist Test Started at Thu Sep 25 07:28:40 AM EDT 2025 === -[INFO] Checking dependencies... -[SUCCESS] Dependencies check complete -[INFO] Generated keypair for TEST1: pubkey=36e6521000b2ddda... -[INFO] Generated keypair for TEST2: pubkey=9cdd32f27fffeea8... -[INFO] Generated keypair for TEST3: pubkey=e05928b64d3ad54a... -[SUCCESS] Test environment setup complete -[07:28:42] Test 1: Admin Authentication -[INFO] === DEBUG: Full admin event being sent === -[INFO] === END DEBUG EVENT === -[INFO] === DEBUG: Full WebSocket message === -[INFO] === END DEBUG MESSAGE === -[INFO] Sending WebSocket message (full): -[INFO] ["EVENT",{"kind":33334,"id":"ba07a9d01ef3bf8c424eb5ecd8a162980e5d596f3c8520ea59c4cf80961a347a","pubkey":"6a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3","created_at":1758799722,"tags":[["d","4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"],["test_auth","true"]],"content":"Testing admin authentication","sig":"b8f40558060f402da232f3cc2ff72ea98257a3e3d74bc5c4bebd6fbd24d8d258138f879795b6089c01c4afa4c64088306dc917fdcad7b054d3c12513581b9228"}] -[INFO] WebSocket response (full): -[INFO] -[INFO] === DEBUG: Full server response === -[INFO] === END DEBUG RESPONSE === -[ERROR] Test 1: FAILED - Admin authentication failed: [INFO] Sending WebSocket message (full): -[INFO] ["EVENT",{"kind":33334,"id":"ba07a9d01ef3bf8c424eb5ecd8a162980e5d596f3c8520ea59c4cf80961a347a","pubkey":"6a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3","created_at":1758799722,"tags":[["d","4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"],["test_auth","true"]],"content":"Testing admin authentication","sig":"b8f40558060f402da232f3cc2ff72ea98257a3e3d74bc5c4bebd6fbd24d8d258138f879795b6089c01c4afa4c64088306dc917fdcad7b054d3c12513581b9228"}] -[INFO] WebSocket response (full): -[INFO] -[ERROR] 1 out of 1 tests failed. -[ERROR] Some tests failed. Check the log for details. -[07:28:42] Cleaning up test environment... -[INFO] Temporary directory removed: /tmp/c_relay_test_184904 -[07:28:42] Test cleanup completed.