From 036b0823b9092a7038f5e5ecdc3180b389f18508 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 25 Sep 2025 11:25:50 -0400 Subject: [PATCH] v0.3.11 - Working on admin api --- README.md | 325 ++++++++++++++++++ api/index.html | 38 +-- docs/admin_api_plan.md | 537 ++++++++++++++++++++++++++++++ relay.pid | 2 +- src/config.c | 586 +++++++++++++++++++++++++++++---- src/main.c | 41 ++- src/request_validator.c | 50 ++- tests/white_black_list_test.sh | 231 +++++++++---- whitelist_blacklist_test.log | 26 +- 9 files changed, 1635 insertions(+), 201 deletions(-) create mode 100644 docs/admin_api_plan.md diff --git a/README.md b/README.md index d519b12..6250b4f 100644 --- a/README.md +++ b/README.md @@ -22,4 +22,329 @@ Do NOT modify the formatting, add emojis, or change the text. Keep the simple fo - [ ] NIP-50: Keywords filter - [ ] NIP-70: Protected Events +## 🔧 Administrator API + +C-Relay uses an innovative **event-based administration system** where all configuration and management commands are sent as signed Nostr events using the admin private key generated during first startup. Admin commands use ephemeral kinds 23455 and 23456 with **optional NIP-44 encryption** for enhanced security. + +### 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 + +**Standard Mode (Plaintext):** Commands sent in tags as normal +**Encrypted Mode (NIP-44):** Commands encrypted in content field, no tags used + +### Kind 23455: Configuration Management (Ephemeral) +Update relay configuration parameters or query available settings. + +**Configuration Update:** +```json +{ + "kind": 23455, + "content": "", + "tags": [ + ["config_key1", "config_value1"], + ["config_key2", "config_value2"] + ] +} +``` + +**Query Available Config Keys:** +```json +{ + "kind": 23455, + "content": "", + "tags": [ + ["config_query", "list_all_keys"] + ] +} +``` + +**Get Current Configuration:** +```json +{ + "kind": 23455, + "content": "", + "tags": [ + ["config_query", "get_current_config"] + ] +} +``` + +**Available Configuration Keys:** + +**Basic Relay Settings:** +- `relay_description`: Relay description text +- `relay_contact`: Contact information +- `max_connections`: Maximum concurrent connections +- `max_subscriptions_per_client`: Max subscriptions per client +- `max_event_tags`: Maximum tags per event +- `max_content_length`: Maximum event content length + +**Authentication & Access Control:** +- `auth_enabled`: Enable whitelist/blacklist auth rules (`true`/`false`) +- `nip42_auth_required`: Enable NIP-42 cryptographic authentication (`true`/`false`) +- `nip42_auth_required_kinds`: Event kinds requiring NIP-42 auth (comma-separated) +- `nip42_challenge_timeout`: NIP-42 challenge expiration seconds + +**Proof of Work & Validation:** +- `pow_min_difficulty`: Minimum proof-of-work difficulty +- `nip40_expiration_enabled`: Enable event expiration (`true`/`false`) + +### 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: + +**Success Response:** +```json +["OK", "event_id", true, "Operation completed successfully"] +``` + +**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"]] +}] +``` + diff --git a/api/index.html b/api/index.html index fa67823..13d4c3f 100644 --- a/api/index.html +++ b/api/index.html @@ -951,12 +951,12 @@ relayStatus.textContent = 'CONNECTED - SUBSCRIBING...'; relayStatus.className = 'status connected'; - // Send REQ message to subscribe to kind 33334 events + // Send REQ message to subscribe to kind 23455 events (ephemeral config events) const reqMessage = [ "REQ", subscriptionId, { - "kinds": [33334], + "kinds": [23455], "limit": 50 } ]; @@ -1176,10 +1176,10 @@ 'max_limit': 'Maximum Query Limit' }; - // Process configuration tags + // Process configuration tags (no d tag filtering for ephemeral events) const configData = {}; event.tags.forEach(tag => { - if (tag.length >= 2 && tag[0] !== 'd') { // Skip 'd' tag (relay identifier) + if (tag.length >= 2) { configData[tag[0]] = tag[1]; } }); @@ -1270,26 +1270,23 @@ const formInputs = configForm.querySelectorAll('input, select'); const newTags = []; - // Preserve the 'd' tag (relay identifier) from original event - const dTag = currentConfig.tags.find(tag => tag[0] === 'd'); - if (dTag) { - newTags.push(dTag); - } - - // Add updated configuration tags + // Add updated configuration tags (no d tag needed for ephemeral events) formInputs.forEach(input => { if (!input.disabled && input.name) { newTags.push([input.name, input.value]); } }); - // Create new kind 33334 event + // Create new kind 23455 event (ephemeral configuration event) const newEvent = { - kind: 33334, + kind: 23455, pubkey: userPubkey, created_at: Math.floor(Date.now() / 1000), tags: newTags, - content: currentConfig.content || 'C Nostr Relay Configuration' + content: JSON.stringify({ + action: 'update_config', + config_data: Object.fromEntries(newTags.filter(tag => tag[0] !== 'd')) + }) }; console.log('Signing event with window.nostr.signEvent()...'); @@ -1660,7 +1657,7 @@ try { log(`Deleting auth rule: ${rule.rule_type} - ${rule.pattern_value || rule.rule_target}`, 'INFO'); - // TODO: Implement actual rule deletion via WebSocket kind 33335 event + // TODO: Implement actual rule deletion via WebSocket kind 23456 event // For now, just remove from local array currentAuthRules.splice(index, 1); displayAuthRules(currentAuthRules); @@ -1746,7 +1743,7 @@ if (editingAuthRule) { log(`Updating auth rule: ${ruleData.rule_type} - ${ruleData.pattern_value}`, 'INFO'); - // TODO: Implement actual rule update via WebSocket kind 33335 event + // TODO: Implement actual rule update via WebSocket kind 23456 event // For now, just update local array currentAuthRules[editingAuthRule.index] = { ...ruleData, id: editingAuthRule.id || Date.now() }; @@ -1754,7 +1751,7 @@ } else { log(`Adding new auth rule: ${ruleData.rule_type} - ${ruleData.pattern_value}`, 'INFO'); - // TODO: Implement actual rule creation via WebSocket kind 33335 event + // TODO: Implement actual rule creation via WebSocket kind 23456 event // For now, just add to local array currentAuthRules.push({ ...ruleData, id: Date.now() }); @@ -2021,7 +2018,7 @@ }); } - // Add auth rule via WebSocket (kind 33335 event) + // Add auth rule via WebSocket (kind 23456 event) async function addAuthRuleViaWebSocket(ruleData) { if (!isLoggedIn || !userPubkey) { throw new Error('Must be logged in to add auth rules'); @@ -2064,13 +2061,12 @@ dbPatternType = 'pubkey'; } - // Create kind 33335 auth rule event with database schema values + // Create kind 23456 auth rule event (ephemeral auth management) const authEvent = { - kind: 33335, + kind: 23456, pubkey: userPubkey, created_at: Math.floor(Date.now() / 1000), tags: [ - ['d', 'auth-rules'], // Addressable event identifier [dbRuleType, dbPatternType, ruleData.pattern_value] ], content: JSON.stringify({ diff --git a/docs/admin_api_plan.md b/docs/admin_api_plan.md new file mode 100644 index 0000000..f20df22 --- /dev/null +++ b/docs/admin_api_plan.md @@ -0,0 +1,537 @@ +# C-Relay Administrator API Implementation Plan + +## Problem Analysis + +### Current Issues Identified: + +1. **Schema Mismatch**: Storage system (config.c) vs Validation system (request_validator.c) use different column names and values +2. **Missing API Endpoint**: No way to clear auth_rules table for testing +3. **Configuration Gap**: Auth rules enforcement may not be properly enabled +4. **Documentation Gap**: Admin API commands not documented + +### Root Cause: Auth Rules Schema Inconsistency + +**Current Schema (sql_schema.h lines 140-150):** +```sql +CREATE TABLE auth_rules ( + rule_type TEXT CHECK (rule_type IN ('whitelist', 'blacklist')), + pattern_type TEXT CHECK (pattern_type IN ('pubkey', 'hash')), + pattern_value TEXT, + action TEXT CHECK (action IN ('allow', 'deny')), + active INTEGER DEFAULT 1 +); +``` + +**Storage Implementation (config.c):** +- Stores: `rule_type='blacklist'`, `pattern_type='pubkey'`, `pattern_value='hex'`, `action='allow'` + +**Validation Implementation (request_validator.c):** +- Queries: `rule_type='pubkey_blacklist'`, `rule_target='hex'`, `operation='event'`, `enabled=1` + +**MISMATCH**: Validator looks for non-existent columns and wrong rule_type values! + +## Proposed Solution Architecture + +### Phase 1: API Documentation & Standardization + +#### Admin API Commands (via WebSocket with admin private key) + +**Kind 23455: Configuration Management (Ephemeral)** +- Update relay settings, limits, authentication policies +- **Standard Mode**: Commands in tags `["config_key", "config_value"]` +- **Encrypted Mode**: Commands NIP-44 encrypted in content `{"encrypted_tags": "..."}` +- Content: Descriptive text or encrypted payload +- Security: Optional NIP-44 encryption for sensitive operations + +**Kind 23456: Auth Rules & System Management (Ephemeral)** +- Auth rules: Add/remove/query whitelist/blacklist rules +- System commands: clear rules, status, cache management +- **Standard Mode**: Commands in tags + - Rule format: `["rule_type", "pattern_type", "pattern_value"]` + - Query format: `["auth_query", "filter"]` + - System format: `["system_command", "command_name"]` +- **Encrypted Mode**: Commands NIP-44 encrypted in content `{"encrypted_tags": "..."}` +- Content: Action description + optional encrypted payload +- Security: Optional NIP-44 encryption for sensitive operations + +#### Configuration Query Commands (using Kind 23455) + +1. **List All Configuration Keys (Standard)**: + ```json + { + "kind": 23455, + "content": "Discovery query", + "tags": [["config_query", "list_all_keys"]] + } + ``` + +2. **List All Configuration Keys (Encrypted)**: + ```json + { + "kind": 23455, + "content": "{\"query\":\"list_config_keys\",\"encrypted_tags\":\"nip44_encrypted_payload\"}", + "tags": [] + } + ``` + *Encrypted payload contains:* `[["config_query", "list_all_keys"]]` + +3. **Get Current Configuration (Standard)**: + ```json + { + "kind": 23455, + "content": "Config query", + "tags": [["config_query", "get_current_config"]] + } + ``` + +4. **Get Current Configuration (Encrypted)**: + ```json + { + "kind": 23455, + "content": "{\"query\":\"get_config\",\"encrypted_tags\":\"nip44_encrypted_payload\"}", + "tags": [] + } + ``` + *Encrypted payload contains:* `[["config_query", "get_current_config"]]` + +#### System Management Commands (using Kind 23456) + +1. **Clear All Auth Rules (Standard)**: + ```json + { + "kind": 23456, + "content": "{\"action\":\"clear_all\"}", + "tags": [["system_command", "clear_all_auth_rules"]] + } + ``` + +2. **Clear All Auth Rules (Encrypted)**: + ```json + { + "kind": 23456, + "content": "{\"action\":\"clear_all\",\"encrypted_tags\":\"nip44_encrypted_payload\"}", + "tags": [] + } + ``` + *Encrypted payload contains:* `[["system_command", "clear_all_auth_rules"]]` + +3. **Query All Auth Rules (Standard)**: + ```json + { + "kind": 23456, + "content": "{\"query\":\"list_auth_rules\"}", + "tags": [["auth_query", "all"]] + } + ``` + +4. **Query All Auth Rules (Encrypted)**: + ```json + { + "kind": 23456, + "content": "{\"query\":\"list_auth_rules\",\"encrypted_tags\":\"nip44_encrypted_payload\"}", + "tags": [] + } + ``` + *Encrypted payload contains:* `[["auth_query", "all"]]` + +5. **Add Blacklist Rule (Standard)**: + ```json + { + "kind": 23456, + "content": "{\"action\":\"add\"}", + "tags": [["blacklist", "pubkey", "deadbeef1234abcd..."]] + } + ``` + +6. **Add Blacklist Rule (Encrypted)**: + ```json + { + "kind": 23456, + "content": "{\"action\":\"add\",\"encrypted_tags\":\"nip44_encrypted_payload\"}", + "tags": [] + } + ``` + *Encrypted payload contains:* `[["blacklist", "pubkey", "deadbeef1234abcd..."]]` + +### Phase 2: Auth Rules Schema Alignment + +#### Option A: Fix Validator to Match Schema (RECOMMENDED) + +**Update request_validator.c:** +```sql +-- OLD (broken): +WHERE rule_type = 'pubkey_blacklist' AND rule_target = ? AND operation = ? AND enabled = 1 + +-- NEW (correct): +WHERE rule_type = 'blacklist' AND pattern_type = 'pubkey' AND pattern_value = ? AND active = 1 +``` + +**Benefits:** +- Matches actual database schema +- Simpler rule_type values ('blacklist' vs 'pubkey_blacklist') +- Uses existing columns (pattern_value vs rule_target) +- Consistent with storage implementation + +#### Option B: Update Schema to Match Validator (NOT RECOMMENDED) + +Would require changing schema, migration scripts, and storage logic. + +### Phase 3: Implementation Priority + +#### High Priority (Critical for blacklist functionality): +1. Fix request_validator.c schema mismatch +2. Ensure auth_required configuration is enabled +3. Update tests to use ephemeral event kinds (23455/23456) +4. Test blacklist enforcement + +#### Medium Priority (Enhanced Admin Features): +1. **Implement NIP-44 Encryption Support**: + - Detect empty tags array for Kind 23455/23456 events + - Parse `encrypted_tags` field from content JSON + - Decrypt using admin privkey and relay pubkey + - Process decrypted tags as normal commands +2. Add clear_all_auth_rules system command +3. Add auth rule query functionality (both standard and encrypted modes) +4. Add configuration discovery (list available config keys) +5. Enhanced error reporting in admin API +6. Conflict resolution (same pubkey in whitelist + blacklist) + +#### Security Priority (NIP-44 Implementation): +1. **Encryption Detection Logic**: Check for empty tags + encrypted_tags field +2. **Key Pair Management**: Use admin private key + relay public key for NIP-44 +3. **Backward Compatibility**: Support both standard and encrypted modes +4. **Error Handling**: Graceful fallback if decryption fails +5. **Performance**: Cache decrypted results to avoid repeated decryption + +#### Low Priority (Documentation & Polish): +1. Complete README.md API documentation +2. Example usage scripts +3. Admin client tools + +### Phase 4: Expected API Structure + +#### README.md Documentation Format: + +```markdown +# C-Relay Administrator API + +## Authentication +All admin commands require signing with the admin private key generated during first startup. + +## Configuration Management (Kind 23455 - Ephemeral) +Update relay configuration parameters or query available settings. + +**Configuration Update Event:** +```json +{ + "kind": 23455, + "content": "Configuration update", + "tags": [ + ["config_key1", "config_value1"], + ["config_key2", "config_value2"] + ] +} +``` + +**List Available Config Keys:** +```json +{ + "kind": 23455, + "content": "{\"query\":\"list_config_keys\",\"description\":\"Get editable config keys\"}", + "tags": [ + ["config_query", "list_all_keys"] + ] +} +``` + +**Get Current Configuration:** +```json +{ + "kind": 23455, + "content": "{\"query\":\"get_config\",\"description\":\"Get current config values\"}", + "tags": [ + ["config_query", "get_current_config"] + ] +} +``` + +## Auth Rules Management (Kind 23456 - Ephemeral) +Manage whitelist and blacklist rules. + +**Add Rule Event:** +```json +{ + "kind": 23456, + "content": "{\"action\":\"add\",\"description\":\"Block malicious user\"}", + "tags": [ + ["blacklist", "pubkey", "deadbeef1234..."] + ] +} +``` + +**Remove Rule Event:** +```json +{ + "kind": 23456, + "content": "{\"action\":\"remove\",\"description\":\"Unblock user\"}", + "tags": [ + ["blacklist", "pubkey", "deadbeef1234..."] + ] +} +``` + +**Query All Auth Rules:** +```json +{ + "kind": 23456, + "content": "{\"query\":\"list_auth_rules\",\"description\":\"Get all rules\"}", + "tags": [ + ["auth_query", "all"] + ] +} +``` + +**Query Whitelist Rules Only:** +```json +{ + "kind": 23456, + "content": "{\"query\":\"list_auth_rules\",\"description\":\"Get whitelist\"}", + "tags": [ + ["auth_query", "whitelist"] + ] +} +``` + +**Check Specific Pattern:** +```json +{ + "kind": 23456, + "content": "{\"query\":\"check_pattern\",\"description\":\"Check if pattern exists\"}", + "tags": [ + ["auth_query", "pattern", "deadbeef1234..."] + ] +} +``` + +## System Management (Kind 23456 - Ephemeral) +System administration commands using the same kind as auth rules. + +**Clear All Auth Rules:** +```json +{ + "kind": 23456, + "content": "{\"action\":\"clear_all\",\"description\":\"Clear all auth rules\"}", + "tags": [ + ["system_command", "clear_all_auth_rules"] + ] +} +``` + +**System Status:** +```json +{ + "kind": 23456, + "content": "{\"action\":\"system_status\",\"description\":\"Get system status\"}", + "tags": [ + ["system_command", "system_status"] + ] +} +``` + +## Response Format +All admin commands return JSON responses via WebSocket: + +**Success Response:** +```json +["OK", "event_id", true, "success_message"] +``` + +**Error Response:** +```json +["OK", "event_id", false, "error_message"] +``` + +## Configuration Keys +- `relay_description`: Relay description text +- `relay_contact`: Contact information +- `auth_enabled`: Enable authentication system +- `max_connections`: Maximum concurrent connections +- `pow_min_difficulty`: Minimum proof-of-work difficulty +- ... (full list of config keys) + +## Examples + +### Enable Authentication & Add Blacklist +```bash +# 1. Enable auth system +nak event -k 23455 --content "Enable authentication" \ + -t "auth_enabled=true" \ + --sec $ADMIN_PRIVKEY | nak event ws://localhost:8888 + +# 2. Add user to blacklist +nak event -k 23456 --content '{"action":"add","description":"Spam user"}' \ + -t "blacklist=pubkey;$SPAM_USER_PUBKEY" \ + --sec $ADMIN_PRIVKEY | nak event ws://localhost:8888 + +# 3. Query all auth rules +nak event -k 23456 --content '{"query":"list_auth_rules","description":"Get all rules"}' \ + -t "auth_query=all" \ + --sec $ADMIN_PRIVKEY | nak event ws://localhost:8888 + +# 4. Clear all rules for testing +nak event -k 23456 --content '{"action":"clear_all","description":"Clear all rules"}' \ + -t "system_command=clear_all_auth_rules" \ + --sec $ADMIN_PRIVKEY | nak event ws://localhost:8888 +``` + +## Expected Response Formats + +### Configuration Query Response +```json +["EVENT", "subscription_id", { + "kind": 23455, + "content": "{\"config_keys\": [\"auth_enabled\", \"max_connections\"], \"descriptions\": {\"auth_enabled\": \"Enable whitelist/blacklist rules\"}}", + "tags": [["response_type", "config_keys_list"]] +}] +``` + +### Current Config Response +```json +["EVENT", "subscription_id", { + "kind": 23455, + "content": "{\"current_config\": {\"auth_enabled\": \"true\", \"max_connections\": \"1000\"}}", + "tags": [["response_type", "current_config"]] +}] +``` + +### Auth Rules Query Response +```json +["EVENT", "subscription_id", { + "kind": 23456, + "content": "{\"auth_rules\": [{\"rule_type\": \"blacklist\", \"pattern_type\": \"pubkey\", \"pattern_value\": \"deadbeef...\"}, {\"rule_type\": \"whitelist\", \"pattern_type\": \"pubkey\", \"pattern_value\": \"cafebabe...\"}]}", + "tags": [["response_type", "auth_rules_list"], ["query_type", "all"]] +}] +``` + +### Pattern Check Response +```json +["EVENT", "subscription_id", { + "kind": 23456, + "content": "{\"pattern_exists\": true, \"rule_type\": \"blacklist\", \"pattern_value\": \"deadbeef...\"}", + "tags": [["response_type", "pattern_check"], ["pattern", "deadbeef..."]] +}] +``` + +## Implementation Steps + +1. **Document API** (this file) ✅ +2. **Update to ephemeral event kinds** ✅ +3. **Fix request_validator.c** schema mismatch +4. **Update tests** to use Kind 23455/23456 +5. **Add auth rule query functionality** +6. **Add configuration discovery feature** +7. **Test blacklist functionality** +8. **Add remaining system commands** + +## Testing Plan + +1. Fix schema mismatch and test basic blacklist +2. Add clear_auth_rules and test table cleanup +3. Test whitelist/blacklist conflict scenarios +4. Test all admin API commands end-to-end +5. Update integration tests + +This plan addresses the immediate blacklist issue while establishing a comprehensive admin API framework for future expansion. + +## NIP-44 Encryption Implementation Details + +### Server-Side Detection Logic +```c +// In admin event processing function +bool is_encrypted_command(struct nostr_event *event) { + // Check if Kind 23455 or 23456 with empty tags + if ((event->kind == 23455 || event->kind == 23456) && + event->tags_count == 0) { + return true; + } + return false; +} + +cJSON *decrypt_admin_tags(struct nostr_event *event) { + cJSON *content_json = cJSON_Parse(event->content); + if (!content_json) return NULL; + + cJSON *encrypted_tags = cJSON_GetObjectItem(content_json, "encrypted_tags"); + if (!encrypted_tags) { + cJSON_Delete(content_json); + return NULL; + } + + // Decrypt using NIP-44 with admin pubkey and relay privkey + char *decrypted = nip44_decrypt( + cJSON_GetStringValue(encrypted_tags), + admin_pubkey, // Shared secret with admin + relay_private_key // Our private key + ); + + cJSON *decrypted_tags = cJSON_Parse(decrypted); + free(decrypted); + cJSON_Delete(content_json); + + return decrypted_tags; // Returns tag array: [["key1", "val1"], ["key2", "val2"]] +} +``` + +### Admin Event Processing Flow +1. **Receive Event**: Kind 23455/23456 with admin signature +2. **Check Mode**: Empty tags = encrypted, populated tags = standard +3. **Decrypt if Needed**: Extract and decrypt `encrypted_tags` from content +4. **Process Commands**: Use decrypted/standard tags for command processing +5. **Execute**: Same logic for both modes after tag extraction +6. **Respond**: Standard response format (optionally encrypt response) + +### Security Benefits +- **Command Privacy**: Admin operations invisible in event tags +- **Replay Protection**: NIP-44 includes timestamp/randomness +- **Key Management**: Uses existing admin/relay key pair +- **Backward Compatible**: Standard mode still works +- **Performance**: Only decrypt when needed (empty tags detection) + +### NIP-44 Library Integration +The relay will need to integrate a NIP-44 encryption/decryption library: + +```c +// Required NIP-44 functions +char* nip44_encrypt(const char* plaintext, const char* sender_privkey, const char* recipient_pubkey); +char* nip44_decrypt(const char* ciphertext, const char* recipient_privkey, const char* sender_pubkey); +``` + +### Implementation Priority (Updated) + +#### Phase 1: Core Infrastructure (Complete) +- [x] Event-based admin authentication system +- [x] Kind 23455/23456 (Configuration/Auth Rules) processing +- [x] Basic configuration parameter updates +- [x] Auth rule add/remove/clear functionality +- [x] Updated to ephemeral event kinds +- [x] Designed NIP-44 encryption support + +#### Phase 2: NIP-44 Encryption Support (Next Priority) +- [ ] **Add NIP-44 library dependency** to project +- [ ] **Implement encryption detection logic** (`is_encrypted_command()`) +- [ ] **Add decrypt_admin_tags() function** with NIP-44 support +- [ ] **Update admin command processing** to handle both modes +- [ ] **Test encrypted admin commands** end-to-end + +#### Phase 3: Enhanced Features +- [ ] **Auth rule query functionality** (both standard and encrypted modes) +- [ ] **Configuration discovery API** (list available config keys) +- [ ] **Enhanced error messages** with encryption status +- [ ] **Performance optimization** (caching, async decrypt) + +#### Phase 4: Schema Fixes (Critical) +- [ ] **Fix request_validator.c** schema mismatch +- [ ] **Enable blacklist enforcement** with encrypted commands +- [ ] **Update tests** to use both standard and encrypted modes + +This enhanced admin API provides enterprise-grade security while maintaining ease of use for basic operations. \ No newline at end of file diff --git a/relay.pid b/relay.pid index 875a07e..9bb7b91 100644 --- a/relay.pid +++ b/relay.pid @@ -1 +1 @@ -1950163 +238618 diff --git a/src/config.c b/src/config.c index 471417a..565a922 100644 --- a/src/config.c +++ b/src/config.c @@ -73,6 +73,16 @@ 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); + // Current configuration cache static cJSON* g_current_config = NULL; @@ -2055,7 +2065,7 @@ int add_pubkeys_to_config_table(void) { // ADMIN EVENT PROCESSING FUNCTIONS // ================================ -// Process admin events (moved from main.c) +// Process admin events (updated for new Kind 23455/23456) int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size) { cJSON* kind_obj = cJSON_GetObjectItem(event, "kind"); if (!kind_obj || !cJSON_IsNumber(kind_obj)) { @@ -2081,26 +2091,64 @@ int process_admin_event_in_config(cJSON* event, char* error_message, size_t erro int kind = (int)cJSON_GetNumberValue(kind_obj); switch (kind) { - case 33334: + case 23455: // New ephemeral configuration management return process_admin_config_event(event, error_message, error_size); - case 33335: + case 23456: // New ephemeral auth rules management + return process_admin_auth_event(event, error_message, error_size); + 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); default: - snprintf(error_message, error_size, "invalid: unsupported admin event kind"); + snprintf(error_message, error_size, "invalid: unsupported admin event kind %d", kind); return -1; } } -// Handle kind 33334 config events +// Handle Kind 23455 configuration management events and legacy Kind 33334 int process_admin_config_event(cJSON* event, char* error_message, size_t error_size) { + cJSON* kind_obj = cJSON_GetObjectItem(event, "kind"); + int kind = kind_obj ? (int)cJSON_GetNumberValue(kind_obj) : 0; + + // Parse content for action commands + cJSON* content_obj = cJSON_GetObjectItem(event, "content"); + const char* content = content_obj ? cJSON_GetStringValue(content_obj) : ""; + + // 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'; + } + } + 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"); if (!tags_obj || !cJSON_IsArray(tags_obj)) { snprintf(error_message, error_size, "invalid: configuration event must have tags"); return -1; } - // Config table should already exist from embedded schema - // Begin transaction for atomic config updates int rc = sqlite3_exec(g_db, "BEGIN IMMEDIATE TRANSACTION", NULL, NULL, NULL); if (rc != SQLITE_OK) { @@ -2127,8 +2175,8 @@ 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 relay identifier tag - if (strcmp(key, "d") == 0) { + // Skip relay identifier tag (only for legacy addressable events) + if (kind == 33334 && strcmp(key, "d") == 0) { continue; } @@ -2154,35 +2202,21 @@ int process_admin_config_event(cJSON* event, char* error_message, size_t error_s return 0; } -// Handle kind 33335 auth rule events +// Handle Kind 23456 auth rules management and legacy Kind 33335 int process_admin_auth_event(cJSON* event, char* error_message, size_t error_size) { - log_info("=== SERVER-SIDE AUTH RULE EVENT DEBUG ==="); + cJSON* kind_obj = cJSON_GetObjectItem(event, "kind"); + int kind = kind_obj ? (int)cJSON_GetNumberValue(kind_obj) : 0; - // Print the entire received event for debugging - char* debug_event_str = cJSON_Print(event); - if (debug_event_str) { - printf("Received Auth Event JSON: %s\n", debug_event_str); - free(debug_event_str); - } + log_info("Processing admin auth rule event"); - cJSON* tags_obj = cJSON_GetObjectItem(event, "tags"); - if (!tags_obj || !cJSON_IsArray(tags_obj)) { - log_error("Auth event missing or invalid tags array"); - snprintf(error_message, error_size, "invalid: auth rule event must have tags"); - return -1; - } - - printf("Tags array size: %d\n", cJSON_GetArraySize(tags_obj)); - - // Extract action from content or tags + // Parse content for action commands cJSON* content_obj = cJSON_GetObjectItem(event, "content"); const char* content = content_obj ? cJSON_GetStringValue(content_obj) : ""; - printf("Event content: '%s'\n", content); - // Parse the action from content (should be "add" or "remove") cJSON* content_json = cJSON_Parse(content); - char action_buffer[16] = "add"; // Local buffer for action string - const char* action = action_buffer; // default + 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)) { @@ -2194,7 +2228,35 @@ int process_admin_auth_event(cJSON* event, char* error_message, size_t error_siz } cJSON_Delete(content_json); } - printf("Parsed action: '%s'\n", action); + + 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); + } + + // 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); @@ -2204,33 +2266,11 @@ int process_admin_auth_event(cJSON* event, char* error_message, size_t error_siz } int rules_processed = 0; - int tags_examined = 0; - int tags_skipped = 0; // Process each tag as an auth rule specification cJSON* tag = NULL; cJSON_ArrayForEach(tag, tags_obj) { - tags_examined++; - - printf("Examining tag #%d:\n", tags_examined); - char* tag_debug_str = cJSON_Print(tag); - if (tag_debug_str) { - printf(" Tag JSON: %s\n", tag_debug_str); - free(tag_debug_str); - } - - if (!cJSON_IsArray(tag)) { - printf(" SKIPPED: Not an array\n"); - tags_skipped++; - continue; - } - - int tag_size = cJSON_GetArraySize(tag); - printf(" Tag array size: %d\n", tag_size); - - if (tag_size < 3) { - printf(" SKIPPED: Array size < 3 (need at least 3 elements for auth rules)\n"); - tags_skipped++; + if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 3) { continue; } @@ -2241,8 +2281,6 @@ int process_admin_auth_event(cJSON* event, char* error_message, size_t error_siz if (!cJSON_IsString(rule_type_obj) || !cJSON_IsString(pattern_type_obj) || !cJSON_IsString(pattern_value_obj)) { - printf(" SKIPPED: One or more elements are not strings\n"); - tags_skipped++; continue; } @@ -2250,33 +2288,18 @@ int process_admin_auth_event(cJSON* event, char* error_message, size_t error_siz const char* pattern_type = cJSON_GetStringValue(pattern_type_obj); const char* pattern_value = cJSON_GetStringValue(pattern_value_obj); - printf(" Extracted rule: type='%s', pattern_type='%s', pattern_value='%s'\n", - rule_type, pattern_type, pattern_value); - // Process the auth rule based on action if (strcmp(action, "add") == 0) { - printf(" Attempting to add rule to database...\n"); if (add_auth_rule_from_config(rule_type, pattern_type, pattern_value, "allow") == 0) { - printf(" SUCCESS: Rule added to database\n"); rules_processed++; - } else { - printf(" FAILED: Could not add rule to database\n"); } } else if (strcmp(action, "remove") == 0) { - printf(" Attempting to remove rule from database...\n"); if (remove_auth_rule_from_config(rule_type, pattern_type, pattern_value) == 0) { - printf(" SUCCESS: Rule removed from database\n"); rules_processed++; - } else { - printf(" FAILED: Could not remove rule from database\n"); } } } - printf("Processing summary: examined=%d, skipped=%d, processed=%d\n", - tags_examined, tags_skipped, rules_processed); - log_info("=== END SERVER-SIDE AUTH RULE EVENT DEBUG ==="); - if (rules_processed > 0) { sqlite3_exec(g_db, "COMMIT", NULL, NULL, NULL); @@ -2348,6 +2371,425 @@ int remove_auth_rule_from_config(const char* rule_type, const char* pattern_type return (rc == SQLITE_DONE) ? 0 : -1; } +// ================================ +// ADMIN API QUERY HANDLERS +// ================================ + +// 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 + + if (!g_db) { + snprintf(error_message, error_size, "database not available"); + return -1; + } + + log_info("Processing config list keys query"); + + const char* sql = "SELECT key, 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 config keys query"); + return -1; + } + + 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); + + printf(" [%s] %s (%s)\n", category ? category : "general", key ? key : "", data_type ? data_type : "string"); + key_count++; + } + + sqlite3_finalize(stmt); + + printf("Total configuration keys: %d\n", key_count); + log_success("Configuration keys listed successfully"); + + 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 + + if (!g_db) { + snprintf(error_message, error_size, "database not available"); + return -1; + } + + log_info("Processing get current config query"); + + 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; +} + +// 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 + + if (!g_db) { + snprintf(error_message, error_size, "database not available"); + return -1; + } + + log_info("Processing auth rules list all query"); + + const char* sql = "SELECT rule_type, pattern_type, pattern_value, action FROM auth_rules ORDER BY rule_type, 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 auth rules query"); + return -1; + } + + printf("=== All Auth Rules ===\n"); + int rule_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 : "allow"); + rule_count++; + } + + sqlite3_finalize(stmt); + + printf("Total auth rules: %d\n", rule_count); + log_success("Auth rules listed successfully"); + + return 0; +} + +// Handle whitelist only query +int handle_auth_whitelist_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 whitelist rules query"); + + 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"); + 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); + + printf(" %s %s:%s -> %s\n", + rule_type ? rule_type : "", + pattern_type ? pattern_type : "", + pattern_value ? pattern_value : "", + action ? action : "allow"); + whitelist_count++; + } + + 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 + 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"); + return -1; + } + + const char* check_pattern_type = NULL; + const char* check_pattern_value = NULL; + + // 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; + } + } + } + } + + 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); + + 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"); + } else { + printf("Auth rules table was already empty - no rules to clear\n"); + log_success("Clear auth rules completed successfully (table was already empty)"); + } + + 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/main.c b/src/main.c index a50a0af..611437f 100644 --- a/src/main.c +++ b/src/main.c @@ -972,28 +972,28 @@ static void get_timestamp_string(char* buffer, size_t buffer_size) { void log_info(const char* message) { char timestamp[32]; get_timestamp_string(timestamp, sizeof(timestamp)); - printf("[%s] " BLUE "[INFO]" RESET " %s\n", timestamp, message); + printf("[%s] [INFO] %s\n", timestamp, message); fflush(stdout); } void log_success(const char* message) { char timestamp[32]; get_timestamp_string(timestamp, sizeof(timestamp)); - printf("[%s] " GREEN "[SUCCESS]" RESET " %s\n", timestamp, message); + printf("[%s] [SUCCESS] %s\n", timestamp, message); fflush(stdout); } void log_error(const char* message) { char timestamp[32]; get_timestamp_string(timestamp, sizeof(timestamp)); - printf("[%s] " RED "[ERROR]" RESET " %s\n", timestamp, message); + printf("[%s] [ERROR] %s\n", timestamp, message); fflush(stdout); } void log_warning(const char* message) { char timestamp[32]; get_timestamp_string(timestamp, sizeof(timestamp)); - printf("[%s] " YELLOW "[WARNING]" RESET " %s\n", timestamp, message); + printf("[%s] [WARNING] %s\n", timestamp, message); fflush(stdout); } @@ -3243,7 +3243,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso // Cleanup event JSON string free(event_json_str); - // Check for admin events (kinds 33334 and 33335) and intercept them + // Check for admin events (kinds 33334, 33335, 23455, and 23456) and intercept them if (result == 0) { cJSON* kind_obj = cJSON_GetObjectItem(event, "kind"); if (kind_obj && cJSON_IsNumber(kind_obj)) { @@ -3251,7 +3251,21 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso log_info("DEBUG ADMIN: Checking if admin event processing is needed"); - if (event_kind == 33334 || event_kind == 33335) { + // Log reception of Kind 23455 and 23456 events + if (event_kind == 23455 || event_kind == 23456) { + char* event_json_debug = cJSON_Print(event); + char debug_received_msg[1024]; + snprintf(debug_received_msg, sizeof(debug_received_msg), + "RECEIVED Kind %d event: %s", event_kind, + event_json_debug ? event_json_debug : "Failed to serialize"); + log_info(debug_received_msg); + + if (event_json_debug) { + free(event_json_debug); + } + } + + 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"); @@ -3263,6 +3277,21 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso "DEBUG ADMIN: process_admin_event_in_config returned %d", admin_result); log_info(debug_admin_msg); + // Log results for Kind 23455 and 23456 events + if (event_kind == 23455 || event_kind == 23456) { + 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; diff --git a/src/request_validator.c b/src/request_validator.c index c548d44..c9d01e8 100644 --- a/src/request_validator.c +++ b/src/request_validator.c @@ -629,28 +629,26 @@ static int check_database_auth_rules(const char *pubkey, const char *operation, // Step 1: Check pubkey blacklist (highest priority) const char *blacklist_sql = - "SELECT rule_type, description FROM auth_rules WHERE rule_type = " - "'pubkey_blacklist' AND rule_target = ? AND operation = ? AND enabled = " - "1 ORDER BY priority LIMIT 1"; + "SELECT rule_type, action FROM auth_rules WHERE rule_type = " + "'blacklist' AND pattern_type = 'pubkey' AND pattern_value = ? LIMIT 1"; rc = sqlite3_prepare_v2(db, blacklist_sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_text(stmt, 1, pubkey, -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 2, operation ? operation : "", -1, SQLITE_STATIC); if (sqlite3_step(stmt) == SQLITE_ROW) { - const char *description = (const char *)sqlite3_column_text(stmt, 1); + const char *action = (const char *)sqlite3_column_text(stmt, 1); validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 1 FAILED - " "Pubkey blacklisted\n"); char blacklist_msg[256]; sprintf(blacklist_msg, - "VALIDATOR_DEBUG: RULES ENGINE - Blacklist rule matched: %s\n", - description ? description : "Unknown"); + "VALIDATOR_DEBUG: RULES ENGINE - Blacklist rule matched: action=%s\n", + action ? action : "deny"); validator_debug_log(blacklist_msg); // Set specific violation details for status code mapping strcpy(g_last_rule_violation.violation_type, "pubkey_blacklist"); - sprintf(g_last_rule_violation.reason, "%s: Public key blacklisted", - description ? description : "TEST_PUBKEY_BLACKLIST"); + sprintf(g_last_rule_violation.reason, "Public key blacklisted: %s", + action ? action : "PUBKEY_BLACKLIST"); sqlite3_finalize(stmt); sqlite3_close(db); @@ -664,29 +662,27 @@ static int check_database_auth_rules(const char *pubkey, const char *operation, // Step 2: Check hash blacklist if (resource_hash) { const char *hash_blacklist_sql = - "SELECT rule_type, description FROM auth_rules WHERE rule_type = " - "'hash_blacklist' AND rule_target = ? AND operation = ? AND enabled = " - "1 ORDER BY priority LIMIT 1"; + "SELECT rule_type, action FROM auth_rules WHERE rule_type = " + "'blacklist' AND pattern_type = 'hash' AND pattern_value = ? LIMIT 1"; rc = sqlite3_prepare_v2(db, hash_blacklist_sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_text(stmt, 1, resource_hash, -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 2, operation ? operation : "", -1, SQLITE_STATIC); if (sqlite3_step(stmt) == SQLITE_ROW) { - const char *description = (const char *)sqlite3_column_text(stmt, 1); + const char *action = (const char *)sqlite3_column_text(stmt, 1); validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 2 FAILED - " "Hash blacklisted\n"); char hash_blacklist_msg[256]; sprintf( hash_blacklist_msg, - "VALIDATOR_DEBUG: RULES ENGINE - Hash blacklist rule matched: %s\n", - description ? description : "Unknown"); + "VALIDATOR_DEBUG: RULES ENGINE - Hash blacklist rule matched: action=%s\n", + action ? action : "deny"); validator_debug_log(hash_blacklist_msg); // Set specific violation details for status code mapping strcpy(g_last_rule_violation.violation_type, "hash_blacklist"); - sprintf(g_last_rule_violation.reason, "%s: File hash blacklisted", - description ? description : "TEST_HASH_BLACKLIST"); + sprintf(g_last_rule_violation.reason, "File hash blacklisted: %s", + action ? action : "HASH_BLACKLIST"); sqlite3_finalize(stmt); sqlite3_close(db); @@ -703,22 +699,20 @@ static int check_database_auth_rules(const char *pubkey, const char *operation, // Step 3: Check pubkey whitelist const char *whitelist_sql = - "SELECT rule_type, description FROM auth_rules WHERE rule_type = " - "'pubkey_whitelist' AND rule_target = ? AND operation = ? AND enabled = " - "1 ORDER BY priority LIMIT 1"; + "SELECT rule_type, action FROM auth_rules WHERE rule_type = " + "'whitelist' AND pattern_type = 'pubkey' AND pattern_value = ? LIMIT 1"; rc = sqlite3_prepare_v2(db, whitelist_sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_text(stmt, 1, pubkey, -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 2, operation ? operation : "", -1, SQLITE_STATIC); if (sqlite3_step(stmt) == SQLITE_ROW) { - const char *description = (const char *)sqlite3_column_text(stmt, 1); + const char *action = (const char *)sqlite3_column_text(stmt, 1); validator_debug_log("VALIDATOR_DEBUG: RULES ENGINE - STEP 3 PASSED - " "Pubkey whitelisted\n"); char whitelist_msg[256]; sprintf(whitelist_msg, - "VALIDATOR_DEBUG: RULES ENGINE - Whitelist rule matched: %s\n", - description ? description : "Unknown"); + "VALIDATOR_DEBUG: RULES ENGINE - Whitelist rule matched: action=%s\n", + action ? action : "allow"); validator_debug_log(whitelist_msg); sqlite3_finalize(stmt); sqlite3_close(db); @@ -731,12 +725,10 @@ static int check_database_auth_rules(const char *pubkey, const char *operation, // Step 4: Check if any whitelist rules exist - if yes, deny by default const char *whitelist_exists_sql = - "SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'pubkey_whitelist' " - "AND operation = ? AND enabled = 1 LIMIT 1"; + "SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'whitelist' " + "AND pattern_type = 'pubkey' LIMIT 1"; rc = sqlite3_prepare_v2(db, whitelist_exists_sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { - sqlite3_bind_text(stmt, 1, operation ? operation : "", -1, SQLITE_STATIC); - if (sqlite3_step(stmt) == SQLITE_ROW) { int whitelist_count = sqlite3_column_int(stmt, 0); if (whitelist_count > 0) { diff --git a/tests/white_black_list_test.sh b/tests/white_black_list_test.sh index 8a10f75..ee7b5bd 100755 --- a/tests/white_black_list_test.sh +++ b/tests/white_black_list_test.sh @@ -20,19 +20,18 @@ set -e # Exit on any error # CONFIGURATION # ======================================================================= -# Test mode credentials (provided by user) +# Test mode credentials (from current relay startup) ADMIN_PRIVKEY="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" ADMIN_PUBKEY="6a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3" RELAY_PUBKEY="4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa" # Server configuration -RELAY_HOST="localhost" +RELAY_HOST="127.0.0.1" RELAY_PORT="8888" RELAY_URL="ws://${RELAY_HOST}:${RELAY_PORT}" # Test configuration TIMEOUT=5 -LOG_FILE="whitelist_blacklist_test.log" TEMP_DIR="/tmp/c_relay_test_$$" # Color codes for output @@ -53,23 +52,23 @@ TESTS_FAILED=0 # ======================================================================= log() { - echo -e "${BLUE}[$(date '+%H:%M:%S')]${RESET} $1" | tee -a "$LOG_FILE" + echo -e "${BLUE}[$(date '+%H:%M:%S')]${RESET} $1" } log_success() { - echo -e "${GREEN}[SUCCESS]${RESET} $1" | tee -a "$LOG_FILE" + echo -e "${GREEN}[SUCCESS]${RESET} $1" } log_error() { - echo -e "${RED}[ERROR]${RESET} $1" | tee -a "$LOG_FILE" + echo -e "${RED}[ERROR]${RESET} $1" } log_warning() { - echo -e "${YELLOW}[WARNING]${RESET} $1" | tee -a "$LOG_FILE" + echo -e "${YELLOW}[WARNING]${RESET} $1" } log_info() { - echo -e "${BLUE}[INFO]${RESET} $1" | tee -a "$LOG_FILE" + echo -e "${BLUE}[INFO]${RESET} $1" } increment_test() { @@ -79,11 +78,15 @@ increment_test() { pass_test() { TESTS_PASSED=$((TESTS_PASSED + 1)) log_success "Test $TESTS_RUN: PASSED - $1" + echo "" + echo "" } fail_test() { TESTS_FAILED=$((TESTS_FAILED + 1)) log_error "Test $TESTS_RUN: FAILED - $1" + echo "" + echo "" } # Generate test keypairs @@ -123,14 +126,21 @@ send_websocket_message() { local expected_response="$2" local timeout="${3:-$TIMEOUT}" - log_info "Sending WebSocket message: ${message:0:100}..." - - # Use wscat to send message and capture response + # Use websocat to send message and capture response (following pattern from tests/1_nip_test.sh) local response="" - if command -v wscat &> /dev/null; then - response=$(echo "$message" | timeout "$timeout" wscat -c "$RELAY_URL" 2>/dev/null | head -1) + if command -v websocat &> /dev/null; then + # Capture output from websocat (following working pattern from 1_nip_test.sh) + response=$(echo "$message" | timeout "$timeout" websocat "$RELAY_URL" 2>&1 || echo "Connection failed") + + # Check if connection failed + if [[ "$response" == *"Connection failed"* ]]; then + log_error "Failed to connect to relay" + return 1 + fi + else - log_error "wscat not found - required for WebSocket testing" + log_error "websocat not found - required for WebSocket testing" + log_error "Please install websocat for WebSocket communication" return 1 fi @@ -147,13 +157,12 @@ 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 - match the working NIP-42 pattern + # 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 local event_json - event_json=$(nak event -k 33335 --content "{\"action\":\"$action\",\"description\":\"$description\"}" \ - -t "d=$RELAY_PUBKEY" \ - -t "$rule_type=$pattern_type" \ - -t "pattern=$pattern_value" \ - -t "action=$action" \ + event_json=$(nak event -k 23456 --content "{\"action\":\"$action\",\"description\":\"$description\"}" \ + -t "$rule_type=$pattern_type" -t "pattern_value=$pattern_value" \ --sec "$ADMIN_PRIVKEY" 2>/dev/null) if [ $? -ne 0 ] || [ -z "$event_json" ]; then @@ -161,7 +170,7 @@ send_auth_rule_event() { return 1 fi - # Send the event using nak directly to relay (more reliable than wscat) + # Send the event using nak directly to relay (more reliable than websocat) log_info "Publishing auth rule event to relay..." local result result=$(echo "$event_json" | timeout 10s nak event "$RELAY_URL" 2>&1) @@ -179,6 +188,40 @@ send_auth_rule_event() { fi } +# Clear all auth rules using the new system command functionality +clear_all_auth_rules() { + log_info "Clearing all existing auth rules..." + + # Create system command event to clear all auth rules + # Using Kind 23456 (ephemeral auth rules management) + local event_json + event_json=$(nak event -k 23456 --content "{\"action\":\"clear_all\"}" \ + -t "system_command=clear_all_auth_rules" \ + --sec "$ADMIN_PRIVKEY" 2>/dev/null) + + if [ $? -ne 0 ] || [ -z "$event_json" ]; then + log_error "Failed to create clear auth rules event with nak" + return 1 + fi + + # Send the event using nak directly to relay + 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 + else + log_error "Failed to clear auth rules: $result (exit code: $exit_code)" + return 1 + fi +} + # Test event publishing with a specific key test_event_publishing() { local test_privkey="$1" @@ -198,7 +241,7 @@ test_event_publishing() { return 1 fi - # Send the event using nak directly (more reliable than wscat) + # Send the event using nak directly (more reliable than websocat) log_info "Publishing test event to relay..." local result result=$(echo "$test_event" | timeout 10s nak event "$RELAY_URL" 2>&1) @@ -236,9 +279,6 @@ setup_test_environment() { # Create temporary directory mkdir -p "$TEMP_DIR" - # Clear log file - echo "=== C-Relay Whitelist/Blacklist Test Started at $(date) ===" > "$LOG_FILE" - # Check if required tools are available - like NIP-42 test log_info "Checking dependencies..." @@ -258,9 +298,10 @@ setup_test_environment() { exit 1 fi - if ! command -v wscat &> /dev/null; then - log_warning "wscat not found. Some WebSocket tests may be limited" - log_warning "Install with: npm install -g wscat" + if ! command -v websocat &> /dev/null; then + log_error "websocat not found - required for WebSocket testing" + log_error "Please install websocat for WebSocket communication" + exit 1 fi log_success "Dependencies check complete" @@ -283,10 +324,10 @@ test_admin_authentication() { log "Test $TESTS_RUN: Admin Authentication" # Create a simple configuration event to test admin authentication - local content="Testing admin authentication" + # Using Kind 23455 (ephemeral configuration management) - no d tag needed + local content="{\"action\":\"set\",\"description\":\"Testing admin authentication\"}" local config_event - config_event=$(nak event -k 33334 --content "$content" \ - -t "d=$RELAY_PUBKEY" \ + config_event=$(nak event -k 23455 --content "$content" \ -t "test_auth=true" \ --sec "$ADMIN_PRIVKEY" 2>/dev/null) @@ -295,25 +336,11 @@ test_admin_authentication() { return fi - # DEBUG: Print the full event that will be sent - log_info "=== DEBUG: Full admin event being sent ===" - echo "$config_event" | jq . 2>/dev/null || echo "$config_event" - log_info "=== END DEBUG EVENT ===" - # Send admin event local message="[\"EVENT\",$config_event]" - log_info "=== DEBUG: Full WebSocket message ===" - echo "$message" - log_info "=== END DEBUG MESSAGE ===" - local response response=$(send_websocket_message "$message" "OK" 10) - # DEBUG: Print the full response from server - log_info "=== DEBUG: Full server response ===" - echo "$response" - log_info "=== END DEBUG RESPONSE ===" - if echo "$response" | grep -q '"OK".*true'; then pass_test "Admin authentication successful" else @@ -321,11 +348,65 @@ test_admin_authentication() { fi } -# Test 2: Basic Whitelist Functionality +# Test 2: Auth Rules Storage and Query Test +test_auth_rules_storage_query() { + increment_test + log "Test $TESTS_RUN: Auth Rules Storage and Query Test" + + # Clear all existing rules to start fresh + clear_all_auth_rules + + # Add a simple blacklist rule + log_info "Adding test blacklist rule..." + if send_auth_rule_event "add" "blacklist" "pubkey" "$TEST1_PUBKEY" "Test storage blacklist entry"; then + log_success "Auth rule added successfully" + + # Wait a moment for rule to be processed + sleep 1 + + # Query all auth rules using admin query + log_info "Querying all auth rules..." + local query_event + query_event=$(nak event -k 23456 --content "{\"action\":\"list_all\"}" \ + -t "auth_query=list_all" \ + --sec "$ADMIN_PRIVKEY" 2>/dev/null) + + if [ $? -ne 0 ] || [ -z "$query_event" ]; then + fail_test "Failed to create auth query event" + return + fi + + # Send the query event + log_info "Sending auth query to relay..." + local query_result + query_result=$(echo "$query_event" | timeout 10s nak event "$RELAY_URL" 2>&1) + local exit_code=$? + + log_info "Auth query result: $query_result" + + # Check if we got a response and if it contains our test rule + if [ $exit_code -eq 0 ]; then + if echo "$query_result" | grep -q "$TEST1_PUBKEY"; then + pass_test "Auth rule storage and query working - found test rule in query results" + else + fail_test "Auth rule not found in query results - rule may not have been stored" + fi + else + fail_test "Auth query failed: $query_result" + fi + else + fail_test "Failed to add auth rule for storage test" + fi +} + +# Test 3: Basic Whitelist Functionality test_basic_whitelist() { increment_test log "Test $TESTS_RUN: Basic Whitelist Functionality" + # Clear all existing rules to start fresh + clear_all_auth_rules + # Add TEST1 pubkey to whitelist if send_auth_rule_event "add" "whitelist" "pubkey" "$TEST1_PUBKEY" "Test whitelist entry"; then # Test that whitelisted pubkey can publish @@ -339,11 +420,14 @@ test_basic_whitelist() { fi } -# Test 3: Basic Blacklist Functionality +# Test 4: Basic Blacklist Functionality test_basic_blacklist() { increment_test log "Test $TESTS_RUN: Basic Blacklist Functionality" + # Clear all existing rules to start fresh + clear_all_auth_rules + # Add TEST2 pubkey to blacklist if send_auth_rule_event "add" "blacklist" "pubkey" "$TEST2_PUBKEY" "Test blacklist entry"; then # Test that blacklisted pubkey cannot publish @@ -357,11 +441,20 @@ test_basic_blacklist() { fi } -# Test 4: Rule Removal +# Test 5: Rule Removal test_rule_removal() { increment_test log "Test $TESTS_RUN: Rule Removal" + # Clear all existing rules to start fresh + clear_all_auth_rules + + # First add TEST2 to blacklist to test removal + if ! send_auth_rule_event "add" "blacklist" "pubkey" "$TEST2_PUBKEY" "Test blacklist for removal"; then + fail_test "Failed to add pubkey to blacklist for removal test" + return + fi + # Remove TEST2 from blacklist if send_auth_rule_event "remove" "blacklist" "pubkey" "$TEST2_PUBKEY" "Remove test blacklist entry"; then # Test that previously blacklisted pubkey can now publish @@ -375,11 +468,14 @@ test_rule_removal() { fi } -# Test 5: Multiple Users Scenario +# Test 6: Multiple Users Scenario test_multiple_users() { increment_test log "Test $TESTS_RUN: Multiple Users Scenario" + # Clear all existing rules to start fresh + clear_all_auth_rules + # Add TEST1 to whitelist and TEST3 to blacklist local success_count=0 @@ -408,11 +504,14 @@ test_multiple_users() { fi } -# Test 6: Priority Testing (Blacklist vs Whitelist) +# Test 7: Priority Testing (Blacklist vs Whitelist) test_priority_rules() { increment_test log "Test $TESTS_RUN: Priority Rules Testing" + # Clear all existing rules to start fresh + clear_all_auth_rules + # Add same pubkey to both whitelist and blacklist local setup_success=0 @@ -438,11 +537,14 @@ test_priority_rules() { fi } -# Test 7: Hash-based Blacklist +# Test 8: Hash-based Blacklist test_hash_blacklist() { increment_test log "Test $TESTS_RUN: Hash-based Blacklist" + # Clear all existing rules to start fresh + clear_all_auth_rules + # Create a test event to get its hash local test_content="Content to be blacklisted by hash" local test_event @@ -482,11 +584,14 @@ test_hash_blacklist() { fi } -# Test 8: WebSocket Connection Behavior +# Test 9: WebSocket Connection Behavior test_websocket_behavior() { increment_test log "Test $TESTS_RUN: WebSocket Connection Behavior" + # Clear all existing rules to start fresh + clear_all_auth_rules + # Test that the WebSocket connection handles multiple rapid requests local rapid_success_count=0 @@ -516,11 +621,14 @@ test_websocket_behavior() { fi } -# Test 9: Rule Persistence Verification +# Test 10: Rule Persistence Verification test_rule_persistence() { increment_test log "Test $TESTS_RUN: Rule Persistence Verification" + # Clear all existing rules to start fresh + clear_all_auth_rules + # Add a rule, then verify it persists by testing enforcement if send_auth_rule_event "add" "blacklist" "pubkey" "$TEST3_PUBKEY" "Persistence test blacklist"; then # Wait a moment for rule to be processed @@ -546,7 +654,7 @@ test_rule_persistence() { fi } -# Test 10: Cleanup and Final Verification +# Test 11: Cleanup and Final Verification test_cleanup_verification() { increment_test log "Test $TESTS_RUN: Cleanup and Final Verification" @@ -589,10 +697,11 @@ run_all_tests() { # Setup setup_test_environment - # Run only test 1 for debugging admin authentication - test_admin_authentication + # Clear all auth rules before starting tests + clear_all_auth_rules - # Comment out other tests for now to focus on debugging + # test_admin_authentication + # test_auth_rules_storage_query # test_basic_whitelist # test_basic_blacklist # test_rule_removal @@ -648,8 +757,8 @@ main() { echo -e "${BLUE}===============================================${RESET}" echo "" - # Check if relay is running - use the same method we verified manually - if ! echo '["REQ","connection_test",{}]' | timeout 5 wscat -c "$RELAY_URL" >/dev/null 2>&1; then + # Check if relay is running - using websocat like the working tests + if ! echo '["REQ","connection_test",{}]' | timeout 5 websocat "$RELAY_URL" >/dev/null 2>&1; then log_error "Cannot connect to relay at $RELAY_URL" log_error "Please ensure the C-Relay server is running in test mode" exit 1 @@ -661,12 +770,10 @@ main() { if run_all_tests; then echo "" log_success "All whitelist/blacklist tests completed successfully!" - echo -e "Test log saved to: ${YELLOW}$LOG_FILE${RESET}" exit 0 else echo "" - log_error "Some tests failed. Check the log for details." - echo -e "Test log saved to: ${YELLOW}$LOG_FILE${RESET}" + log_error "Some tests failed." exit 1 fi } diff --git a/whitelist_blacklist_test.log b/whitelist_blacklist_test.log index 7e1a121..0d11259 100644 --- a/whitelist_blacklist_test.log +++ b/whitelist_blacklist_test.log @@ -1,21 +1,27 @@ -=== C-Relay Whitelist/Blacklist Test Started at Tue Sep 23 11:20:40 AM EDT 2025 === +=== 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=eab7cac03049d07f... -[INFO] Generated keypair for TEST2: pubkey=4e07a99f656d5301... -[INFO] Generated keypair for TEST3: pubkey=bf48b836426805cb... +[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 -[11:20:41] Test 1: Admin Authentication +[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: ["EVENT",{"kind":33334,"id":"ce73fa326eb558505742770eb927a50edc16a69512089939f76da90c7ca5291f","pubk... +[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: ["EVENT",{"kind":33334,"id":"ce73fa326eb558505742770eb927a50edc16a69512089939f76da90c7ca5291f","pubk... +[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. -[11:20:42] Cleaning up test environment... -[INFO] Temporary directory removed: /tmp/c_relay_test_1773069 -[11:20:42] Test cleanup completed. +[07:28:42] Cleaning up test environment... +[INFO] Temporary directory removed: /tmp/c_relay_test_184904 +[07:28:42] Test cleanup completed.