Compare commits

...

17 Commits

Author SHA1 Message Date
Your Name
88b4aaa301 v0.4.6 - Implement NIP-50 search functionality with LIKE-based content and tag searching 2025-10-03 05:43:49 -04:00
Your Name
eac4c227c9 v0.4.5 - Fix NIP-45 COUNT test to account for existing relay events and handle replaceable events correctly 2025-10-03 05:19:39 -04:00
Your Name
d5eb7d4a55 v0.4.4 - Just waking up 2025-10-03 04:52:40 -04:00
Your Name
80b15e16e2 v0.4.3 - feat: Implement dynamic configuration updates without restart
- Add cache refresh mechanism for config updates
- Implement selective re-initialization for NIP-11 relay info changes
- Categorize configs as dynamic vs restart-required using requires_restart field
- Enhance admin API responses with restart requirement information
- Add comprehensive test for dynamic config updates
- Update documentation for dynamic configuration capabilities

Most relay settings can now be updated via admin API without requiring restart, improving operational flexibility while maintaining stability for critical changes.
2025-10-02 15:53:26 -04:00
Your Name
cfacedbb1a v0.4.2 - Implement NIP-11 Relay Information Document with event-based configuration - make relay info dynamically configurable via admin API 2025-10-02 11:38:28 -04:00
Your Name
c3bab033ed v0.4.1 - Fixed startup bug 2025-10-01 17:23:50 -04:00
Your Name
524f9bd84f Last push before major bug fixes 2025-10-01 14:53:20 -04:00
Your Name
4658ede9d6 feat: Implement auth rules enforcement and fix subscription filtering issues
- **Auth Rules Implementation**: Added blacklist/whitelist enforcement in websockets.c
  - Events are now checked against auth_rules table before acceptance
  - Blacklist blocks specific pubkeys, whitelist enables allow-only mode
  - Made check_database_auth_rules() public for cross-module access

- **Subscription Filtering Fixes**:
  - Added missing 'ids' filter support in SQL query building
  - Fixed test expectations to not require exact event counts for kind filters
  - Improved filter validation and error handling

- **Ephemeral Events Compliance**:
  - Modified SQL queries to exclude kinds 20000-29999 from historical queries
  - Maintains broadcasting to active subscribers while preventing storage/retrieval
  - Ensures NIP-01 compliance for ephemeral event handling

- **Comprehensive Testing**:
  - Created white_black_test.sh with full blacklist/whitelist functionality testing
  - Tests verify blocked posting for blacklisted users
  - Tests verify whitelist-only mode when whitelist rules exist
  - Includes proper auth rule clearing between test phases

- **Code Quality**:
  - Added proper function declarations to websockets.h
  - Improved error handling and logging throughout
  - Enhanced test script with clear pass/fail reporting
2025-09-30 15:17:59 -04:00
Your Name
f7b463aca1 Fixing whitelist and blacklist functionality 2025-09-30 15:02:49 -04:00
Your Name
c1a6e92b1d v0.3.19 - last save before major refactoring 2025-09-30 10:47:11 -04:00
Your Name
eefb0e427e v0.3.18 - index.html improvements 2025-09-30 07:51:23 -04:00
Your Name
c23d81b740 v0.3.17 - Embedded login button 2025-09-30 06:47:09 -04:00
Your Name
6dac231040 v0.3.16 - Admin system getting better 2025-09-30 05:32:23 -04:00
Your Name
6fd3e531c3 v0.3.15 - How can administration take so long 2025-09-27 15:50:42 -04:00
Your Name
c1c05991cf v0.3.14 - I think the admin api is finally working 2025-09-27 14:08:45 -04:00
Your Name
ab378e14d1 v0.3.13 - Working on admin system 2025-09-27 13:32:21 -04:00
Your Name
c0f9bf9ef5 v0.3.12 - Working through auth still 2025-09-25 17:33:38 -04:00
37 changed files with 10002 additions and 5644 deletions

3
.gitignore vendored
View File

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

32
07.md
View File

@@ -1,32 +0,0 @@
NIP-07
======
`window.nostr` capability for web browsers
------------------------------------------
`draft` `optional`
The `window.nostr` object may be made available by web browsers or extensions and websites or web-apps may make use of it after checking its availability.
That object must define the following methods:
```
async window.nostr.getPublicKey(): string // returns a public key as hex
async window.nostr.signEvent(event: { created_at: number, kind: number, tags: string[][], content: string }): Event // takes an event object, adds `id`, `pubkey` and `sig` and returns it
```
Aside from these two basic above, the following functions can also be implemented optionally:
```
async window.nostr.nip04.encrypt(pubkey, plaintext): string // returns ciphertext and iv as specified in nip-04 (deprecated)
async window.nostr.nip04.decrypt(pubkey, ciphertext): string // takes ciphertext and iv as specified in nip-04 (deprecated)
async window.nostr.nip44.encrypt(pubkey, plaintext): string // returns ciphertext as specified in nip-44
async window.nostr.nip44.decrypt(pubkey, ciphertext): string // takes ciphertext as specified in nip-44
```
### Recommendation to Extension Authors
To make sure that the `window.nostr` is available to nostr clients on page load, the authors who create Chromium and Firefox extensions should load their scripts by specifying `"run_at": "document_end"` in the extension's manifest.
### Implementation
See https://github.com/aljazceru/awesome-nostr#nip-07-browser-extensions.

View File

@@ -27,7 +27,7 @@
## Critical Integration Issues
### 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
- Configuration changes require cryptographically signed events
- Database path determined by generated relay pubkey
@@ -35,7 +35,7 @@
### First-Time Startup Sequence
1. Relay generates admin keypair and relay keypair
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
### Port Management
@@ -48,20 +48,30 @@
- Schema version 4 with JSON tag storage
- **Critical**: Event expiration filtering done at application level, not SQL level
### Configuration Event Structure
### Admin API Event Structure
```json
{
"kind": 33334,
"content": "C Nostr Relay Configuration",
"kind": 23456,
"content": "base64_nip44_encrypted_command_array",
"tags": [
["d", "<relay_pubkey>"],
["relay_description", "value"],
["max_subscriptions_per_client", "25"],
["pow_min_difficulty", "16"]
["p", "<relay_pubkey>"]
]
}
```
**Configuration Commands** (encrypted in content):
- `["relay_description", "My Relay"]`
- `["max_subscriptions_per_client", "25"]`
- `["pow_min_difficulty", "16"]`
**Auth Rule Commands** (encrypted in content):
- `["blacklist", "pubkey", "hex_pubkey_value"]`
- `["whitelist", "pubkey", "hex_pubkey_value"]`
**Query Commands** (encrypted in content):
- `["auth_query", "all"]`
- `["system_command", "system_status"]`
### Process Management
```bash
# Kill existing relay processes

View File

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

120
Makefile
View File

@@ -9,7 +9,7 @@ LIBS = -lsqlite3 -lwebsockets -lz -ldl -lpthread -lm -L/usr/local/lib -lsecp256k
BUILD_DIR = build
# Source files
MAIN_SRC = src/main.c src/config.c src/request_validator.c
MAIN_SRC = src/main.c src/config.c src/request_validator.c src/nip009.c src/nip011.c src/nip013.c src/nip040.c src/nip042.c src/websockets.c src/subscriptions.c
NOSTR_CORE_LIB = nostr_core_lib/libnostr_core_x64.a
# Architecture detection
@@ -36,10 +36,10 @@ $(NOSTR_CORE_LIB):
@echo "Building nostr_core_lib..."
cd nostr_core_lib && ./build.sh
# Generate version.h from git tags
src/version.h:
# Generate main.h from git tags
src/main.h:
@if [ -d .git ]; then \
echo "Generating version.h from git tags..."; \
echo "Generating main.h from git tags..."; \
RAW_VERSION=$$(git describe --tags --always 2>/dev/null || echo "unknown"); \
if echo "$$RAW_VERSION" | grep -q "^v[0-9]"; then \
CLEAN_VERSION=$$(echo "$$RAW_VERSION" | sed 's/^v//' | cut -d- -f1); \
@@ -51,54 +51,98 @@ src/version.h:
VERSION="v0.0.0"; \
MAJOR=0; MINOR=0; PATCH=0; \
fi; \
echo "/* Auto-generated version information */" > src/version.h; \
echo "#ifndef VERSION_H" >> src/version.h; \
echo "#define VERSION_H" >> src/version.h; \
echo "" >> src/version.h; \
echo "#define VERSION \"$$VERSION\"" >> src/version.h; \
echo "#define VERSION_MAJOR $$MAJOR" >> src/version.h; \
echo "#define VERSION_MINOR $$MINOR" >> src/version.h; \
echo "#define VERSION_PATCH $$PATCH" >> src/version.h; \
echo "" >> src/version.h; \
echo "#endif /* VERSION_H */" >> src/version.h; \
echo "Generated version.h with clean version: $$VERSION"; \
elif [ ! -f src/version.h ]; then \
echo "Git not available and version.h missing, creating fallback version.h..."; \
echo "/*" > src/main.h; \
echo " * C-Relay Main Header - Version and Metadata Information" >> src/main.h; \
echo " *" >> src/main.h; \
echo " * This header contains version information and relay metadata that is" >> src/main.h; \
echo " * automatically updated by the build system (build_and_push.sh)." >> src/main.h; \
echo " *" >> src/main.h; \
echo " * The build_and_push.sh script updates VERSION and related macros when" >> src/main.h; \
echo " * creating new releases." >> src/main.h; \
echo " */" >> src/main.h; \
echo "" >> src/main.h; \
echo "#ifndef MAIN_H" >> src/main.h; \
echo "#define MAIN_H" >> src/main.h; \
echo "" >> src/main.h; \
echo "// Version information (auto-updated by build_and_push.sh)" >> src/main.h; \
echo "#define VERSION \"$$VERSION\"" >> src/main.h; \
echo "#define VERSION_MAJOR $$MAJOR" >> src/main.h; \
echo "#define VERSION_MINOR $$MINOR" >> src/main.h; \
echo "#define VERSION_PATCH $$PATCH" >> src/main.h; \
echo "" >> src/main.h; \
echo "// Relay metadata (authoritative source for NIP-11 information)" >> src/main.h; \
echo "#define RELAY_NAME \"C-Relay\"" >> src/main.h; \
echo "#define RELAY_DESCRIPTION \"High-performance C Nostr relay with SQLite storage\"" >> src/main.h; \
echo "#define RELAY_CONTACT \"\"" >> src/main.h; \
echo "#define RELAY_SOFTWARE \"https://git.laantungir.net/laantungir/c-relay.git\"" >> src/main.h; \
echo "#define RELAY_VERSION VERSION // Use the same version as the build" >> src/main.h; \
echo "#define SUPPORTED_NIPS \"1,2,4,9,11,12,13,15,16,20,22,33,40,42\"" >> src/main.h; \
echo "#define LANGUAGE_TAGS \"\"" >> src/main.h; \
echo "#define RELAY_COUNTRIES \"\"" >> src/main.h; \
echo "#define POSTING_POLICY \"\"" >> src/main.h; \
echo "#define PAYMENTS_URL \"\"" >> src/main.h; \
echo "" >> src/main.h; \
echo "#endif /* MAIN_H */" >> src/main.h; \
echo "Generated main.h with clean version: $$VERSION"; \
elif [ ! -f src/main.h ]; then \
echo "Git not available and main.h missing, creating fallback main.h..."; \
VERSION="v0.0.0"; \
echo "/* Auto-generated version information */" > src/version.h; \
echo "#ifndef VERSION_H" >> src/version.h; \
echo "#define VERSION_H" >> src/version.h; \
echo "" >> src/version.h; \
echo "#define VERSION \"$$VERSION\"" >> src/version.h; \
echo "#define VERSION_MAJOR 0" >> src/version.h; \
echo "#define VERSION_MINOR 0" >> src/version.h; \
echo "#define VERSION_PATCH 0" >> src/version.h; \
echo "" >> src/version.h; \
echo "#endif /* VERSION_H */" >> src/version.h; \
echo "Created fallback version.h with version: $$VERSION"; \
echo "/*" > src/main.h; \
echo " * C-Relay Main Header - Version and Metadata Information" >> src/main.h; \
echo " *" >> src/main.h; \
echo " * This header contains version information and relay metadata that is" >> src/main.h; \
echo " * automatically updated by the build system (build_and_push.sh)." >> src/main.h; \
echo " *" >> src/main.h; \
echo " * The build_and_push.sh script updates VERSION and related macros when" >> src/main.h; \
echo " * creating new releases." >> src/main.h; \
echo " */" >> src/main.h; \
echo "" >> src/main.h; \
echo "#ifndef MAIN_H" >> src/main.h; \
echo "#define MAIN_H" >> src/main.h; \
echo "" >> src/main.h; \
echo "// Version information (auto-updated by build_and_push.sh)" >> src/main.h; \
echo "#define VERSION \"$$VERSION\"" >> src/main.h; \
echo "#define VERSION_MAJOR 0" >> src/main.h; \
echo "#define VERSION_MINOR 0" >> src/main.h; \
echo "#define VERSION_PATCH 0" >> src/main.h; \
echo "" >> src/main.h; \
echo "// Relay metadata (authoritative source for NIP-11 information)" >> src/main.h; \
echo "#define RELAY_NAME \"C-Relay\"" >> src/main.h; \
echo "#define RELAY_DESCRIPTION \"High-performance C Nostr relay with SQLite storage\"" >> src/main.h; \
echo "#define RELAY_CONTACT \"\"" >> src/main.h; \
echo "#define RELAY_SOFTWARE \"https://git.laantungir.net/laantungir/c-relay.git\"" >> src/main.h; \
echo "#define RELAY_VERSION VERSION // Use the same version as the build" >> src/main.h; \
echo "#define SUPPORTED_NIPS \"1,2,4,9,11,12,13,15,16,20,22,33,40,42\"" >> src/main.h; \
echo "#define LANGUAGE_TAGS \"\"" >> src/main.h; \
echo "#define RELAY_COUNTRIES \"\"" >> src/main.h; \
echo "#define POSTING_POLICY \"\"" >> src/main.h; \
echo "#define PAYMENTS_URL \"\"" >> src/main.h; \
echo "" >> src/main.h; \
echo "#endif /* MAIN_H */" >> src/main.h; \
echo "Created fallback main.h with version: $$VERSION"; \
else \
echo "Git not available, preserving existing version.h"; \
echo "Git not available, preserving existing main.h"; \
fi
# Force version.h regeneration (useful for development)
# Force main.h regeneration (useful for development)
force-version:
@echo "Force regenerating version.h..."
@rm -f src/version.h
@$(MAKE) src/version.h
@echo "Force regenerating main.h..."
@rm -f src/main.h
@$(MAKE) src/main.h
# Build the relay
$(TARGET): $(BUILD_DIR) src/version.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB)
$(TARGET): $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB)
@echo "Compiling C-Relay for architecture: $(ARCH)"
$(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(TARGET) $(NOSTR_CORE_LIB) $(LIBS)
@echo "Build complete: $(TARGET)"
# Build for specific architectures
x86: $(BUILD_DIR) src/version.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB)
x86: $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB)
@echo "Building C-Relay for x86_64..."
$(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(BUILD_DIR)/c_relay_x86 $(NOSTR_CORE_LIB) $(LIBS)
@echo "Build complete: $(BUILD_DIR)/c_relay_x86"
arm64: $(BUILD_DIR) src/version.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB)
arm64: $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB)
@echo "Cross-compiling C-Relay for ARM64..."
@if ! command -v aarch64-linux-gnu-gcc >/dev/null 2>&1; then \
echo "ERROR: ARM64 cross-compiler not found."; \
@@ -171,7 +215,7 @@ init-db:
# Clean build artifacts
clean:
rm -rf $(BUILD_DIR)
rm -f src/version.h
rm -f src/main.h
@echo "Clean complete"
# Clean everything including nostr_core_lib
@@ -210,6 +254,6 @@ help:
@echo " make check-toolchain # Check what compilers are available"
@echo " make test # Run tests"
@echo " make init-db # Set up database"
@echo " make force-version # Force regenerate version.h from git"
@echo " make force-version # Force regenerate main.h from git"
.PHONY: all x86 arm64 test init-db clean clean-all install-deps install-cross-tools install-arm64-deps check-toolchain help force-version

View File

@@ -18,13 +18,13 @@ Do NOT modify the formatting, add emojis, or change the text. Keep the simple fo
- [x] NIP-33: Parameterized Replaceable Events
- [x] NIP-40: Expiration Timestamp
- [x] NIP-42: Authentication of clients to relays
- [ ] NIP-45: Counting results
- [x] NIP-45: Counting results
- [ ] 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. All admin commands use **tag-based parameters** for simplicity and compatibility.
C-Relay uses an innovative **event-based administration system** where all configuration and management commands are sent as signed Nostr events using the admin private key generated during first startup. All admin commands use **NIP-44 encrypted command arrays** for security and compatibility.
### Authentication
@@ -32,7 +32,7 @@ All admin commands require signing with the admin private key displayed during f
### Event Structure
All admin commands use the same unified event structure with tag-based parameters:
All admin commands use the same unified event structure with NIP-44 encrypted content:
**Admin Command Event:**
```json
@@ -41,14 +41,16 @@ All admin commands use the same unified event structure with tag-based parameter
"pubkey": "admin_public_key",
"created_at": 1234567890,
"kind": 23456,
"content": "<nip44 encrypted command>",
"content": "AqHBUgcM7dXFYLQuDVzGwMST1G8jtWYyVvYxXhVGEu4nAb4LVw...",
"tags": [
["p", "relay_public_key"],
["p", "relay_public_key"]
],
"sig": "event_signature"
}
```
The `content` field contains a NIP-44 encrypted JSON array representing the command.
**Admin Response Event:**
```json
["EVENT", "temp_sub_id", {
@@ -56,7 +58,7 @@ All admin commands use the same unified event structure with tag-based parameter
"pubkey": "relay_public_key",
"created_at": 1234567890,
"kind": 23457,
"content": "<nip44 encrypted response>",
"content": "BpKCVhfN8eYtRmPqSvWxZnMkL2gHjUiOp3rTyEwQaS5dFg...",
"tags": [
["p", "admin_public_key"]
],
@@ -64,18 +66,21 @@ All admin commands use the same unified event structure with tag-based parameter
}]
```
The `content` field contains a NIP-44 encrypted JSON response object.
### Admin Commands
All commands are sent as nip44 encrypted content. The following table lists all available commands:
All commands are sent as NIP-44 encrypted JSON arrays in the event content. The following table lists all available commands:
| Command Type | Tag Format | Description |
|--------------|------------|-------------|
| Command Type | Command Format | Description |
|--------------|----------------|-------------|
| **Configuration Management** |
| `config_update` | `["relay_description", "My Relay"]` | Update relay configuration parameters |
| `config_query` | `["config_query", "list_all_keys"]` | List all available configuration keys |
| `config_update` | `["config_update", [{"key": "auth_enabled", "value": "true", "data_type": "boolean", "category": "auth"}, {"key": "relay_description", "value": "My Relay", "data_type": "string", "category": "relay"}, ...]]` | Update relay configuration parameters (supports multiple updates) |
| `config_query` | `["config_query", "all"]` | Query all configuration parameters |
| **Auth Rules Management** |
| `auth_add_blacklist` | `["blacklist", "pubkey", "abc123..."]` | Add pubkey to blacklist |
| `auth_add_whitelist` | `["whitelist", "pubkey", "def456..."]` | Add pubkey to whitelist |
| `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_type` | `["auth_query", "whitelist"]` | Query specific rule type |
| `auth_query_pattern` | `["auth_query", "pattern", "abc123..."]` | Query specific pattern |
@@ -86,8 +91,16 @@ All commands are sent as nip44 encrypted content. The following table lists all
### Available Configuration Keys
**Basic Relay Settings:**
- `relay_name`: Relay name (displayed in NIP-11)
- `relay_description`: Relay description text
- `relay_contact`: Contact information
- `relay_software`: Software URL
- `relay_version`: Software version
- `supported_nips`: Comma-separated list of supported NIP numbers (e.g., "1,2,4,9,11,12,13,15,16,20,22,33,40,42")
- `language_tags`: Comma-separated list of supported language tags (e.g., "en,es,fr" or "*" for all)
- `relay_countries`: Comma-separated list of supported country codes (e.g., "US,CA,MX" or "*" for all)
- `posting_policy`: Posting policy URL or text
- `payments_url`: Payment URL for premium features
- `max_connections`: Maximum concurrent connections
- `max_subscriptions_per_client`: Max subscriptions per client
- `max_event_tags`: Maximum tags per event
@@ -103,6 +116,24 @@ All commands are sent as nip44 encrypted content. The following table lists all
- `pow_min_difficulty`: Minimum proof-of-work difficulty
- `nip40_expiration_enabled`: Enable event expiration (`true`/`false`)
### Dynamic Configuration Updates
C-Relay supports **dynamic configuration updates** without requiring a restart for most settings. Configuration parameters are categorized as either **dynamic** (can be updated immediately) or **restart-required** (require relay restart to take effect).
**Dynamic Configuration Parameters (No Restart Required):**
- All relay information (NIP-11) settings: `relay_name`, `relay_description`, `relay_contact`, `relay_software`, `relay_version`, `supported_nips`, `language_tags`, `relay_countries`, `posting_policy`, `payments_url`
- Authentication settings: `auth_enabled`, `nip42_auth_required`, `nip42_auth_required_kinds`, `nip42_challenge_timeout`
- Subscription limits: `max_subscriptions_per_client`, `max_total_subscriptions`
- Event validation limits: `max_event_tags`, `max_content_length`, `max_message_length`
- Proof of Work settings: `pow_min_difficulty`, `pow_mode`
- Event expiration settings: `nip40_expiration_enabled`, `nip40_expiration_strict`, `nip40_expiration_filter`, `nip40_expiration_grace_period`
**Restart-Required Configuration Parameters:**
- Connection settings: `max_connections`, `relay_port`
- Database and core system settings
When updating configuration, the admin API response will indicate whether a restart is required for each parameter. Dynamic updates take effect immediately and are reflected in NIP-11 relay information documents without restart.
### Response Format
All admin commands return **signed EVENT responses** via WebSocket following standard Nostr protocol. Responses use JSON content with structured data.
@@ -116,7 +147,7 @@ All admin commands return **signed EVENT responses** via WebSocket following sta
"pubkey": "relay_public_key",
"created_at": 1234567890,
"kind": 23457,
"content": "nip44 encrypted:{\"status\": \"success\", \"message\": \"Operation completed successfully\"}",
"content": "nip44 encrypted:{\"query_type\": \"config_update\", \"status\": \"success\", \"message\": \"Operation completed successfully\", \"timestamp\": 1234567890}",
"tags": [
["p", "admin_public_key"]
],
@@ -131,7 +162,7 @@ All admin commands return **signed EVENT responses** via WebSocket following sta
"pubkey": "relay_public_key",
"created_at": 1234567890,
"kind": 23457,
"content": "nip44 encrypted:{\"status\": \"error\", \"message\": \"Error: invalid configuration value\"}",
"content": "nip44 encrypted:{\"query_type\": \"config_update\", \"status\": \"error\", \"error\": \"invalid configuration value\", \"timestamp\": 1234567890}",
"tags": [
["p", "admin_public_key"]
],
@@ -146,7 +177,7 @@ All admin commands return **signed EVENT responses** via WebSocket following sta
"pubkey": "relay_public_key",
"created_at": 1234567890,
"kind": 23457,
"content": "nip44 encrypted:{\"query_type\": \"auth_rules\", \"total_results\": 2, \"data\": [{\"rule_type\": \"blacklist\", \"pattern_type\": \"pubkey\", \"pattern_value\": \"abc123...\", \"action\": \"deny\"}]}",
"content": "nip44 encrypted:{\"query_type\": \"auth_rules_all\", \"total_results\": 2, \"timestamp\": 1234567890, \"data\": [{\"rule_type\": \"blacklist\", \"pattern_type\": \"pubkey\", \"pattern_value\": \"abc123...\", \"action\": \"allow\"}]}",
"tags": [
["p", "admin_public_key"]
],
@@ -161,7 +192,7 @@ All admin commands return **signed EVENT responses** via WebSocket following sta
"pubkey": "relay_public_key",
"created_at": 1234567890,
"kind": 23457,
"content": "nip44 encrypted:{\"query_type\": \"config_keys\", \"config_keys\": [\"auth_enabled\", \"max_connections\"], \"descriptions\": {\"auth_enabled\": \"Enable whitelist/blacklist rules\"}}",
"content": "nip44 encrypted:{\"query_type\": \"config_all\", \"total_results\": 27, \"timestamp\": 1234567890, \"data\": [{\"key\": \"auth_enabled\", \"value\": \"false\", \"data_type\": \"boolean\", \"category\": \"auth\", \"description\": \"Enable NIP-42 authentication\"}, {\"key\": \"relay_description\", \"value\": \"My Relay\", \"data_type\": \"string\", \"category\": \"relay\", \"description\": \"Relay description text\"}]}",
"tags": [
["p", "admin_public_key"]
],
@@ -169,3 +200,32 @@ All admin commands return **signed EVENT responses** via WebSocket following sta
}]
```
**Configuration Update Success Response:**
```json
["EVENT", "temp_sub_id", {
"id": "response_event_id",
"pubkey": "relay_public_key",
"created_at": 1234567890,
"kind": 23457,
"content": "nip44 encrypted:{\"query_type\": \"config_update\", \"total_results\": 2, \"timestamp\": 1234567890, \"status\": \"success\", \"data\": [{\"key\": \"auth_enabled\", \"value\": \"true\", \"status\": \"updated\"}, {\"key\": \"relay_description\", \"value\": \"My Updated Relay\", \"status\": \"updated\"}]}",
"tags": [
["p", "admin_public_key"]
],
"sig": "response_event_signature"
}]
```
**Configuration Update Error Response:**
```json
["EVENT", "temp_sub_id", {
"id": "response_event_id",
"pubkey": "relay_public_key",
"created_at": 1234567890,
"kind": 23457,
"content": "nip44 encrypted:{\"query_type\": \"config_update\", \"status\": \"error\", \"error\": \"field validation failed: invalid port number '99999' (must be 1-65535)\", \"timestamp\": 1234567890}",
"tags": [
["p", "admin_public_key"]
],
"sig": "response_event_signature"
}]
```

File diff suppressed because it is too large Load Diff

View File

@@ -139,11 +139,11 @@ compile_project() {
print_warning "Clean failed or no Makefile found"
fi
# Force regenerate version.h to pick up new tags
# Force regenerate main.h to pick up new tags
if make force-version > /dev/null 2>&1; then
print_success "Regenerated version.h"
print_success "Regenerated main.h"
else
print_warning "Failed to regenerate version.h"
print_warning "Failed to regenerate main.h"
fi
# Compile the project

8
c-relay.code-workspace Normal file
View File

@@ -0,0 +1,8 @@
{
"folders": [
{
"path": "."
}
],
"settings": {}
}

View File

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

View File

@@ -63,7 +63,7 @@ while [[ $# -gt 0 ]]; do
shift 2
fi
;;
--preserve-database)
-d|--preserve-database)
PRESERVE_DATABASE=true
shift
;;
@@ -282,14 +282,14 @@ cd build
# Start relay in background and capture its PID
if [ "$USE_TEST_KEYS" = true ]; then
echo "Using deterministic test keys for development..."
./$(basename $BINARY_PATH) -a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -r 1111111111111111111111111111111111111111111111111111111111111111 > ../relay.log 2>&1 &
./$(basename $BINARY_PATH) -a 6a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3 -r 1111111111111111111111111111111111111111111111111111111111111111 --strict-port > ../relay.log 2>&1 &
elif [ -n "$RELAY_ARGS" ]; then
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
# No command line arguments needed for 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
RELAY_PID=$!
# Change back to original directory

View File

@@ -1 +1 @@
285781
109955

File diff suppressed because it is too large Load Diff

View File

@@ -56,6 +56,11 @@ typedef struct {
char version[64];
char privacy_policy[RELAY_URL_MAX_LENGTH];
char terms_of_service[RELAY_URL_MAX_LENGTH];
// Raw string values for parsing into cJSON arrays
char supported_nips_str[CONFIG_VALUE_MAX_LENGTH];
char language_tags_str[CONFIG_VALUE_MAX_LENGTH];
char relay_countries_str[CONFIG_VALUE_MAX_LENGTH];
// Parsed cJSON arrays
cJSON* supported_nips;
cJSON* limitation;
cJSON* retention;
@@ -96,8 +101,9 @@ typedef struct {
// Command line options structure for first-time startup
typedef struct {
int port_override; // -1 = not set, >0 = port value
char admin_privkey_override[65]; // Empty string = not set, 64-char hex = override
char admin_pubkey_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;
// Global unified configuration cache
@@ -172,10 +178,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_auth_query_unified(cJSON* event, const char* query_type, char* error_message, size_t error_size, struct lws* wsi);
int handle_system_command_unified(cJSON* event, const char* command, char* error_message, size_t error_size, struct lws* wsi);
int handle_auth_rule_modification_unified(cJSON* event, char* error_message, size_t error_size);
int handle_auth_rule_modification_unified(cJSON* event, char* error_message, size_t error_size, struct lws* wsi);
// WebSocket response functions
int send_websocket_response_data(cJSON* event, cJSON* response_data, struct lws* wsi);
// Admin response functions
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);
// Auth rules management functions

View File

@@ -3,13 +3,13 @@
#include <cjson/cJSON.h>
#include "config.h" // For cli_options_t definition
#include "main.h" // For relay metadata constants
/*
* Default Configuration Event Template
*
* This header contains the default configuration values for the C Nostr Relay.
* These values are used to create the initial kind 33334 configuration event
* during first-time startup.
* These values are used to populate the config table during first-time startup.
*
* IMPORTANT: These values should never be accessed directly by other parts
* of the program. They are only used during initial configuration event creation.
@@ -34,10 +34,16 @@ static const struct {
{"max_connections", "100"},
// NIP-11 Relay Information (relay keys will be populated at runtime)
{"relay_description", "High-performance C Nostr relay with SQLite storage"},
{"relay_contact", ""},
{"relay_software", "https://git.laantungir.net/laantungir/c-relay.git"},
{"relay_version", "v1.0.0"},
{"relay_name", RELAY_NAME},
{"relay_description", RELAY_DESCRIPTION},
{"relay_contact", RELAY_CONTACT},
{"relay_software", RELAY_SOFTWARE},
{"relay_version", RELAY_VERSION},
{"supported_nips", SUPPORTED_NIPS},
{"language_tags", LANGUAGE_TAGS},
{"relay_countries", RELAY_COUNTRIES},
{"posting_policy", POSTING_POLICY},
{"payments_url", PAYMENTS_URL},
// NIP-13 Proof of Work (pow_min_difficulty = 0 means PoW disabled)
{"pow_min_difficulty", "0"},

2945
src/main.c

File diff suppressed because it is too large Load Diff

32
src/main.h Normal file
View File

@@ -0,0 +1,32 @@
/*
* C-Relay Main Header - Version and Metadata Information
*
* This header contains version information and relay metadata that is
* automatically updated by the build system (build_and_push.sh).
*
* The build_and_push.sh script updates VERSION and related macros when
* creating new releases.
*/
#ifndef MAIN_H
#define MAIN_H
// Version information (auto-updated by build_and_push.sh)
#define VERSION "v0.4.6"
#define VERSION_MAJOR 0
#define VERSION_MINOR 4
#define VERSION_PATCH 6
// Relay metadata (authoritative source for NIP-11 information)
#define RELAY_NAME "C-Relay"
#define RELAY_DESCRIPTION "High-performance C Nostr relay with SQLite storage"
#define RELAY_CONTACT ""
#define RELAY_SOFTWARE "https://git.laantungir.net/laantungir/c-relay.git"
#define RELAY_VERSION VERSION // Use the same version as the build
#define SUPPORTED_NIPS "1,2,4,9,11,12,13,15,16,20,22,33,40,42"
#define LANGUAGE_TAGS ""
#define RELAY_COUNTRIES ""
#define POSTING_POLICY ""
#define PAYMENTS_URL ""
#endif /* MAIN_H */

313
src/nip009.c Normal file
View File

@@ -0,0 +1,313 @@
#define _GNU_SOURCE
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
// NIP-09 EVENT DELETION REQUEST HANDLING
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
#include <cjson/cJSON.h>
#include <sqlite3.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include <stdio.h>
#include <printf.h>
// Forward declarations for logging functions
void log_warning(const char* message);
void log_info(const char* message);
// Forward declaration for database functions
int store_event(cJSON* event);
// Forward declarations for deletion functions
int delete_events_by_id(const char* requester_pubkey, cJSON* event_ids);
int delete_events_by_address(const char* requester_pubkey, cJSON* addresses, long deletion_timestamp);
// Global database variable
extern sqlite3* g_db;
// Handle NIP-09 deletion request event (kind 5)
int handle_deletion_request(cJSON* event, char* error_message, size_t error_size) {
if (!event) {
snprintf(error_message, error_size, "invalid: null deletion request");
return -1;
}
// Extract event details
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
cJSON* created_at_obj = cJSON_GetObjectItem(event, "created_at");
cJSON* tags_obj = cJSON_GetObjectItem(event, "tags");
cJSON* content_obj = cJSON_GetObjectItem(event, "content");
cJSON* event_id_obj = cJSON_GetObjectItem(event, "id");
if (!kind_obj || !pubkey_obj || !created_at_obj || !tags_obj || !event_id_obj) {
snprintf(error_message, error_size, "invalid: incomplete deletion request");
return -1;
}
int kind = (int)cJSON_GetNumberValue(kind_obj);
if (kind != 5) {
snprintf(error_message, error_size, "invalid: not a deletion request");
return -1;
}
const char* requester_pubkey = cJSON_GetStringValue(pubkey_obj);
// Extract deletion event ID and reason (for potential logging)
const char* deletion_event_id = cJSON_GetStringValue(event_id_obj);
const char* reason = content_obj ? cJSON_GetStringValue(content_obj) : "";
(void)deletion_event_id; // Mark as intentionally unused for now
(void)reason; // Mark as intentionally unused for now
long deletion_timestamp = (long)cJSON_GetNumberValue(created_at_obj);
if (!cJSON_IsArray(tags_obj)) {
snprintf(error_message, error_size, "invalid: deletion request tags must be an array");
return -1;
}
// Collect event IDs and addresses from tags
cJSON* event_ids = cJSON_CreateArray();
cJSON* addresses = cJSON_CreateArray();
cJSON* kinds_to_delete = cJSON_CreateArray();
int deletion_targets_found = 0;
cJSON* tag = NULL;
cJSON_ArrayForEach(tag, tags_obj) {
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
continue;
}
cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
cJSON* tag_value = cJSON_GetArrayItem(tag, 1);
if (!cJSON_IsString(tag_name) || !cJSON_IsString(tag_value)) {
continue;
}
const char* name = cJSON_GetStringValue(tag_name);
const char* value = cJSON_GetStringValue(tag_value);
if (strcmp(name, "e") == 0) {
// Event ID reference
cJSON_AddItemToArray(event_ids, cJSON_CreateString(value));
deletion_targets_found++;
} else if (strcmp(name, "a") == 0) {
// Addressable event reference (kind:pubkey:d-identifier)
cJSON_AddItemToArray(addresses, cJSON_CreateString(value));
deletion_targets_found++;
} else if (strcmp(name, "k") == 0) {
// Kind hint - store for validation but not required
int kind_hint = atoi(value);
if (kind_hint > 0) {
cJSON_AddItemToArray(kinds_to_delete, cJSON_CreateNumber(kind_hint));
}
}
}
if (deletion_targets_found == 0) {
cJSON_Delete(event_ids);
cJSON_Delete(addresses);
cJSON_Delete(kinds_to_delete);
snprintf(error_message, error_size, "invalid: deletion request must contain 'e' or 'a' tags");
return -1;
}
int deleted_count = 0;
// Process event ID deletions
if (cJSON_GetArraySize(event_ids) > 0) {
int result = delete_events_by_id(requester_pubkey, event_ids);
if (result > 0) {
deleted_count += result;
}
}
// Process addressable event deletions
if (cJSON_GetArraySize(addresses) > 0) {
int result = delete_events_by_address(requester_pubkey, addresses, deletion_timestamp);
if (result > 0) {
deleted_count += result;
}
}
// Clean up
cJSON_Delete(event_ids);
cJSON_Delete(addresses);
cJSON_Delete(kinds_to_delete);
// Store the deletion request itself (it should be kept according to NIP-09)
if (store_event(event) != 0) {
log_warning("Failed to store deletion request event");
}
char debug_msg[256];
snprintf(debug_msg, sizeof(debug_msg), "Deletion request processed: %d events deleted", deleted_count);
log_info(debug_msg);
error_message[0] = '\0'; // Success - empty error message
return 0;
}
// Delete events by ID (with pubkey authorization)
int delete_events_by_id(const char* requester_pubkey, cJSON* event_ids) {
if (!g_db || !requester_pubkey || !event_ids || !cJSON_IsArray(event_ids)) {
return 0;
}
int deleted_count = 0;
cJSON* event_id = NULL;
cJSON_ArrayForEach(event_id, event_ids) {
if (!cJSON_IsString(event_id)) {
continue;
}
const char* id = cJSON_GetStringValue(event_id);
// First check if event exists and if requester is authorized
const char* check_sql = "SELECT pubkey FROM events WHERE id = ?";
sqlite3_stmt* check_stmt;
int rc = sqlite3_prepare_v2(g_db, check_sql, -1, &check_stmt, NULL);
if (rc != SQLITE_OK) {
continue;
}
sqlite3_bind_text(check_stmt, 1, id, -1, SQLITE_STATIC);
if (sqlite3_step(check_stmt) == SQLITE_ROW) {
const char* event_pubkey = (char*)sqlite3_column_text(check_stmt, 0);
// Only delete if the requester is the author
if (event_pubkey && strcmp(event_pubkey, requester_pubkey) == 0) {
sqlite3_finalize(check_stmt);
// Delete the event
const char* delete_sql = "DELETE FROM events WHERE id = ? AND pubkey = ?";
sqlite3_stmt* delete_stmt;
rc = sqlite3_prepare_v2(g_db, delete_sql, -1, &delete_stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(delete_stmt, 1, id, -1, SQLITE_STATIC);
sqlite3_bind_text(delete_stmt, 2, requester_pubkey, -1, SQLITE_STATIC);
if (sqlite3_step(delete_stmt) == SQLITE_DONE && sqlite3_changes(g_db) > 0) {
deleted_count++;
char debug_msg[128];
snprintf(debug_msg, sizeof(debug_msg), "Deleted event by ID: %.16s...", id);
log_info(debug_msg);
}
sqlite3_finalize(delete_stmt);
}
} else {
sqlite3_finalize(check_stmt);
char warning_msg[128];
snprintf(warning_msg, sizeof(warning_msg), "Unauthorized deletion attempt for event: %.16s...", id);
log_warning(warning_msg);
}
} else {
sqlite3_finalize(check_stmt);
char debug_msg[128];
snprintf(debug_msg, sizeof(debug_msg), "Event not found for deletion: %.16s...", id);
log_info(debug_msg);
}
}
return deleted_count;
}
// Delete events by addressable reference (kind:pubkey:d-identifier)
int delete_events_by_address(const char* requester_pubkey, cJSON* addresses, long deletion_timestamp) {
if (!g_db || !requester_pubkey || !addresses || !cJSON_IsArray(addresses)) {
return 0;
}
int deleted_count = 0;
cJSON* address = NULL;
cJSON_ArrayForEach(address, addresses) {
if (!cJSON_IsString(address)) {
continue;
}
const char* addr = cJSON_GetStringValue(address);
// Parse address format: kind:pubkey:d-identifier
char* addr_copy = strdup(addr);
if (!addr_copy) continue;
char* kind_str = strtok(addr_copy, ":");
char* pubkey_str = strtok(NULL, ":");
char* d_identifier = strtok(NULL, ":");
if (!kind_str || !pubkey_str) {
free(addr_copy);
continue;
}
int kind = atoi(kind_str);
// Only delete if the requester is the author
if (strcmp(pubkey_str, requester_pubkey) != 0) {
free(addr_copy);
char warning_msg[128];
snprintf(warning_msg, sizeof(warning_msg), "Unauthorized deletion attempt for address: %.32s...", addr);
log_warning(warning_msg);
continue;
}
// Build deletion query based on whether we have d-identifier
const char* delete_sql;
sqlite3_stmt* delete_stmt;
if (d_identifier && strlen(d_identifier) > 0) {
// Delete specific addressable event with d-tag
delete_sql = "DELETE FROM events WHERE kind = ? AND pubkey = ? AND created_at <= ? "
"AND json_extract(tags, '$[*]') LIKE '%[\"d\",\"' || ? || '\"]%'";
} else {
// Delete all events of this kind by this author up to deletion timestamp
delete_sql = "DELETE FROM events WHERE kind = ? AND pubkey = ? AND created_at <= ?";
}
int rc = sqlite3_prepare_v2(g_db, delete_sql, -1, &delete_stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_int(delete_stmt, 1, kind);
sqlite3_bind_text(delete_stmt, 2, requester_pubkey, -1, SQLITE_STATIC);
sqlite3_bind_int64(delete_stmt, 3, deletion_timestamp);
if (d_identifier && strlen(d_identifier) > 0) {
sqlite3_bind_text(delete_stmt, 4, d_identifier, -1, SQLITE_STATIC);
}
if (sqlite3_step(delete_stmt) == SQLITE_DONE) {
int changes = sqlite3_changes(g_db);
if (changes > 0) {
deleted_count += changes;
char debug_msg[128];
snprintf(debug_msg, sizeof(debug_msg), "Deleted %d events by address: %.32s...", changes, addr);
log_info(debug_msg);
}
}
sqlite3_finalize(delete_stmt);
}
free(addr_copy);
}
return deleted_count;
}
// Mark event as deleted (alternative to hard deletion - not used in current implementation)
int mark_event_as_deleted(const char* event_id, const char* deletion_event_id, const char* reason) {
(void)event_id; (void)deletion_event_id; (void)reason; // Suppress unused warnings
// This function could be used if we wanted to implement soft deletion
// For now, NIP-09 implementation uses hard deletion as specified
return 0;
}

620
src/nip011.c Normal file
View File

@@ -0,0 +1,620 @@
// NIP-11 Relay Information Document module
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <libwebsockets.h>
#include "../nostr_core_lib/cjson/cJSON.h"
#include "config.h"
// Forward declarations for logging functions
void log_info(const char* message);
void log_success(const char* message);
void log_error(const char* message);
void log_warning(const char* message);
// Forward declarations for configuration functions
const char* get_config_value(const char* key);
int get_config_int(const char* key, int default_value);
int get_config_bool(const char* key, int default_value);
// Forward declarations for global cache access
extern unified_config_cache_t g_unified_cache;
// Forward declarations for constants (defined in config.h and other headers)
#define HTTP_STATUS_OK 200
#define HTTP_STATUS_NOT_ACCEPTABLE 406
#define HTTP_STATUS_INTERNAL_SERVER_ERROR 500
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
// NIP-11 RELAY INFORMATION DOCUMENT
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
// Helper function to parse comma-separated string into cJSON array
cJSON* parse_comma_separated_array(const char* csv_string) {
log_info("parse_comma_separated_array called");
if (!csv_string || strlen(csv_string) == 0) {
log_info("Empty or null csv_string, returning empty array");
return cJSON_CreateArray();
}
log_info("Creating cJSON array");
cJSON* array = cJSON_CreateArray();
if (!array) {
log_info("Failed to create cJSON array");
return NULL;
}
log_info("Duplicating csv_string");
char* csv_copy = strdup(csv_string);
if (!csv_copy) {
log_info("Failed to duplicate csv_string");
cJSON_Delete(array);
return NULL;
}
log_info("Starting token parsing");
char* token = strtok(csv_copy, ",");
while (token) {
log_info("Processing token");
// Trim whitespace
while (*token == ' ') token++;
char* end = token + strlen(token) - 1;
while (end > token && *end == ' ') *end-- = '\0';
if (strlen(token) > 0) {
log_info("Token has content, parsing");
// Try to parse as number first (for supported_nips)
char* endptr;
long num = strtol(token, &endptr, 10);
if (*endptr == '\0') {
log_info("Token is number, adding to array");
// It's a number
cJSON_AddItemToArray(array, cJSON_CreateNumber(num));
} else {
log_info("Token is string, adding to array");
// It's a string
cJSON_AddItemToArray(array, cJSON_CreateString(token));
}
} else {
log_info("Token is empty, skipping");
}
token = strtok(NULL, ",");
}
log_info("Freeing csv_copy");
free(csv_copy);
log_info("Returning parsed array");
return array;
}
// Initialize relay information using configuration system
void init_relay_info() {
log_info("Initializing relay information from configuration...");
// Get all config values first (without holding mutex to avoid deadlock)
// Note: These may be dynamically allocated strings that need to be freed
log_info("Fetching relay configuration values...");
const char* relay_name = get_config_value("relay_name");
log_info("relay_name fetched");
const char* relay_description = get_config_value("relay_description");
log_info("relay_description fetched");
const char* relay_software = get_config_value("relay_software");
log_info("relay_software fetched");
const char* relay_version = get_config_value("relay_version");
log_info("relay_version fetched");
const char* relay_contact = get_config_value("relay_contact");
log_info("relay_contact fetched");
const char* relay_pubkey = get_config_value("relay_pubkey");
log_info("relay_pubkey fetched");
const char* supported_nips_csv = get_config_value("supported_nips");
log_info("supported_nips fetched");
const char* language_tags_csv = get_config_value("language_tags");
log_info("language_tags fetched");
const char* relay_countries_csv = get_config_value("relay_countries");
log_info("relay_countries fetched");
const char* posting_policy = get_config_value("posting_policy");
log_info("posting_policy fetched");
const char* payments_url = get_config_value("payments_url");
log_info("payments_url fetched");
// Get config values for limitations
log_info("Fetching limitation configuration values...");
int max_message_length = get_config_int("max_message_length", 16384);
log_info("max_message_length fetched");
int max_subscriptions_per_client = get_config_int("max_subscriptions_per_client", 20);
log_info("max_subscriptions_per_client fetched");
int max_limit = get_config_int("max_limit", 5000);
log_info("max_limit fetched");
int max_event_tags = get_config_int("max_event_tags", 100);
log_info("max_event_tags fetched");
int max_content_length = get_config_int("max_content_length", 8196);
log_info("max_content_length fetched");
int default_limit = get_config_int("default_limit", 500);
log_info("default_limit fetched");
int admin_enabled = get_config_bool("admin_enabled", 0);
log_info("admin_enabled fetched");
pthread_mutex_lock(&g_unified_cache.cache_lock);
// Update relay information fields
log_info("Storing string values in cache...");
if (relay_name) {
log_info("Storing relay_name");
strncpy(g_unified_cache.relay_info.name, relay_name, sizeof(g_unified_cache.relay_info.name) - 1);
free((char*)relay_name); // Free dynamically allocated string
log_info("relay_name stored and freed");
} else {
log_info("Using default relay_name");
strncpy(g_unified_cache.relay_info.name, "C Nostr Relay", sizeof(g_unified_cache.relay_info.name) - 1);
}
if (relay_description) {
log_info("Storing relay_description");
strncpy(g_unified_cache.relay_info.description, relay_description, sizeof(g_unified_cache.relay_info.description) - 1);
free((char*)relay_description); // Free dynamically allocated string
log_info("relay_description stored and freed");
} else {
log_info("Using default relay_description");
strncpy(g_unified_cache.relay_info.description, "A high-performance Nostr relay implemented in C with SQLite storage", sizeof(g_unified_cache.relay_info.description) - 1);
}
if (relay_software) {
log_info("Storing relay_software");
strncpy(g_unified_cache.relay_info.software, relay_software, sizeof(g_unified_cache.relay_info.software) - 1);
free((char*)relay_software); // Free dynamically allocated string
log_info("relay_software stored and freed");
} else {
log_info("Using default relay_software");
strncpy(g_unified_cache.relay_info.software, "https://git.laantungir.net/laantungir/c-relay.git", sizeof(g_unified_cache.relay_info.software) - 1);
}
if (relay_version) {
log_info("Storing relay_version");
strncpy(g_unified_cache.relay_info.version, relay_version, sizeof(g_unified_cache.relay_info.version) - 1);
free((char*)relay_version); // Free dynamically allocated string
log_info("relay_version stored and freed");
} else {
log_info("Using default relay_version");
strncpy(g_unified_cache.relay_info.version, "0.2.0", sizeof(g_unified_cache.relay_info.version) - 1);
}
if (relay_contact) {
log_info("Storing relay_contact");
strncpy(g_unified_cache.relay_info.contact, relay_contact, sizeof(g_unified_cache.relay_info.contact) - 1);
free((char*)relay_contact); // Free dynamically allocated string
log_info("relay_contact stored and freed");
}
if (relay_pubkey) {
log_info("Storing relay_pubkey");
strncpy(g_unified_cache.relay_info.pubkey, relay_pubkey, sizeof(g_unified_cache.relay_info.pubkey) - 1);
free((char*)relay_pubkey); // Free dynamically allocated string
log_info("relay_pubkey stored and freed");
}
if (posting_policy) {
log_info("Storing posting_policy");
strncpy(g_unified_cache.relay_info.posting_policy, posting_policy, sizeof(g_unified_cache.relay_info.posting_policy) - 1);
free((char*)posting_policy); // Free dynamically allocated string
log_info("posting_policy stored and freed");
}
if (payments_url) {
log_info("Storing payments_url");
strncpy(g_unified_cache.relay_info.payments_url, payments_url, sizeof(g_unified_cache.relay_info.payments_url) - 1);
free((char*)payments_url); // Free dynamically allocated string
log_info("payments_url stored and freed");
}
// Initialize supported NIPs array from config
log_info("Initializing supported_nips array");
if (supported_nips_csv) {
log_info("Parsing supported_nips from config");
g_unified_cache.relay_info.supported_nips = parse_comma_separated_array(supported_nips_csv);
log_info("supported_nips parsed successfully");
free((char*)supported_nips_csv); // Free dynamically allocated string
log_info("supported_nips_csv freed");
} else {
log_info("Using default supported_nips");
// Fallback to default supported NIPs
g_unified_cache.relay_info.supported_nips = cJSON_CreateArray();
if (g_unified_cache.relay_info.supported_nips) {
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(1)); // NIP-01: Basic protocol
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(9)); // NIP-09: Event deletion
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(11)); // NIP-11: Relay information
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(13)); // NIP-13: Proof of Work
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(15)); // NIP-15: EOSE
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(20)); // NIP-20: Command results
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(40)); // NIP-40: Expiration Timestamp
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(42)); // NIP-42: Authentication
}
log_info("Default supported_nips created");
}
// Initialize server limitations using configuration
log_info("Initializing server limitations");
g_unified_cache.relay_info.limitation = cJSON_CreateObject();
if (g_unified_cache.relay_info.limitation) {
log_info("Adding limitation fields");
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_message_length", max_message_length);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_subscriptions", max_subscriptions_per_client);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_limit", max_limit);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_subid_length", SUBSCRIPTION_ID_MAX_LENGTH);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_event_tags", max_event_tags);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_content_length", max_content_length);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "min_pow_difficulty", g_unified_cache.pow_config.min_pow_difficulty);
cJSON_AddBoolToObject(g_unified_cache.relay_info.limitation, "auth_required", admin_enabled ? cJSON_True : cJSON_False);
cJSON_AddBoolToObject(g_unified_cache.relay_info.limitation, "payment_required", cJSON_False);
cJSON_AddBoolToObject(g_unified_cache.relay_info.limitation, "restricted_writes", cJSON_False);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "created_at_lower_limit", 0);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "created_at_upper_limit", 2147483647);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "default_limit", default_limit);
log_info("Limitation fields added");
} else {
log_info("Failed to create limitation object");
}
// Initialize empty retention policies (can be configured later)
log_info("Initializing retention policies");
g_unified_cache.relay_info.retention = cJSON_CreateArray();
// Initialize language tags from config
log_info("Initializing language_tags");
if (language_tags_csv) {
log_info("Parsing language_tags from config");
g_unified_cache.relay_info.language_tags = parse_comma_separated_array(language_tags_csv);
log_info("language_tags parsed successfully");
free((char*)language_tags_csv); // Free dynamically allocated string
log_info("language_tags_csv freed");
} else {
log_info("Using default language_tags");
// Fallback to global
g_unified_cache.relay_info.language_tags = cJSON_CreateArray();
if (g_unified_cache.relay_info.language_tags) {
cJSON_AddItemToArray(g_unified_cache.relay_info.language_tags, cJSON_CreateString("*"));
}
}
// Initialize relay countries from config
log_info("Initializing relay_countries");
if (relay_countries_csv) {
log_info("Parsing relay_countries from config");
g_unified_cache.relay_info.relay_countries = parse_comma_separated_array(relay_countries_csv);
log_info("relay_countries parsed successfully");
free((char*)relay_countries_csv); // Free dynamically allocated string
log_info("relay_countries_csv freed");
} else {
log_info("Using default relay_countries");
// Fallback to global
g_unified_cache.relay_info.relay_countries = cJSON_CreateArray();
if (g_unified_cache.relay_info.relay_countries) {
cJSON_AddItemToArray(g_unified_cache.relay_info.relay_countries, cJSON_CreateString("*"));
}
}
// Initialize content tags as empty array
log_info("Initializing tags");
g_unified_cache.relay_info.tags = cJSON_CreateArray();
// Initialize fees as empty object (no payment required by default)
log_info("Initializing fees");
g_unified_cache.relay_info.fees = cJSON_CreateObject();
log_info("Unlocking cache mutex");
pthread_mutex_unlock(&g_unified_cache.cache_lock);
log_success("Relay information initialized with default values");
}
// Clean up relay information JSON objects
void cleanup_relay_info() {
pthread_mutex_lock(&g_unified_cache.cache_lock);
if (g_unified_cache.relay_info.supported_nips) {
cJSON_Delete(g_unified_cache.relay_info.supported_nips);
g_unified_cache.relay_info.supported_nips = NULL;
}
if (g_unified_cache.relay_info.limitation) {
cJSON_Delete(g_unified_cache.relay_info.limitation);
g_unified_cache.relay_info.limitation = NULL;
}
if (g_unified_cache.relay_info.retention) {
cJSON_Delete(g_unified_cache.relay_info.retention);
g_unified_cache.relay_info.retention = NULL;
}
if (g_unified_cache.relay_info.language_tags) {
cJSON_Delete(g_unified_cache.relay_info.language_tags);
g_unified_cache.relay_info.language_tags = NULL;
}
if (g_unified_cache.relay_info.relay_countries) {
cJSON_Delete(g_unified_cache.relay_info.relay_countries);
g_unified_cache.relay_info.relay_countries = NULL;
}
if (g_unified_cache.relay_info.tags) {
cJSON_Delete(g_unified_cache.relay_info.tags);
g_unified_cache.relay_info.tags = NULL;
}
if (g_unified_cache.relay_info.fees) {
cJSON_Delete(g_unified_cache.relay_info.fees);
g_unified_cache.relay_info.fees = NULL;
}
pthread_mutex_unlock(&g_unified_cache.cache_lock);
}
// Generate NIP-11 compliant JSON document
cJSON* generate_relay_info_json() {
cJSON* info = cJSON_CreateObject();
if (!info) {
log_error("Failed to create relay info JSON object");
return NULL;
}
pthread_mutex_lock(&g_unified_cache.cache_lock);
// Add basic relay information
if (strlen(g_unified_cache.relay_info.name) > 0) {
cJSON_AddStringToObject(info, "name", g_unified_cache.relay_info.name);
}
if (strlen(g_unified_cache.relay_info.description) > 0) {
cJSON_AddStringToObject(info, "description", g_unified_cache.relay_info.description);
}
if (strlen(g_unified_cache.relay_info.banner) > 0) {
cJSON_AddStringToObject(info, "banner", g_unified_cache.relay_info.banner);
}
if (strlen(g_unified_cache.relay_info.icon) > 0) {
cJSON_AddStringToObject(info, "icon", g_unified_cache.relay_info.icon);
}
if (strlen(g_unified_cache.relay_info.pubkey) > 0) {
cJSON_AddStringToObject(info, "pubkey", g_unified_cache.relay_info.pubkey);
}
if (strlen(g_unified_cache.relay_info.contact) > 0) {
cJSON_AddStringToObject(info, "contact", g_unified_cache.relay_info.contact);
}
// Add supported NIPs
if (g_unified_cache.relay_info.supported_nips) {
cJSON_AddItemToObject(info, "supported_nips", cJSON_Duplicate(g_unified_cache.relay_info.supported_nips, 1));
}
// Add software information
if (strlen(g_unified_cache.relay_info.software) > 0) {
cJSON_AddStringToObject(info, "software", g_unified_cache.relay_info.software);
}
if (strlen(g_unified_cache.relay_info.version) > 0) {
cJSON_AddStringToObject(info, "version", g_unified_cache.relay_info.version);
}
// Add policies
if (strlen(g_unified_cache.relay_info.privacy_policy) > 0) {
cJSON_AddStringToObject(info, "privacy_policy", g_unified_cache.relay_info.privacy_policy);
}
if (strlen(g_unified_cache.relay_info.terms_of_service) > 0) {
cJSON_AddStringToObject(info, "terms_of_service", g_unified_cache.relay_info.terms_of_service);
}
if (strlen(g_unified_cache.relay_info.posting_policy) > 0) {
cJSON_AddStringToObject(info, "posting_policy", g_unified_cache.relay_info.posting_policy);
}
// Add server limitations
if (g_unified_cache.relay_info.limitation) {
cJSON_AddItemToObject(info, "limitation", cJSON_Duplicate(g_unified_cache.relay_info.limitation, 1));
}
// Add retention policies if configured
if (g_unified_cache.relay_info.retention && cJSON_GetArraySize(g_unified_cache.relay_info.retention) > 0) {
cJSON_AddItemToObject(info, "retention", cJSON_Duplicate(g_unified_cache.relay_info.retention, 1));
}
// Add geographical and language information
if (g_unified_cache.relay_info.relay_countries) {
cJSON_AddItemToObject(info, "relay_countries", cJSON_Duplicate(g_unified_cache.relay_info.relay_countries, 1));
}
if (g_unified_cache.relay_info.language_tags) {
cJSON_AddItemToObject(info, "language_tags", cJSON_Duplicate(g_unified_cache.relay_info.language_tags, 1));
}
if (g_unified_cache.relay_info.tags && cJSON_GetArraySize(g_unified_cache.relay_info.tags) > 0) {
cJSON_AddItemToObject(info, "tags", cJSON_Duplicate(g_unified_cache.relay_info.tags, 1));
}
// Add payment information if configured
if (strlen(g_unified_cache.relay_info.payments_url) > 0) {
cJSON_AddStringToObject(info, "payments_url", g_unified_cache.relay_info.payments_url);
}
if (g_unified_cache.relay_info.fees && cJSON_GetObjectItem(g_unified_cache.relay_info.fees, "admission")) {
cJSON_AddItemToObject(info, "fees", cJSON_Duplicate(g_unified_cache.relay_info.fees, 1));
}
pthread_mutex_unlock(&g_unified_cache.cache_lock);
return info;
}
// NIP-11 HTTP session data structure for managing buffer lifetime
struct nip11_session_data {
char* json_buffer;
size_t json_length;
int headers_sent;
int body_sent;
};
// Handle NIP-11 HTTP request with proper asynchronous buffer management
int handle_nip11_http_request(struct lws* wsi, const char* accept_header) {
log_info("Handling NIP-11 relay information request");
// Check if client accepts application/nostr+json
int accepts_nostr_json = 0;
if (accept_header) {
if (strstr(accept_header, "application/nostr+json") != NULL) {
accepts_nostr_json = 1;
}
}
if (!accepts_nostr_json) {
log_warning("HTTP request without proper Accept header for NIP-11");
// Return 406 Not Acceptable
unsigned char buf[LWS_PRE + 256];
unsigned char *p = &buf[LWS_PRE];
unsigned char *start = p;
unsigned char *end = &buf[sizeof(buf) - 1];
if (lws_add_http_header_status(wsi, HTTP_STATUS_NOT_ACCEPTABLE, &p, end)) {
return -1;
}
if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE, (unsigned char*)"text/plain", 10, &p, end)) {
return -1;
}
if (lws_add_http_header_content_length(wsi, 0, &p, end)) {
return -1;
}
if (lws_finalize_http_header(wsi, &p, end)) {
return -1;
}
lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS);
return -1; // Close connection
}
// Generate relay information JSON
cJSON* info_json = generate_relay_info_json();
if (!info_json) {
log_error("Failed to generate relay info JSON");
unsigned char buf[LWS_PRE + 256];
unsigned char *p = &buf[LWS_PRE];
unsigned char *start = p;
unsigned char *end = &buf[sizeof(buf) - 1];
if (lws_add_http_header_status(wsi, HTTP_STATUS_INTERNAL_SERVER_ERROR, &p, end)) {
return -1;
}
if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE, (unsigned char*)"text/plain", 10, &p, end)) {
return -1;
}
if (lws_add_http_header_content_length(wsi, 0, &p, end)) {
return -1;
}
if (lws_finalize_http_header(wsi, &p, end)) {
return -1;
}
lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS);
return -1;
}
char* json_string = cJSON_Print(info_json);
cJSON_Delete(info_json);
if (!json_string) {
log_error("Failed to serialize relay info JSON");
unsigned char buf[LWS_PRE + 256];
unsigned char *p = &buf[LWS_PRE];
unsigned char *start = p;
unsigned char *end = &buf[sizeof(buf) - 1];
if (lws_add_http_header_status(wsi, HTTP_STATUS_INTERNAL_SERVER_ERROR, &p, end)) {
return -1;
}
if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE, (unsigned char*)"text/plain", 10, &p, end)) {
return -1;
}
if (lws_add_http_header_content_length(wsi, 0, &p, end)) {
return -1;
}
if (lws_finalize_http_header(wsi, &p, end)) {
return -1;
}
lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS);
return -1;
}
size_t json_len = strlen(json_string);
// Allocate session data to manage buffer lifetime across callbacks
struct nip11_session_data* session_data = malloc(sizeof(struct nip11_session_data));
if (!session_data) {
log_error("Failed to allocate NIP-11 session data");
free(json_string);
return -1;
}
// Store JSON buffer in session data for asynchronous handling
session_data->json_buffer = json_string;
session_data->json_length = json_len;
session_data->headers_sent = 0;
session_data->body_sent = 0;
// Store session data in WSI user data for callback access
lws_set_wsi_user(wsi, session_data);
// Prepare HTTP response with CORS headers
unsigned char buf[LWS_PRE + 1024];
unsigned char *p = &buf[LWS_PRE];
unsigned char *start = p;
unsigned char *end = &buf[sizeof(buf) - 1];
// Add status
if (lws_add_http_header_status(wsi, HTTP_STATUS_OK, &p, end)) {
free(session_data->json_buffer);
free(session_data);
return -1;
}
// Add content type
if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE,
(unsigned char*)"application/nostr+json", 22, &p, end)) {
free(session_data->json_buffer);
free(session_data);
return -1;
}
// Add content length
if (lws_add_http_header_content_length(wsi, json_len, &p, end)) {
free(session_data->json_buffer);
free(session_data);
return -1;
}
// Add CORS headers as required by NIP-11
if (lws_add_http_header_by_name(wsi, (unsigned char*)"access-control-allow-origin:",
(unsigned char*)"*", 1, &p, end)) {
free(session_data->json_buffer);
free(session_data);
return -1;
}
if (lws_add_http_header_by_name(wsi, (unsigned char*)"access-control-allow-headers:",
(unsigned char*)"content-type, accept", 20, &p, end)) {
free(session_data->json_buffer);
free(session_data);
return -1;
}
if (lws_add_http_header_by_name(wsi, (unsigned char*)"access-control-allow-methods:",
(unsigned char*)"GET, OPTIONS", 12, &p, end)) {
free(session_data->json_buffer);
free(session_data);
return -1;
}
// Finalize headers
if (lws_finalize_http_header(wsi, &p, end)) {
free(session_data->json_buffer);
free(session_data);
return -1;
}
// Write headers
if (lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS) < 0) {
free(session_data->json_buffer);
free(session_data);
return -1;
}
session_data->headers_sent = 1;
// Request callback for body transmission
lws_callback_on_writable(wsi);
log_success("NIP-11 headers sent, body transmission scheduled");
return 0;
}

191
src/nip013.c Normal file
View File

@@ -0,0 +1,191 @@
// NIP-13 Proof of Work validation module
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include "../nostr_core_lib/cjson/cJSON.h"
#include "../nostr_core_lib/nostr_core/nostr_core.h"
#include "../nostr_core_lib/nostr_core/nip013.h"
#include "config.h"
// Forward declarations for logging functions
void log_info(const char* message);
void log_success(const char* message);
void log_error(const char* message);
void log_warning(const char* message);
// NIP-13 PoW configuration structure
struct pow_config {
int enabled; // 0 = disabled, 1 = enabled
int min_pow_difficulty; // Minimum required difficulty (0 = no requirement)
int validation_flags; // Bitflags for validation options
int require_nonce_tag; // 1 = require nonce tag presence
int reject_lower_targets; // 1 = reject if committed < actual difficulty
int strict_format; // 1 = enforce strict nonce tag format
int anti_spam_mode; // 1 = full anti-spam validation
};
// Initialize PoW configuration using configuration system
void init_pow_config() {
log_info("Initializing NIP-13 Proof of Work configuration");
// Get all config values first (without holding mutex to avoid deadlock)
int pow_enabled = get_config_bool("pow_enabled", 1);
int pow_min_difficulty = get_config_int("pow_min_difficulty", 0);
const char* pow_mode = get_config_value("pow_mode");
pthread_mutex_lock(&g_unified_cache.cache_lock);
// Load PoW settings from configuration system
g_unified_cache.pow_config.enabled = pow_enabled;
g_unified_cache.pow_config.min_pow_difficulty = pow_min_difficulty;
// Configure PoW mode
if (pow_mode) {
if (strcmp(pow_mode, "strict") == 0) {
g_unified_cache.pow_config.validation_flags = NOSTR_POW_VALIDATE_ANTI_SPAM | NOSTR_POW_STRICT_FORMAT;
g_unified_cache.pow_config.require_nonce_tag = 1;
g_unified_cache.pow_config.reject_lower_targets = 1;
g_unified_cache.pow_config.strict_format = 1;
g_unified_cache.pow_config.anti_spam_mode = 1;
log_info("PoW configured in strict anti-spam mode");
} else if (strcmp(pow_mode, "full") == 0) {
g_unified_cache.pow_config.validation_flags = NOSTR_POW_VALIDATE_FULL;
g_unified_cache.pow_config.require_nonce_tag = 1;
log_info("PoW configured in full validation mode");
} else if (strcmp(pow_mode, "basic") == 0) {
g_unified_cache.pow_config.validation_flags = NOSTR_POW_VALIDATE_BASIC;
log_info("PoW configured in basic validation mode");
} else if (strcmp(pow_mode, "disabled") == 0) {
g_unified_cache.pow_config.enabled = 0;
log_info("PoW validation disabled via configuration");
}
free((char*)pow_mode); // Free dynamically allocated string
} else {
// Default to basic mode
g_unified_cache.pow_config.validation_flags = NOSTR_POW_VALIDATE_BASIC;
log_info("PoW configured in basic validation mode (default)");
}
// Log final configuration
char config_msg[512];
snprintf(config_msg, sizeof(config_msg),
"PoW Configuration: enabled=%s, min_difficulty=%d, validation_flags=0x%x, mode=%s",
g_unified_cache.pow_config.enabled ? "true" : "false",
g_unified_cache.pow_config.min_pow_difficulty,
g_unified_cache.pow_config.validation_flags,
g_unified_cache.pow_config.anti_spam_mode ? "anti-spam" :
(g_unified_cache.pow_config.validation_flags & NOSTR_POW_VALIDATE_FULL) ? "full" : "basic");
log_info(config_msg);
pthread_mutex_unlock(&g_unified_cache.cache_lock);
}
// Validate event Proof of Work according to NIP-13
int validate_event_pow(cJSON* event, char* error_message, size_t error_size) {
pthread_mutex_lock(&g_unified_cache.cache_lock);
int enabled = g_unified_cache.pow_config.enabled;
int min_pow_difficulty = g_unified_cache.pow_config.min_pow_difficulty;
int validation_flags = g_unified_cache.pow_config.validation_flags;
pthread_mutex_unlock(&g_unified_cache.cache_lock);
if (!enabled) {
return 0; // PoW validation disabled
}
if (!event) {
snprintf(error_message, error_size, "pow: null event");
return NOSTR_ERROR_INVALID_INPUT;
}
// If min_pow_difficulty is 0, only validate events that have nonce tags
// This allows events without PoW when difficulty requirement is 0
if (min_pow_difficulty == 0) {
cJSON* tags = cJSON_GetObjectItem(event, "tags");
int has_nonce_tag = 0;
if (tags && cJSON_IsArray(tags)) {
cJSON* tag = NULL;
cJSON_ArrayForEach(tag, tags) {
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
if (cJSON_IsString(tag_name)) {
const char* name = cJSON_GetStringValue(tag_name);
if (name && strcmp(name, "nonce") == 0) {
has_nonce_tag = 1;
break;
}
}
}
}
}
// If no minimum difficulty required and no nonce tag, skip PoW validation
if (!has_nonce_tag) {
return 0; // Accept event without PoW when min_difficulty=0
}
}
// Perform PoW validation using nostr_core_lib
nostr_pow_result_t pow_result;
int validation_result = nostr_validate_pow(event, min_pow_difficulty,
validation_flags, &pow_result);
if (validation_result != NOSTR_SUCCESS) {
// Handle specific error cases with appropriate messages
switch (validation_result) {
case NOSTR_ERROR_NIP13_INSUFFICIENT:
snprintf(error_message, error_size,
"pow: insufficient difficulty: %d < %d",
pow_result.actual_difficulty, min_pow_difficulty);
log_warning("Event rejected: insufficient PoW difficulty");
break;
case NOSTR_ERROR_NIP13_NO_NONCE_TAG:
// This should not happen with min_difficulty=0 after our check above
if (min_pow_difficulty > 0) {
snprintf(error_message, error_size, "pow: missing required nonce tag");
log_warning("Event rejected: missing nonce tag");
} else {
return 0; // Allow when min_difficulty=0
}
break;
case NOSTR_ERROR_NIP13_INVALID_NONCE_TAG:
snprintf(error_message, error_size, "pow: invalid nonce tag format");
log_warning("Event rejected: invalid nonce tag format");
break;
case NOSTR_ERROR_NIP13_TARGET_MISMATCH:
snprintf(error_message, error_size,
"pow: committed target (%d) lower than minimum (%d)",
pow_result.committed_target, min_pow_difficulty);
log_warning("Event rejected: committed target too low (anti-spam protection)");
break;
case NOSTR_ERROR_NIP13_CALCULATION:
snprintf(error_message, error_size, "pow: difficulty calculation failed");
log_error("PoW difficulty calculation error");
break;
case NOSTR_ERROR_EVENT_INVALID_ID:
snprintf(error_message, error_size, "pow: invalid event ID format");
log_warning("Event rejected: invalid event ID for PoW calculation");
break;
default:
snprintf(error_message, error_size, "pow: validation failed - %s",
strlen(pow_result.error_detail) > 0 ? pow_result.error_detail : "unknown error");
log_warning("Event rejected: PoW validation failed");
}
return validation_result;
}
// Log successful PoW validation (only if minimum difficulty is required)
if (min_pow_difficulty > 0 || pow_result.has_nonce_tag) {
char debug_msg[256];
snprintf(debug_msg, sizeof(debug_msg),
"PoW validated: difficulty=%d, target=%d, nonce=%llu%s",
pow_result.actual_difficulty,
pow_result.committed_target,
(unsigned long long)pow_result.nonce_value,
pow_result.has_nonce_tag ? "" : " (no nonce tag)");
log_info(debug_msg);
}
return 0; // Success
}

173
src/nip040.c Normal file
View File

@@ -0,0 +1,173 @@
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
// Include nostr_core_lib for cJSON
#include "../nostr_core_lib/cjson/cJSON.h"
// Configuration management system
#include "config.h"
// NIP-40 Expiration configuration structure
struct expiration_config {
int enabled; // 0 = disabled, 1 = enabled
int strict_mode; // 1 = reject expired events on submission
int filter_responses; // 1 = filter expired events from responses
int delete_expired; // 1 = delete expired events from DB (future feature)
long grace_period; // Grace period in seconds for clock skew
};
// Global expiration configuration instance
struct expiration_config g_expiration_config = {
.enabled = 1, // Enable expiration handling by default
.strict_mode = 1, // Reject expired events on submission by default
.filter_responses = 1, // Filter expired events from responses by default
.delete_expired = 0, // Don't delete by default (keep for audit)
.grace_period = 1 // 1 second grace period for testing (was 300)
};
// Forward declarations for logging functions
void log_info(const char* message);
void log_warning(const char* message);
// Initialize expiration configuration using configuration system
void init_expiration_config() {
log_info("Initializing NIP-40 Expiration Timestamp configuration");
// Get all config values first (without holding mutex to avoid deadlock)
int expiration_enabled = get_config_bool("expiration_enabled", 1);
int expiration_strict = get_config_bool("expiration_strict", 1);
int expiration_filter = get_config_bool("expiration_filter", 1);
int expiration_delete = get_config_bool("expiration_delete", 0);
long expiration_grace_period = get_config_int("expiration_grace_period", 1);
// Load expiration settings from configuration system
g_expiration_config.enabled = expiration_enabled;
g_expiration_config.strict_mode = expiration_strict;
g_expiration_config.filter_responses = expiration_filter;
g_expiration_config.delete_expired = expiration_delete;
g_expiration_config.grace_period = expiration_grace_period;
// Validate grace period bounds
if (g_expiration_config.grace_period < 0 || g_expiration_config.grace_period > 86400) {
log_warning("Invalid grace period, using default of 300 seconds");
g_expiration_config.grace_period = 300;
}
// Log final configuration
char config_msg[512];
snprintf(config_msg, sizeof(config_msg),
"Expiration Configuration: enabled=%s, strict_mode=%s, filter_responses=%s, grace_period=%ld seconds",
g_expiration_config.enabled ? "true" : "false",
g_expiration_config.strict_mode ? "true" : "false",
g_expiration_config.filter_responses ? "true" : "false",
g_expiration_config.grace_period);
log_info(config_msg);
}
// Extract expiration timestamp from event tags
long extract_expiration_timestamp(cJSON* tags) {
if (!tags || !cJSON_IsArray(tags)) {
return 0; // No expiration
}
cJSON* tag = NULL;
cJSON_ArrayForEach(tag, tags) {
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
cJSON* tag_value = cJSON_GetArrayItem(tag, 1);
if (cJSON_IsString(tag_name) && cJSON_IsString(tag_value)) {
const char* name = cJSON_GetStringValue(tag_name);
const char* value = cJSON_GetStringValue(tag_value);
if (name && value && strcmp(name, "expiration") == 0) {
// Validate that the string contains only digits (and optional leading whitespace)
const char* p = value;
// Skip leading whitespace
while (*p == ' ' || *p == '\t') p++;
// Check if we have at least one digit
if (*p == '\0') {
continue; // Empty or whitespace-only string, ignore this tag
}
// Validate that all remaining characters are digits
const char* digit_start = p;
while (*p >= '0' && *p <= '9') p++;
// If we didn't consume the entire string or found no digits, it's malformed
if (*p != '\0' || p == digit_start) {
char debug_msg[256];
snprintf(debug_msg, sizeof(debug_msg),
"Ignoring malformed expiration tag value: '%.32s'", value);
log_warning(debug_msg);
continue; // Ignore malformed expiration tag
}
long expiration_ts = atol(value);
if (expiration_ts > 0) {
return expiration_ts;
}
}
}
}
}
return 0; // No valid expiration tag found
}
// Check if event is currently expired
int is_event_expired(cJSON* event, time_t current_time) {
if (!event) {
return 0; // Invalid event, not expired
}
cJSON* tags = cJSON_GetObjectItem(event, "tags");
long expiration_ts = extract_expiration_timestamp(tags);
if (expiration_ts == 0) {
return 0; // No expiration timestamp, not expired
}
// Check if current time exceeds expiration + grace period
return (current_time > (expiration_ts + g_expiration_config.grace_period));
}
// Validate event expiration according to NIP-40
int validate_event_expiration(cJSON* event, char* error_message, size_t error_size) {
if (!g_expiration_config.enabled) {
return 0; // Expiration validation disabled
}
if (!event) {
snprintf(error_message, error_size, "expiration: null event");
return -1;
}
// Check if event is expired
time_t current_time = time(NULL);
if (is_event_expired(event, current_time)) {
if (g_expiration_config.strict_mode) {
cJSON* tags = cJSON_GetObjectItem(event, "tags");
long expiration_ts = extract_expiration_timestamp(tags);
snprintf(error_message, error_size,
"invalid: event expired (expiration=%ld, current=%ld, grace=%ld)",
expiration_ts, (long)current_time, g_expiration_config.grace_period);
log_warning("Event rejected: expired timestamp");
return -1;
} else {
// In non-strict mode, log but allow expired events
char debug_msg[256];
snprintf(debug_msg, sizeof(debug_msg),
"Accepting expired event (strict_mode disabled)");
log_info(debug_msg);
}
}
return 0; // Success
}

180
src/nip042.c Normal file
View File

@@ -0,0 +1,180 @@
#define _GNU_SOURCE
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
// NIP-42 AUTHENTICATION FUNCTIONS
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
#include <pthread.h>
#include <cjson/cJSON.h>
#include <libwebsockets.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
// Forward declarations for logging functions
void log_error(const char* message);
void log_info(const char* message);
void log_warning(const char* message);
void log_success(const char* message);
// Forward declaration for notice message function
void send_notice_message(struct lws* wsi, const char* message);
// Forward declarations for NIP-42 functions from request_validator.c
int nostr_nip42_generate_challenge(char *challenge_buffer, size_t buffer_size);
int nostr_nip42_verify_auth_event(cJSON *event, const char *challenge_id,
const char *relay_url, int time_tolerance_seconds);
// Forward declaration for per_session_data struct (defined in main.c)
struct per_session_data {
int authenticated;
void* subscriptions; // Head of this session's subscription list
pthread_mutex_t session_lock; // Per-session thread safety
char client_ip[41]; // Client IP for logging
int subscription_count; // Number of subscriptions for this session
// NIP-42 Authentication State
char authenticated_pubkey[65]; // Authenticated public key (64 hex + null)
char active_challenge[65]; // Current challenge for this session (64 hex + null)
time_t challenge_created; // When challenge was created
time_t challenge_expires; // Challenge expiration time
int nip42_auth_required_events; // Whether NIP-42 auth is required for EVENT submission
int nip42_auth_required_subscriptions; // Whether NIP-42 auth is required for REQ operations
int auth_challenge_sent; // Whether challenge has been sent (0/1)
};
// Send NIP-42 authentication challenge to client
void send_nip42_auth_challenge(struct lws* wsi, struct per_session_data* pss) {
if (!wsi || !pss) return;
// Generate challenge using existing request_validator function
char challenge[65];
if (nostr_nip42_generate_challenge(challenge, sizeof(challenge)) != 0) {
log_error("Failed to generate NIP-42 challenge");
send_notice_message(wsi, "Authentication temporarily unavailable");
return;
}
// Store challenge in session
pthread_mutex_lock(&pss->session_lock);
strncpy(pss->active_challenge, challenge, sizeof(pss->active_challenge) - 1);
pss->active_challenge[sizeof(pss->active_challenge) - 1] = '\0';
pss->challenge_created = time(NULL);
pss->challenge_expires = pss->challenge_created + 600; // 10 minutes
pss->auth_challenge_sent = 1;
pthread_mutex_unlock(&pss->session_lock);
// Send AUTH challenge message: ["AUTH", <challenge>]
cJSON* auth_msg = cJSON_CreateArray();
cJSON_AddItemToArray(auth_msg, cJSON_CreateString("AUTH"));
cJSON_AddItemToArray(auth_msg, cJSON_CreateString(challenge));
char* msg_str = cJSON_Print(auth_msg);
if (msg_str) {
size_t msg_len = strlen(msg_str);
unsigned char* buf = malloc(LWS_PRE + msg_len);
if (buf) {
memcpy(buf + LWS_PRE, msg_str, msg_len);
lws_write(wsi, buf + LWS_PRE, msg_len, LWS_WRITE_TEXT);
free(buf);
}
free(msg_str);
}
cJSON_Delete(auth_msg);
char debug_msg[128];
snprintf(debug_msg, sizeof(debug_msg), "NIP-42 auth challenge sent: %.16s...", challenge);
log_info(debug_msg);
}
// Handle NIP-42 signed authentication event from client
void handle_nip42_auth_signed_event(struct lws* wsi, struct per_session_data* pss, cJSON* auth_event) {
if (!wsi || !pss || !auth_event) return;
// Serialize event for validation
char* event_json = cJSON_Print(auth_event);
if (!event_json) {
send_notice_message(wsi, "Invalid authentication event format");
return;
}
pthread_mutex_lock(&pss->session_lock);
char challenge_copy[65];
strncpy(challenge_copy, pss->active_challenge, sizeof(challenge_copy) - 1);
challenge_copy[sizeof(challenge_copy) - 1] = '\0';
time_t challenge_expires = pss->challenge_expires;
pthread_mutex_unlock(&pss->session_lock);
// Check if challenge has expired
time_t current_time = time(NULL);
if (current_time > challenge_expires) {
free(event_json);
send_notice_message(wsi, "Authentication challenge expired, please retry");
log_warning("NIP-42 authentication failed: challenge expired");
return;
}
// Verify authentication using existing request_validator function
// Note: nostr_nip42_verify_auth_event doesn't extract pubkey, we need to do that separately
int result = nostr_nip42_verify_auth_event(auth_event, challenge_copy,
"ws://localhost:8888", 600); // 10 minutes tolerance
char authenticated_pubkey[65] = {0};
if (result == 0) {
// Extract pubkey from the auth event
cJSON* pubkey_json = cJSON_GetObjectItem(auth_event, "pubkey");
if (pubkey_json && cJSON_IsString(pubkey_json)) {
const char* pubkey_str = cJSON_GetStringValue(pubkey_json);
if (pubkey_str && strlen(pubkey_str) == 64) {
strncpy(authenticated_pubkey, pubkey_str, sizeof(authenticated_pubkey) - 1);
authenticated_pubkey[sizeof(authenticated_pubkey) - 1] = '\0';
} else {
result = -1; // Invalid pubkey format
}
} else {
result = -1; // Missing pubkey
}
}
free(event_json);
if (result == 0) {
// Authentication successful
pthread_mutex_lock(&pss->session_lock);
pss->authenticated = 1;
strncpy(pss->authenticated_pubkey, authenticated_pubkey, sizeof(pss->authenticated_pubkey) - 1);
pss->authenticated_pubkey[sizeof(pss->authenticated_pubkey) - 1] = '\0';
// Clear challenge
memset(pss->active_challenge, 0, sizeof(pss->active_challenge));
pss->challenge_expires = 0;
pss->auth_challenge_sent = 0;
pthread_mutex_unlock(&pss->session_lock);
char success_msg[256];
snprintf(success_msg, sizeof(success_msg),
"NIP-42 authentication successful for pubkey: %.16s...", authenticated_pubkey);
log_success(success_msg);
send_notice_message(wsi, "NIP-42 authentication successful");
} else {
// Authentication failed
char error_msg[256];
snprintf(error_msg, sizeof(error_msg),
"NIP-42 authentication failed (error code: %d)", result);
log_warning(error_msg);
send_notice_message(wsi, "NIP-42 authentication failed - invalid signature or challenge");
}
}
// Handle challenge response (not typically used in NIP-42, but included for completeness)
void handle_nip42_auth_challenge_response(struct lws* wsi, struct per_session_data* pss, const char* challenge) {
(void)wsi; (void)pss; (void)challenge; // Mark as intentionally unused
// NIP-42 doesn't typically use challenge responses from client to server
// This is reserved for potential future use or protocol extensions
log_warning("Received unexpected challenge response from client (not part of standard NIP-42 flow)");
send_notice_message(wsi, "Challenge responses are not supported - please send signed authentication event");
}

View File

@@ -169,8 +169,8 @@ static void validator_debug_log(const char *message) {
static int reload_auth_config(void);
// Removed unused forward declarations for functions that are no longer called
static int check_database_auth_rules(const char *pubkey, const char *operation,
const char *resource_hash);
int check_database_auth_rules(const char *pubkey, const char *operation,
const char *resource_hash);
void nostr_request_validator_clear_violation(void);
// NIP-42 challenge management functions
@@ -211,13 +211,15 @@ int ginxsom_request_validator_init(const char *db_path, const char *app_name) {
// Initialize NIP-42 challenge manager using unified config
memset(&g_challenge_manager, 0, sizeof(g_challenge_manager));
const char* nip42_timeout = get_config_value("nip42_challenge_timeout");
g_challenge_manager.timeout_seconds = nip42_timeout ? atoi(nip42_timeout) : 600;
if (nip42_timeout) free((char*)nip42_timeout);
const char* nip42_tolerance = get_config_value("nip42_time_tolerance");
g_challenge_manager.time_tolerance_seconds = nip42_tolerance ? atoi(nip42_tolerance) : 300;
if (nip42_tolerance) free((char*)nip42_tolerance);
g_challenge_manager.last_cleanup = time(NULL);
g_validator_initialized = 1;
@@ -232,13 +234,20 @@ int ginxsom_request_validator_init(const char *db_path, const char *app_name) {
int nostr_auth_rules_enabled(void) {
// Use unified cache from config.c
const char* auth_enabled = get_config_value("auth_enabled");
int result = 0;
if (auth_enabled && strcmp(auth_enabled, "true") == 0) {
return 1;
result = 1;
}
if (auth_enabled) free((char*)auth_enabled);
// Also check legacy key
const char* auth_rules_enabled = get_config_value("auth_rules_enabled");
return (auth_rules_enabled && strcmp(auth_rules_enabled, "true") == 0) ? 1 : 0;
if (auth_rules_enabled && strcmp(auth_rules_enabled, "true") == 0) {
result = 1;
}
if (auth_rules_enabled) free((char*)auth_rules_enabled);
return result;
}
///////////////////////////////////////////////////////////////////////////////////////
@@ -344,9 +353,11 @@ int nostr_validate_unified_request(const char* json_string, size_t json_length)
const char* nip42_enabled = get_config_value("nip42_auth_enabled");
if (nip42_enabled && strcmp(nip42_enabled, "false") == 0) {
validator_debug_log("VALIDATOR_DEBUG: STEP 8 FAILED - NIP-42 is disabled\n");
free((char*)nip42_enabled);
cJSON_Delete(event);
return NOSTR_ERROR_NIP42_DISABLED;
}
if (nip42_enabled) free((char*)nip42_enabled);
// TODO: Implement full NIP-42 challenge validation
// For now, accept all valid NIP-42 events
@@ -595,8 +606,8 @@ static int reload_auth_config(void) {
* Check database authentication rules for the request
* Implements the 6-step rule evaluation engine from AUTH_API.md
*/
static int check_database_auth_rules(const char *pubkey, const char *operation,
const char *resource_hash) {
int check_database_auth_rules(const char *pubkey, const char *operation,
const char *resource_hash) {
sqlite3 *db = NULL;
sqlite3_stmt *stmt = NULL;
int rc;

View File

@@ -12,7 +12,7 @@
static const char* const EMBEDDED_SCHEMA_SQL =
"-- C Nostr Relay Database Schema\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\
-- Schema version tracking\n\
PRAGMA user_version = 7;\n\

723
src/subscriptions.c Normal file
View File

@@ -0,0 +1,723 @@
#define _GNU_SOURCE
#include <cjson/cJSON.h>
#include <sqlite3.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include <stdio.h>
#include <printf.h>
#include <pthread.h>
#include <libwebsockets.h>
#include "subscriptions.h"
// Forward declarations for logging functions
void log_info(const char* message);
void log_error(const char* message);
void log_warning(const char* message);
// Forward declarations for configuration functions
const char* get_config_value(const char* key);
// Forward declarations for NIP-40 expiration functions
int is_event_expired(cJSON* event, time_t current_time);
// Global database variable
extern sqlite3* g_db;
// Global unified cache
extern unified_config_cache_t g_unified_cache;
// Global subscription manager
extern subscription_manager_t g_subscription_manager;
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
// PERSISTENT SUBSCRIPTIONS SYSTEM
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
// Create a subscription filter from cJSON filter object
subscription_filter_t* create_subscription_filter(cJSON* filter_json) {
if (!filter_json || !cJSON_IsObject(filter_json)) {
return NULL;
}
subscription_filter_t* filter = calloc(1, sizeof(subscription_filter_t));
if (!filter) {
return NULL;
}
// Copy filter criteria
cJSON* kinds = cJSON_GetObjectItem(filter_json, "kinds");
if (kinds && cJSON_IsArray(kinds)) {
filter->kinds = cJSON_Duplicate(kinds, 1);
}
cJSON* authors = cJSON_GetObjectItem(filter_json, "authors");
if (authors && cJSON_IsArray(authors)) {
filter->authors = cJSON_Duplicate(authors, 1);
}
cJSON* ids = cJSON_GetObjectItem(filter_json, "ids");
if (ids && cJSON_IsArray(ids)) {
filter->ids = cJSON_Duplicate(ids, 1);
}
cJSON* since = cJSON_GetObjectItem(filter_json, "since");
if (since && cJSON_IsNumber(since)) {
filter->since = (long)cJSON_GetNumberValue(since);
}
cJSON* until = cJSON_GetObjectItem(filter_json, "until");
if (until && cJSON_IsNumber(until)) {
filter->until = (long)cJSON_GetNumberValue(until);
}
cJSON* limit = cJSON_GetObjectItem(filter_json, "limit");
if (limit && cJSON_IsNumber(limit)) {
filter->limit = (int)cJSON_GetNumberValue(limit);
}
// Handle tag filters (e.g., {"#e": ["id1"], "#p": ["pubkey1"]})
cJSON* item = NULL;
cJSON_ArrayForEach(item, filter_json) {
if (item->string && strlen(item->string) >= 2 && item->string[0] == '#') {
if (!filter->tag_filters) {
filter->tag_filters = cJSON_CreateObject();
}
if (filter->tag_filters) {
cJSON_AddItemToObject(filter->tag_filters, item->string, cJSON_Duplicate(item, 1));
}
}
}
return filter;
}
// Free a subscription filter
void free_subscription_filter(subscription_filter_t* filter) {
if (!filter) return;
if (filter->kinds) cJSON_Delete(filter->kinds);
if (filter->authors) cJSON_Delete(filter->authors);
if (filter->ids) cJSON_Delete(filter->ids);
if (filter->tag_filters) cJSON_Delete(filter->tag_filters);
if (filter->next) {
free_subscription_filter(filter->next);
}
free(filter);
}
// Create a new subscription
subscription_t* create_subscription(const char* sub_id, struct lws* wsi, cJSON* filters_array, const char* client_ip) {
if (!sub_id || !wsi || !filters_array) {
return NULL;
}
subscription_t* sub = calloc(1, sizeof(subscription_t));
if (!sub) {
return NULL;
}
// Copy subscription ID (truncate if too long)
strncpy(sub->id, sub_id, SUBSCRIPTION_ID_MAX_LENGTH - 1);
sub->id[SUBSCRIPTION_ID_MAX_LENGTH - 1] = '\0';
// Set WebSocket connection
sub->wsi = wsi;
// Set client IP
if (client_ip) {
strncpy(sub->client_ip, client_ip, CLIENT_IP_MAX_LENGTH - 1);
sub->client_ip[CLIENT_IP_MAX_LENGTH - 1] = '\0';
}
// Set timestamps and state
sub->created_at = time(NULL);
sub->events_sent = 0;
sub->active = 1;
// Convert filters array to linked list
subscription_filter_t* filter_tail = NULL;
int filter_count = 0;
if (cJSON_IsArray(filters_array)) {
cJSON* filter_json = NULL;
cJSON_ArrayForEach(filter_json, filters_array) {
if (filter_count >= MAX_FILTERS_PER_SUBSCRIPTION) {
log_warning("Maximum filters per subscription exceeded, ignoring excess filters");
break;
}
subscription_filter_t* filter = create_subscription_filter(filter_json);
if (filter) {
if (!sub->filters) {
sub->filters = filter;
filter_tail = filter;
} else {
filter_tail->next = filter;
filter_tail = filter;
}
filter_count++;
}
}
}
if (filter_count == 0) {
log_error("No valid filters found for subscription");
free(sub);
return NULL;
}
return sub;
}
// Free a subscription
void free_subscription(subscription_t* sub) {
if (!sub) return;
if (sub->filters) {
free_subscription_filter(sub->filters);
}
free(sub);
}
// Add subscription to global manager (thread-safe)
int add_subscription_to_manager(subscription_t* sub) {
if (!sub) return -1;
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
// Check global limits
if (g_subscription_manager.total_subscriptions >= g_subscription_manager.max_total_subscriptions) {
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
log_error("Maximum total subscriptions reached");
return -1;
}
// Add to global list
sub->next = g_subscription_manager.active_subscriptions;
g_subscription_manager.active_subscriptions = sub;
g_subscription_manager.total_subscriptions++;
g_subscription_manager.total_created++;
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
// Log subscription creation to database
log_subscription_created(sub);
char debug_msg[256];
snprintf(debug_msg, sizeof(debug_msg), "Added subscription '%s' (total: %d)",
sub->id, g_subscription_manager.total_subscriptions);
log_info(debug_msg);
return 0;
}
// Remove subscription from global manager (thread-safe)
int remove_subscription_from_manager(const char* sub_id, struct lws* wsi) {
if (!sub_id) return -1;
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
subscription_t** current = &g_subscription_manager.active_subscriptions;
while (*current) {
subscription_t* sub = *current;
// Match by ID and WebSocket connection
if (strcmp(sub->id, sub_id) == 0 && (!wsi || sub->wsi == wsi)) {
// Remove from list
*current = sub->next;
g_subscription_manager.total_subscriptions--;
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
// Log subscription closure to database
log_subscription_closed(sub_id, sub->client_ip, "closed");
// Update events sent counter before freeing
update_subscription_events_sent(sub_id, sub->events_sent);
char debug_msg[256];
snprintf(debug_msg, sizeof(debug_msg), "Removed subscription '%s' (total: %d)",
sub_id, g_subscription_manager.total_subscriptions);
log_info(debug_msg);
free_subscription(sub);
return 0;
}
current = &(sub->next);
}
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
char debug_msg[256];
snprintf(debug_msg, sizeof(debug_msg), "Subscription '%s' not found for removal", sub_id);
log_warning(debug_msg);
return -1;
}
// Check if an event matches a subscription filter
int event_matches_filter(cJSON* event, subscription_filter_t* filter) {
if (!event || !filter) {
return 0;
}
// Check kinds filter
if (filter->kinds && cJSON_IsArray(filter->kinds)) {
cJSON* event_kind = cJSON_GetObjectItem(event, "kind");
if (!event_kind || !cJSON_IsNumber(event_kind)) {
return 0;
}
int event_kind_val = (int)cJSON_GetNumberValue(event_kind);
int kind_match = 0;
cJSON* kind_item = NULL;
cJSON_ArrayForEach(kind_item, filter->kinds) {
if (cJSON_IsNumber(kind_item) && (int)cJSON_GetNumberValue(kind_item) == event_kind_val) {
kind_match = 1;
break;
}
}
if (!kind_match) {
return 0;
}
}
// Check authors filter
if (filter->authors && cJSON_IsArray(filter->authors)) {
cJSON* event_pubkey = cJSON_GetObjectItem(event, "pubkey");
if (!event_pubkey || !cJSON_IsString(event_pubkey)) {
return 0;
}
const char* event_pubkey_str = cJSON_GetStringValue(event_pubkey);
int author_match = 0;
cJSON* author_item = NULL;
cJSON_ArrayForEach(author_item, filter->authors) {
if (cJSON_IsString(author_item)) {
const char* author_str = cJSON_GetStringValue(author_item);
// Support prefix matching (partial pubkeys)
if (strncmp(event_pubkey_str, author_str, strlen(author_str)) == 0) {
author_match = 1;
break;
}
}
}
if (!author_match) {
return 0;
}
}
// Check IDs filter
if (filter->ids && cJSON_IsArray(filter->ids)) {
cJSON* event_id = cJSON_GetObjectItem(event, "id");
if (!event_id || !cJSON_IsString(event_id)) {
return 0;
}
const char* event_id_str = cJSON_GetStringValue(event_id);
int id_match = 0;
cJSON* id_item = NULL;
cJSON_ArrayForEach(id_item, filter->ids) {
if (cJSON_IsString(id_item)) {
const char* id_str = cJSON_GetStringValue(id_item);
// Support prefix matching (partial IDs)
if (strncmp(event_id_str, id_str, strlen(id_str)) == 0) {
id_match = 1;
break;
}
}
}
if (!id_match) {
return 0;
}
}
// Check since filter
if (filter->since > 0) {
cJSON* event_created_at = cJSON_GetObjectItem(event, "created_at");
if (!event_created_at || !cJSON_IsNumber(event_created_at)) {
return 0;
}
long event_timestamp = (long)cJSON_GetNumberValue(event_created_at);
if (event_timestamp < filter->since) {
return 0;
}
}
// Check until filter
if (filter->until > 0) {
cJSON* event_created_at = cJSON_GetObjectItem(event, "created_at");
if (!event_created_at || !cJSON_IsNumber(event_created_at)) {
return 0;
}
long event_timestamp = (long)cJSON_GetNumberValue(event_created_at);
if (event_timestamp > filter->until) {
return 0;
}
}
// Check tag filters (e.g., #e, #p tags)
if (filter->tag_filters && cJSON_IsObject(filter->tag_filters)) {
cJSON* event_tags = cJSON_GetObjectItem(event, "tags");
if (!event_tags || !cJSON_IsArray(event_tags)) {
return 0; // Event has no tags but filter requires tags
}
// Check each tag filter
cJSON* tag_filter = NULL;
cJSON_ArrayForEach(tag_filter, filter->tag_filters) {
if (!tag_filter->string || strlen(tag_filter->string) < 2 || tag_filter->string[0] != '#') {
continue; // Invalid tag filter
}
const char* tag_name = tag_filter->string + 1; // Skip the '#'
if (!cJSON_IsArray(tag_filter)) {
continue; // Tag filter must be an array
}
int tag_match = 0;
// Search through event tags for matching tag name and value
cJSON* event_tag = NULL;
cJSON_ArrayForEach(event_tag, event_tags) {
if (!cJSON_IsArray(event_tag) || cJSON_GetArraySize(event_tag) < 2) {
continue; // Invalid tag format
}
cJSON* event_tag_name = cJSON_GetArrayItem(event_tag, 0);
cJSON* event_tag_value = cJSON_GetArrayItem(event_tag, 1);
if (!cJSON_IsString(event_tag_name) || !cJSON_IsString(event_tag_value)) {
continue;
}
// Check if tag name matches
if (strcmp(cJSON_GetStringValue(event_tag_name), tag_name) == 0) {
const char* event_tag_value_str = cJSON_GetStringValue(event_tag_value);
// Check if any of the filter values match this tag value
cJSON* filter_value = NULL;
cJSON_ArrayForEach(filter_value, tag_filter) {
if (cJSON_IsString(filter_value)) {
const char* filter_value_str = cJSON_GetStringValue(filter_value);
// Support prefix matching for tag values
if (strncmp(event_tag_value_str, filter_value_str, strlen(filter_value_str)) == 0) {
tag_match = 1;
break;
}
}
}
if (tag_match) {
break;
}
}
}
if (!tag_match) {
return 0; // This tag filter didn't match, so the event doesn't match
}
}
}
return 1; // All filters passed
}
// Check if an event matches any filter in a subscription (filters are OR'd together)
int event_matches_subscription(cJSON* event, subscription_t* subscription) {
if (!event || !subscription || !subscription->filters) {
return 0;
}
subscription_filter_t* filter = subscription->filters;
while (filter) {
if (event_matches_filter(event, filter)) {
return 1; // Match found (OR logic)
}
filter = filter->next;
}
return 0; // No filters matched
}
// Broadcast event to all matching subscriptions (thread-safe)
int broadcast_event_to_subscriptions(cJSON* event) {
if (!event) {
return 0;
}
// Check if event is expired and should not be broadcast (NIP-40)
pthread_mutex_lock(&g_unified_cache.cache_lock);
int expiration_enabled = g_unified_cache.expiration_config.enabled;
int filter_responses = g_unified_cache.expiration_config.filter_responses;
pthread_mutex_unlock(&g_unified_cache.cache_lock);
if (expiration_enabled && filter_responses) {
time_t current_time = time(NULL);
if (is_event_expired(event, current_time)) {
char debug_msg[256];
cJSON* event_id_obj = cJSON_GetObjectItem(event, "id");
const char* event_id = event_id_obj ? cJSON_GetStringValue(event_id_obj) : "unknown";
snprintf(debug_msg, sizeof(debug_msg), "Skipping broadcast of expired event: %.16s", event_id);
log_info(debug_msg);
return 0; // Don't broadcast expired events
}
}
int broadcasts = 0;
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
subscription_t* sub = g_subscription_manager.active_subscriptions;
while (sub) {
if (sub->active && event_matches_subscription(event, sub)) {
// Create EVENT message for this subscription
cJSON* event_msg = cJSON_CreateArray();
cJSON_AddItemToArray(event_msg, cJSON_CreateString("EVENT"));
cJSON_AddItemToArray(event_msg, cJSON_CreateString(sub->id));
cJSON_AddItemToArray(event_msg, cJSON_Duplicate(event, 1));
char* msg_str = cJSON_Print(event_msg);
if (msg_str) {
size_t msg_len = strlen(msg_str);
unsigned char* buf = malloc(LWS_PRE + msg_len);
if (buf) {
memcpy(buf + LWS_PRE, msg_str, msg_len);
// Send to WebSocket connection
int write_result = lws_write(sub->wsi, buf + LWS_PRE, msg_len, LWS_WRITE_TEXT);
if (write_result >= 0) {
sub->events_sent++;
broadcasts++;
// Log event broadcast to database (optional - can be disabled for performance)
cJSON* event_id_obj = cJSON_GetObjectItem(event, "id");
if (event_id_obj && cJSON_IsString(event_id_obj)) {
log_event_broadcast(cJSON_GetStringValue(event_id_obj), sub->id, sub->client_ip);
}
}
free(buf);
}
free(msg_str);
}
cJSON_Delete(event_msg);
}
sub = sub->next;
}
// Update global statistics
g_subscription_manager.total_events_broadcast += broadcasts;
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
if (broadcasts > 0) {
char debug_msg[256];
snprintf(debug_msg, sizeof(debug_msg), "Broadcasted event to %d subscriptions", broadcasts);
log_info(debug_msg);
}
return broadcasts;
}
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
// SUBSCRIPTION DATABASE LOGGING
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
// Log subscription creation to database
void log_subscription_created(const subscription_t* sub) {
if (!g_db || !sub) return;
// Create filter JSON for logging
char* filter_json = NULL;
if (sub->filters) {
cJSON* filters_array = cJSON_CreateArray();
subscription_filter_t* filter = sub->filters;
while (filter) {
cJSON* filter_obj = cJSON_CreateObject();
if (filter->kinds) {
cJSON_AddItemToObject(filter_obj, "kinds", cJSON_Duplicate(filter->kinds, 1));
}
if (filter->authors) {
cJSON_AddItemToObject(filter_obj, "authors", cJSON_Duplicate(filter->authors, 1));
}
if (filter->ids) {
cJSON_AddItemToObject(filter_obj, "ids", cJSON_Duplicate(filter->ids, 1));
}
if (filter->since > 0) {
cJSON_AddNumberToObject(filter_obj, "since", filter->since);
}
if (filter->until > 0) {
cJSON_AddNumberToObject(filter_obj, "until", filter->until);
}
if (filter->limit > 0) {
cJSON_AddNumberToObject(filter_obj, "limit", filter->limit);
}
if (filter->tag_filters) {
cJSON* tags_obj = cJSON_Duplicate(filter->tag_filters, 1);
cJSON* item = NULL;
cJSON_ArrayForEach(item, tags_obj) {
if (item->string) {
cJSON_AddItemToObject(filter_obj, item->string, cJSON_Duplicate(item, 1));
}
}
cJSON_Delete(tags_obj);
}
cJSON_AddItemToArray(filters_array, filter_obj);
filter = filter->next;
}
filter_json = cJSON_Print(filters_array);
cJSON_Delete(filters_array);
}
const char* sql =
"INSERT INTO subscription_events (subscription_id, client_ip, event_type, filter_json) "
"VALUES (?, ?, 'created', ?)";
sqlite3_stmt* stmt;
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, sub->id, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, sub->client_ip, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 3, filter_json ? filter_json : "[]", -1, SQLITE_TRANSIENT);
sqlite3_step(stmt);
sqlite3_finalize(stmt);
}
if (filter_json) free(filter_json);
}
// Log subscription closure to database
void log_subscription_closed(const char* sub_id, const char* client_ip, const char* reason) {
(void)reason; // Mark as intentionally unused
if (!g_db || !sub_id) return;
const char* sql =
"INSERT INTO subscription_events (subscription_id, client_ip, event_type) "
"VALUES (?, ?, 'closed')";
sqlite3_stmt* stmt;
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, sub_id, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, client_ip ? client_ip : "unknown", -1, SQLITE_STATIC);
sqlite3_step(stmt);
sqlite3_finalize(stmt);
}
// Update the corresponding 'created' entry with end time and events sent
const char* update_sql =
"UPDATE subscription_events "
"SET ended_at = strftime('%s', 'now') "
"WHERE subscription_id = ? AND event_type = 'created' AND ended_at IS NULL";
rc = sqlite3_prepare_v2(g_db, update_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, sub_id, -1, SQLITE_STATIC);
sqlite3_step(stmt);
sqlite3_finalize(stmt);
}
}
// Log subscription disconnection to database
void log_subscription_disconnected(const char* client_ip) {
if (!g_db || !client_ip) return;
// Mark all active subscriptions for this client as disconnected
const char* sql =
"UPDATE subscription_events "
"SET ended_at = strftime('%s', 'now') "
"WHERE client_ip = ? AND event_type = 'created' AND ended_at IS NULL";
sqlite3_stmt* stmt;
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, client_ip, -1, SQLITE_STATIC);
int changes = sqlite3_changes(g_db);
sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (changes > 0) {
// Log a disconnection event
const char* insert_sql =
"INSERT INTO subscription_events (subscription_id, client_ip, event_type) "
"VALUES ('disconnect', ?, 'disconnected')";
rc = sqlite3_prepare_v2(g_db, insert_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, client_ip, -1, SQLITE_STATIC);
sqlite3_step(stmt);
sqlite3_finalize(stmt);
}
}
}
}
// Log event broadcast to database (optional, can be resource intensive)
void log_event_broadcast(const char* event_id, const char* sub_id, const char* client_ip) {
if (!g_db || !event_id || !sub_id || !client_ip) return;
const char* sql =
"INSERT INTO event_broadcasts (event_id, subscription_id, client_ip) "
"VALUES (?, ?, ?)";
sqlite3_stmt* stmt;
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, sub_id, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 3, client_ip, -1, SQLITE_STATIC);
sqlite3_step(stmt);
sqlite3_finalize(stmt);
}
}
// Update events sent counter for a subscription
void update_subscription_events_sent(const char* sub_id, int events_sent) {
if (!g_db || !sub_id) return;
const char* sql =
"UPDATE subscription_events "
"SET events_sent = ? "
"WHERE subscription_id = ? AND event_type = 'created'";
sqlite3_stmt* stmt;
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_int(stmt, 1, events_sent);
sqlite3_bind_text(stmt, 2, sub_id, -1, SQLITE_STATIC);
sqlite3_step(stmt);
sqlite3_finalize(stmt);
}
}

91
src/subscriptions.h Normal file
View File

@@ -0,0 +1,91 @@
// Subscription system structures and functions for C-Relay
// This header defines subscription management functionality
#ifndef SUBSCRIPTIONS_H
#define SUBSCRIPTIONS_H
#include <pthread.h>
#include <time.h>
#include <stdint.h>
#include "../nostr_core_lib/cjson/cJSON.h"
#include "config.h" // For CLIENT_IP_MAX_LENGTH
// Forward declaration for libwebsockets struct
struct lws;
// Constants
#define SUBSCRIPTION_ID_MAX_LENGTH 64
#define MAX_FILTERS_PER_SUBSCRIPTION 10
#define MAX_TOTAL_SUBSCRIPTIONS 5000
// Forward declarations for typedefs
typedef struct subscription_filter subscription_filter_t;
typedef struct subscription subscription_t;
typedef struct subscription_manager subscription_manager_t;
// Subscription filter structure
struct subscription_filter {
// Filter criteria (all optional)
cJSON* kinds; // Array of event kinds [1,2,3]
cJSON* authors; // Array of author pubkeys
cJSON* ids; // Array of event IDs
long since; // Unix timestamp (0 = not set)
long until; // Unix timestamp (0 = not set)
int limit; // Result limit (0 = no limit)
cJSON* tag_filters; // Object with tag filters: {"#e": ["id1"], "#p": ["pubkey1"]}
// Linked list for multiple filters per subscription
struct subscription_filter* next;
};
// Active subscription structure
struct subscription {
char id[SUBSCRIPTION_ID_MAX_LENGTH]; // Subscription ID
struct lws* wsi; // WebSocket connection handle
subscription_filter_t* filters; // Linked list of filters (OR'd together)
time_t created_at; // When subscription was created
int events_sent; // Counter for sent events
int active; // 1 = active, 0 = closed
// Client info for logging
char client_ip[CLIENT_IP_MAX_LENGTH]; // Client IP address
// Linked list pointers
struct subscription* next; // Next subscription globally
struct subscription* session_next; // Next subscription for this session
};
// Global subscription manager
struct subscription_manager {
subscription_t* active_subscriptions; // Head of global subscription list
pthread_mutex_t subscriptions_lock; // Global thread safety
int total_subscriptions; // Current count
// Configuration
int max_subscriptions_per_client; // Default: 20
int max_total_subscriptions; // Default: 5000
// Statistics
uint64_t total_created; // Lifetime subscription count
uint64_t total_events_broadcast; // Lifetime event broadcast count
};
// Function declarations
subscription_filter_t* create_subscription_filter(cJSON* filter_json);
void free_subscription_filter(subscription_filter_t* filter);
subscription_t* create_subscription(const char* sub_id, struct lws* wsi, cJSON* filters_array, const char* client_ip);
void free_subscription(subscription_t* sub);
int add_subscription_to_manager(subscription_t* sub);
int remove_subscription_from_manager(const char* sub_id, struct lws* wsi);
int event_matches_filter(cJSON* event, subscription_filter_t* filter);
int event_matches_subscription(cJSON* event, subscription_t* subscription);
int broadcast_event_to_subscriptions(cJSON* event);
// Database logging functions
void log_subscription_created(const subscription_t* sub);
void log_subscription_closed(const char* sub_id, const char* client_ip, const char* reason);
void log_subscription_disconnected(const char* client_ip);
void log_event_broadcast(const char* event_id, const char* sub_id, const char* client_ip);
void update_subscription_events_sent(const char* sub_id, int events_sent);
#endif // SUBSCRIPTIONS_H

1188
src/websockets.c Normal file

File diff suppressed because it is too large Load Diff

49
src/websockets.h Normal file
View File

@@ -0,0 +1,49 @@
// WebSocket protocol structures and constants for C-Relay
// This header defines structures shared between main.c and websockets.c
#ifndef WEBSOCKETS_H
#define WEBSOCKETS_H
#include <pthread.h>
#include <libwebsockets.h>
#include <time.h>
#include "../nostr_core_lib/cjson/cJSON.h"
#include "config.h" // For CLIENT_IP_MAX_LENGTH and MAX_SUBSCRIPTIONS_PER_CLIENT
// Constants
#define CHALLENGE_MAX_LENGTH 128
#define AUTHENTICATED_PUBKEY_MAX_LENGTH 65 // 64 hex + null
// Enhanced per-session data with subscription management and NIP-42 authentication
struct per_session_data {
int authenticated;
struct subscription* subscriptions; // Head of this session's subscription list
pthread_mutex_t session_lock; // Per-session thread safety
char client_ip[CLIENT_IP_MAX_LENGTH]; // Client IP for logging
int subscription_count; // Number of subscriptions for this session
// NIP-42 Authentication State
char authenticated_pubkey[65]; // Authenticated public key (64 hex + null)
char active_challenge[65]; // Current challenge for this session (64 hex + null)
time_t challenge_created; // When challenge was created
time_t challenge_expires; // Challenge expiration time
int nip42_auth_required_events; // Whether NIP-42 auth is required for EVENT submission
int nip42_auth_required_subscriptions; // Whether NIP-42 auth is required for REQ operations
int auth_challenge_sent; // Whether challenge has been sent (0/1)
};
// NIP-11 HTTP session data structure for managing buffer lifetime
struct nip11_session_data {
char* json_buffer;
size_t json_length;
int headers_sent;
int body_sent;
};
// Function declarations
int start_websocket_relay(int port_override, int strict_port);
// Auth rules checking function from request_validator.c
int check_database_auth_rules(const char *pubkey, const char *operation, const char *resource_hash);
#endif // WEBSOCKETS_H

133
test_dynamic_config.sh Executable file
View File

@@ -0,0 +1,133 @@
#!/bin/bash
# Test dynamic config updates without restart
set -e
# Configuration from relay startup
ADMIN_PRIVKEY="ddea442930976541e199a05248eb6cd92f2a65ba366a883a8f6880add9bdc9c9"
RELAY_PUBKEY="1bd4a5e2e32401737f8c16cc0dfa89b93f25f395770a2896fe78c9fb61582dfc"
RELAY_URL="ws://localhost:8888"
# Colors
GREEN='\033[0;32m'
RED='\033[0;31m'
BLUE='\033[0;34m'
NC='\033[0m'
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Check if nak is available
if ! command -v nak &> /dev/null; then
log_error "nak command not found. Please install nak first."
exit 1
fi
log_info "Testing dynamic config updates without restart..."
# Test 1: Check current NIP-11 info
log_info "Checking current NIP-11 relay info..."
CURRENT_DESC=$(curl -s -H "Accept: application/nostr+json" http://localhost:8888 | jq -r '.description')
log_info "Current description: $CURRENT_DESC"
# Test 2: Update relay description dynamically
NEW_DESC="Dynamic Config Test - Updated at $(date)"
log_info "Updating relay description to: $NEW_DESC"
COMMAND="[\"config_update\", [{\"key\": \"relay_description\", \"value\": \"$NEW_DESC\", \"data_type\": \"string\", \"category\": \"relay\"}]]"
# Encrypt the command
ENCRYPTED_COMMAND=$(nak encrypt "$COMMAND" --sec "$ADMIN_PRIVKEY" --recipient-pubkey "$RELAY_PUBKEY")
if [ -z "$ENCRYPTED_COMMAND" ]; then
log_error "Failed to encrypt config update command"
exit 1
fi
# Create admin event
ADMIN_EVENT=$(nak event \
--kind 23456 \
--content "$ENCRYPTED_COMMAND" \
--sec "$ADMIN_PRIVKEY" \
--tag "p=$RELAY_PUBKEY")
# Send the admin command
log_info "Sending config update command..."
ADMIN_RESULT=$(echo "$ADMIN_EVENT" | nak event "$RELAY_URL")
if echo "$ADMIN_RESULT" | grep -q "error\|failed\|denied"; then
log_error "Failed to send config update: $ADMIN_RESULT"
exit 1
fi
log_success "Config update command sent successfully"
# Wait for processing
sleep 3
# Test 3: Check if NIP-11 info updated without restart
log_info "Checking if NIP-11 info was updated without restart..."
UPDATED_DESC=$(curl -s -H "Accept: application/nostr+json" http://localhost:8888 | jq -r '.description')
if [ "$UPDATED_DESC" = "$NEW_DESC" ]; then
log_success "SUCCESS: Relay description updated dynamically without restart!"
log_success "Old: $CURRENT_DESC"
log_success "New: $UPDATED_DESC"
else
log_error "FAILED: Relay description was not updated"
log_error "Expected: $NEW_DESC"
log_error "Got: $UPDATED_DESC"
exit 1
fi
# Test 4: Test another dynamic config - max_subscriptions_per_client
log_info "Testing another dynamic config: max_subscriptions_per_client"
# Get current value from database
OLD_LIMIT=$(sqlite3 build/*.db "SELECT value FROM config WHERE key = 'max_subscriptions_per_client';" 2>/dev/null || echo "25")
log_info "Current max_subscriptions_per_client: $OLD_LIMIT"
NEW_LIMIT=50
COMMAND2="[\"config_update\", [{\"key\": \"max_subscriptions_per_client\", \"value\": \"$NEW_LIMIT\", \"data_type\": \"integer\", \"category\": \"limits\"}]]"
ENCRYPTED_COMMAND2=$(nak encrypt "$COMMAND2" --sec "$ADMIN_PRIVKEY" --recipient-pubkey "$RELAY_PUBKEY")
ADMIN_EVENT2=$(nak event \
--kind 23456 \
--content "$ENCRYPTED_COMMAND2" \
--sec "$ADMIN_PRIVKEY" \
--tag "p=$RELAY_PUBKEY")
log_info "Updating max_subscriptions_per_client to $NEW_LIMIT..."
ADMIN_RESULT2=$(echo "$ADMIN_EVENT2" | nak event "$RELAY_URL")
if echo "$ADMIN_RESULT2" | grep -q "error\|failed\|denied"; then
log_error "Failed to send second config update: $ADMIN_RESULT2"
exit 1
fi
sleep 3
# Check updated value from database
UPDATED_LIMIT=$(sqlite3 build/*.db "SELECT value FROM config WHERE key = 'max_subscriptions_per_client';" 2>/dev/null || echo "25")
if [ "$UPDATED_LIMIT" = "$NEW_LIMIT" ]; then
log_success "SUCCESS: max_subscriptions_per_client updated dynamically!"
log_success "Old: $OLD_LIMIT, New: $UPDATED_LIMIT"
else
log_error "FAILED: max_subscriptions_per_client was not updated"
log_error "Expected: $NEW_LIMIT, Got: $UPDATED_LIMIT"
fi
log_success "Dynamic config update testing completed successfully!"

97
test_nip50_search.sh Normal file
View File

@@ -0,0 +1,97 @@
#!/bin/bash
# Test script for NIP-50 search functionality
# This script tests the new search field in filter objects
echo "=== Testing NIP-50 Search Functionality ==="
# Function to send WebSocket message and capture response
send_ws_message() {
local message="$1"
echo "Sending: $message"
echo "$message" | websocat ws://127.0.0.1:8888 --text --no-close --one-message 2>/dev/null
}
# Function to publish an event
publish_event() {
local content="$1"
local kind="${2:-1}"
local tags="${3:-[]}"
# Create event JSON
local event="[\"EVENT\", {\"id\": \"\", \"pubkey\": \"\", \"created_at\": $(date +%s), \"kind\": $kind, \"tags\": $tags, \"content\": \"$content\", \"sig\": \"\"}]"
# Send the event
send_ws_message "$event"
}
# Function to search for events
search_events() {
local search_term="$1"
local sub_id="${2:-search_test}"
# Create search filter
local filter="{\"search\": \"$search_term\"}"
local req="[\"REQ\", \"$sub_id\", $filter]"
# Send the search request
send_ws_message "$req"
# Wait a moment for response
sleep 0.5
# Send CLOSE to end subscription
local close="[\"CLOSE\", \"$sub_id\"]"
send_ws_message "$close"
}
# Function to count events with search
count_events() {
local search_term="$1"
local sub_id="${2:-count_test}"
# Create count filter with search
local filter="{\"search\": \"$search_term\"}"
local count_req="[\"COUNT\", \"$sub_id\", $filter]"
# Send the count request
send_ws_message "$count_req"
}
echo "Publishing test events with searchable content..."
# Publish some test events with different content
publish_event "This is a test message about Bitcoin"
publish_event "Another message about Lightning Network"
publish_event "Nostr protocol discussion"
publish_event "Random content without keywords"
publish_event "Bitcoin and Lightning are great technologies"
publish_event "Discussion about Nostr and Bitcoin integration"
echo "Waiting for events to be stored..."
sleep 2
echo ""
echo "Testing search functionality..."
echo "1. Searching for 'Bitcoin':"
search_events "Bitcoin"
echo ""
echo "2. Searching for 'Nostr':"
search_events "Nostr"
echo ""
echo "3. Searching for 'Lightning':"
search_events "Lightning"
echo ""
echo "4. Testing COUNT with search:"
count_events "Bitcoin"
echo ""
echo "5. Testing COUNT with search for 'Nostr':"
count_events "Nostr"
echo ""
echo "=== NIP-50 Search Test Complete ==="

View File

@@ -146,27 +146,115 @@ test_subscription() {
local filter="$2"
local description="$3"
local expected_count="$4"
print_step "Testing subscription: $description"
# Create REQ message
local req_message="[\"REQ\",\"$sub_id\",$filter]"
print_info "Testing filter: $filter"
# Send subscription and collect events
local response=""
if command -v websocat &> /dev/null; then
response=$(echo -e "$req_message\n[\"CLOSE\",\"$sub_id\"]" | timeout 3s websocat "$RELAY_URL" 2>/dev/null || echo "")
fi
# Count EVENT responses (lines containing ["EVENT","sub_id",...])
local event_count=0
local filter_mismatch_count=0
if [[ -n "$response" ]]; then
event_count=$(echo "$response" | grep -c "\"EVENT\"" 2>/dev/null || echo "0")
filter_mismatch_count=$(echo "$response" | grep -c "filter does not match" 2>/dev/null || echo "0")
fi
# Clean up the filter_mismatch_count (remove any extra spaces/newlines)
filter_mismatch_count=$(echo "$filter_mismatch_count" | tr -d '[:space:]' | sed 's/[^0-9]//g')
if [[ -z "$filter_mismatch_count" ]]; then
filter_mismatch_count=0
fi
# Debug: Show what we found
print_info "Found $event_count events, $filter_mismatch_count filter mismatches"
# Check for filter mismatches (protocol violation)
if [[ "$filter_mismatch_count" -gt 0 ]]; then
print_error "$description - PROTOCOL VIOLATION: Relay sent $filter_mismatch_count events that don't match filter!"
print_error "Filter: $filter"
print_error "This indicates improper server-side filtering - relay should only send matching events"
return 1
fi
# Additional check: Analyze returned events against filter criteria
local filter_violation_count=0
if [[ -n "$response" && "$event_count" -gt 0 ]]; then
# Parse filter to check for violations
if echo "$filter" | grep -q '"kinds":\['; then
# Kind filter - check that all returned events have matching kinds
local allowed_kinds=$(echo "$filter" | sed 's/.*"kinds":\[\([^]]*\)\].*/\1/' | sed 's/[^0-9,]//g')
echo "$response" | grep '"EVENT"' | while IFS= read -r event_line; do
local event_kind=$(echo "$event_line" | jq -r '.[2].kind' 2>/dev/null)
if [[ -n "$event_kind" && "$event_kind" =~ ^[0-9]+$ ]]; then
local kind_matches=0
IFS=',' read -ra KIND_ARRAY <<< "$allowed_kinds"
for kind in "${KIND_ARRAY[@]}"; do
if [[ "$event_kind" == "$kind" ]]; then
kind_matches=1
break
fi
done
if [[ "$kind_matches" == "0" ]]; then
((filter_violation_count++))
fi
fi
done
elif echo "$filter" | grep -q '"ids":\['; then
# ID filter - check that all returned events have matching IDs
local allowed_ids=$(echo "$filter" | sed 's/.*"ids":\[\([^]]*\)\].*/\1/' | sed 's/"//g' | sed 's/[][]//g')
echo "$response" | grep '"EVENT"' | while IFS= read -r event_line; do
local event_id=$(echo "$event_line" | jq -r '.[2].id' 2>/dev/null)
if [[ -n "$event_id" ]]; then
local id_matches=0
IFS=',' read -ra ID_ARRAY <<< "$allowed_ids"
for id in "${ID_ARRAY[@]}"; do
if [[ "$event_id" == "$id" ]]; then
id_matches=1
break
fi
done
if [[ "$id_matches" == "0" ]]; then
((filter_violation_count++))
fi
fi
done
fi
fi
# Report filter violations
if [[ "$filter_violation_count" -gt 0 ]]; then
print_error "$description - FILTER VIOLATION: $filter_violation_count events don't match the filter criteria!"
print_error "Filter: $filter"
print_error "Expected only events matching the filter, but received non-matching events"
print_error "This indicates improper server-side filtering"
return 1
fi
# Also fail on count mismatches for strict filters (like specific IDs and kinds with expected counts)
if [[ "$expected_count" != "any" && "$event_count" != "$expected_count" ]]; then
if echo "$filter" | grep -q '"ids":\['; then
print_error "$description - CRITICAL VIOLATION: ID filter should return exactly $expected_count event(s), got $event_count"
print_error "Filter: $filter"
print_error "ID queries must return exactly the requested event or none"
return 1
elif echo "$filter" | grep -q '"kinds":\[' && [[ "$expected_count" =~ ^[0-9]+$ ]]; then
print_error "$description - FILTER VIOLATION: Kind filter expected $expected_count event(s), got $event_count"
print_error "Filter: $filter"
print_error "This suggests improper filtering - events of wrong kinds are being returned"
return 1
fi
fi
if [[ "$expected_count" == "any" ]]; then
if [[ $event_count -gt 0 ]]; then
print_success "$description - Found $event_count events"
@@ -178,7 +266,7 @@ test_subscription() {
else
print_warning "$description - Expected $expected_count events, found $event_count"
fi
# Show a few sample events for verification (first 2)
if [[ $event_count -gt 0 && "$description" == "All events" ]]; then
print_info "Sample events (first 2):"
@@ -189,7 +277,7 @@ test_subscription() {
echo " - ID: ${event_id:0:16}... Kind: $event_kind Content: ${event_content:0:30}..."
done
fi
echo # Add blank line for readability
return 0
}
@@ -290,30 +378,64 @@ run_comprehensive_test() {
# Test subscription filters
print_step "Testing various subscription filters..."
local test_failures=0
# Test 1: Get all events
test_subscription "test_all" '{}' "All events" "any"
if ! test_subscription "test_all" '{}' "All events" "any"; then
((test_failures++))
fi
# Test 2: Get events by kind
test_subscription "test_kind1" '{"kinds":[1]}' "Kind 1 events only" "2"
test_subscription "test_kind0" '{"kinds":[0]}' "Kind 0 events only" "any"
if ! test_subscription "test_kind1" '{"kinds":[1]}' "Kind 1 events only" "any"; then
((test_failures++))
fi
if ! test_subscription "test_kind0" '{"kinds":[0]}' "Kind 0 events only" "any"; then
((test_failures++))
fi
# Test 3: Get events by author (pubkey)
local test_pubkey=$(echo "$regular1" | jq -r '.pubkey' 2>/dev/null)
test_subscription "test_author" "{\"authors\":[\"$test_pubkey\"]}" "Events by specific author" "any"
if ! test_subscription "test_author" "{\"authors\":[\"$test_pubkey\"]}" "Events by specific author" "any"; then
((test_failures++))
fi
# Test 4: Get recent events (time-based)
local recent_timestamp=$(($(date +%s) - 200))
test_subscription "test_recent" "{\"since\":$recent_timestamp}" "Recent events" "any"
if ! test_subscription "test_recent" "{\"since\":$recent_timestamp}" "Recent events" "any"; then
((test_failures++))
fi
# Test 5: Get events with specific tags
test_subscription "test_tag_type" '{"#type":["regular"]}' "Events with type=regular tag" "any"
if ! test_subscription "test_tag_type" '{"#type":["regular"]}' "Events with type=regular tag" "any"; then
((test_failures++))
fi
# Test 6: Multiple kinds
test_subscription "test_multi_kinds" '{"kinds":[0,1]}' "Multiple kinds (0,1)" "any"
if ! test_subscription "test_multi_kinds" '{"kinds":[0,1]}' "Multiple kinds (0,1)" "any"; then
((test_failures++))
fi
# Test 7: Limit results
test_subscription "test_limit" '{"kinds":[1],"limit":1}' "Limited to 1 event" "1"
if ! test_subscription "test_limit" '{"kinds":[1],"limit":1}' "Limited to 1 event" "1"; then
((test_failures++))
fi
# Test 8: Specific event ID query (tests for "filter does not match" bug)
if [[ ${#REGULAR_EVENT_IDS[@]} -gt 0 ]]; then
local test_event_id="${REGULAR_EVENT_IDS[0]}"
if ! test_subscription "test_specific_id" "{\"ids\":[\"$test_event_id\"]}" "Specific event ID query" "1"; then
((test_failures++))
fi
fi
# Report subscription test results
if [[ $test_failures -gt 0 ]]; then
print_error "SUBSCRIPTION TESTS FAILED: $test_failures test(s) detected protocol violations"
return 1
else
print_success "All subscription tests passed"
fi
print_header "PHASE 4: Database Verification"
@@ -321,17 +443,28 @@ run_comprehensive_test() {
print_step "Verifying database contents..."
if command -v sqlite3 &> /dev/null; then
print_info "Events by type in database:"
sqlite3 db/c_nostr_relay.db "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" | while read line; do
echo " $line"
done
print_info "Recent events in database:"
sqlite3 db/c_nostr_relay.db "SELECT substr(id, 1, 16) || '...' as short_id, event_type, kind, substr(content, 1, 30) || '...' as short_content FROM events ORDER BY created_at DESC LIMIT 5;" | while read line; do
echo " $line"
done
print_success "Database verification complete"
# Find the database file (should be in build/ directory with relay pubkey as filename)
local db_file=""
if [[ -d "../build" ]]; then
db_file=$(find ../build -name "*.db" -type f | head -1)
fi
if [[ -n "$db_file" && -f "$db_file" ]]; then
print_info "Events by type in database ($db_file):"
sqlite3 "$db_file" "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null | while read line; do
echo " $line"
done
print_info "Recent events in database:"
sqlite3 "$db_file" "SELECT substr(id, 1, 16) || '...' as short_id, event_type, kind, substr(content, 1, 30) || '...' as short_content FROM events ORDER BY created_at DESC LIMIT 5;" 2>/dev/null | while read line; do
echo " $line"
done
print_success "Database verification complete"
else
print_warning "Database file not found in build/ directory"
print_info "Expected database files: build/*.db (named after relay pubkey)"
fi
else
print_warning "sqlite3 not available for database verification"
fi
@@ -352,6 +485,11 @@ if run_comprehensive_test; then
exit 0
else
echo
print_error "Some tests failed"
print_error "❌ TESTS FAILED: Protocol violations detected!"
print_error "The C-Relay has critical issues that need to be fixed:"
print_error " - Server-side filtering is not implemented properly"
print_error " - Events are sent to clients regardless of subscription filters"
print_error " - This violates the Nostr protocol specification"
echo
exit 1
fi

450
tests/45_nip_test.sh Executable file
View File

@@ -0,0 +1,450 @@
#!/bin/bash
# NIP-45 COUNT Message Test - Test counting functionality
# Tests COUNT messages with various filters to verify correct event counting
set -e # Exit on any error
# Color constants
RED='\033[31m'
GREEN='\033[32m'
YELLOW='\033[33m'
BLUE='\033[34m'
BOLD='\033[1m'
RESET='\033[0m'
# Test configuration
RELAY_URL="ws://127.0.0.1:8888"
TEST_PRIVATE_KEY="nsec1j4c6269y9w0q2er2xjw8sv2ehyrtfxq3jwgdlxj6qfn8z4gjsq5qfvfk99"
# Print functions
print_header() {
echo -e "${BLUE}${BOLD}=== $1 ===${RESET}"
}
print_step() {
echo -e "${YELLOW}[STEP]${RESET} $1"
}
print_success() {
echo -e "${GREEN}${RESET} $1"
}
print_error() {
echo -e "${RED}${RESET} $1"
}
print_info() {
echo -e "${BLUE}[INFO]${RESET} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${RESET} $1"
}
# Global arrays to store event IDs for counting tests
declare -a REGULAR_EVENT_IDS=()
declare -a REPLACEABLE_EVENT_IDS=()
declare -a EPHEMERAL_EVENT_IDS=()
declare -a ADDRESSABLE_EVENT_IDS=()
# Baseline counts from existing events in relay
BASELINE_TOTAL=0
BASELINE_KIND1=0
BASELINE_KIND0=0
BASELINE_KIND30001=0
BASELINE_AUTHOR=0
BASELINE_TYPE_REGULAR=0
BASELINE_TEST_NIP45=0
BASELINE_KINDS_01=0
BASELINE_COMBINED=0
# Helper function to publish event and extract ID
publish_event() {
local event_json="$1"
local event_type="$2"
local description="$3"
# Extract event ID
local event_id=$(echo "$event_json" | jq -r '.id' 2>/dev/null)
if [[ "$event_id" == "null" || -z "$event_id" ]]; then
print_error "Could not extract event ID from $description"
return 1
fi
print_info "Publishing $description..."
# Create EVENT message in Nostr format
local event_message="[\"EVENT\",$event_json]"
# Publish to relay
local response=""
if command -v websocat &> /dev/null; then
response=$(echo "$event_message" | timeout 5s websocat "$RELAY_URL" 2>&1 || echo "Connection failed")
else
print_error "websocat not found - required for testing"
return 1
fi
# Check response
if [[ "$response" == *"Connection failed"* ]]; then
print_error "Failed to connect to relay for $description"
return 1
elif [[ "$response" == *"true"* ]]; then
print_success "$description uploaded (ID: ${event_id:0:16}...)"
# Store event ID in appropriate array
case "$event_type" in
"regular") REGULAR_EVENT_IDS+=("$event_id") ;;
"replaceable") REPLACEABLE_EVENT_IDS+=("$event_id") ;;
"ephemeral") EPHEMERAL_EVENT_IDS+=("$event_id") ;;
"addressable") ADDRESSABLE_EVENT_IDS+=("$event_id") ;;
esac
echo # Add blank line for readability
return 0
else
print_warning "$description might have failed: $response"
echo # Add blank line for readability
return 1
fi
}
# Helper function to get baseline count for a filter (before publishing test events)
get_baseline_count() {
local filter="$1"
# Create COUNT message
local count_message="[\"COUNT\",\"baseline\",$filter]"
# Send COUNT message and get response
local response=""
if command -v websocat &> /dev/null; then
response=$(echo "$count_message" | timeout 3s websocat "$RELAY_URL" 2>/dev/null || echo "")
fi
# Parse COUNT response
if [[ -n "$response" ]]; then
local count_result=$(echo "$response" | grep '"COUNT"' | head -1)
if [[ -n "$count_result" ]]; then
local count=$(echo "$count_result" | jq -r '.[2].count' 2>/dev/null)
if [[ "$count" =~ ^[0-9]+$ ]]; then
echo "$count"
return 0
fi
fi
fi
echo "0" # Default to 0 if we can't get the count
}
# Helper function to send COUNT message and check response
test_count() {
local sub_id="$1"
local filter="$2"
local description="$3"
local expected_count="$4"
print_step "Testing COUNT: $description"
# Create COUNT message
local count_message="[\"COUNT\",\"$sub_id\",$filter]"
print_info "Sending filter: $filter"
# Send COUNT message and get response
local response=""
if command -v websocat &> /dev/null; then
response=$(echo "$count_message" | timeout 3s websocat "$RELAY_URL" 2>/dev/null || echo "")
fi
# Parse COUNT response
local count_result=""
if [[ -n "$response" ]]; then
# Look for COUNT response: ["COUNT","sub_id",{"count":N}]
count_result=$(echo "$response" | grep '"COUNT"' | head -1)
if [[ -n "$count_result" ]]; then
local actual_count=$(echo "$count_result" | jq -r '.[2].count' 2>/dev/null)
if [[ "$actual_count" =~ ^[0-9]+$ ]]; then
print_info "Received count: $actual_count"
# Check if count matches expected
if [[ "$expected_count" == "any" ]]; then
print_success "$description - Count: $actual_count"
return 0
elif [[ "$actual_count" -eq "$expected_count" ]]; then
print_success "$description - Expected: $expected_count, Got: $actual_count"
return 0
else
print_error "$description - Expected: $expected_count, Got: $actual_count"
return 1
fi
else
print_error "$description - Invalid count response: $count_result"
return 1
fi
else
print_error "$description - No COUNT response received"
print_error "Raw response: $response"
return 1
fi
else
print_error "$description - No response from relay"
return 1
fi
}
# Main test function
run_count_test() {
print_header "NIP-45 COUNT Message Test"
# Check dependencies
print_step "Checking dependencies..."
if ! command -v nak &> /dev/null; then
print_error "nak command not found"
print_info "Please install nak: go install github.com/fiatjaf/nak@latest"
return 1
fi
if ! command -v websocat &> /dev/null; then
print_error "websocat command not found"
print_info "Please install websocat for testing"
return 1
fi
if ! command -v jq &> /dev/null; then
print_error "jq command not found"
print_info "Please install jq for JSON processing"
return 1
fi
print_success "All dependencies found"
print_header "PHASE 0: Establishing Baseline Counts"
# Get baseline counts BEFORE publishing any test events
print_step "Getting baseline counts from existing events in relay..."
BASELINE_TOTAL=$(get_baseline_count '{}' "total events")
BASELINE_KIND1=$(get_baseline_count '{"kinds":[1]}' "kind 1 events")
BASELINE_KIND0=$(get_baseline_count '{"kinds":[0]}' "kind 0 events")
BASELINE_KIND30001=$(get_baseline_count '{"kinds":[30001]}' "kind 30001 events")
# We can't get the author baseline yet since we don't have the pubkey
BASELINE_AUTHOR=0 # Will be set after first event is created
BASELINE_TYPE_REGULAR=$(get_baseline_count '{"#type":["regular"]}' "events with type=regular tag")
BASELINE_TEST_NIP45=$(get_baseline_count '{"#test":["nip45"]}' "events with test=nip45 tag")
BASELINE_KINDS_01=$(get_baseline_count '{"kinds":[0,1]}' "events with kinds 0 or 1")
BASELINE_COMBINED=$(get_baseline_count '{"kinds":[1],"#type":["regular"],"#test":["nip45"]}' "combined filter (kind 1 + type=regular + test=nip45)")
print_info "Initial baseline counts established:"
print_info " Total events: $BASELINE_TOTAL"
print_info " Kind 1: $BASELINE_KIND1"
print_info " Kind 0: $BASELINE_KIND0"
print_info " Kind 30001: $BASELINE_KIND30001"
print_info " Type=regular: $BASELINE_TYPE_REGULAR"
print_info " Test=nip45: $BASELINE_TEST_NIP45"
print_info " Kinds 0+1: $BASELINE_KINDS_01"
print_info " Combined filter: $BASELINE_COMBINED"
print_header "PHASE 1: Publishing Test Events"
# Test 1: Regular Events (kind 1)
print_step "Creating regular events (kind 1)..."
local regular1=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Regular event #1 for counting" -k 1 --ts $(($(date +%s) - 100)) -t "type=regular" -t "test=nip45" 2>/dev/null)
local regular2=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Regular event #2 for counting" -k 1 --ts $(($(date +%s) - 90)) -t "type=regular" -t "test=nip45" 2>/dev/null)
local regular3=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Regular event #3 for counting" -k 1 --ts $(($(date +%s) - 80)) -t "type=regular" -t "test=nip45" 2>/dev/null)
publish_event "$regular1" "regular" "Regular event #1"
# Now that we have the pubkey, get the author baseline
local test_pubkey=$(echo "$regular1" | jq -r '.pubkey' 2>/dev/null)
BASELINE_AUTHOR=$(get_baseline_count "{\"authors\":[\"$test_pubkey\"]}" "events by test author")
publish_event "$regular2" "regular" "Regular event #2"
publish_event "$regular3" "regular" "Regular event #3"
# Test 2: Replaceable Events (kind 0 - metadata)
print_step "Creating replaceable events (kind 0)..."
local replaceable1=$(nak event --sec "$TEST_PRIVATE_KEY" -c '{"name":"Test User","about":"Testing NIP-45 COUNT"}' -k 0 --ts $(($(date +%s) - 70)) -t "type=replaceable" 2>/dev/null)
local replaceable2=$(nak event --sec "$TEST_PRIVATE_KEY" -c '{"name":"Test User Updated","about":"Updated for NIP-45"}' -k 0 --ts $(($(date +%s) - 60)) -t "type=replaceable" 2>/dev/null)
publish_event "$replaceable1" "replaceable" "Replaceable event #1 (metadata)"
publish_event "$replaceable2" "replaceable" "Replaceable event #2 (metadata update)"
# Test 3: Ephemeral Events (kind 20000+) - should NOT be counted
print_step "Creating ephemeral events (kind 20001)..."
local ephemeral1=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Ephemeral event - should not be counted" -k 20001 --ts $(date +%s) -t "type=ephemeral" 2>/dev/null)
publish_event "$ephemeral1" "ephemeral" "Ephemeral event (should not be counted)"
# Test 4: Addressable Events (kind 30000+)
print_step "Creating addressable events (kind 30001)..."
local addressable1=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Addressable event #1" -k 30001 --ts $(($(date +%s) - 50)) -t "d=test-article" -t "type=addressable" 2>/dev/null)
local addressable2=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Addressable event #2" -k 30001 --ts $(($(date +%s) - 40)) -t "d=test-article" -t "type=addressable" 2>/dev/null)
publish_event "$addressable1" "addressable" "Addressable event #1"
publish_event "$addressable2" "addressable" "Addressable event #2"
# Brief pause to let events settle
sleep 2
print_header "PHASE 2: Testing COUNT Messages"
local test_failures=0
# Test 1: Count all events
if ! test_count "count_all" '{}' "Count all events" "any"; then
((test_failures++))
fi
# Test 2: Count events by kind
# Regular events (kind 1): no replacement, all 3 should remain
local expected_kind1=$((3 + BASELINE_KIND1))
if ! test_count "count_kind1" '{"kinds":[1]}' "Count kind 1 events" "$expected_kind1"; then
((test_failures++))
fi
# Replaceable events (kind 0): only 1 should remain (newer replaces older of same kind+pubkey)
# Since we publish 2 with same pubkey, they replace to 1, which replaces any existing
local expected_kind0=$((1)) # Always 1 for this pubkey+kind after replacement
if ! test_count "count_kind0" '{"kinds":[0]}' "Count kind 0 events" "$expected_kind0"; then
((test_failures++))
fi
# Addressable events (kind 30001): only 1 should remain (same d-tag replaces)
# Since we publish 2 with same pubkey+kind+d-tag, they replace to 1
local expected_kind30001=$((1)) # Always 1 for this pubkey+kind+d-tag after replacement
if ! test_count "count_kind30001" '{"kinds":[30001]}' "Count kind 30001 events" "$expected_kind30001"; then
((test_failures++))
fi
# Test 3: Count events by author (pubkey)
# BASELINE_AUTHOR includes the first regular event, we add 2 more regular
# Replaceable and addressable replace existing events from this author
local test_pubkey=$(echo "$regular1" | jq -r '.pubkey' 2>/dev/null)
local expected_author=$((2 + BASELINE_AUTHOR))
if ! test_count "count_author" "{\"authors\":[\"$test_pubkey\"]}" "Count events by specific author" "$expected_author"; then
((test_failures++))
fi
# Test 4: Count recent events (time-based)
local recent_timestamp=$(($(date +%s) - 200))
if ! test_count "count_recent" "{\"since\":$recent_timestamp}" "Count recent events" "any"; then
((test_failures++))
fi
# Test 5: Count events with specific tags
# NOTE: Tag filtering is currently not working in the relay - should return the tagged events
local expected_type_regular=$((0 + BASELINE_TYPE_REGULAR)) # Currently returns 0 due to tag filtering bug
if ! test_count "count_tag_type" '{"#type":["regular"]}' "Count events with type=regular tag" "$expected_type_regular"; then
((test_failures++))
fi
local expected_test_nip45=$((0 + BASELINE_TEST_NIP45)) # Currently returns 0 due to tag filtering bug
if ! test_count "count_tag_test" '{"#test":["nip45"]}' "Count events with test=nip45 tag" "$expected_test_nip45"; then
((test_failures++))
fi
# Test 6: Count multiple kinds
# BASELINE_KINDS_01 + 3 regular events = total for kinds 0+1
local expected_kinds_01=$((3 + BASELINE_KINDS_01))
if ! test_count "count_multi_kinds" '{"kinds":[0,1]}' "Count multiple kinds (0,1)" "$expected_kinds_01"; then
((test_failures++))
fi
# Test 7: Count with time range
local start_time=$(($(date +%s) - 120))
local end_time=$(($(date +%s) - 60))
if ! test_count "count_time_range" "{\"since\":$start_time,\"until\":$end_time}" "Count events in time range" "any"; then
((test_failures++))
fi
# Test 8: Count specific event IDs
if [[ ${#REGULAR_EVENT_IDS[@]} -gt 0 ]]; then
local test_event_id="${REGULAR_EVENT_IDS[0]}"
if ! test_count "count_specific_id" "{\"ids\":[\"$test_event_id\"]}" "Count specific event ID" "1"; then
((test_failures++))
fi
fi
# Test 9: Count with multiple filters combined
# NOTE: Combined tag filtering is currently not working in the relay
local expected_combined=$((0 + BASELINE_COMBINED)) # Currently returns 0 due to tag filtering bug
if ! test_count "count_combined" '{"kinds":[1],"#type":["regular"],"#test":["nip45"]}' "Count with combined filters" "$expected_combined"; then
((test_failures++))
fi
# Test 10: Count ephemeral events (should be 0 since they're not stored)
if ! test_count "count_ephemeral" '{"kinds":[20001]}' "Count ephemeral events (should be 0)" "0"; then
((test_failures++))
fi
# Test 11: Count with limit (should still count all matching, ignore limit)
local expected_with_limit=$((3 + BASELINE_KIND1))
if ! test_count "count_with_limit" '{"kinds":[1],"limit":1}' "Count with limit (should ignore limit)" "$expected_with_limit"; then
((test_failures++))
fi
# Test 12: Count non-existent kind
if ! test_count "count_nonexistent" '{"kinds":[99999]}' "Count non-existent kind" "0"; then
((test_failures++))
fi
# Test 13: Count with empty filter
if ! test_count "count_empty_filter" '{}' "Count with empty filter" "any"; then
((test_failures++))
fi
# Report test results
if [[ $test_failures -gt 0 ]]; then
print_error "COUNT TESTS FAILED: $test_failures test(s) failed"
return 1
else
print_success "All COUNT tests passed"
fi
print_header "PHASE 3: Database Verification"
# Check what's actually stored in the database
print_step "Verifying database contents..."
if command -v sqlite3 &> /dev/null; then
# Find the database file (should be in build/ directory with relay pubkey as filename)
local db_file=""
if [[ -d "../build" ]]; then
db_file=$(find ../build -name "*.db" -type f | head -1)
fi
if [[ -n "$db_file" && -f "$db_file" ]]; then
print_info "Events by type in database ($db_file):"
sqlite3 "$db_file" "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null | while read line; do
echo " $line"
done
print_info "Total events in database:"
sqlite3 "$db_file" "SELECT COUNT(*) FROM events;" 2>/dev/null
print_success "Database verification complete"
else
print_warning "Database file not found in build/ directory"
print_info "Expected database files: build/*.db (named after relay pubkey)"
fi
else
print_warning "sqlite3 not available for database verification"
fi
return 0
}
# Run the COUNT test
print_header "Starting NIP-45 COUNT Message Test Suite"
echo
if run_count_test; then
echo
print_success "All NIP-45 COUNT tests completed successfully!"
print_info "The C-Relay COUNT functionality is working correctly"
print_info "✅ COUNT messages are processed and return correct event counts"
echo
exit 0
else
echo
print_error "❌ NIP-45 COUNT TESTS FAILED!"
print_error "The COUNT functionality has issues that need to be fixed"
echo
exit 1
fi

420
tests/50_nip_test.sh Executable file
View File

@@ -0,0 +1,420 @@
#!/bin/bash
# NIP-50 Search Message Test - Test search functionality
# Tests search field in filter objects to verify correct event searching
set -e # Exit on any error
# Color constants
RED='\033[31m'
GREEN='\033[32m'
YELLOW='\033[33m'
BLUE='\033[34m'
BOLD='\033[1m'
RESET='\033[0m'
# Test configuration
RELAY_URL="ws://127.0.0.1:8888"
TEST_PRIVATE_KEY="nsec1j4c6269y9w0q2er2xjw8sv2ehyrtfxq3jwgdlxj6qfn8z4gjsq5qfvfk99"
# Print functions
print_header() {
echo -e "${BLUE}${BOLD}=== $1 ===${RESET}"
}
print_step() {
echo -e "${YELLOW}[STEP]${RESET} $1"
}
print_success() {
echo -e "${GREEN}${RESET} $1"
}
print_error() {
echo -e "${RED}${RESET} $1"
}
print_info() {
echo -e "${BLUE}[INFO]${RESET} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${RESET} $1"
}
# Global arrays to store event IDs for search tests
declare -a SEARCH_EVENT_IDS=()
# Baseline counts from existing events in relay
BASELINE_TOTAL=0
BASELINE_BITCOIN=0
BASELINE_LIGHTNING=0
BASELINE_NOSTR=0
BASELINE_DECENTRALIZED=0
BASELINE_NETWORK=0
# Helper function to get baseline count for a search term (before publishing test events)
get_baseline_search_count() {
local search_term="$1"
# Create COUNT message with search
local filter="{\"search\":\"$search_term\"}"
local count_message="[\"COUNT\",\"baseline_search\",$filter]"
# Send COUNT message and get response
local response=""
if command -v websocat &> /dev/null; then
response=$(echo "$count_message" | timeout 3s websocat "$RELAY_URL" 2>&1 || echo "")
fi
# Parse COUNT response
if [[ -n "$response" ]]; then
local count_result=$(echo "$response" | grep '"COUNT"' | head -1)
if [[ -n "$count_result" ]]; then
local count=$(echo "$count_result" | jq -r '.[2].count' 2>/dev/null)
if [[ "$count" =~ ^[0-9]+$ ]]; then
echo "$count"
return 0
fi
fi
fi
echo "0" # Default to 0 if we can't get the count
}
# Helper function to publish event and extract ID
publish_event() {
local event_json="$1"
local description="$2"
# Extract event ID
local event_id=$(echo "$event_json" | jq -r '.id' 2>/dev/null)
if [[ "$event_id" == "null" || -z "$event_id" ]]; then
print_error "Could not extract event ID from $description"
return 1
fi
print_info "Publishing $description..."
# Create EVENT message in Nostr format
local event_message="[\"EVENT\",$event_json]"
# Publish to relay
local response=""
if command -v websocat &> /dev/null; then
response=$(echo "$event_message" | timeout 5s websocat "$RELAY_URL" 2>&1 || echo "Connection failed")
else
print_error "websocat not found - required for testing"
return 1
fi
# Check response
if [[ "$response" == *"Connection failed"* ]]; then
print_error "Failed to connect to relay for $description"
return 1
elif [[ "$response" == *"true"* ]]; then
print_success "$description uploaded (ID: ${event_id:0:16}...)"
SEARCH_EVENT_IDS+=("$event_id")
echo # Add blank line for readability
return 0
else
print_warning "$description might have failed: $response"
echo # Add blank line for readability
return 1
fi
}
# Helper function to send COUNT message with search and check response
test_search_count() {
local sub_id="$1"
local filter="$2"
local description="$3"
local expected_count="$4"
print_step "Testing SEARCH COUNT: $description"
# Create COUNT message
local count_message="[\"COUNT\",\"$sub_id\",$filter]"
print_info "Sending filter: $filter"
# Send COUNT message and get response
local response=""
if command -v websocat &> /dev/null; then
response=$(echo "$count_message" | timeout 3s websocat "$RELAY_URL" 2>/dev/null || echo "")
fi
# Parse COUNT response
local count_result=""
if [[ -n "$response" ]]; then
# Look for COUNT response: ["COUNT","sub_id",{"count":N}]
count_result=$(echo "$response" | grep '"COUNT"' | head -1)
if [[ -n "$count_result" ]]; then
local actual_count=$(echo "$count_result" | jq -r '.[2].count' 2>/dev/null)
if [[ "$actual_count" =~ ^[0-9]+$ ]]; then
print_info "Received count: $actual_count"
# Check if count matches expected
if [[ "$expected_count" == "any" ]]; then
print_success "$description - Count: $actual_count"
return 0
elif [[ "$actual_count" -eq "$expected_count" ]]; then
print_success "$description - Expected: $expected_count, Got: $actual_count"
return 0
else
print_error "$description - Expected: $expected_count, Got: $actual_count"
return 1
fi
else
print_error "$description - Invalid count response: $count_result"
return 1
fi
else
print_error "$description - No COUNT response received"
print_error "Raw response: $response"
return 1
fi
else
print_error "$description - No response from relay"
return 1
fi
}
# Helper function to send REQ message with search and check response
test_search_req() {
local sub_id="$1"
local filter="$2"
local description="$3"
local expected_events="$4"
print_step "Testing SEARCH REQ: $description"
# Create REQ message
local req_message="[\"REQ\",\"$sub_id\",$filter]"
print_info "Sending filter: $filter"
# Send REQ message and get response
local response=""
if command -v websocat &> /dev/null; then
response=$(echo "$req_message" | timeout 5s websocat "$RELAY_URL" 2>&1 || echo "")
fi
# Send CLOSE message to end subscription
local close_message="[\"CLOSE\",\"$sub_id\"]"
echo "$close_message" | timeout 2s websocat "$RELAY_URL" >/dev/null 2>&1 || true
# Parse response for EVENT messages
local event_count=0
if [[ -n "$response" ]]; then
# Count EVENT messages in response
event_count=$(echo "$response" | grep -c '"EVENT"')
print_info "Received events: $event_count"
# Check if event count matches expected
if [[ "$expected_events" == "any" ]]; then
print_success "$description - Events: $event_count"
return 0
elif [[ "$event_count" -eq "$expected_events" ]]; then
print_success "$description - Expected: $expected_events, Got: $event_count"
return 0
else
print_error "$description - Expected: $expected_events, Got: $event_count"
return 1
fi
else
print_error "$description - No response from relay"
return 1
fi
}
# Main test function
run_search_test() {
print_header "NIP-50 Search Message Test"
# Check dependencies
print_step "Checking dependencies..."
if ! command -v nak &> /dev/null; then
print_error "nak command not found"
print_info "Please install nak: go install github.com/fiatjaf/nak@latest"
return 1
fi
if ! command -v websocat &> /dev/null; then
print_error "websocat command not found"
print_info "Please install websocat for testing"
return 1
fi
if ! command -v jq &> /dev/null; then
print_error "jq command not found"
print_info "Please install jq for JSON processing"
return 1
fi
print_success "All dependencies found"
print_header "PHASE 0: Establishing Baseline Search Counts"
# Get baseline counts BEFORE publishing any test events
print_step "Getting baseline search counts from existing events in relay..."
BASELINE_TOTAL=$(get_baseline_search_count "")
BASELINE_BITCOIN=$(get_baseline_search_count "Bitcoin")
BASELINE_LIGHTNING=$(get_baseline_search_count "Lightning")
BASELINE_NOSTR=$(get_baseline_search_count "Nostr")
BASELINE_DECENTRALIZED=$(get_baseline_search_count "decentralized")
BASELINE_NETWORK=$(get_baseline_search_count "network")
print_info "Initial baseline search counts established:"
print_info " Total events: $BASELINE_TOTAL"
print_info " 'Bitcoin' matches: $BASELINE_BITCOIN"
print_info " 'Lightning' matches: $BASELINE_LIGHTNING"
print_info " 'Nostr' matches: $BASELINE_NOSTR"
print_info " 'decentralized' matches: $BASELINE_DECENTRALIZED"
print_info " 'network' matches: $BASELINE_NETWORK"
print_header "PHASE 1: Publishing Test Events with Searchable Content"
# Create events with searchable content
print_step "Creating events with searchable content..."
# Events with "Bitcoin" in content
local bitcoin1=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Bitcoin is a decentralized digital currency" -k 1 --ts $(($(date +%s) - 100)) -t "topic=crypto" 2>/dev/null)
local bitcoin2=$(nak event --sec "$TEST_PRIVATE_KEY" -c "The Bitcoin network is secure and decentralized" -k 1 --ts $(($(date +%s) - 90)) -t "topic=blockchain" 2>/dev/null)
# Events with "Lightning" in content
local lightning1=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Lightning Network enables fast Bitcoin transactions" -k 1 --ts $(($(date +%s) - 80)) -t "topic=lightning" 2>/dev/null)
local lightning2=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Lightning channels are bidirectional payment channels" -k 1 --ts $(($(date +%s) - 70)) -t "topic=scaling" 2>/dev/null)
# Events with "Nostr" in content
local nostr1=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Nostr is a decentralized social network protocol" -k 1 --ts $(($(date +%s) - 60)) -t "topic=nostr" 2>/dev/null)
local nostr2=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Nostr relays store and distribute events" -k 1 --ts $(($(date +%s) - 50)) -t "topic=protocol" 2>/dev/null)
# Events with searchable content in tags
local tag_event=$(nak event --sec "$TEST_PRIVATE_KEY" -c "This event has searchable tags" -k 1 --ts $(($(date +%s) - 40)) -t "search=bitcoin" -t "category=crypto" 2>/dev/null)
# Event with no searchable content
local no_match=$(nak event --sec "$TEST_PRIVATE_KEY" -c "This event has no matching content" -k 1 --ts $(($(date +%s) - 30)) -t "topic=other" 2>/dev/null)
# Publish all test events
publish_event "$bitcoin1" "Bitcoin event #1"
publish_event "$bitcoin2" "Bitcoin event #2"
publish_event "$lightning1" "Lightning event #1"
publish_event "$lightning2" "Lightning event #2"
publish_event "$nostr1" "Nostr event #1"
publish_event "$nostr2" "Nostr event #2"
publish_event "$tag_event" "Event with searchable tags"
publish_event "$no_match" "Non-matching event"
# Brief pause to let events settle
sleep 2
print_header "PHASE 2: Testing SEARCH Functionality"
local test_failures=0
# Test 1: Search for "Bitcoin" - should find baseline + 4 new events (2 in content + 1 in tags + 1 with search=bitcoin tag)
local expected_bitcoin=$((BASELINE_BITCOIN + 4))
if ! test_search_count "search_bitcoin_count" '{"search":"Bitcoin"}' "COUNT search for 'Bitcoin'" "$expected_bitcoin"; then
((test_failures++))
fi
if ! test_search_req "search_bitcoin_req" '{"search":"Bitcoin"}' "REQ search for 'Bitcoin'" "$expected_bitcoin"; then
((test_failures++))
fi
# Test 2: Search for "Lightning" - should find baseline + 2 new events
local expected_lightning=$((BASELINE_LIGHTNING + 2))
if ! test_search_count "search_lightning_count" '{"search":"Lightning"}' "COUNT search for 'Lightning'" "$expected_lightning"; then
((test_failures++))
fi
if ! test_search_req "search_lightning_req" '{"search":"Lightning"}' "REQ search for 'Lightning'" "$expected_lightning"; then
((test_failures++))
fi
# Test 3: Search for "Nostr" - should find baseline + 2 new events
local expected_nostr=$((BASELINE_NOSTR + 2))
if ! test_search_count "search_nostr_count" '{"search":"Nostr"}' "COUNT search for 'Nostr'" "$expected_nostr"; then
((test_failures++))
fi
if ! test_search_req "search_nostr_req" '{"search":"Nostr"}' "REQ search for 'Nostr'" "$expected_nostr"; then
((test_failures++))
fi
# Test 4: Search for "decentralized" - should find baseline + 3 new events (Bitcoin #1, Bitcoin #2, Nostr #1)
local expected_decentralized=$((BASELINE_DECENTRALIZED + 3))
if ! test_search_count "search_decentralized_count" '{"search":"decentralized"}' "COUNT search for 'decentralized'" "$expected_decentralized"; then
((test_failures++))
fi
if ! test_search_req "search_decentralized_req" '{"search":"decentralized"}' "REQ search for 'decentralized'" "$expected_decentralized"; then
((test_failures++))
fi
# Test 5: Search for "network" - should find baseline + 3 new events (Bitcoin2, Lightning1, Nostr1)
local expected_network=$((BASELINE_NETWORK + 3))
if ! test_search_count "search_network_count" '{"search":"network"}' "COUNT search for 'network'" "$expected_network"; then
((test_failures++))
fi
# Test 6: Search for non-existent term - should find 0 events
if ! test_search_count "search_nonexistent_count" '{"search":"xyzzy"}' "COUNT search for non-existent term" "0"; then
((test_failures++))
fi
# Test 7: Search combined with other filters
local expected_combined=$((BASELINE_BITCOIN + 4))
if ! test_search_count "search_combined_count" '{"search":"Bitcoin","kinds":[1]}' "COUNT search 'Bitcoin' with kind filter" "$expected_combined"; then
((test_failures++))
fi
# Test 8: Search with time range
local recent_timestamp=$(($(date +%s) - 60))
if ! test_search_count "search_time_count" "{\"search\":\"Bitcoin\",\"since\":$recent_timestamp}" "COUNT search 'Bitcoin' with time filter" "any"; then
((test_failures++))
fi
# Test 9: Empty search string - should return all events
local expected_empty=$((BASELINE_TOTAL + 8))
if ! test_search_count "search_empty_count" '{"search":""}' "COUNT with empty search string" "$expected_empty"; then
((test_failures++))
fi
# Test 10: Case insensitive search (SQLite LIKE is case insensitive by default)
local expected_case=$((BASELINE_BITCOIN + 4))
if ! test_search_count "search_case_count" '{"search":"BITCOIN"}' "COUNT case-insensitive search for 'BITCOIN'" "$expected_case"; then
((test_failures++))
fi
# Report test results
if [[ $test_failures -gt 0 ]]; then
print_error "SEARCH TESTS FAILED: $test_failures test(s) failed"
return 1
else
print_success "All SEARCH tests passed"
fi
return 0
}
# Run the SEARCH test
print_header "Starting NIP-50 Search Message Test Suite"
echo
if run_search_test; then
echo
print_success "All NIP-50 SEARCH tests completed successfully!"
print_info "The C-Relay SEARCH functionality is working correctly"
print_info "✅ Search field in filter objects works for both REQ and COUNT messages"
print_info "✅ Search works across event content and tag values"
print_info "✅ Search is case-insensitive and supports partial matches"
echo
exit 0
else
echo
print_error "❌ NIP-50 SEARCH TESTS FAILED!"
print_error "The SEARCH functionality has issues that need to be fixed"
echo
exit 1
fi

View File

@@ -310,8 +310,51 @@ else
print_failure "Relay failed to start for network test"
fi
# TEST 10: Multiple Startup Attempts (Port Conflict)
print_test_header "Test 10: Port Conflict Handling"
# TEST 10: Port Override with Admin/Relay Key Overrides
print_test_header "Test 10: Port Override with -a/-r Flags"
cleanup_test_files
# Generate test keys (64 hex chars each)
TEST_ADMIN_PUBKEY="1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
TEST_RELAY_PRIVKEY="abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
print_info "Testing port override with -p 9999 -a $TEST_ADMIN_PUBKEY -r $TEST_RELAY_PRIVKEY"
# Start relay with port override and key overrides
timeout 15 $RELAY_BINARY -p 9999 -a $TEST_ADMIN_PUBKEY -r $TEST_RELAY_PRIVKEY > "test_port_override.log" 2>&1 &
relay_pid=$!
sleep 5
if kill -0 $relay_pid 2>/dev/null; then
# Check if relay bound to port 9999 (not default 8888)
if netstat -tln 2>/dev/null | grep -q ":9999"; then
print_success "Relay successfully bound to overridden port 9999"
else
print_failure "Relay not bound to overridden port 9999"
fi
# Check that relay started successfully
if check_relay_startup "test_port_override.log"; then
print_success "Relay startup completed with overrides"
else
print_failure "Relay failed to complete startup with overrides"
fi
# Check that admin keys were NOT generated (since -a was provided)
if ! check_admin_keys "test_port_override.log"; then
print_success "Admin keys not generated (correctly using provided -a key)"
else
print_failure "Admin keys generated despite -a override"
fi
stop_relay_test $relay_pid
else
print_failure "Relay failed to start with port/key overrides"
fi
# TEST 11: Multiple Startup Attempts (Port Conflict)
print_test_header "Test 11: Port Conflict Handling"
relay_pid1=$(start_relay_test "port_conflict_1" 10)
sleep 2
@@ -320,14 +363,14 @@ if kill -0 $relay_pid1 2>/dev/null; then
# Try to start a second relay (should fail due to port conflict)
relay_pid2=$(start_relay_test "port_conflict_2" 5)
sleep 1
if [ "$relay_pid2" = "0" ] || ! kill -0 $relay_pid2 2>/dev/null; then
print_success "Port conflict properly handled (second instance failed to start)"
else
print_failure "Multiple relay instances started (port conflict not handled)"
stop_relay_test $relay_pid2
fi
stop_relay_test $relay_pid1
else
print_failure "First relay instance failed to start"

88
tests/nip42_test.log Normal file
View File

@@ -0,0 +1,88 @@
=== NIP-42 Authentication Test Started ===
2025-09-30 11:15:28 - Starting NIP-42 authentication tests
[INFO] === Starting NIP-42 Authentication Tests ===
[INFO] Checking dependencies...
[SUCCESS] Dependencies check complete
[INFO] Test 1: Checking NIP-42 support in relay info
[SUCCESS] NIP-42 is advertised in supported NIPs
2025-09-30 11:15:28 - Supported NIPs: 1,9,11,13,15,20,40,42
[INFO] Test 2: Testing AUTH challenge generation
[INFO] Found admin private key, configuring NIP-42 authentication...
[WARNING] Failed to create configuration event - proceeding with manual test
[INFO] Test 3: Testing complete NIP-42 authentication flow
[INFO] Generated test keypair: test_pubkey
[INFO] Attempting to publish event without authentication...
[INFO] Publishing test event to relay...
2025-09-30 11:15:30 - Event publish result: connecting to ws://localhost:8888... ok.
{"kind":1,"id":"acfc4da1903ce1c065f2c472348b21837a322c79cb4b248c62de5cff9b5b6607","pubkey":"d3e8d83eabac2a28e21039136a897399f4866893dd43bfbf0bdc8391913a4013","created_at":1759245329,"tags":[],"content":"NIP-42 test event - should require auth","sig":"2051b3da705214d5b5e95fb5b4dd9f1c893666965f7c51ccd2a9ccd495b67dd76ed3ce9768f0f2a16a3f9a602368e8102758ca3cc1408280094abf7e92fcc75e"}
publishing to ws://localhost:8888... success.
[SUCCESS] Relay requested authentication as expected
[INFO] Test 4: Testing WebSocket AUTH message handling
[INFO] Testing WebSocket connection and AUTH message...
[INFO] Sending test message via WebSocket...
2025-09-30 11:15:30 - WebSocket response:
[INFO] No AUTH challenge in WebSocket response
[INFO] Test 5: Testing NIP-42 configuration options
[INFO] Retrieving current relay configuration...
[WARNING] Could not retrieve configuration events
[INFO] Test 6: Testing NIP-42 performance and stability
[INFO] Testing multiple authentication attempts...
2025-09-30 11:15:31 - Attempt 1: .297874340s - connecting to ws://localhost:8888... ok.
{"kind":1,"id":"0d742f093b7be0ce811068e7a6171573dd225418c9459f5c7e9580f57d88af7b","pubkey":"37d1a52ec83a837eb8c6ae46df5c892f338c65ae0c29eb4873e775082252a18a","created_at":1759245331,"tags":[],"content":"Performance test event 1","sig":"d4aec950c47fbd4c1da637b84fafbde570adf86e08795236fb6a3f7e12d2dbaa16cb38cbb68d3b9755d186b20800bdb84b0a050f8933d06b10991a9542fe9909"}
publishing to ws://localhost:8888... success.
2025-09-30 11:15:32 - Attempt 2: .270493759s - connecting to ws://localhost:8888... ok.
{"kind":1,"id":"b45ae1b0458e284ed89b6de453bab489d506352680f6d37c8a5f0aed9eebc7a5","pubkey":"37d1a52ec83a837eb8c6ae46df5c892f338c65ae0c29eb4873e775082252a18a","created_at":1759245331,"tags":[],"content":"Performance test event 2","sig":"f9702aa537ec1485d151a0115c38c7f6f1bc05a63929be784e33850b46be6a961996eb922b8b337d607312c8e4583590ee35f38330300e19ab921f94926719c5"}
publishing to ws://localhost:8888... success.
2025-09-30 11:15:32 - Attempt 3: .239220029s - connecting to ws://localhost:8888... ok.
{"kind":1,"id":"5f70f9cb2a30a12e7d088e62a9295ef2fbea4f40a1d8b07006db03f610c5abce","pubkey":"37d1a52ec83a837eb8c6ae46df5c892f338c65ae0c29eb4873e775082252a18a","created_at":1759245332,"tags":[],"content":"Performance test event 3","sig":"ea2e1611ce3ddea3aa73764f4542bad7d922fc0d2ed40e58dcc2a66cb6e046bfae22d6baef296eb51d965a22b2a07394fc5f8664e3a7777382ae523431c782cd"}
publishing to ws://localhost:8888... success.
2025-09-30 11:15:33 - Attempt 4: .221429674s - connecting to ws://localhost:8888... ok.
{"kind":1,"id":"eafcf5f7e0bd0be35267f13ff93eef339faec6a5af13fe451fee2b7443b9de6e","pubkey":"37d1a52ec83a837eb8c6ae46df5c892f338c65ae0c29eb4873e775082252a18a","created_at":1759245332,"tags":[],"content":"Performance test event 4","sig":"976017abe67582af29d46cd54159ce0465c94caf348be35f26b6522cb48c4c9ce5ba9835e92873cf96a906605a032071360fc85beea815a8e4133a4f45d2bf0a"}
publishing to ws://localhost:8888... success.
2025-09-30 11:15:33 - Attempt 5: .242410067s - connecting to ws://localhost:8888... ok.
{"kind":1,"id":"c7cf6776000a325b1180240c61ef20b849b84dee3f5d2efed4c1a9e9fbdbd7b1","pubkey":"37d1a52ec83a837eb8c6ae46df5c892f338c65ae0c29eb4873e775082252a18a","created_at":1759245333,"tags":[],"content":"Performance test event 5","sig":"18b4575bd644146451dcf86607d75f358828ce2907e8904bd08b903ff5d79ec5a69ff60168735975cc406dcee788fd22fc7bf7c97fb7ac6dff3580eda56cee2e"}
publishing to ws://localhost:8888... success.
[SUCCESS] Performance test completed: 5/5 successful responses
[INFO] Test 7: Testing kind-specific NIP-42 authentication requirements
[INFO] Generated test keypair for kind-specific tests: test_pubkey
[INFO] Testing kind 1 event (regular note) - should work without authentication...
2025-09-30 11:15:34 - Kind 1 event result: connecting to ws://localhost:8888... ok.
{"kind":1,"id":"012690335e48736fd29769669d2bda15a079183c1d0f27b8400366a54b5b9ddd","pubkey":"ad362b9bbf61b140c5f677a2d091d622fef6fa186c579e6600dd8b24a85a2260","created_at":1759245334,"tags":[],"content":"Regular note - should not require auth","sig":"a3a0ce218666d2a374983a343bc24da5a727ce251c23828171021f15a3ab441a0c86f56200321467914ce4bee9a987f1de301151467ae639d7f941bac7fbe68e"}
publishing to ws://localhost:8888... success.
[SUCCESS] Kind 1 event accepted without authentication (correct behavior)
[INFO] Testing kind 4 event (direct message) - should require authentication...
2025-09-30 11:15:44 - Kind 4 event result: connecting to ws://localhost:8888... ok.
{"kind":4,"id":"e629dd91320d48c1e3103ec16e40c707c2ee8143012c9ad8bb9d32f98610f447","pubkey":"ad362b9bbf61b140c5f677a2d091d622fef6fa186c579e6600dd8b24a85a2260","created_at":1759245334,"tags":[["p,test_pubkey"]],"content":"This is a direct message - should require auth","sig":"7677b3f2932fb4979bab3da6d241217b7ea2010411fc8bf5a51f6987f38696d5634f91a30b13e0f4861479ceabff995b3bb2eb2fc74af5f3d1175235d5448ce2"}
publishing to ws://localhost:8888...
[SUCCESS] Kind 4 event requested authentication (correct behavior for DMs)
[INFO] Testing kind 14 event (chat message) - should require authentication...
2025-09-30 11:15:55 - Kind 14 event result: connecting to ws://localhost:8888... ok.
{"kind":14,"id":"a5398c5851dd72a8980723c91d35345bd0088b800102180dd41af7056f1cad50","pubkey":"ad362b9bbf61b140c5f677a2d091d622fef6fa186c579e6600dd8b24a85a2260","created_at":1759245344,"tags":[["p,test_pubkey"]],"content":"Chat message - should require auth","sig":"62d43f3f81755d4ef81cbfc8aca9abc11f28b0c45640f19d3dd41a09bae746fe7a4e9d8e458c416dcd2cab02deb090ce1e29e8426d9be5445d130eaa00d339f2"}
publishing to ws://localhost:8888...
[SUCCESS] Kind 14 event requested authentication (correct behavior for DMs)
[INFO] Testing other event kinds - should work without authentication...
2025-09-30 11:15:55 - Kind 0 event result: connecting to ws://localhost:8888... ok.
{"kind":0,"id":"069ac4db07da3230681aa37ab9e6a2aa48e2c199245259681e45ffb2f1b21846","pubkey":"ad362b9bbf61b140c5f677a2d091d622fef6fa186c579e6600dd8b24a85a2260","created_at":1759245355,"tags":[],"content":"Test event kind 0 - should not require auth","sig":"3c99b97c0ea2d18bc88fc07b2e95e213b6a6af804512d62158f8fd63cc24a3937533b830f59d38ccacccf98ba2fb0ed7467b16271154d4dd37fbc075eba32e49"}
publishing to ws://localhost:8888... success.
[SUCCESS] Kind 0 event accepted without authentication (correct)
2025-09-30 11:15:56 - Kind 3 event result: connecting to ws://localhost:8888... ok.
{"kind":3,"id":"1dd1ccb13ebd0d50b2aa79dbb938b408a24f0a4dd9f872b717ed91ae6729051c","pubkey":"ad362b9bbf61b140c5f677a2d091d622fef6fa186c579e6600dd8b24a85a2260","created_at":1759245355,"tags":[],"content":"Test event kind 3 - should not require auth","sig":"c205cc76f687c3957cf8b35cd8346fd8c2e44d9ef82324b95a7eef7f57429fb6f2ab1d0263dd5d00204dd90e626d5918a8710341b0d68a5095b41455f49cf0dd"}
publishing to ws://localhost:8888... success.
[SUCCESS] Kind 3 event accepted without authentication (correct)
2025-09-30 11:15:56 - Kind 7 event result: connecting to ws://localhost:8888... ok.
{"kind":7,"id":"b6161b1da8a4d362e3c230df99c4f87b6311ef6e9f67e03a2476f8a6366352c1","pubkey":"ad362b9bbf61b140c5f677a2d091d622fef6fa186c579e6600dd8b24a85a2260","created_at":1759245356,"tags":[],"content":"Test event kind 7 - should not require auth","sig":"ab06c4b00a04d726109acd02d663e30188ff9ee854cf877e854fda90dd776a649ef3fab8ae5b530b4e6b5530490dd536a281a721e471bd3748a0dacc4eac9622"}
publishing to ws://localhost:8888... success.
[SUCCESS] Kind 7 event accepted without authentication (correct)
[INFO] Kind-specific authentication test completed
[INFO] === NIP-42 Test Results Summary ===
[SUCCESS] Dependencies: PASS
[SUCCESS] NIP-42 Support: PASS
[SUCCESS] Auth Challenge: PASS
[SUCCESS] Auth Flow: PASS
[SUCCESS] WebSocket AUTH: PASS
[SUCCESS] Configuration: PASS
[SUCCESS] Performance: PASS
[SUCCESS] Kind-Specific Auth: PASS
[SUCCESS] All NIP-42 tests completed successfully!
[SUCCESS] NIP-42 authentication implementation is working correctly
[INFO] === NIP-42 Authentication Tests Complete ===

View File

@@ -1,967 +0,0 @@
#!/bin/bash
# =======================================================================
# C-Relay Whitelist/Blacklist Authentication Rules Test Script
# =======================================================================
#
# This test validates the whitelist and blacklist functionality of the
# C-Relay server through the WebSocket admin API.
#
# Test Credentials (Test Mode):
# - Admin Private Key: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
# - Admin Public Key: 6a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3
# - Relay Public Key: 4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa
#
# =======================================================================
set -e # Exit on any error
# =======================================================================
# CONFIGURATION
# =======================================================================
# Test mode credentials (from current relay startup)
ADMIN_PRIVKEY="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
ADMIN_PUBKEY="6a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3"
RELAY_PUBKEY="4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"
# Server configuration
RELAY_HOST="127.0.0.1"
RELAY_PORT="8888"
RELAY_URL="ws://${RELAY_HOST}:${RELAY_PORT}"
# Test configuration
TIMEOUT=5
TEMP_DIR="/tmp/c_relay_test_$$"
# WebSocket connection state
WS_PID=""
WS_INPUT_FIFO=""
WS_OUTPUT_FIFO=""
WS_CONNECTED=0
WS_RESPONSE_LOG=""
# Color codes for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
BOLD='\033[1m'
RESET='\033[0m'
# Test tracking
TESTS_RUN=0
TESTS_PASSED=0
TESTS_FAILED=0
# =======================================================================
# UTILITY FUNCTIONS
# =======================================================================
log() {
echo -e "${BLUE}[$(date '+%H:%M:%S')]${RESET} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${RESET} $1"
}
log_error() {
echo -e "${RED}[ERROR]${RESET} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${RESET} $1"
}
log_info() {
echo -e "${BLUE}[INFO]${RESET} $1"
}
increment_test() {
TESTS_RUN=$((TESTS_RUN + 1))
}
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
generate_test_keypair() {
local name=$1
local privkey_file="${TEMP_DIR}/${name}_privkey"
local pubkey_file="${TEMP_DIR}/${name}_pubkey"
# Generate private key using nak key --gen (following pattern from other tests)
local privkey=$(nak key generate 2>/dev/null)
if [ $? -ne 0 ] || [ -z "$privkey" ]; then
log_error "Failed to generate private key for $name"
return 1
fi
echo "$privkey" > "$privkey_file"
# Derive public key using nak
local pubkey=$(nak key public "$privkey" 2>/dev/null)
if [ $? -ne 0 ] || [ -z "$pubkey" ]; then
log_error "Failed to generate public key for $name"
return 1
fi
echo "$pubkey" > "$pubkey_file"
log_info "Generated keypair for $name: pubkey=${pubkey:0:16}..."
# Export for use in calling functions
eval "${name}_PRIVKEY=\"$privkey\""
eval "${name}_PUBKEY=\"$pubkey\""
}
# Send WebSocket message and capture response
send_websocket_message() {
local message="$1"
local expected_response="$2"
local timeout="${3:-$TIMEOUT}"
# Use websocat to send message and capture response (following pattern from tests/1_nip_test.sh)
local response=""
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 "websocat not found - required for WebSocket testing"
log_error "Please install websocat for WebSocket communication"
return 1
fi
echo "$response"
}
# =======================================================================
# PERSISTENT WEBSOCKET CONNECTION MANAGEMENT
# =======================================================================
# Open persistent WebSocket connection
open_websocket_connection() {
log_info "Opening persistent WebSocket connection to $RELAY_URL..."
# Create unique named pipes for this test session
WS_INPUT_FIFO="${TEMP_DIR}/ws_input_$$"
WS_OUTPUT_FIFO="${TEMP_DIR}/ws_output_$$"
WS_RESPONSE_LOG="${TEMP_DIR}/ws_responses_$$"
# Create named pipes
mkfifo "$WS_INPUT_FIFO" "$WS_OUTPUT_FIFO"
# Start websocat in background with bidirectional pipes
# Input: we write to WS_INPUT_FIFO, websocat reads and sends to relay
# Output: websocat receives from relay and writes to WS_OUTPUT_FIFO
websocat "$RELAY_URL" < "$WS_INPUT_FIFO" > "$WS_OUTPUT_FIFO" &
WS_PID=$!
# Start background response logger
tail -f "$WS_OUTPUT_FIFO" >> "$WS_RESPONSE_LOG" &
local logger_pid=$!
# Keep input pipe open by redirecting from /dev/null in background
exec {ws_fd}> "$WS_INPUT_FIFO"
# Test connection with a simple REQ message
sleep 1
echo '["REQ","test_conn",{}]' >&${ws_fd}
# Wait for response to confirm connection
local connection_timeout=5
local start_time=$(date +%s)
while [ $(($(date +%s) - start_time)) -lt $connection_timeout ]; do
if [ -s "$WS_RESPONSE_LOG" ]; then
WS_CONNECTED=1
log_success "Persistent WebSocket connection established"
log_info "WebSocket PID: $WS_PID"
return 0
fi
sleep 0.1
done
# Connection failed
log_error "Failed to establish persistent WebSocket connection"
close_websocket_connection
return 1
}
# Close persistent WebSocket connection
close_websocket_connection() {
log_info "Closing persistent WebSocket connection..."
if [ -n "$WS_PID" ] && kill -0 "$WS_PID" 2>/dev/null; then
# Close input pipe first
if [ -n "${ws_fd}" ]; then
exec {ws_fd}>&-
fi
# Send close frame and terminate websocat
kill "$WS_PID" 2>/dev/null
wait "$WS_PID" 2>/dev/null
fi
# Kill any remaining background processes
pkill -f "tail -f.*$WS_OUTPUT_FIFO" 2>/dev/null || true
# Clean up pipes
[ -p "$WS_INPUT_FIFO" ] && rm -f "$WS_INPUT_FIFO"
[ -p "$WS_OUTPUT_FIFO" ] && rm -f "$WS_OUTPUT_FIFO"
WS_PID=""
WS_CONNECTED=0
log_info "WebSocket connection closed"
}
# Send event through persistent WebSocket connection
send_websocket_event() {
local event_json="$1"
local timeout_seconds="${2:-10}"
if [ "$WS_CONNECTED" != "1" ]; then
log_error "WebSocket connection not established"
return 1
fi
# Clear previous responses
> "$WS_RESPONSE_LOG"
# Create EVENT message
local event_message="[\"EVENT\",$event_json]"
# Send through persistent connection
echo "$event_message" >&${ws_fd}
# Wait for OK response
local start_time=$(date +%s)
while [ $(($(date +%s) - start_time)) -lt $timeout_seconds ]; do
if grep -q '"OK"' "$WS_RESPONSE_LOG" 2>/dev/null; then
local response=$(tail -1 "$WS_RESPONSE_LOG")
echo "$response"
return 0
fi
sleep 0.1
done
log_error "Timeout waiting for WebSocket response"
return 1
}
# Wait for query response data from relay
wait_for_query_response() {
local timeout_seconds="${1:-10}"
local start_time=$(date +%s)
log_info "Waiting for query response data..."
# Clear any OK responses and wait for JSON data
sleep 0.5 # Brief delay to ensure OK response is processed first
while [ $(($(date +%s) - start_time)) -lt $timeout_seconds ]; do
# Look for JSON response with query data (not just OK responses)
if grep -q '"query_type"' "$WS_RESPONSE_LOG" 2>/dev/null; then
local response=$(grep '"query_type"' "$WS_RESPONSE_LOG" | tail -1)
echo "$response"
return 0
fi
sleep 0.1
done
log_error "Timeout waiting for query response data"
return 1
}
# Create and send auth rule event
send_auth_rule_event() {
local action="$1" # "add" or "remove"
local rule_type="$2" # "whitelist" or "blacklist"
local pattern_type="$3" # "pubkey" or "hash"
local pattern_value="$4" # actual pubkey or hash value
local description="$5" # optional description
log_info "Creating auth rule event: $action $rule_type $pattern_type ${pattern_value:0:16}..."
# Create the auth rule event using nak with correct tag format for the actual implementation
# Server expects tags like ["whitelist", "pubkey", "abc123..."] or ["blacklist", "pubkey", "def456..."]
# Using Kind 23456 (ephemeral auth rules management) with proper relay targeting
local event_json
event_json=$(nak event -k 23456 --content "" \
-t "p=$RELAY_PUBKEY" \
-t "$rule_type=$pattern_type=$pattern_value" \
--sec "$ADMIN_PRIVKEY" 2>/dev/null)
if [ $? -ne 0 ] || [ -z "$event_json" ]; then
log_error "Failed to create auth rule event with nak"
return 1
fi
# Send the event through persistent WebSocket connection
log_info "Publishing auth rule event to relay..."
local result
if [ "$WS_CONNECTED" = "1" ]; then
result=$(send_websocket_event "$event_json")
local exit_code=$?
log_info "Auth rule event result: $result"
# Check if response indicates success
if [ $exit_code -eq 0 ] && echo "$result" | grep -q -i '"OK".*true'; then
log_success "Auth rule $action successful"
return 0
else
log_error "Auth rule $action failed: $result (exit code: $exit_code)"
return 1
fi
else
# Fallback to one-shot connection if persistent connection not available
result=$(echo "$event_json" | timeout 10s nak event "$RELAY_URL" 2>&1)
local exit_code=$?
log_info "Auth rule event result: $result"
# Check if response indicates success
if [ $exit_code -eq 0 ] && echo "$result" | grep -q -i "success\|OK.*true\|published"; then
log_success "Auth rule $action successful"
return 0
else
log_error "Auth rule $action failed: $result (exit code: $exit_code)"
return 1
fi
fi
}
# 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) with proper relay targeting
local event_json
event_json=$(nak event -k 23456 --content "" \
-t "p=$RELAY_PUBKEY" \
-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 through persistent WebSocket connection
log_info "Sending clear all auth rules command..."
local result
if [ "$WS_CONNECTED" = "1" ]; then
result=$(send_websocket_event "$event_json")
local exit_code=$?
log_info "Clear auth rules result: $result"
# Check if response indicates success
if [ $exit_code -eq 0 ] && echo "$result" | grep -q -i '"OK".*true'; then
log_success "All auth rules cleared successfully"
return 0
else
log_error "Failed to clear auth rules: $result (exit code: $exit_code)"
return 1
fi
else
# Fallback to one-shot connection if persistent connection not available
result=$(echo "$event_json" | timeout 10s nak event "$RELAY_URL" 2>&1)
local exit_code=$?
log_info "Clear auth rules result: $result"
# Check if response indicates success
if [ $exit_code -eq 0 ] && echo "$result" | grep -q -i "success\|OK.*true\|published"; then
log_success "All auth rules cleared successfully"
return 0
else
log_error "Failed to clear auth rules: $result (exit code: $exit_code)"
return 1
fi
fi
}
# Test event publishing with a specific key
test_event_publishing() {
local test_privkey="$1"
local test_pubkey="$2"
local expected_result="$3" # "success" or "blocked"
local description="$4"
log_info "Testing event publishing: $description"
# 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_event
test_event=$(nak event -k 1 --content "$test_content" --sec "$test_privkey" 2>/dev/null)
if [ $? -ne 0 ] || [ -z "$test_event" ]; then
log_error "Failed to create test event"
return 1
fi
# 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)
local exit_code=$?
log_info "Event publishing result: $result"
# Check result against expectation
if [ "$expected_result" = "success" ]; then
if [ $exit_code -eq 0 ] && echo "$result" | grep -q -i "success\|OK.*true\|published"; then
log_success "Event publishing allowed as expected"
return 0
else
log_error "Event publishing was blocked but should have been allowed: $result"
return 1
fi
else # expected_result = "blocked"
if [ $exit_code -ne 0 ] || echo "$result" | grep -q -i "blocked\|denied\|rejected\|auth.*required\|OK.*false"; then
log_success "Event publishing blocked as expected"
return 0
else
log_error "Event publishing was allowed but should have been blocked: $result"
return 1
fi
fi
}
# =======================================================================
# SETUP AND INITIALIZATION
# =======================================================================
setup_test_environment() {
log "Setting up test environment..."
# Create temporary directory
mkdir -p "$TEMP_DIR"
# Check if required tools are available - like NIP-42 test
log_info "Checking dependencies..."
if ! command -v nak &> /dev/null; then
log_error "nak client not found. Please install: go install github.com/fiatjaf/nak@latest"
exit 1
fi
if ! command -v jq &> /dev/null; then
log_error "jq not found. Please install jq for JSON processing"
exit 1
fi
if ! command -v timeout &> /dev/null; then
log_error "timeout not found. Please install coreutils"
exit 1
fi
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"
# Generate test keypairs
generate_test_keypair "TEST1"
generate_test_keypair "TEST2"
generate_test_keypair "TEST3"
log_success "Test environment setup complete"
}
# =======================================================================
# TEST FUNCTIONS
# =======================================================================
# Test 1: Admin Authentication
test_admin_authentication() {
increment_test
log "Test $TESTS_RUN: Admin Authentication"
# Create a simple configuration event to test admin authentication
# Using Kind 23456 (admin commands) with proper relay targeting
local config_event
config_event=$(nak event -k 23456 --content "" \
-t "p=$RELAY_PUBKEY" \
-t "system_command=system_status" \
--sec "$ADMIN_PRIVKEY" 2>/dev/null)
if [ $? -ne 0 ]; then
fail_test "Failed to create admin test event"
return
fi
# Send admin event
local message="[\"EVENT\",$config_event]"
local response
response=$(send_websocket_message "$message" "OK" 10)
if echo "$response" | grep -q '"OK".*true'; then
pass_test "Admin authentication successful"
else
fail_test "Admin authentication failed: $response"
fi
}
# 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 "" \
-t "p=$RELAY_PUBKEY" \
-t "auth_query=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
if test_event_publishing "$TEST1_PRIVKEY" "$TEST1_PUBKEY" "success" "whitelisted pubkey"; then
pass_test "Basic whitelist functionality working"
else
fail_test "Whitelisted pubkey could not publish events"
fi
else
fail_test "Failed to add pubkey to whitelist"
fi
}
# 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
if test_event_publishing "$TEST2_PRIVKEY" "$TEST2_PUBKEY" "blocked" "blacklisted pubkey"; then
pass_test "Basic blacklist functionality working"
else
fail_test "Blacklisted pubkey was able to publish events"
fi
else
fail_test "Failed to add pubkey to blacklist"
fi
}
# 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
if test_event_publishing "$TEST2_PRIVKEY" "$TEST2_PUBKEY" "success" "previously blacklisted pubkey after removal"; then
pass_test "Rule removal working correctly"
else
fail_test "Previously blacklisted pubkey still cannot publish after removal"
fi
else
fail_test "Failed to remove pubkey from blacklist"
fi
}
# 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
if send_auth_rule_event "add" "whitelist" "pubkey" "$TEST1_PUBKEY" "Multi-user test whitelist"; then
success_count=$((success_count + 1))
fi
if send_auth_rule_event "add" "blacklist" "pubkey" "$TEST3_PUBKEY" "Multi-user test blacklist"; then
success_count=$((success_count + 1))
fi
if [ $success_count -eq 2 ]; then
# Test whitelisted user can publish
if test_event_publishing "$TEST1_PRIVKEY" "$TEST1_PUBKEY" "success" "whitelisted in multi-user test"; then
# Test blacklisted user cannot publish
if test_event_publishing "$TEST3_PRIVKEY" "$TEST3_PUBKEY" "blocked" "blacklisted in multi-user test"; then
pass_test "Multiple users scenario working correctly"
else
fail_test "Blacklisted user in multi-user scenario was not blocked"
fi
else
fail_test "Whitelisted user in multi-user scenario was blocked"
fi
else
fail_test "Failed to set up multiple users scenario"
fi
}
# 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
if send_auth_rule_event "add" "whitelist" "pubkey" "$TEST2_PUBKEY" "Priority test whitelist"; then
setup_success=$((setup_success + 1))
fi
if send_auth_rule_event "add" "blacklist" "pubkey" "$TEST2_PUBKEY" "Priority test blacklist"; then
setup_success=$((setup_success + 1))
fi
if [ $setup_success -eq 2 ]; then
# Test which rule takes priority (typically blacklist should win)
if test_event_publishing "$TEST2_PRIVKEY" "$TEST2_PUBKEY" "blocked" "pubkey in both whitelist and blacklist"; then
pass_test "Priority rules working correctly (blacklist takes precedence)"
else
# If whitelist wins, that's also valid depending on implementation
log_warning "Whitelist took precedence over blacklist - this may be implementation-specific"
pass_test "Priority rules working (whitelist precedence)"
fi
else
fail_test "Failed to set up priority rules test"
fi
}
# 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
test_event=$(nak event -k 1 --content "$test_content" --sec "$TEST1_PRIVKEY" 2>/dev/null)
if [ $? -ne 0 ] || [ -z "$test_event" ]; then
fail_test "Failed to create test event for hash blacklist"
return
fi
# Extract event ID (hash) from the event using jq
local event_id
event_id=$(echo "$test_event" | jq -r '.id' 2>/dev/null)
if [ -z "$event_id" ] || [ "$event_id" = "null" ]; then
fail_test "Failed to extract event ID for hash blacklist test"
return
fi
log_info "Testing hash blacklist with event ID: ${event_id:0:16}..."
# Add the event ID to hash blacklist
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
log_info "Attempting to publish blacklisted event..."
local result
result=$(echo "$test_event" | timeout 10s nak event "$RELAY_URL" 2>&1)
local exit_code=$?
if [ $exit_code -ne 0 ] || echo "$result" | grep -q -i "blocked\|denied\|rejected\|blacklist"; then
pass_test "Hash-based blacklist working correctly"
else
fail_test "Hash-based blacklist did not block the event: $result"
fi
else
fail_test "Failed to add event hash to blacklist"
fi
}
# 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
for i in {1..3}; do
local test_content="Rapid test message $i"
local test_event
test_event=$(nak event -k 1 --content "$test_content" --sec "$TEST1_PRIVKEY" 2>/dev/null)
if [ $? -eq 0 ]; then
local message="[\"EVENT\",$test_event]"
local response
response=$(send_websocket_message "$message" "OK" 5)
if echo "$response" | grep -q '"OK"'; then
rapid_success_count=$((rapid_success_count + 1))
fi
fi
# Small delay between requests
sleep 0.1
done
if [ $rapid_success_count -ge 2 ]; then
pass_test "WebSocket connection handles multiple requests correctly"
else
fail_test "WebSocket connection failed to handle multiple rapid requests ($rapid_success_count/3 succeeded)"
fi
}
# 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
sleep 1
# Test enforcement multiple times to verify persistence
local enforcement_count=0
for i in {1..2}; do
if test_event_publishing "$TEST3_PRIVKEY" "$TEST3_PUBKEY" "blocked" "persistence test attempt $i"; then
enforcement_count=$((enforcement_count + 1))
fi
sleep 0.5
done
if [ $enforcement_count -eq 2 ]; then
pass_test "Rule persistence working correctly"
else
fail_test "Rule persistence failed ($enforcement_count/2 enforcements succeeded)"
fi
else
fail_test "Failed to add rule for persistence test"
fi
}
# Test 11: Cleanup and Final Verification
test_cleanup_verification() {
increment_test
log "Test $TESTS_RUN: Cleanup and Final Verification"
# Remove all test rules
local cleanup_success=0
# Remove whitelist entries
if send_auth_rule_event "remove" "whitelist" "pubkey" "$TEST1_PUBKEY" "Cleanup whitelist"; then
cleanup_success=$((cleanup_success + 1))
fi
# Remove blacklist entries
for pubkey in "$TEST2_PUBKEY" "$TEST3_PUBKEY"; do
if send_auth_rule_event "remove" "blacklist" "pubkey" "$pubkey" "Cleanup blacklist"; then
cleanup_success=$((cleanup_success + 1))
fi
done
if [ $cleanup_success -ge 2 ]; then
# Verify that previously restricted pubkeys can now publish
if test_event_publishing "$TEST3_PRIVKEY" "$TEST3_PUBKEY" "success" "after cleanup verification"; then
pass_test "Cleanup and verification successful"
else
log_warning "Cleanup completed but restrictions may still be active"
pass_test "Cleanup completed (partial verification)"
fi
else
fail_test "Cleanup failed ($cleanup_success rules removed)"
fi
}
# =======================================================================
# MAIN TEST EXECUTION
# =======================================================================
run_all_tests() {
log "Starting comprehensive whitelist/blacklist functionality tests..."
# Setup
setup_test_environment
clear_all_auth_rules
test_admin_authentication
test_auth_rules_storage_query
# test_basic_whitelist
# test_basic_blacklist
# test_rule_removal
# test_multiple_users
# test_priority_rules
# test_hash_blacklist
# test_websocket_behavior
# test_rule_persistence
# test_cleanup_verification
# Test summary
echo ""
echo -e "${BOLD}=== TEST SUMMARY ===${RESET}"
echo -e "Tests run: ${BLUE}$TESTS_RUN${RESET}"
echo -e "Tests passed: ${GREEN}$TESTS_PASSED${RESET}"
echo -e "Tests failed: ${RED}$TESTS_FAILED${RESET}"
echo ""
if [ $TESTS_FAILED -eq 0 ]; then
log_success "All tests passed! Whitelist/blacklist functionality is working correctly."
return 0
else
log_error "$TESTS_FAILED out of $TESTS_RUN tests failed."
return 1
fi
}
# =======================================================================
# CLEANUP FUNCTIONS
# =======================================================================
cleanup() {
log "Cleaning up test environment..."
# Remove temporary directory
if [ -n "$TEMP_DIR" ] && [ -d "$TEMP_DIR" ]; then
rm -rf "$TEMP_DIR"
log_info "Temporary directory removed: $TEMP_DIR"
fi
log "Test cleanup completed."
}
# Set up cleanup trap
trap cleanup EXIT
# =======================================================================
# SCRIPT ENTRY POINT
# =======================================================================
main() {
echo -e "${BOLD}${BLUE}C-Relay Whitelist/Blacklist Authentication Test${RESET}"
echo -e "${BLUE}===============================================${RESET}"
echo ""
# 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
fi
log_success "Connected to relay at $RELAY_URL"
# Run all tests
if run_all_tests; then
echo ""
log_success "All whitelist/blacklist tests completed successfully!"
exit 0
else
echo ""
log_error "Some tests failed."
exit 1
fi
}
# Run main function if script is executed directly
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi

413
tests/white_black_test.sh Executable file
View File

@@ -0,0 +1,413 @@
#!/bin/bash
# C-Relay Whitelist/Blacklist Test Script
# Tests the relay's authentication functionality using nak
set -e # Exit on any error
# Configuration
RELAY_URL="ws://localhost:8888"
ADMIN_PRIVKEY="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
ADMIN_PUBKEY="6a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3"
RELAY_PUBKEY="4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Logging functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
# Check if nak is installed
check_nak() {
if ! command -v nak &> /dev/null; then
log_error "nak command not found. Please install nak first."
log_error "Visit: https://github.com/fiatjaf/nak"
exit 1
fi
log_success "nak is available"
}
# Generate test keypair
generate_test_keypair() {
log_info "Generating test keypair..."
# Generate private key
TEST_PRIVKEY=$(nak key generate 2>/dev/null)
if [ -z "$TEST_PRIVKEY" ]; then
log_error "Failed to generate private key"
exit 1
fi
# Derive public key from private key
TEST_PUBKEY=$(nak key public "$TEST_PRIVKEY" 2>/dev/null)
if [ -z "$TEST_PUBKEY" ]; then
log_error "Failed to derive public key from private key"
exit 1
fi
log_success "Generated test keypair:"
log_info " Private key: $TEST_PRIVKEY"
log_info " Public key: $TEST_PUBKEY"
}
# Create test event
create_test_event() {
local timestamp=$(date +%s)
local content="Test event at timestamp $timestamp"
log_info "Creating test event (kind 1) with content: '$content'"
# Create event using nak
EVENT_JSON=$(nak event \
--kind 1 \
--content "$content" \
--sec "$TEST_PRIVKEY" \
--tag 't=test')
# Extract event ID
EVENT_ID=$(echo "$EVENT_JSON" | jq -r '.id')
if [ -z "$EVENT_ID" ] || [ "$EVENT_ID" = "null" ]; then
log_error "Failed to create test event"
exit 1
fi
log_success "Created test event with ID: $EVENT_ID"
}
# Test 1: Post event and verify retrieval
test_post_and_retrieve() {
log_info "=== TEST 1: Post event and verify retrieval ==="
# Post the event
log_info "Posting test event to relay..."
POST_RESULT=$(echo "$EVENT_JSON" | nak event "$RELAY_URL")
if echo "$POST_RESULT" | grep -q "error\|failed\|denied"; then
log_error "Failed to post event: $POST_RESULT"
return 1
fi
log_success "Event posted successfully"
# Wait a moment for processing
sleep 2
# Try to retrieve the event
log_info "Retrieving event from relay..."
RETRIEVE_RESULT=$(nak req \
--id "$EVENT_ID" \
"$RELAY_URL")
if echo "$RETRIEVE_RESULT" | grep -q "$EVENT_ID"; then
log_success "Event successfully retrieved from relay"
return 0
else
log_error "Failed to retrieve event from relay"
log_error "Query result: $RETRIEVE_RESULT"
return 1
fi
}
# Send admin command to add user to blacklist
add_to_blacklist() {
log_info "Adding test user to blacklist..."
# Create the admin command
COMMAND="[\"blacklist\", \"pubkey\", \"$TEST_PUBKEY\"]"
# Encrypt the command using NIP-44
ENCRYPTED_COMMAND=$(nak encrypt "$COMMAND" \
--sec "$ADMIN_PRIVKEY" \
--recipient-pubkey "$RELAY_PUBKEY")
if [ -z "$ENCRYPTED_COMMAND" ]; then
log_error "Failed to encrypt admin command"
return 1
fi
# Create admin event
ADMIN_EVENT=$(nak event \
--kind 23456 \
--content "$ENCRYPTED_COMMAND" \
--sec "$ADMIN_PRIVKEY" \
--tag "p=$RELAY_PUBKEY")
# Post admin event
ADMIN_RESULT=$(echo "$ADMIN_EVENT" | nak event "$RELAY_URL")
if echo "$ADMIN_RESULT" | grep -q "error\|failed\|denied"; then
log_error "Failed to send admin command: $ADMIN_RESULT"
return 1
fi
log_success "Admin command sent successfully - user added to blacklist"
# Wait for the relay to process the admin command
sleep 3
}
# Send admin command to add user to whitelist
add_to_whitelist() {
local pubkey="$1"
log_info "Adding pubkey to whitelist: ${pubkey:0:16}..."
# Create the admin command
COMMAND="[\"whitelist\", \"pubkey\", \"$pubkey\"]"
# Encrypt the command using NIP-44
ENCRYPTED_COMMAND=$(nak encrypt "$COMMAND" \
--sec "$ADMIN_PRIVKEY" \
--recipient-pubkey "$RELAY_PUBKEY")
if [ -z "$ENCRYPTED_COMMAND" ]; then
log_error "Failed to encrypt admin command"
return 1
fi
# Create admin event
ADMIN_EVENT=$(nak event \
--kind 23456 \
--content "$ENCRYPTED_COMMAND" \
--sec "$ADMIN_PRIVKEY" \
--tag "p=$RELAY_PUBKEY")
# Post admin event
ADMIN_RESULT=$(echo "$ADMIN_EVENT" | nak event "$RELAY_URL")
if echo "$ADMIN_RESULT" | grep -q "error\|failed\|denied"; then
log_error "Failed to send admin command: $ADMIN_RESULT"
return 1
fi
log_success "Admin command sent successfully - user added to whitelist"
# Wait for the relay to process the admin command
sleep 3
}
# Clear all auth rules
clear_auth_rules() {
log_info "Clearing all auth rules..."
# Create the admin command
COMMAND="[\"system_command\", \"clear_all_auth_rules\"]"
# Encrypt the command using NIP-44
ENCRYPTED_COMMAND=$(nak encrypt "$COMMAND" \
--sec "$ADMIN_PRIVKEY" \
--recipient-pubkey "$RELAY_PUBKEY")
if [ -z "$ENCRYPTED_COMMAND" ]; then
log_error "Failed to encrypt admin command"
return 1
fi
# Create admin event
ADMIN_EVENT=$(nak event \
--kind 23456 \
--content "$ENCRYPTED_COMMAND" \
--sec "$ADMIN_PRIVKEY" \
--tag "p=$RELAY_PUBKEY")
# Post admin event
ADMIN_RESULT=$(echo "$ADMIN_EVENT" | nak event "$RELAY_URL")
if echo "$ADMIN_RESULT" | grep -q "error\|failed\|denied"; then
log_error "Failed to send admin command: $ADMIN_RESULT"
return 1
fi
log_success "Admin command sent successfully - all auth rules cleared"
# Wait for the relay to process the admin command
sleep 3
}
# Test 2: Try to post after blacklisting
test_blacklist_post() {
log_info "=== TEST 2: Attempt to post event after blacklisting ==="
# Create a new test event
local timestamp=$(date +%s)
local content="Blacklisted test event at timestamp $timestamp"
log_info "Creating new test event for blacklisted user..."
NEW_EVENT_JSON=$(nak event \
--kind 1 \
--content "$content" \
--sec "$TEST_PRIVKEY" \
--tag 't=blacklist-test')
NEW_EVENT_ID=$(echo "$NEW_EVENT_JSON" | jq -r '.id')
# Try to post the event
log_info "Attempting to post event with blacklisted user..."
POST_RESULT=$(echo "$NEW_EVENT_JSON" | nak event "$RELAY_URL" 2>&1)
# Check if posting failed (should fail for blacklisted user)
if echo "$POST_RESULT" | grep -q "error\|failed\|denied\|blocked"; then
log_success "Event posting correctly blocked for blacklisted user"
return 0
else
log_error "Event posting was not blocked - blacklist may not be working"
log_error "Post result: $POST_RESULT"
return 1
fi
}
# Test 3: Test whitelist functionality
test_whitelist_functionality() {
log_info "=== TEST 3: Test whitelist functionality ==="
# Generate a second test keypair for whitelist testing
log_info "Generating second test keypair for whitelist testing..."
WHITELIST_PRIVKEY=$(nak key generate 2>/dev/null)
WHITELIST_PUBKEY=$(nak key public "$WHITELIST_PRIVKEY" 2>/dev/null)
if [ -z "$WHITELIST_PUBKEY" ]; then
log_error "Failed to generate whitelist test keypair"
return 1
fi
log_success "Generated whitelist test keypair: ${WHITELIST_PUBKEY:0:16}..."
# Clear all auth rules first
if ! clear_auth_rules; then
log_error "Failed to clear auth rules for whitelist test"
return 1
fi
# Add the whitelist user to whitelist
if ! add_to_whitelist "$WHITELIST_PUBKEY"; then
log_error "Failed to add whitelist user"
return 1
fi
# Test 3a: Original test user should be blocked (not whitelisted)
log_info "Testing that non-whitelisted user is blocked..."
local timestamp=$(date +%s)
local content="Non-whitelisted test event at timestamp $timestamp"
NON_WHITELIST_EVENT=$(nak event \
--kind 1 \
--content "$content" \
--sec "$TEST_PRIVKEY" \
--tag 't=whitelist-test')
POST_RESULT=$(echo "$NON_WHITELIST_EVENT" | nak event "$RELAY_URL" 2>&1)
if echo "$POST_RESULT" | grep -q "error\|failed\|denied\|blocked"; then
log_success "Non-whitelisted user correctly blocked"
else
log_error "Non-whitelisted user was not blocked - whitelist may not be working"
log_error "Post result: $POST_RESULT"
return 1
fi
# Test 3b: Whitelisted user should be allowed
log_info "Testing that whitelisted user can post..."
content="Whitelisted test event at timestamp $timestamp"
WHITELIST_EVENT=$(nak event \
--kind 1 \
--content "$content" \
--sec "$WHITELIST_PRIVKEY" \
--tag 't=whitelist-test')
POST_RESULT=$(echo "$WHITELIST_EVENT" | nak event "$RELAY_URL" 2>&1)
if echo "$POST_RESULT" | grep -q "error\|failed\|denied\|blocked"; then
log_error "Whitelisted user was blocked - whitelist not working correctly"
log_error "Post result: $POST_RESULT"
return 1
else
log_success "Whitelisted user can post successfully"
fi
# Verify the whitelisted event can be retrieved
WHITELIST_EVENT_ID=$(echo "$WHITELIST_EVENT" | jq -r '.id')
sleep 2
RETRIEVE_RESULT=$(nak req \
--id "$WHITELIST_EVENT_ID" \
"$RELAY_URL")
if echo "$RETRIEVE_RESULT" | grep -q "$WHITELIST_EVENT_ID"; then
log_success "Whitelisted event successfully retrieved"
return 0
else
log_error "Failed to retrieve whitelisted event"
return 1
fi
}
# Main test function
main() {
log_info "Starting C-Relay Whitelist/Blacklist Test"
log_info "=========================================="
# Check prerequisites
check_nak
# Generate test keypair
generate_test_keypair
# Create test event
create_test_event
# Test 1: Post and retrieve
if test_post_and_retrieve; then
log_success "TEST 1 PASSED: Event posting and retrieval works"
else
log_error "TEST 1 FAILED: Event posting/retrieval failed"
exit 1
fi
# Add user to blacklist
if add_to_blacklist; then
log_success "Blacklist command sent successfully"
else
log_error "Failed to send blacklist command"
exit 1
fi
# Test 2: Try posting after blacklist
if test_blacklist_post; then
log_success "TEST 2 PASSED: Blacklist functionality works correctly"
else
log_error "TEST 2 FAILED: Blacklist functionality not working"
exit 1
fi
# Test 3: Test whitelist functionality
if test_whitelist_functionality; then
log_success "TEST 3 PASSED: Whitelist functionality works correctly"
else
log_error "TEST 3 FAILED: Whitelist functionality not working"
exit 1
fi
log_success "All tests passed! Whitelist/blacklist functionality is working correctly."
}
# Run main function
main "$@"