Compare commits

...

4 Commits

Author SHA1 Message Date
Your Name
6fd3e531c3 v0.3.15 - How can administration take so long 2025-09-27 15:50:42 -04:00
Your Name
c1c05991cf v0.3.14 - I think the admin api is finally working 2025-09-27 14:08:45 -04:00
Your Name
ab378e14d1 v0.3.13 - Working on admin system 2025-09-27 13:32:21 -04:00
Your Name
c0f9bf9ef5 v0.3.12 - Working through auth still 2025-09-25 17:33:38 -04:00
11 changed files with 2001 additions and 764 deletions

View File

@@ -27,7 +27,7 @@
## Critical Integration Issues ## Critical Integration Issues
### Event-Based Configuration System ### Event-Based Configuration System
- **No traditional config files** - all configuration stored as kind 33334 Nostr events - **No traditional config files** - all configuration stored in config table
- Admin private key shown **only once** on first startup - Admin private key shown **only once** on first startup
- Configuration changes require cryptographically signed events - Configuration changes require cryptographically signed events
- Database path determined by generated relay pubkey - Database path determined by generated relay pubkey
@@ -35,7 +35,7 @@
### First-Time Startup Sequence ### First-Time Startup Sequence
1. Relay generates admin keypair and relay keypair 1. Relay generates admin keypair and relay keypair
2. Creates database file with relay pubkey as filename 2. Creates database file with relay pubkey as filename
3. Stores default configuration as kind 33334 event 3. Stores default configuration in config table
4. **CRITICAL**: Admin private key displayed once and never stored on disk 4. **CRITICAL**: Admin private key displayed once and never stored on disk
### Port Management ### Port Management
@@ -51,7 +51,7 @@
### Configuration Event Structure ### Configuration Event Structure
```json ```json
{ {
"kind": 33334, "kind": 23455,
"content": "C Nostr Relay Configuration", "content": "C Nostr Relay Configuration",
"tags": [ "tags": [
["d", "<relay_pubkey>"], ["d", "<relay_pubkey>"],

View File

@@ -76,6 +76,7 @@ All commands are sent as nip44 encrypted content. The following table lists all
| **Auth Rules Management** | | **Auth Rules Management** |
| `auth_add_blacklist` | `["blacklist", "pubkey", "abc123..."]` | Add pubkey to blacklist | | `auth_add_blacklist` | `["blacklist", "pubkey", "abc123..."]` | Add pubkey to blacklist |
| `auth_add_whitelist` | `["whitelist", "pubkey", "def456..."]` | Add pubkey to whitelist | | `auth_add_whitelist` | `["whitelist", "pubkey", "def456..."]` | Add pubkey to whitelist |
| `auth_delete_rule` | `["delete_auth_rule", "blacklist", "pubkey", "abc123..."]` | Delete specific auth rule |
| `auth_query_all` | `["auth_query", "all"]` | Query all auth rules | | `auth_query_all` | `["auth_query", "all"]` | Query all auth rules |
| `auth_query_type` | `["auth_query", "whitelist"]` | Query specific rule type | | `auth_query_type` | `["auth_query", "whitelist"]` | Query specific rule type |
| `auth_query_pattern` | `["auth_query", "pattern", "abc123..."]` | Query specific pattern | | `auth_query_pattern` | `["auth_query", "pattern", "abc123..."]` | Query specific pattern |

File diff suppressed because it is too large Load Diff

View File

@@ -282,14 +282,14 @@ cd build
# Start relay in background and capture its PID # Start relay in background and capture its PID
if [ "$USE_TEST_KEYS" = true ]; then if [ "$USE_TEST_KEYS" = true ]; then
echo "Using deterministic test keys for development..." echo "Using deterministic test keys for development..."
./$(basename $BINARY_PATH) -a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -r 1111111111111111111111111111111111111111111111111111111111111111 > ../relay.log 2>&1 & ./$(basename $BINARY_PATH) -a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -r 1111111111111111111111111111111111111111111111111111111111111111 --strict-port > ../relay.log 2>&1 &
elif [ -n "$RELAY_ARGS" ]; then elif [ -n "$RELAY_ARGS" ]; then
echo "Starting relay with custom configuration..." echo "Starting relay with custom configuration..."
./$(basename $BINARY_PATH) $RELAY_ARGS > ../relay.log 2>&1 & ./$(basename $BINARY_PATH) $RELAY_ARGS --strict-port > ../relay.log 2>&1 &
else else
# No command line arguments needed for random key generation # No command line arguments needed for random key generation
echo "Starting relay with random key generation..." echo "Starting relay with random key generation..."
./$(basename $BINARY_PATH) > ../relay.log 2>&1 & ./$(basename $BINARY_PATH) --strict-port > ../relay.log 2>&1 &
fi fi
RELAY_PID=$! RELAY_PID=$!
# Change back to original directory # Change back to original directory

View File

@@ -1 +1 @@
285781 659207

File diff suppressed because it is too large Load Diff

View File

@@ -98,6 +98,7 @@ typedef struct {
int port_override; // -1 = not set, >0 = port value int port_override; // -1 = not set, >0 = port value
char admin_privkey_override[65]; // Empty string = not set, 64-char hex = override char admin_privkey_override[65]; // Empty string = not set, 64-char hex = override
char relay_privkey_override[65]; // Empty string = not set, 64-char hex = override char relay_privkey_override[65]; // Empty string = not set, 64-char hex = override
int strict_port; // 0 = allow port increment, 1 = fail if exact port unavailable
} cli_options_t; } cli_options_t;
// Global unified configuration cache // Global unified configuration cache
@@ -172,10 +173,10 @@ int process_admin_auth_event(cJSON* event, char* error_message, size_t error_siz
int handle_kind_23456_unified(cJSON* event, char* error_message, size_t error_size, struct lws* wsi); int handle_kind_23456_unified(cJSON* event, char* error_message, size_t error_size, struct lws* wsi);
int handle_auth_query_unified(cJSON* event, const char* query_type, char* error_message, size_t error_size, struct lws* wsi); int handle_auth_query_unified(cJSON* event, const char* query_type, char* error_message, size_t error_size, struct lws* wsi);
int handle_system_command_unified(cJSON* event, const char* command, char* error_message, size_t error_size, struct lws* wsi); int handle_system_command_unified(cJSON* event, const char* command, char* error_message, size_t error_size, struct lws* wsi);
int handle_auth_rule_modification_unified(cJSON* event, char* error_message, size_t error_size); int handle_auth_rule_modification_unified(cJSON* event, char* error_message, size_t error_size, struct lws* wsi);
// WebSocket response functions // Admin response functions
int send_websocket_response_data(cJSON* event, cJSON* response_data, struct lws* wsi); int send_admin_response_event(const cJSON* response_data, const char* recipient_pubkey, struct lws* wsi);
cJSON* build_query_response(const char* query_type, cJSON* results_array, int total_count); cJSON* build_query_response(const char* query_type, cJSON* results_array, int total_count);
// Auth rules management functions // Auth rules management functions

View File

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

View File

@@ -224,10 +224,7 @@ int handle_event_message(cJSON* event, char* error_message, size_t error_size);
// Forward declaration for unified validation // Forward declaration for unified validation
int nostr_validate_unified_request(const char* json_string, size_t json_length); int nostr_validate_unified_request(const char* json_string, size_t json_length);
// Forward declaration for configuration event handling (kind 33334) // Forward declaration for admin event processing (kinds 23455 and 23456)
int handle_configuration_event(cJSON* event, char* error_message, size_t error_size);
// Forward declaration for admin event processing (kinds 33334 and 33335)
int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size, struct lws* wsi); int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size, struct lws* wsi);
// Forward declaration for enhanced admin event authorization // Forward declaration for enhanced admin event authorization
@@ -3035,7 +3032,7 @@ int is_authorized_admin_event(cJSON* event, char* error_buffer, size_t error_buf
} }
int event_kind = kind_json->valueint; int event_kind = kind_json->valueint;
if (event_kind != 33334 && event_kind != 33335 && event_kind != 23455 && event_kind != 23456) { if (event_kind != 23455 && event_kind != 23456) {
snprintf(error_buffer, error_buffer_size, "Event kind %d is not an admin event type", event_kind); snprintf(error_buffer, error_buffer_size, "Event kind %d is not an admin event type", event_kind);
return -1; return -1;
} }
@@ -3203,11 +3200,18 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
memcpy(message, in, len); memcpy(message, in, len);
message[len] = '\0'; message[len] = '\0';
log_info("Received WebSocket message"); // Parse JSON message (this is the normal program flow)
// Parse JSON message
cJSON* json = cJSON_Parse(message); cJSON* json = cJSON_Parse(message);
if (json && cJSON_IsArray(json)) { if (json && cJSON_IsArray(json)) {
// Log the complete parsed JSON message once
char* complete_message = cJSON_Print(json);
if (complete_message) {
char debug_msg[2048];
snprintf(debug_msg, sizeof(debug_msg),
"Received complete WebSocket message: %s", complete_message);
log_info(debug_msg);
free(complete_message);
}
// Get message type // Get message type
cJSON* type = cJSON_GetArrayItem(json, 0); cJSON* type = cJSON_GetArrayItem(json, 0);
if (type && cJSON_IsString(type)) { if (type && cJSON_IsString(type)) {
@@ -3349,7 +3353,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
// Cleanup event JSON string // Cleanup event JSON string
free(event_json_str); free(event_json_str);
// Check for admin events (kinds 33334, 33335, 23455, and 23456) and intercept them // Check for admin events (kinds 23455 and 23456) and intercept them
if (result == 0) { if (result == 0) {
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind"); cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
if (kind_obj && cJSON_IsNumber(kind_obj)) { if (kind_obj && cJSON_IsNumber(kind_obj)) {
@@ -3371,7 +3375,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
} }
} }
if (event_kind == 33334 || event_kind == 33335 || event_kind == 23455 || event_kind == 23456) { if (event_kind == 23455 || event_kind == 23456) {
// Enhanced admin event security - check authorization first // Enhanced admin event security - check authorization first
log_info("DEBUG ADMIN: Admin event detected, checking authorization"); log_info("DEBUG ADMIN: Admin event detected, checking authorization");
@@ -3690,7 +3694,7 @@ int check_port_available(int port) {
} }
// Start libwebsockets-based WebSocket Nostr relay server // Start libwebsockets-based WebSocket Nostr relay server
int start_websocket_relay(int port_override) { int start_websocket_relay(int port_override, int strict_port) {
struct lws_context_creation_info info; struct lws_context_creation_info info;
log_info("Starting libwebsockets-based Nostr relay server..."); log_info("Starting libwebsockets-based Nostr relay server...");
@@ -3700,7 +3704,7 @@ int start_websocket_relay(int port_override) {
int configured_port = (port_override > 0) ? port_override : get_config_int("relay_port", DEFAULT_PORT); int configured_port = (port_override > 0) ? port_override : get_config_int("relay_port", DEFAULT_PORT);
int actual_port = configured_port; int actual_port = configured_port;
int port_attempts = 0; int port_attempts = 0;
const int max_port_attempts = 5; const int max_port_attempts = 10; // Increased from 5 to 10
// Minimal libwebsockets configuration // Minimal libwebsockets configuration
info.protocols = protocols; info.protocols = protocols;
@@ -3719,8 +3723,8 @@ int start_websocket_relay(int port_override) {
// Max payload size for Nostr events // Max payload size for Nostr events
info.max_http_header_data = 4096; info.max_http_header_data = 4096;
// Find an available port with pre-checking // Find an available port with pre-checking (or fail immediately in strict mode)
while (port_attempts < max_port_attempts) { while (port_attempts < (strict_port ? 1 : max_port_attempts)) {
char attempt_msg[256]; char attempt_msg[256];
snprintf(attempt_msg, sizeof(attempt_msg), "Checking port availability: %d", actual_port); snprintf(attempt_msg, sizeof(attempt_msg), "Checking port availability: %d", actual_port);
log_info(attempt_msg); log_info(attempt_msg);
@@ -3728,7 +3732,13 @@ int start_websocket_relay(int port_override) {
// Pre-check if port is available // Pre-check if port is available
if (!check_port_available(actual_port)) { if (!check_port_available(actual_port)) {
port_attempts++; port_attempts++;
if (port_attempts < max_port_attempts) { if (strict_port) {
char error_msg[256];
snprintf(error_msg, sizeof(error_msg),
"Strict port mode: port %d is not available", actual_port);
log_error(error_msg);
return -1;
} else if (port_attempts < max_port_attempts) {
char retry_msg[256]; char retry_msg[256];
snprintf(retry_msg, sizeof(retry_msg), "Port %d is in use, trying port %d (attempt %d/%d)", snprintf(retry_msg, sizeof(retry_msg), "Port %d is in use, trying port %d (attempt %d/%d)",
actual_port, actual_port + 1, port_attempts + 1, max_port_attempts); actual_port, actual_port + 1, port_attempts + 1, max_port_attempts);
@@ -3767,7 +3777,13 @@ int start_websocket_relay(int port_override) {
log_warning(lws_error_msg); log_warning(lws_error_msg);
port_attempts++; port_attempts++;
if (port_attempts < max_port_attempts) { if (strict_port) {
char error_msg[256];
snprintf(error_msg, sizeof(error_msg),
"Strict port mode: failed to bind to port %d", actual_port);
log_error(error_msg);
break;
} else if (port_attempts < max_port_attempts) {
actual_port++; actual_port++;
continue; continue;
} }
@@ -3833,6 +3849,7 @@ void print_usage(const char* program_name) {
printf(" -p, --port PORT Override relay port (first-time startup only)\n"); printf(" -p, --port PORT Override relay port (first-time startup only)\n");
printf(" -a, --admin-privkey HEX Override admin private key (64-char hex)\n"); printf(" -a, --admin-privkey HEX Override admin private key (64-char hex)\n");
printf(" -r, --relay-privkey HEX Override relay private key (64-char hex)\n"); printf(" -r, --relay-privkey HEX Override relay private key (64-char hex)\n");
printf(" --strict-port Fail if exact port is unavailable (no port increment)\n");
printf("\n"); printf("\n");
printf("Configuration:\n"); printf("Configuration:\n");
printf(" This relay uses event-based configuration stored in the database.\n"); printf(" This relay uses event-based configuration stored in the database.\n");
@@ -3841,10 +3858,16 @@ void print_usage(const char* program_name) {
printf(" After initial setup, all configuration is managed via database events.\n"); printf(" After initial setup, all configuration is managed via database events.\n");
printf(" Database file: <relay_pubkey>.db (created automatically)\n"); printf(" Database file: <relay_pubkey>.db (created automatically)\n");
printf("\n"); printf("\n");
printf("Port Binding:\n");
printf(" Default: Try up to 10 consecutive ports if requested port is busy\n");
printf(" --strict-port: Fail immediately if exact requested port is unavailable\n");
printf("\n");
printf("Examples:\n"); printf("Examples:\n");
printf(" %s # Start relay (auto-configure on first run)\n", program_name); printf(" %s # Start relay (auto-configure on first run)\n", program_name);
printf(" %s -p 8080 # First-time setup with port 8080\n", program_name); printf(" %s -p 8080 # First-time setup with port 8080\n", program_name);
printf(" %s --port 9000 # First-time setup with port 9000\n", program_name); printf(" %s --port 9000 # First-time setup with port 9000\n", program_name);
printf(" %s --strict-port # Fail if default port 8888 is unavailable\n", program_name);
printf(" %s -p 8080 --strict-port # Fail if port 8080 is unavailable\n", program_name);
printf(" %s --help # Show this help\n", program_name); printf(" %s --help # Show this help\n", program_name);
printf(" %s --version # Show version info\n", program_name); printf(" %s --version # Show version info\n", program_name);
printf("\n"); printf("\n");
@@ -3863,7 +3886,8 @@ int main(int argc, char* argv[]) {
cli_options_t cli_options = { cli_options_t cli_options = {
.port_override = -1, // -1 = not set .port_override = -1, // -1 = not set
.admin_privkey_override = {0}, // Empty string = not set .admin_privkey_override = {0}, // Empty string = not set
.relay_privkey_override = {0} // Empty string = not set .relay_privkey_override = {0}, // Empty string = not set
.strict_port = 0 // 0 = allow port increment (default)
}; };
// Parse command line arguments // Parse command line arguments
@@ -3958,6 +3982,10 @@ int main(int argc, char* argv[]) {
i++; // Skip the key argument i++; // Skip the key argument
log_info("Relay private key override specified"); log_info("Relay private key override specified");
} else if (strcmp(argv[i], "--strict-port") == 0) {
// Strict port mode option
cli_options.strict_port = 1;
log_info("Strict port mode enabled - will fail if exact port is unavailable");
} else { } else {
log_error("Unknown argument. Use --help for usage information."); log_error("Unknown argument. Use --help for usage information.");
print_usage(argv[0]); print_usage(argv[0]);
@@ -4179,7 +4207,7 @@ int main(int argc, char* argv[]) {
log_info("Starting relay server..."); log_info("Starting relay server...");
// Start WebSocket Nostr relay server (port from configuration) // Start WebSocket Nostr relay server (port from configuration)
int result = start_websocket_relay(-1); // Let config system determine port int result = start_websocket_relay(-1, cli_options.strict_port); // Let config system determine port, pass strict_port flag
// Cleanup // Cleanup
cleanup_relay_info(); cleanup_relay_info();

View File

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

View File

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