diff --git a/plans/main_thread_offload_plan.md b/plans/main_thread_offload_plan.md new file mode 100644 index 0000000..2032b7f --- /dev/null +++ b/plans/main_thread_offload_plan.md @@ -0,0 +1,194 @@ +# Main Thread CPU Offload Plan + +## Problem Statement + +The main `c_relay` thread consumes ~56% CPU while the DB worker threads (`db-read-1..4`, `db-write`) sit near 0%. All protocol handling, JSON parsing, event validation, subscription matching, and message queueing happens synchronously inside `nostr_relay_callback()` on the main libwebsockets event-loop thread. + +## Current Architecture + +```mermaid +flowchart TD + LWS[lws_service - main thread] --> CB[nostr_relay_callback] + CB --> PARSE[JSON parse - cJSON_Parse] + CB --> VALIDATE[Signature verify - nostr_validate_unified_request] + CB --> STORE[store_event - builds payload] + STORE --> SYNC_WRITE[thread_pool_execute_store_event_sync] + SYNC_WRITE --> |blocks main thread| DB_WRITE[db-write thread] + DB_WRITE --> |signal| SYNC_WRITE + CB --> REQ[handle_req_message - builds SQL] + REQ --> SYNC_READ[thread_pool_execute_req_sync] + SYNC_READ --> |blocks main thread| DB_READ[db-read-N thread] + DB_READ --> |signal| SYNC_READ + CB --> BROADCAST[broadcast_event_to_subscriptions] + BROADCAST --> QUEUE[queue_message per subscriber] + CB --> WRITEABLE[LWS_CALLBACK_SERVER_WRITEABLE] + WRITEABLE --> DRAIN[process_message_queue - lws_write] +``` + +### Why the main thread is hot + +| Work item | Where | Cost | +|-----------|-------|------| +| JSON parsing | `cJSON_Parse` in `LWS_CALLBACK_RECEIVE` | Medium - per message | +| Signature verification | `nostr_validate_unified_request` - ed25519 crypto | **High** - per EVENT | +| SQL query building | `handle_req_message` filter-to-SQL loop | Low-Medium | +| Sync DB wait | `thread_pool_execute_*_sync` - pthread_cond_wait | Blocks but yields CPU | +| Result iteration + expiration check | Row loop in `handle_req_message` with `cJSON_Parse` per row | Medium-High for large result sets | +| Subscription matching | `broadcast_event_to_subscriptions` - filter matching | Medium - scales with subscriber count | +| Message serialization + queueing | `snprintf` + `queue_message_take_ownership` per subscriber | Medium | +| Config lookups | `get_config_int/bool` called repeatedly in hot paths | Low but frequent | + +### Key constraint: libwebsockets is single-threaded + +libwebsockets requires that **all `lws_write`, `lws_callback_on_writable`, and `lws_close_reason` calls happen from the service thread** (the thread running `lws_service`). This means we cannot directly write to WebSocket connections from worker threads. However, we **can** do computation on worker threads and post results back to the main thread for I/O. + +## Offload Strategy + +### Phase 1: Async EVENT Processing (Highest Impact) + +Convert EVENT handling from synchronous to async. Currently the main thread does: parse → validate → store → broadcast, all blocking. Instead: + +1. **Main thread**: Parse JSON (fast), extract event ID for dedup check, then submit a job to a new "event processing" worker thread +2. **Worker thread**: Signature verification (expensive crypto), store_event (already goes to DB thread), prepare broadcast payload +3. **Main thread callback**: Receive result via `wake_loop_cb` + `lws_cancel_service`, send OK response and broadcast to subscribers + +```mermaid +flowchart TD + LWS[lws_service - main thread] --> RECEIVE[LWS_CALLBACK_RECEIVE] + RECEIVE --> PARSE[JSON parse + dedup check] + PARSE --> SUBMIT[Submit to event-worker queue] + SUBMIT --> LWS + + WORKER[event-worker thread] --> VERIFY[Signature verification] + VERIFY --> STORE_DB[store_event via DB pool] + STORE_DB --> PREP[Prepare broadcast payload] + PREP --> RESULT_Q[Push result to completion queue] + RESULT_Q --> WAKE[lws_cancel_service] + + LWS2[lws_service wakes] --> POLL[Poll completion queue] + POLL --> OK[Send OK response via queue_message] + POLL --> BCAST[broadcast_event_to_subscriptions] +``` + +**What this offloads**: ed25519 signature verification (~the most expensive per-event operation), event classification, tag serialization, and the synchronous DB store wait. + +**What stays on main thread**: JSON parse (needed to extract event ID for dedup), OK response writing, broadcast fan-out (requires lws access). + +### Phase 2: Async REQ Query Execution (Medium Impact) + +Convert REQ handling from sync to async: + +1. **Main thread**: Parse filters, build SQL, create subscription, submit query job +2. **DB reader thread**: Execute query (already happens, but currently blocks main thread via `_sync`) +3. **Main thread callback**: Iterate results, queue EVENT messages, send EOSE + +This is simpler than Phase 1 because the thread pool already supports async submission via `thread_pool_submit_read` with a `result_cb`. The `_sync` wrappers just add a condvar wait on top. We need to: +- Use `thread_pool_submit_read` directly instead of `thread_pool_execute_req_sync` +- Store pending REQ context (sub_id, wsi, pss) so the callback can complete the work +- In the result callback, push results to a completion queue and call `lws_cancel_service` +- On the main thread, drain the completion queue and send EVENT + EOSE messages + +### Phase 3: Reduce Per-Row Overhead in REQ Results (Low-Medium Impact) + +Currently each row from a REQ query gets `cJSON_Parse` just to check NIP-40 expiration. This is wasteful: + +- Option A: Add an `expiration` column to the events table so expiration filtering can be done in SQL +- Option B: Store expiration timestamp in a fast-parse format (extract during INSERT, store as integer column) +- Option C: Use string search on the raw JSON for the expiration tag instead of full parse + +### Phase 4: Config Value Caching (Low Impact, Easy Win) + +`get_config_int` and `get_config_bool` are called on every message in hot paths. These do SQLite queries. Cache config values in memory with a TTL or invalidation signal, so the main loop only refreshes them periodically (already done for `debug_level` every 60s — extend to all hot-path config values). + +## Implementation Priority + +| Phase | Impact | Risk | Complexity | +|-------|--------|------|------------| +| Phase 1: Async EVENT | **High** - removes crypto from main thread | Medium - async state management | Medium-High | +| Phase 2: Async REQ | **Medium** - unblocks main thread during queries | Low - infrastructure exists | Medium | +| Phase 3: Expiration optimization | **Low-Medium** - reduces per-row parse cost | Low | Low | +| Phase 4: Config caching | **Low** - reduces DB round-trips | Very Low | Low | + +## Detailed Design: Phase 1 (Async EVENT Processing) + +### New Components + +#### Completion Queue (`src/completion_queue.h/.c`) +A thread-safe FIFO queue for posting results from worker threads back to the main thread: + +```c +typedef struct { + int type; // COMPLETION_TYPE_EVENT_RESULT, COMPLETION_TYPE_REQ_RESULT, etc. + void* data; // Type-specific result data + struct lws* wsi; // Target WebSocket connection + void* pss; // Per-session data +} completion_item_t; + +int completion_queue_init(void); +int completion_queue_push(completion_item_t* item); +completion_item_t* completion_queue_pop(void); // Non-blocking +void completion_queue_shutdown(void); +``` + +#### Event Worker Thread +A dedicated pthread that processes EVENT validation/storage: + +```c +typedef struct { + cJSON* event; // Parsed event JSON - ownership transferred + cJSON* full_message; // Full message JSON for context + struct lws* wsi; + void* pss; + char sub_id[64]; // For response routing +} event_work_item_t; +``` + +#### Main Loop Integration +Add a completion queue drain step to the main event loop. After `lws_service` returns (either from timeout or `lws_cancel_service` wake), check the completion queue: + +```c +while (g_server_running && !g_shutdown_flag) { + int result = lws_service(ws_context, 1000); + + // NEW: Drain completion queue + completion_item_t* item; + while ((item = completion_queue_pop()) != NULL) { + process_completion(item); // Send OK, broadcast, etc. + free(item); + } + + // ... existing periodic checks ... +} +``` + +### Changes to Existing Code + +1. **`nostr_relay_callback` EVENT path**: After JSON parse and dedup check, instead of calling `nostr_validate_unified_request` + `store_event` + `broadcast_event_to_subscriptions` synchronously, submit an `event_work_item_t` to the event worker queue and return 0 immediately. + +2. **`store_event`**: No changes needed — it already uses `thread_pool_execute_store_event_sync` which will run on the DB writer thread. The event worker thread will call it. + +3. **`broadcast_event_to_subscriptions`**: No changes needed — it will be called from the main thread when processing the completion item, which is correct since it calls `queue_message_take_ownership` (requires lws thread). + +### Thread Safety Considerations + +- The `cJSON* event` object must be fully owned by the worker thread during processing. The main thread must not access it after submission. +- The `wsi` and `pss` pointers could become invalid if the client disconnects while the event is being processed. The completion handler must validate that the connection is still alive before sending the OK response. +- A generation counter or epoch on `pss` can detect stale references. + +## What Cannot Be Offloaded + +- **`lws_write` / `queue_message`**: Must happen on the lws service thread +- **`lws_callback_on_writable`**: Must happen on the lws service thread +- **`lws_close_reason`**: Must happen on the lws service thread +- **Subscription list iteration for broadcast**: Accesses `lws_wsi_user` which is lws-internal + +These are fundamental libwebsockets constraints. The pattern is always: do computation off-thread, post result to completion queue, wake main thread, do I/O on main thread. + +## Expected Impact + +With Phase 1 alone, the main thread would no longer perform: +- ed25519 signature verification (~100-500μs per event depending on CPU) +- Synchronous DB store wait (~50-200μs per event) +- Event classification, tag serialization, JSON serialization for storage + +This should reduce main-thread CPU by roughly 30-50% for EVENT-heavy workloads, shifting that work to the event worker thread and DB threads. diff --git a/postgres_relays.md b/plans/postgres_relays.md similarity index 100% rename from postgres_relays.md rename to plans/postgres_relays.md diff --git a/query_analysis_report.md b/plans/query_analysis_report.md similarity index 100% rename from query_analysis_report.md rename to plans/query_analysis_report.md diff --git a/relay.pid b/relay.pid index f317e92..ee50374 100644 --- a/relay.pid +++ b/relay.pid @@ -1 +1 @@ -513827 +567688 diff --git a/src/db_ops.c b/src/db_ops.c index 472a046..8ee730b 100644 --- a/src/db_ops.c +++ b/src/db_ops.c @@ -13,17 +13,58 @@ extern sqlite3* g_db; extern char g_database_path[512]; +// Optional per-thread connection override (used by thread pool workers) +static __thread sqlite3* g_thread_db = NULL; + +static sqlite3* db_active_connection(void) { + return g_thread_db ? g_thread_db : g_db; +} + typedef struct db_stmt { sqlite3_stmt* stmt; } db_stmt_t; +int db_init(const char* connection_string) { + if (!connection_string || connection_string[0] == '\0') return DB_MISUSE; + if (g_db) return DB_OK; + + int rc = sqlite3_open(connection_string, &g_db); + if (rc != SQLITE_OK) { + if (g_db) { + sqlite3_close(g_db); + g_db = NULL; + } + return DB_ERROR; + } + + strncpy(g_database_path, connection_string, sizeof(g_database_path) - 1); + g_database_path[sizeof(g_database_path) - 1] = '\0'; + return DB_OK; +} + +void db_close(void) { + if (!g_db) return; + sqlite3_close(g_db); + g_db = NULL; +} + +int db_set_thread_connection(void* connection) { + g_thread_db = (sqlite3*)connection; + return DB_OK; +} + +void db_clear_thread_connection(void) { + g_thread_db = NULL; +} + int db_is_available(void) { - return g_db != NULL; + return db_active_connection() != NULL; } const char* db_last_error(void) { - if (!g_db) return "database not available"; - return sqlite3_errmsg(g_db); + sqlite3* db = db_active_connection(); + if (!db) return "database not available"; + return sqlite3_errmsg(db); } const char* db_get_database_path(void) { @@ -31,10 +72,11 @@ const char* db_get_database_path(void) { } int db_prepare(const char* sql, db_stmt_t** out_stmt) { - if (!g_db || !sql || !out_stmt) return DB_MISUSE; + sqlite3* db = db_active_connection(); + if (!db || !sql || !out_stmt) return DB_MISUSE; sqlite3_stmt* raw_stmt = NULL; - int rc = sqlite3_prepare_v2(g_db, sql, -1, &raw_stmt, NULL); + int rc = sqlite3_prepare_v2(db, sql, -1, &raw_stmt, NULL); if (rc != SQLITE_OK) return rc; db_stmt_t* wrapper = (db_stmt_t*)malloc(sizeof(db_stmt_t)); @@ -372,10 +414,11 @@ int db_count_active_whitelist_rules(void) { } int db_count_with_sql(const char* sql, const char** bind_params, int bind_param_count, int* out_count) { - if (!g_db || !sql || !out_count) return -1; + sqlite3* db = db_active_connection(); + if (!db || !sql || !out_count) return -1; sqlite3_stmt* stmt = NULL; - if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) { + if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { return -1; } @@ -886,7 +929,8 @@ int db_insert_event_with_json(const char* id, const char* pubkey, long long crea int kind, const char* event_type, const char* content, const char* sig, const char* tags_json, const char* event_json, int* out_step_rc, int* out_extended_errcode) { - if (!g_db || !id || !pubkey || !event_type || !content || !sig || !tags_json || !event_json) { + sqlite3* db = db_active_connection(); + if (!db || !id || !pubkey || !event_type || !content || !sig || !tags_json || !event_json) { return -1; } @@ -895,7 +939,7 @@ int db_insert_event_with_json(const char* id, const char* pubkey, long long crea "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"; sqlite3_stmt* stmt = NULL; - if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) { + if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { return -1; } @@ -910,7 +954,7 @@ int db_insert_event_with_json(const char* id, const char* pubkey, long long crea sqlite3_bind_text(stmt, 9, event_json, -1, SQLITE_TRANSIENT); int step_rc = sqlite3_step(stmt); - int extended_errcode = sqlite3_extended_errcode(g_db); + int extended_errcode = sqlite3_extended_errcode(db); sqlite3_finalize(stmt); if (out_step_rc) { diff --git a/src/db_ops.h b/src/db_ops.h index 864208e..542e45c 100644 --- a/src/db_ops.h +++ b/src/db_ops.h @@ -6,10 +6,16 @@ #include // Generic helpers +int db_init(const char* connection_string); +void db_close(void); int db_is_available(void); const char* db_last_error(void); const char* db_get_database_path(void); +// Per-thread connection override (used by thread pool workers) +int db_set_thread_connection(void* connection); +void db_clear_thread_connection(void); + // DB result codes (backend-agnostic) #define DB_OK 0 #define DB_ERROR 1 diff --git a/src/main.c b/src/main.c index 58126f8..178e9fb 100644 --- a/src/main.c +++ b/src/main.c @@ -115,6 +115,12 @@ int process_admin_event_in_config(cJSON* event, char* error_message, size_t erro // Forward declaration for NIP-45 COUNT message handling int handle_count_message(const char* sub_id, cJSON* filters, struct lws *wsi, struct per_session_data *pss); +// Forward declaration for NOTICE message support +void send_notice_message(struct lws* wsi, struct per_session_data* pss, const char* message); + +// Thread pool wake callback (called by worker threads) +static void wake_event_loop_from_thread_pool(void* ctx); + // Parameter binding helpers for SQL queries static void add_bind_param(char*** params, int* count, int* capacity, const char* value) { if (*count >= *capacity) { @@ -131,11 +137,308 @@ static void free_bind_params(char** params, int count) { free(params); } +typedef struct req_async_state { + char sub_id[SUBSCRIPTION_ID_MAX_LENGTH]; + struct lws* wsi_token; + pthread_mutex_t mutex; + int pending_jobs; +} req_async_state_t; + +typedef struct req_async_submit_ctx { + req_async_state_t* state; +} req_async_submit_ctx_t; + +typedef struct req_async_completion { + req_async_state_t* state; + thread_pool_status_t status; + thread_pool_req_result_t* req_result; + struct req_async_completion* next; +} req_async_completion_t; + +static pthread_mutex_t g_req_async_completion_mutex = PTHREAD_MUTEX_INITIALIZER; +static req_async_completion_t* g_req_async_completion_head = NULL; +static req_async_completion_t* g_req_async_completion_tail = NULL; + +static void free_req_payload_main(void* p) { + thread_pool_req_payload_t* payload = (thread_pool_req_payload_t*)p; + if (!payload) return; + free(payload->sql); + if (payload->bind_params) { + for (int i = 0; i < payload->bind_param_count; i++) { + free(payload->bind_params[i]); + } + free(payload->bind_params); + } + free(payload); +} + +static req_async_state_t* req_async_state_create(const char* sub_id, struct lws* wsi) { + req_async_state_t* state = calloc(1, sizeof(*state)); + if (!state) { + return NULL; + } + + if (sub_id) { + strncpy(state->sub_id, sub_id, sizeof(state->sub_id) - 1); + state->sub_id[sizeof(state->sub_id) - 1] = '\0'; + } + state->wsi_token = wsi; + pthread_mutex_init(&state->mutex, NULL); + return state; +} + +static void req_async_state_free(req_async_state_t* state) { + if (!state) return; + pthread_mutex_destroy(&state->mutex); + free(state); +} + +static void req_async_state_increment_pending(req_async_state_t* state) { + if (!state) return; + pthread_mutex_lock(&state->mutex); + state->pending_jobs++; + pthread_mutex_unlock(&state->mutex); +} + +static int req_async_state_decrement_pending(req_async_state_t* state) { + if (!state) return 0; + pthread_mutex_lock(&state->mutex); + if (state->pending_jobs > 0) { + state->pending_jobs--; + } + int remaining = state->pending_jobs; + pthread_mutex_unlock(&state->mutex); + return remaining; +} + +static void req_async_completion_push(req_async_completion_t* completion) { + if (!completion) return; + + pthread_mutex_lock(&g_req_async_completion_mutex); + completion->next = NULL; + if (!g_req_async_completion_tail) { + g_req_async_completion_head = completion; + g_req_async_completion_tail = completion; + } else { + g_req_async_completion_tail->next = completion; + g_req_async_completion_tail = completion; + } + pthread_mutex_unlock(&g_req_async_completion_mutex); +} + +static req_async_completion_t* req_async_completion_pop(void) { + pthread_mutex_lock(&g_req_async_completion_mutex); + req_async_completion_t* completion = g_req_async_completion_head; + if (completion) { + g_req_async_completion_head = completion->next; + if (!g_req_async_completion_head) { + g_req_async_completion_tail = NULL; + } + } + pthread_mutex_unlock(&g_req_async_completion_mutex); + return completion; +} + +static int resolve_req_async_target(req_async_state_t* state, struct lws** out_wsi, struct per_session_data** out_pss) { + if (!state || !out_wsi || !out_pss) { + return 0; + } + + *out_wsi = NULL; + *out_pss = NULL; + + pthread_mutex_lock(&g_subscription_manager.subscriptions_lock); + subscription_t* sub = g_subscription_manager.active_subscriptions; + while (sub) { + if (sub->active && sub->wsi == state->wsi_token && strcmp(sub->id, state->sub_id) == 0) { + *out_wsi = sub->wsi; + break; + } + sub = sub->next; + } + pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock); + + if (!*out_wsi) { + return 0; + } + + *out_pss = (struct per_session_data*)lws_wsi_user(*out_wsi); + return (*out_pss != NULL); +} + +static void send_eose_message(struct lws* wsi, struct per_session_data* pss, const char* sub_id) { + if (!wsi || !pss || !sub_id) { + return; + } + + cJSON* eose_response = cJSON_CreateArray(); + if (!eose_response) { + return; + } + + cJSON_AddItemToArray(eose_response, cJSON_CreateString("EOSE")); + cJSON_AddItemToArray(eose_response, cJSON_CreateString(sub_id)); + + char *eose_str = cJSON_Print(eose_response); + if (eose_str) { + size_t eose_len = strlen(eose_str); + DEBUG_TRACE("WS_FRAME_SEND: type=EOSE len=%zu data=%.100s%s", + eose_len, + eose_str, + eose_len > 100 ? "..." : ""); + + if (queue_message(wsi, pss, eose_str, eose_len, LWS_WRITE_TEXT) != 0) { + DEBUG_ERROR("Failed to queue EOSE message"); + } + + free(eose_str); + } + cJSON_Delete(eose_response); +} + +static void req_async_result_cb(const thread_pool_result_t* result, void* user_ctx) { + req_async_submit_ctx_t* ctx = (req_async_submit_ctx_t*)user_ctx; + if (!ctx || !ctx->state || !result) { + if (result && result->result_data) { + thread_pool_free_req_result((thread_pool_req_result_t*)result->result_data); + } + if (ctx) free(ctx); + return; + } + + req_async_completion_t* completion = calloc(1, sizeof(*completion)); + if (!completion) { + if (result->result_data) { + thread_pool_free_req_result((thread_pool_req_result_t*)result->result_data); + } + free(ctx); + return; + } + + completion->state = ctx->state; + completion->status = result->status; + completion->req_result = (thread_pool_req_result_t*)result->result_data; + req_async_completion_push(completion); + wake_event_loop_from_thread_pool(NULL); + free(ctx); +} + +static int submit_req_query_async(req_async_state_t* state, const char* sql, const char** bind_params, int bind_param_count) { + if (!state || !sql) { + return -1; + } + + thread_pool_req_payload_t* payload = calloc(1, sizeof(*payload)); + if (!payload) { + return -1; + } + + payload->sql = strdup(sql); + payload->bind_param_count = bind_param_count; + if (!payload->sql) { + free_req_payload_main(payload); + return -1; + } + + if (bind_param_count > 0) { + payload->bind_params = calloc((size_t)bind_param_count, sizeof(char*)); + if (!payload->bind_params) { + free_req_payload_main(payload); + return -1; + } + + for (int i = 0; i < bind_param_count; i++) { + const char* v = (bind_params && bind_params[i]) ? bind_params[i] : ""; + payload->bind_params[i] = strdup(v); + if (!payload->bind_params[i]) { + free_req_payload_main(payload); + return -1; + } + } + } + + req_async_submit_ctx_t* ctx = calloc(1, sizeof(*ctx)); + if (!ctx) { + free_req_payload_main(payload); + return -1; + } + ctx->state = state; + + thread_pool_job_t job; + memset(&job, 0, sizeof(job)); + job.type = THREAD_POOL_JOB_REQ_QUERY; + job.payload = payload; + job.payload_size = sizeof(*payload); + job.payload_free = free_req_payload_main; + job.result_cb = req_async_result_cb; + job.result_cb_ctx = ctx; + + req_async_state_increment_pending(state); + + thread_pool_status_t submit_rc = thread_pool_submit_read(&job, NULL); + if (submit_rc != THREAD_POOL_STATUS_OK) { + req_async_state_decrement_pending(state); + free(ctx); + free_req_payload_main(payload); + return -1; + } + + return 0; +} + +void process_req_async_completions(void) { + req_async_completion_t* completion = NULL; + while ((completion = req_async_completion_pop()) != NULL) { + struct lws* target_wsi = NULL; + struct per_session_data* target_pss = NULL; + int has_target = resolve_req_async_target(completion->state, &target_wsi, &target_pss); + + if (has_target && completion->status == THREAD_POOL_STATUS_OK && completion->req_result) { + for (int r = 0; r < completion->req_result->row_count; r++) { + const char* event_json_str = completion->req_result->event_json_rows[r]; + if (!event_json_str) { + continue; + } + + size_t sub_id_len = strlen(completion->state->sub_id); + size_t event_json_len = strlen(event_json_str); + size_t msg_len = 10 + sub_id_len + 3 + event_json_len + 1; + + unsigned char* buf = malloc(LWS_PRE + msg_len + 1); + if (!buf) { + continue; + } + + char* msg_ptr = (char*)(buf + LWS_PRE); + snprintf(msg_ptr, msg_len + 1, "[\"EVENT\",\"%s\",%s]", completion->state->sub_id, event_json_str); + size_t actual_len = strlen(msg_ptr); + + if (queue_message_take_ownership(target_wsi, target_pss, buf, actual_len, LWS_WRITE_TEXT) != 0) { + DEBUG_ERROR("Failed to queue async EVENT message for sub=%s", completion->state->sub_id); + } + } + } else if (has_target && completion->status != THREAD_POOL_STATUS_OK) { + send_notice_message(target_wsi, target_pss, "error: failed to execute subscription query"); + } + + int remaining = req_async_state_decrement_pending(completion->state); + if (remaining == 0) { + if (has_target) { + send_eose_message(target_wsi, target_pss, completion->state->sub_id); + } + req_async_state_free(completion->state); + } + + if (completion->req_result) { + thread_pool_free_req_result(completion->req_result); + } + free(completion); + } +} + // Forward declaration for enhanced admin event authorization int is_authorized_admin_event(cJSON* event, char* error_message, size_t error_size); -// Forward declaration for NOTICE message support -void send_notice_message(struct lws* wsi, struct per_session_data* pss, const char* message); // Forward declarations for NIP-42 authentication functions void send_nip42_auth_challenge(struct lws* wsi, struct per_session_data* pss); @@ -164,8 +467,6 @@ int handle_nip11_http_request(struct lws* wsi, const char* accept_header); // Forward declaration for WebSocket relay server int start_websocket_relay(int port_override, int strict_port); -// Thread pool wake callback (called by worker threads) -static void wake_event_loop_from_thread_pool(void* ctx); // Forward declarations for IP ban system void ip_ban_init(void); @@ -425,7 +726,7 @@ int init_database(const char* database_path_override) { // Clean up stale WAL files before opening database cleanup_stale_wal_files(db_path); - int rc = sqlite3_open(db_path, &g_db); + int rc = db_init(db_path); if (rc != DB_OK) { DEBUG_ERROR("Cannot open database"); DEBUG_TRACE("Exiting init_database() - failed to open database"); @@ -437,7 +738,7 @@ int init_database(const char* database_path_override) { // Check config table row count immediately after database open int row_count = 0; if (db_get_config_row_count(&row_count) == 0) { - DEBUG_LOG("Config table row count immediately after sqlite3_open(): %d", row_count); + DEBUG_LOG("Config table row count immediately after db_init(): %d", row_count); } else { DEBUG_LOG("Config table count unavailable immediately after sqlite3_open() (table may not exist yet)"); } @@ -600,8 +901,7 @@ void close_database() { DEBUG_WARN("WAL checkpoint warning"); } - sqlite3_close(g_db); - g_db = NULL; + db_close(); DEBUG_LOG("Database connection closed"); } @@ -1101,7 +1401,18 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru } int events_sent = config_events_sent; // Start with synthetic config events - + int submitted_jobs = 0; + int expiration_enabled = get_config_bool("expiration_enabled", 1); + int filter_responses = get_config_bool("expiration_filter", 1); + time_t query_now = time(NULL); + + req_async_state_t* async_state = req_async_state_create(sub_id, wsi); + if (!async_state) { + DEBUG_ERROR("Failed to allocate async REQ state"); + free_bind_params(bind_params, bind_param_count); + return events_sent; + } + // Process each filter in the array for (int i = 0; i < cJSON_GetArraySize(filters); i++) { cJSON* filter = cJSON_GetArrayItem(filters, i); @@ -1125,13 +1436,26 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru // Build SQL query based on filter - exclude ephemeral events (kinds 20000-29999) from historical queries // Select event_json for fast retrieval (no JSON reconstruction needed) - char sql[1024] = "SELECT event_json FROM events WHERE 1=1 AND (kind < 20000 OR kind >= 30000)"; + char sql[1408] = "SELECT event_json FROM events WHERE 1=1 AND (kind < 20000 OR kind >= 30000)"; char* sql_ptr = sql + strlen(sql); int remaining = sizeof(sql) - strlen(sql); - - // Note: Expiration filtering will be done at application level - // after retrieving events to ensure compatibility with all SQLite versions - + + // Phase 3: push expiration filtering into SQL using indexed event_tags table. + if (expiration_enabled && filter_responses) { + snprintf(sql_ptr, remaining, + " AND NOT EXISTS (SELECT 1 FROM event_tags et_exp " + "WHERE et_exp.event_id = events.id " + "AND et_exp.tag_name = ? " + "AND CAST(et_exp.tag_value AS INTEGER) <= ?)"); + sql_ptr += strlen(sql_ptr); + remaining = sizeof(sql) - strlen(sql); + add_bind_param(&bind_params, &bind_param_count, &bind_param_capacity, "expiration"); + + char now_buf[32]; + snprintf(now_buf, sizeof(now_buf), "%lld", (long long)query_now); + add_bind_param(&bind_params, &bind_param_count, &bind_param_capacity, now_buf); + } + // Handle kinds filter cJSON* kinds = cJSON_GetObjectItemCaseSensitive(filter, "kinds"); if (kinds && cJSON_IsArray(kinds)) { @@ -1140,7 +1464,7 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru snprintf(sql_ptr, remaining, " AND kind IN ("); sql_ptr += strlen(sql_ptr); remaining = sizeof(sql) - strlen(sql); - + for (int k = 0; k < kind_count; k++) { cJSON* kind = cJSON_GetArrayItem(kinds, k); if (cJSON_IsNumber(kind)) { @@ -1159,7 +1483,7 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru remaining = sizeof(sql) - strlen(sql); } } - + // Handle authors filter cJSON* authors = cJSON_GetObjectItemCaseSensitive(filter, "authors"); if (authors && cJSON_IsArray(authors)) { @@ -1205,8 +1529,8 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru if (ids && cJSON_IsArray(ids)) { int id_count = 0; // Count valid ids - for (int i = 0; i < cJSON_GetArraySize(ids); i++) { - cJSON* id = cJSON_GetArrayItem(ids, i); + for (int j = 0; j < cJSON_GetArraySize(ids); j++) { + cJSON* id = cJSON_GetArrayItem(ids, j); if (cJSON_IsString(id)) { id_count++; } @@ -1216,8 +1540,8 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru sql_ptr += strlen(sql_ptr); remaining = sizeof(sql) - strlen(sql); - for (int i = 0; i < id_count; i++) { - if (i > 0) { + for (int j = 0; j < id_count; j++) { + if (j > 0) { snprintf(sql_ptr, remaining, ","); sql_ptr++; remaining--; @@ -1231,8 +1555,8 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru remaining = sizeof(sql) - strlen(sql); // Add id values to bind params - for (int i = 0; i < cJSON_GetArraySize(ids); i++) { - cJSON* id = cJSON_GetArrayItem(ids, i); + for (int j = 0; j < cJSON_GetArraySize(ids); j++) { + cJSON* id = cJSON_GetArrayItem(ids, j); if (cJSON_IsString(id)) { add_bind_param(&bind_params, &bind_param_count, &bind_param_capacity, cJSON_GetStringValue(id)); } @@ -1251,8 +1575,8 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru if (cJSON_IsArray(filter_item)) { int tag_value_count = 0; // Count valid tag values - for (int i = 0; i < cJSON_GetArraySize(filter_item); i++) { - cJSON* tag_value = cJSON_GetArrayItem(filter_item, i); + for (int j = 0; j < cJSON_GetArraySize(filter_item); j++) { + cJSON* tag_value = cJSON_GetArrayItem(filter_item, j); if (cJSON_IsString(tag_value)) { tag_value_count++; } @@ -1263,8 +1587,8 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru sql_ptr += strlen(sql_ptr); remaining = sizeof(sql) - strlen(sql); - for (int i = 0; i < tag_value_count; i++) { - if (i > 0) { + for (int j = 0; j < tag_value_count; j++) { + if (j > 0) { snprintf(sql_ptr, remaining, ","); sql_ptr++; remaining--; @@ -1279,8 +1603,8 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru // Add tag name and values to bind params add_bind_param(&bind_params, &bind_param_count, &bind_param_capacity, tag_name); - for (int i = 0; i < cJSON_GetArraySize(filter_item); i++) { - cJSON* tag_value = cJSON_GetArrayItem(filter_item, i); + for (int j = 0; j < cJSON_GetArraySize(filter_item); j++) { + cJSON* tag_value = cJSON_GetArrayItem(filter_item, j); if (cJSON_IsString(tag_value)) { add_bind_param(&bind_params, &bind_param_count, &bind_param_capacity, cJSON_GetStringValue(tag_value)); } @@ -1299,12 +1623,12 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru // Escape single quotes in search term for SQL safety char escaped_search[256]; size_t escaped_len = 0; - for (size_t i = 0; search_term[i] && escaped_len < sizeof(escaped_search) - 1; i++) { - if (search_term[i] == '\'') { + for (size_t j = 0; search_term[j] && escaped_len < sizeof(escaped_search) - 1; j++) { + if (search_term[j] == '\'') { escaped_search[escaped_len++] = '\''; escaped_search[escaped_len++] = '\''; } else { - escaped_search[escaped_len++] = search_term[i]; + escaped_search[escaped_len++] = search_term[j]; } } escaped_search[escaped_len] = '\0'; @@ -1325,7 +1649,7 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru sql_ptr += strlen(sql_ptr); remaining = sizeof(sql) - strlen(sql); } - + // Handle until filter cJSON* until = cJSON_GetObjectItemCaseSensitive(filter, "until"); if (until && cJSON_IsNumber(until)) { @@ -1333,12 +1657,12 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru sql_ptr += strlen(sql_ptr); remaining = sizeof(sql) - strlen(sql); } - + // Add ordering and limit snprintf(sql_ptr, remaining, " ORDER BY created_at DESC"); sql_ptr += strlen(sql_ptr); remaining = sizeof(sql) - strlen(sql); - + // Handle limit filter cJSON* limit = cJSON_GetObjectItemCaseSensitive(filter, "limit"); if (limit && cJSON_IsNumber(limit)) { @@ -1351,116 +1675,27 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru snprintf(sql_ptr, remaining, " LIMIT 500"); } - // Start query timing - struct timespec query_start, query_end; - clock_gettime(CLOCK_MONOTONIC, &query_start); - - // Execute query through db thread pool helper and send events - thread_pool_req_result_t* req_result = NULL; - if (thread_pool_execute_req_sync(sql, (const char**)bind_params, bind_param_count, 5000, &req_result) != 0 || !req_result) { - char error_msg[256]; - snprintf(error_msg, sizeof(error_msg), "Failed to execute subscription query: %s", db_last_error()); - DEBUG_ERROR(error_msg); - - // Log the failed query so we can see what SQL was generated - if (g_debug_level >= DEBUG_LEVEL_DEBUG) { - time_t now = time(NULL); - struct tm* tm_info = localtime(&now); - char timestamp[32]; - strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", tm_info); - fprintf(stderr, "[%s] [QUERY_FAILED] type=REQ sub=%s ip=%s error=%s sql=%s\n", - timestamp, - sub_id, - pss ? pss->client_ip : "N/A", - db_last_error(), - sql); - fflush(stderr); - } - + // Submit async REQ query (results processed on lws thread via completion queue) + if (submit_req_query_async(async_state, sql, (const char**)bind_params, bind_param_count) != 0) { + DEBUG_ERROR("Failed to submit async REQ query for subscription %s", sub_id); continue; } - // Track query execution for abuse detection + submitted_jobs++; if (pss) { pss->db_queries_executed++; } - - // Cache config values outside the row loop (performance fix) - int expiration_enabled = get_config_bool("expiration_enabled", 1); - int filter_responses = get_config_bool("expiration_filter", 1); - - int row_count = 0; - for (int r = 0; r < req_result->row_count; r++) { - const char* event_json_str = req_result->event_json_rows[r]; - row_count++; - - // Track rows returned for abuse detection - if (pss) { - pss->db_rows_returned++; - } - - if (!event_json_str) { - DEBUG_ERROR("Event has NULL event_json field"); - continue; - } - - // Parse event JSON only for expiration check - cJSON* event = cJSON_Parse(event_json_str); - if (!event) { - DEBUG_ERROR("Failed to parse event_json from database"); - continue; - } - - // Check expiration filtering (NIP-40) at application level - // (expiration_enabled and filter_responses are cached outside the loop) - if (expiration_enabled && filter_responses) { - time_t current_time = time(NULL); - if (is_event_expired(event, current_time)) { - // Skip this expired event - cJSON_Delete(event); - continue; - } - } - - // Build EVENT message using zero-copy path: allocate with LWS_PRE prefix, - // write directly, transfer ownership to queue — no memcpy. - // Format: ["EVENT","",] - size_t sub_id_len = strlen(sub_id); - size_t event_json_len = strlen(event_json_str); - size_t msg_len = 10 + sub_id_len + 3 + event_json_len + 1; - - unsigned char* buf = malloc(LWS_PRE + msg_len + 1); - if (buf) { - char* msg_ptr = (char*)(buf + LWS_PRE); - snprintf(msg_ptr, msg_len + 1, "[\"EVENT\",\"%s\",%s]", sub_id, event_json_str); - size_t actual_len = strlen(msg_ptr); - - // queue_message_take_ownership takes buf ownership — no memcpy, no free needed here - if (queue_message_take_ownership(wsi, pss, buf, actual_len, LWS_WRITE_TEXT) != 0) { - DEBUG_ERROR("Failed to queue EVENT message for sub=%s", sub_id); - // buf already freed by queue_message_take_ownership on failure - } - } - - cJSON_Delete(event); - events_sent++; - } - - thread_pool_free_req_result(req_result); - - // Stop query timing and log - clock_gettime(CLOCK_MONOTONIC, &query_end); - long elapsed_us = (query_end.tv_sec - query_start.tv_sec) * 1000000L + - (query_end.tv_nsec - query_start.tv_nsec) / 1000L; - - log_query_execution("REQ", sub_id, pss ? pss->client_ip : NULL, - sql, elapsed_us, row_count); } // Cleanup bind params free_bind_params(bind_params, bind_param_count); - return events_sent; + if (submitted_jobs == 0) { + req_async_state_free(async_state); + return events_sent; + } + + return HANDLE_REQ_ASYNC_PENDING; } ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// @@ -2042,7 +2277,7 @@ int main(int argc, char* argv[]) { ip_ban_load_from_db(); // Optional thread pool scaffold initialization (execution wiring is future work) - int thread_pool_enabled = get_config_bool("thread_pool_enabled", 0); + int thread_pool_enabled = get_config_bool("thread_pool_enabled", 1); int thread_pool_initialized = 0; if (thread_pool_enabled) { thread_pool_config_t tp_cfg; diff --git a/src/main.h b/src/main.h index 773b8be..cd7794f 100644 --- a/src/main.h +++ b/src/main.h @@ -13,8 +13,8 @@ // Using CRELAY_ prefix to avoid conflicts with nostr_core_lib VERSION macros #define CRELAY_VERSION_MAJOR 2 #define CRELAY_VERSION_MINOR 1 -#define CRELAY_VERSION_PATCH 1 -#define CRELAY_VERSION "v2.1.1" +#define CRELAY_VERSION_PATCH 2 +#define CRELAY_VERSION "v2.1.2" // Relay metadata (authoritative source for NIP-11 information) #define RELAY_NAME "C-Relay" @@ -28,4 +28,10 @@ #define POSTING_POLICY "" #define PAYMENTS_URL "" +// Async REQ handler status used by websocket callback to defer EOSE until worker completion +#define HANDLE_REQ_ASYNC_PENDING (-2) + +// Drains completed async REQ jobs and queues EVENT/EOSE on the lws service thread. +void process_req_async_completions(void); + #endif /* MAIN_H */ diff --git a/src/thread_pool.c b/src/thread_pool.c index a2b5430..757dda8 100644 --- a/src/thread_pool.c +++ b/src/thread_pool.c @@ -4,7 +4,10 @@ #include "debug.h" #include "db_ops.h" +#include #include +#include +#include #include #include @@ -37,6 +40,8 @@ typedef struct { thread_pool_wake_loop_cb wake_loop_cb; void* wake_loop_ctx; + char db_path[512]; + pthread_mutex_t state_mutex; } thread_pool_state_t; @@ -193,6 +198,26 @@ static void wake_event_loop(void) { } } +static sqlite3* open_worker_connection(void) { + if (g_pool.db_path[0] == '\0') { + return NULL; + } + + sqlite3* db = NULL; + int rc = sqlite3_open_v2(g_pool.db_path, &db, SQLITE_OPEN_READWRITE, NULL); + if (rc != SQLITE_OK) { + if (db) { + sqlite3_close(db); + } + return NULL; + } + + sqlite3_exec(db, "PRAGMA journal_mode=WAL;", NULL, NULL, NULL); + sqlite3_busy_timeout(db, 5000); + + return db; +} + static void complete_job_with_result(thread_pool_job_node_t* node, thread_pool_status_t status, const char* message, @@ -374,24 +399,54 @@ static void execute_write_job(thread_pool_job_node_t* node) { } static void* reader_worker_main(void* arg) { - (void)arg; + int reader_index = (int)(intptr_t)arg; + + char thread_name[16]; + snprintf(thread_name, sizeof(thread_name), "db-read-%d", reader_index); + pthread_setname_np(pthread_self(), thread_name); + + sqlite3* worker_db = open_worker_connection(); + if (!worker_db) { + DEBUG_ERROR("Reader worker failed to open SQLite connection"); + return NULL; + } + + db_set_thread_connection(worker_db); + while (g_pool.running) { thread_pool_job_node_t* node = queue_pop(&g_pool.read_q); if (!node) break; execute_read_job(node); } + + db_clear_thread_connection(); + sqlite3_close(worker_db); return NULL; } static void* writer_worker_main(void* arg) { (void)arg; + + pthread_setname_np(pthread_self(), "db-write"); + + sqlite3* worker_db = open_worker_connection(); + if (!worker_db) { + DEBUG_ERROR("Writer worker failed to open SQLite connection"); + return NULL; + } + + db_set_thread_connection(worker_db); + while (g_pool.running) { thread_pool_job_node_t* node = queue_pop(&g_pool.write_q); if (!node) break; execute_write_job(node); } + + db_clear_thread_connection(); + sqlite3_close(worker_db); return NULL; } @@ -417,10 +472,21 @@ int thread_pool_init(const thread_pool_config_t* config) { g_pool.next_job_id = 1; g_pool.wake_loop_cb = config->wake_loop_cb; g_pool.wake_loop_ctx = config->wake_loop_ctx; + + const char* db_path = (config->db_path && config->db_path[0] != '\0') ? config->db_path : db_get_database_path(); + if (!db_path || db_path[0] == '\0') { + free(g_pool.readers); + g_pool.readers = NULL; + pthread_mutex_unlock(&g_pool.state_mutex); + return -1; + } + strncpy(g_pool.db_path, db_path, sizeof(g_pool.db_path) - 1); + g_pool.db_path[sizeof(g_pool.db_path) - 1] = '\0'; + g_pool.running = 1; for (int i = 0; i < g_pool.reader_count; i++) { - if (pthread_create(&g_pool.readers[i], NULL, reader_worker_main, NULL) != 0) { + if (pthread_create(&g_pool.readers[i], NULL, reader_worker_main, (void*)(intptr_t)(i + 1)) != 0) { g_pool.running = 0; pthread_cond_broadcast(&g_pool.read_q.cond); pthread_cond_broadcast(&g_pool.write_q.cond); diff --git a/src/websockets.c b/src/websockets.c index d665035..73969d3 100644 --- a/src/websockets.c +++ b/src/websockets.c @@ -33,6 +33,8 @@ #include "thread_pool.h" // Thread pool scaffold #include "db_ops.h" // DB abstraction wrappers +#include "main.h" // Async REQ completion integration + // Forward declarations for logging functions // Forward declarations for configuration functions @@ -1286,38 +1288,41 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso DEBUG_TRACE("REQ filters validated successfully"); DEBUG_TRACE("About to call handle_req_message for subscription %s", subscription_id); - handle_req_message(subscription_id, filters, wsi, pss); + int req_result = handle_req_message(subscription_id, filters, wsi, pss); DEBUG_TRACE("handle_req_message completed for subscription %s", subscription_id); // Clean up the filters array we created cJSON_Delete(filters); - DEBUG_LOG("REQ subscription %s processed, sending EOSE", subscription_id); + // Async path will send EOSE from completion handler. + if (req_result != HANDLE_REQ_ASYNC_PENDING) { + DEBUG_LOG("REQ subscription %s processed, sending EOSE", subscription_id); - // Send EOSE (End of Stored Events) - cJSON* eose_response = cJSON_CreateArray(); - if (eose_response) { - cJSON_AddItemToArray(eose_response, cJSON_CreateString("EOSE")); - cJSON_AddItemToArray(eose_response, cJSON_CreateString(subscription_id)); + // Send EOSE (End of Stored Events) + cJSON* eose_response = cJSON_CreateArray(); + if (eose_response) { + cJSON_AddItemToArray(eose_response, cJSON_CreateString("EOSE")); + cJSON_AddItemToArray(eose_response, cJSON_CreateString(subscription_id)); - char *eose_str = cJSON_Print(eose_response); - if (eose_str) { - size_t eose_len = strlen(eose_str); + char *eose_str = cJSON_Print(eose_response); + if (eose_str) { + size_t eose_len = strlen(eose_str); - // DEBUG: Log WebSocket frame details before sending - DEBUG_TRACE("WS_FRAME_SEND: type=EOSE len=%zu data=%.100s%s", - eose_len, - eose_str, - eose_len > 100 ? "..." : ""); + // DEBUG: Log WebSocket frame details before sending + DEBUG_TRACE("WS_FRAME_SEND: type=EOSE len=%zu data=%.100s%s", + eose_len, + eose_str, + eose_len > 100 ? "..." : ""); - // Queue message for proper libwebsockets pattern - if (queue_message(wsi, pss, eose_str, eose_len, LWS_WRITE_TEXT) != 0) { - DEBUG_ERROR("Failed to queue EOSE message"); + // Queue message for proper libwebsockets pattern + if (queue_message(wsi, pss, eose_str, eose_len, LWS_WRITE_TEXT) != 0) { + DEBUG_ERROR("Failed to queue EOSE message"); + } + + free(eose_str); } - - free(eose_str); + cJSON_Delete(eose_response); } - cJSON_Delete(eose_response); } } else { send_notice_message(wsi, pss, "error: missing or invalid subscription ID in REQ"); @@ -2058,38 +2063,41 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso DEBUG_TRACE("REQ filters validated successfully"); DEBUG_TRACE("About to call handle_req_message for subscription %s", subscription_id); - handle_req_message(subscription_id, filters, wsi, pss); + int req_result = handle_req_message(subscription_id, filters, wsi, pss); DEBUG_TRACE("handle_req_message completed for subscription %s", subscription_id); // Clean up the filters array we created cJSON_Delete(filters); - DEBUG_LOG("REQ subscription %s processed, sending EOSE", subscription_id); + // Async path will send EOSE from completion handler. + if (req_result != HANDLE_REQ_ASYNC_PENDING) { + DEBUG_LOG("REQ subscription %s processed, sending EOSE", subscription_id); - // Send EOSE (End of Stored Events) - cJSON* eose_response = cJSON_CreateArray(); - if (eose_response) { - cJSON_AddItemToArray(eose_response, cJSON_CreateString("EOSE")); - cJSON_AddItemToArray(eose_response, cJSON_CreateString(subscription_id)); + // Send EOSE (End of Stored Events) + cJSON* eose_response = cJSON_CreateArray(); + if (eose_response) { + cJSON_AddItemToArray(eose_response, cJSON_CreateString("EOSE")); + cJSON_AddItemToArray(eose_response, cJSON_CreateString(subscription_id)); - char *eose_str = cJSON_Print(eose_response); - if (eose_str) { - size_t eose_len = strlen(eose_str); + char *eose_str = cJSON_Print(eose_response); + if (eose_str) { + size_t eose_len = strlen(eose_str); - // DEBUG: Log WebSocket frame details before sending - DEBUG_TRACE("WS_FRAME_SEND: type=EOSE len=%zu data=%.100s%s", - eose_len, - eose_str, - eose_len > 100 ? "..." : ""); + // DEBUG: Log WebSocket frame details before sending + DEBUG_TRACE("WS_FRAME_SEND: type=EOSE len=%zu data=%.100s%s", + eose_len, + eose_str, + eose_len > 100 ? "..." : ""); - // Queue message for proper libwebsockets pattern - if (queue_message(wsi, pss, eose_str, eose_len, LWS_WRITE_TEXT) != 0) { - DEBUG_ERROR("Failed to queue EOSE message"); + // Queue message for proper libwebsockets pattern + if (queue_message(wsi, pss, eose_str, eose_len, LWS_WRITE_TEXT) != 0) { + DEBUG_ERROR("Failed to queue EOSE message"); + } + + free(eose_str); } - - free(eose_str); + cJSON_Delete(eose_response); } - cJSON_Delete(eose_response); } } else { send_notice_message(wsi, pss, "error: missing or invalid subscription ID in REQ"); @@ -2647,6 +2655,9 @@ int start_websocket_relay(int port_override, int strict_port) { break; } + // Drain completed async REQ jobs and emit EVENT/EOSE on service thread. + process_req_async_completions(); + // Check if it's time to post status update time_t current_time = time(NULL); int status_post_hours = get_config_int("kind_1_status_posts_hours", 0); diff --git a/test_results_20260401_091057.log b/test_results_20260401_091057.log deleted file mode 100644 index 1dce4e3..0000000 --- a/test_results_20260401_091057.log +++ /dev/null @@ -1,18 +0,0 @@ -2026-04-01 09:10:57 - ========================================== -2026-04-01 09:10:57 - C-Relay Comprehensive Test Suite Runner -2026-04-01 09:10:57 - ========================================== -2026-04-01 09:10:57 - Relay URL: ws://127.0.0.1:8888 -2026-04-01 09:10:57 - Log file: test_results_20260401_091057.log -2026-04-01 09:10:57 - Report file: test_report_20260401_091057.html -2026-04-01 09:10:57 - -2026-04-01 09:10:57 - Checking relay status at ws://127.0.0.1:8888... -2026-04-01 09:10:57 - \033[0;32m✓ Relay HTTP endpoint is accessible\033[0m -2026-04-01 09:10:57 - -2026-04-01 09:10:57 - Starting comprehensive test execution... -2026-04-01 09:10:57 - -2026-04-01 09:10:57 - \033[0;34m=== SECURITY TEST SUITES ===\033[0m -2026-04-01 09:10:57 - ========================================== -2026-04-01 09:10:57 - Running Test Suite: SQL Injection Tests -2026-04-01 09:10:57 - Description: Comprehensive SQL injection vulnerability testing -2026-04-01 09:10:57 - ========================================== -2026-04-01 09:10:57 - \033[0;31mERROR: Test script sql_injection_tests.sh not found\033[0m diff --git a/test_results_20260401_093238.log b/test_results_20260401_093238.log deleted file mode 100644 index 86fbc0b..0000000 --- a/test_results_20260401_093238.log +++ /dev/null @@ -1,18 +0,0 @@ -2026-04-01 09:32:38 - ========================================== -2026-04-01 09:32:38 - C-Relay Comprehensive Test Suite Runner -2026-04-01 09:32:38 - ========================================== -2026-04-01 09:32:38 - Relay URL: ws://127.0.0.1:8888 -2026-04-01 09:32:38 - Log file: test_results_20260401_093238.log -2026-04-01 09:32:38 - Report file: test_report_20260401_093238.html -2026-04-01 09:32:38 - -2026-04-01 09:32:38 - Checking relay status at ws://127.0.0.1:8888... -2026-04-01 09:32:38 - \033[0;32m✓ Relay HTTP endpoint is accessible\033[0m -2026-04-01 09:32:38 - -2026-04-01 09:32:38 - Starting comprehensive test execution... -2026-04-01 09:32:38 - -2026-04-01 09:32:38 - \033[0;34m=== SECURITY TEST SUITES ===\033[0m -2026-04-01 09:32:38 - ========================================== -2026-04-01 09:32:38 - Running Test Suite: SQL Injection Tests -2026-04-01 09:32:38 - Description: Comprehensive SQL injection vulnerability testing -2026-04-01 09:32:38 - ========================================== -2026-04-01 09:32:38 - \033[0;31mERROR: Test script sql_injection_tests.sh not found\033[0m diff --git a/test_results_20260401_095722.log b/test_results_20260401_095722.log new file mode 100644 index 0000000..ad2780d --- /dev/null +++ b/test_results_20260401_095722.log @@ -0,0 +1,18 @@ +2026-04-01 09:57:22 - ========================================== +2026-04-01 09:57:22 - C-Relay Comprehensive Test Suite Runner +2026-04-01 09:57:22 - ========================================== +2026-04-01 09:57:22 - Relay URL: ws://127.0.0.1:8888 +2026-04-01 09:57:22 - Log file: test_results_20260401_095722.log +2026-04-01 09:57:22 - Report file: test_report_20260401_095722.html +2026-04-01 09:57:22 - +2026-04-01 09:57:22 - Checking relay status at ws://127.0.0.1:8888... +2026-04-01 09:57:22 - \033[0;32m✓ Relay HTTP endpoint is accessible\033[0m +2026-04-01 09:57:22 - +2026-04-01 09:57:22 - Starting comprehensive test execution... +2026-04-01 09:57:22 - +2026-04-01 09:57:22 - \033[0;34m=== SECURITY TEST SUITES ===\033[0m +2026-04-01 09:57:22 - ========================================== +2026-04-01 09:57:22 - Running Test Suite: SQL Injection Tests +2026-04-01 09:57:22 - Description: Comprehensive SQL injection vulnerability testing +2026-04-01 09:57:22 - ========================================== +2026-04-01 09:57:22 - \033[0;31mERROR: Test script sql_injection_tests.sh not found\033[0m diff --git a/tests/nip42_test.log b/tests/nip42_test.log index 8ed825e..2b211d8 100644 --- a/tests/nip42_test.log +++ b/tests/nip42_test.log @@ -1,5 +1,5 @@ === NIP-42 Authentication Test Started === -2026-04-01 05:46:11 - Starting NIP-42 authentication tests +2026-04-01 10:00:44 - Starting NIP-42 authentication tests [INFO] === Starting NIP-42 Authentication Tests === [INFO] Checking dependencies... [WARNING] wscat not found. Some manual WebSocket tests will be skipped @@ -7,7 +7,7 @@ [SUCCESS] Dependencies check complete [INFO] Test 1: Checking NIP-42 support in relay info [SUCCESS] NIP-42 is advertised in supported NIPs -2026-04-01 05:46:11 - Supported NIPs: 1,2,4,9,11,12,13,15,16,20,22,33,40,42,50,70 +2026-04-01 10:00:44 - Supported NIPs: 1,2,4,9,11,12,13,15,16,20,22,33,40,42,50,70 [INFO] Test 2: Testing AUTH challenge generation [WARNING] Could not extract admin private key from relay.log - using manual test approach [INFO] Manual test: Connect to relay and send an event without auth to trigger challenge @@ -15,8 +15,8 @@ [INFO] Generated test keypair: test_pubkey [INFO] Attempting to publish event without authentication... [INFO] Publishing test event to relay... -2026-04-01 05:46:12 - Event publish result: connecting to localhost:8888... ok. -{"kind":1,"id":"334754e42c2bd54bdf733fb2c9a28c1e0f33d2275ca2c70f26062187793b37fe","pubkey":"40d9f28055baeba6a9bcac1942a17370add58a95a46985f66c5c14d485573983","created_at":1775036772,"tags":[],"content":"NIP-42 test event - should require auth","sig":"6e81c7335d34a770c72566250ac363aa5a126383a1efe2effe5b4b7e84bf600c4d9531b26e9be188fe884e9bd32aa384d49739815c7fba4594174277b90cbf7d"} +2026-04-01 10:00:45 - Event publish result: connecting to localhost:8888... ok. +{"kind":1,"id":"28174ed72f9ef5484a6596db6c6be181d2ed10e46d8c22a9f7a267becbc9a47a","pubkey":"b5f9adc3b362c0e81569a46648a3996708d6b47754d36c617683f18e1f1daf6b","created_at":1775052045,"tags":[],"content":"NIP-42 test event - should require auth","sig":"0041347b233a21d6542a263634f52f8ca7e3b5ecf07bc37c86f390f1a7ab0bc2051306b14aa70edff020bd7311b259d03c2c35a035b285f9e19051a924546666"} publishing to ws://localhost:8888... success. [SUCCESS] Relay requested authentication as expected [INFO] Test 4: Testing WebSocket AUTH message handling @@ -26,50 +26,50 @@ publishing to ws://localhost:8888... success. [WARNING] Could not retrieve configuration events [INFO] Test 6: Testing NIP-42 performance and stability [INFO] Testing multiple authentication attempts... -2026-04-01 05:46:13 - Attempt 1: .187311847s - connecting to localhost:8888... ok. -{"kind":1,"id":"470a74d32e1b739e6bfd275c0d03ab1508fb5908c0fa01523e4a3bdce6a95db3","pubkey":"59d792f5cfd57b3a284ce769b5d4b1e8a6eb231c63239cdad7f07d791433e79a","created_at":1775036773,"tags":[],"content":"Performance test event 1","sig":"0c30a125cb67d336413f006a5dfa422d788e177254e496b966854bf45183238d23f6575dba37dec6fa46b6f6b906a25afafcbbcaa7d0c15add89dd7fd1a80c22"} +2026-04-01 10:00:46 - Attempt 1: .195528002s - connecting to localhost:8888... ok. +{"kind":1,"id":"72526c57e50d721bfcdcca23856f2c5d3021c3100f0121538d9d829042e9b8b5","pubkey":"29af457d7eea750c11c836dfbeed3076fa0bf1f1b67a404ee4c25733d9b54238","created_at":1775052046,"tags":[],"content":"Performance test event 1","sig":"1a10db86a39aaf47bda6d0cca5f888691bae32bfd85df8c178dddbf24861503a491bfe4da8d66dfe3e5e0afec7bf8644e7eb69c9f051c601673ae13284f9ab31"} publishing to ws://localhost:8888... success. -2026-04-01 05:46:13 - Attempt 2: .185565503s - connecting to localhost:8888... ok. -{"kind":1,"id":"e4f9db77d30d8e71ef01b16dcb1179d03462aaf368aae30120898f3758a9d0b9","pubkey":"59d792f5cfd57b3a284ce769b5d4b1e8a6eb231c63239cdad7f07d791433e79a","created_at":1775036773,"tags":[],"content":"Performance test event 2","sig":"24a8f38c2cb230d95120e60a246a1a03ac17d4d195d25f2ae0b2f674c3383287eca53cd7fc61bc3cdc58433a3d2c2108558f4f1a98dcfc02a97b4ec8c537cdde"} +2026-04-01 10:00:46 - Attempt 2: .185198180s - connecting to localhost:8888... ok. +{"kind":1,"id":"84eeb907205806bb88fc353bb83f1bf0149f3c93c18695306d8480d36752878b","pubkey":"29af457d7eea750c11c836dfbeed3076fa0bf1f1b67a404ee4c25733d9b54238","created_at":1775052046,"tags":[],"content":"Performance test event 2","sig":"e568dbca3113966f61e69eff05576d296fac42b5ff2ab7341778acc02cf6e9440b59bf651a8a2995d89180a62ec20ffc1143e211cf2960c5abaa358a82992ac6"} publishing to ws://localhost:8888... success. -2026-04-01 05:46:14 - Attempt 3: .185110183s - connecting to localhost:8888... ok. -{"kind":1,"id":"4a85e466756452e90358fe5d98fa41bda93b6bc77cb079bb1616ca8873f4fca1","pubkey":"59d792f5cfd57b3a284ce769b5d4b1e8a6eb231c63239cdad7f07d791433e79a","created_at":1775036774,"tags":[],"content":"Performance test event 3","sig":"72ab716ff3b4786309fd4cef27ae8cd9df1e3c1746b5e49ba45c070cafcfb72a5b7ba045e7d798f23b5d82989526b4d3c818509f0eec639c8ad265be85a4728e"} +2026-04-01 10:00:47 - Attempt 3: .197159540s - connecting to localhost:8888... ok. +{"kind":1,"id":"0874716ba0b2a845d379e703226513f90db6228ce958369f4dce5aff3df64c5a","pubkey":"29af457d7eea750c11c836dfbeed3076fa0bf1f1b67a404ee4c25733d9b54238","created_at":1775052046,"tags":[],"content":"Performance test event 3","sig":"e74e885eb464998bddf655c24c95a18d7299908c63fb82a47f8d14af65992f90e73794a9f289b550bc1dec1c0298112d2b25941be7e26e3f17dd66b4c5dc742e"} publishing to ws://localhost:8888... success. -2026-04-01 05:46:14 - Attempt 4: .185725746s - connecting to localhost:8888... ok. -{"kind":1,"id":"4c05b6359da88f85c0aa4c94a296a465f71bddaa10d849ed0923f972ac0628a9","pubkey":"59d792f5cfd57b3a284ce769b5d4b1e8a6eb231c63239cdad7f07d791433e79a","created_at":1775036774,"tags":[],"content":"Performance test event 4","sig":"3b0c8bd1f3d965a259a0ab228ea4c47defef3ad563ebce860ec65ab0ee341c5305c0988ac88651664d3090ce2ff282a0fe4913231448158c423a74b6e3b2a7a9"} +2026-04-01 10:00:47 - Attempt 4: .202520319s - connecting to localhost:8888... ok. +{"kind":1,"id":"5614d1b795cd028e096720418bb4dbc64870d442c6295b9bb2931564a332099a","pubkey":"29af457d7eea750c11c836dfbeed3076fa0bf1f1b67a404ee4c25733d9b54238","created_at":1775052047,"tags":[],"content":"Performance test event 4","sig":"1c0645a791ab9df573b152b782884bcb5ec2a18b8f4e1c18d7dc2175977b7d1d572674169ea1d2b3e3fa8d228de90f6cc281d56a3ef471ba4f9510e920ed586e"} publishing to ws://localhost:8888... success. -2026-04-01 05:46:15 - Attempt 5: .184467365s - connecting to localhost:8888... ok. -{"kind":1,"id":"0902e868c5e7909f0143fde7738d5e8f4c42ea0ced2c69a404cfeb6cd686ceaa","pubkey":"59d792f5cfd57b3a284ce769b5d4b1e8a6eb231c63239cdad7f07d791433e79a","created_at":1775036774,"tags":[],"content":"Performance test event 5","sig":"a0754e774e5bc3514dc4cf04ec51cab077413251cfce480e80badccc7660bfe8d1eac87d43c6e332d936cc171d24e8318e1ef5df7425a8218e01af661f291c47"} +2026-04-01 10:00:47 - Attempt 5: .222530325s - connecting to localhost:8888... ok. +{"kind":1,"id":"9139f1eb39c31c43cb9f02e60a0f75a0f99f6c7d9693a69a225c8b3e530f774a","pubkey":"29af457d7eea750c11c836dfbeed3076fa0bf1f1b67a404ee4c25733d9b54238","created_at":1775052047,"tags":[],"content":"Performance test event 5","sig":"bd245a7629c33619c0ed426f631e0c2413adb4488c281bea09886ceb6407b40e247d42f82e374e2f53ee5281f0d8361661a4c613c6414c59d09e5bb23eeeb02d"} 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... -2026-04-01 05:46:15 - Kind 1 event result: connecting to localhost:8888... ok. -{"kind":1,"id":"a1d9545c4ac16c2ab1fae4d5d77cabd368d765533b6f6d1f4dbb3c7859696a5e","pubkey":"06ff6b7ca3c72cc7cebb9fe70ea76b6f79b6226c4dc5adc4ebcc59220c23c9bf","created_at":1775036775,"tags":[],"content":"Regular note - should not require auth","sig":"520796b21690bf03b20da28f30ffc84c9c198d0a908c07540f232f789c7a0327861e1f580d499be15b244bdf66c5c4b1cdf621bfdc5f60b1313d7f40036ffab8"} +2026-04-01 10:00:48 - Kind 1 event result: connecting to localhost:8888... ok. +{"kind":1,"id":"def386c66576a91f144a7321e9520a3ed46f9fecc56d9e79d32a162f249cace9","pubkey":"7c9b00749c966e50d18d4b807b44d4612bf9a8f67925fac3f00d21918b6e7f9c","created_at":1775052048,"tags":[],"content":"Regular note - should not require auth","sig":"7ee8fbf45e96d040df1a688e9bf028faaa49b81b26eeae6452b3a32cd9fd941c27b41734f14268d3deef14dbb19e514d29789211fffb27bfa9b73d20cddcc83e"} 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... -2026-04-01 05:46:25 - Kind 4 event result: connecting to localhost:8888... ok. -{"kind":4,"id":"c047f5a9fb6ff46828d0bb862e4bcc32120e50fc7804d651485e2cdbddaf993a","pubkey":"06ff6b7ca3c72cc7cebb9fe70ea76b6f79b6226c4dc5adc4ebcc59220c23c9bf","created_at":1775036775,"tags":[["p,test_pubkey"]],"content":"This is a direct message - should require auth","sig":"a7a027c302faf75e269d866a1b0137f2fbc02643d76435dc68707d2ba6560af95df709787f4a1a1bc7c1b9951615ad081a97cd7f694da94d5606d17100f0b0dc"} +2026-04-01 10:00:58 - Kind 4 event result: connecting to localhost:8888... ok. +{"kind":4,"id":"a2f2d176a835d17a0c9229c0ce3b1ba61a5eb81308e4a3b3446634f468721238","pubkey":"7c9b00749c966e50d18d4b807b44d4612bf9a8f67925fac3f00d21918b6e7f9c","created_at":1775052048,"tags":[["p,test_pubkey"]],"content":"This is a direct message - should require auth","sig":"441f9caba6ebcec6b6c55d13ffd9571f3fe441a54ddd0692f2868429a5087378d791c97289b8b990560dcf40f5c66e86ba6b41f1de78220a5f389c1f119bb887"} 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... -2026-04-01 05:46:36 - Kind 14 event result: connecting to localhost:8888... ok. -{"kind":14,"id":"73e50afdaefdbb01ed021f8c888a87b00aa59e4d68c124101353fabcdfa59ae1","pubkey":"06ff6b7ca3c72cc7cebb9fe70ea76b6f79b6226c4dc5adc4ebcc59220c23c9bf","created_at":1775036786,"tags":[["p,test_pubkey"]],"content":"Chat message - should require auth","sig":"ec2c713e2add876856e6cb0d1d9790c43525c5139a0fe06fbee71898f1042173dc3af9187e6fbccfef7406c1af2f218631a1cdcb7ad73e34553cdf3aecc4f209"} +2026-04-01 10:01:09 - Kind 14 event result: connecting to localhost:8888... ok. +{"kind":14,"id":"e234a6faa63f95cd24f8596fbfac573730533bb0abcde47b4d341ff0acace657","pubkey":"7c9b00749c966e50d18d4b807b44d4612bf9a8f67925fac3f00d21918b6e7f9c","created_at":1775052059,"tags":[["p,test_pubkey"]],"content":"Chat message - should require auth","sig":"fd6ba0ddaa7e2379aa49c5cda6599d0a38264799b228a8e907b948bef645080603f7f910c587f6cfb1ef1bac946c47dc681fdab6abd248376cd552bcd535ef9e"} publishing to ws://localhost:8888... [SUCCESS] Kind 14 event requested authentication (correct behavior for DMs) [INFO] Testing other event kinds - should work without authentication... -2026-04-01 05:46:36 - Kind 0 event result: connecting to localhost:8888... ok. -{"kind":0,"id":"51fe1630721cfa3472ba7c3af911a3164c788a48c6a678179c62be48ac3d1f1c","pubkey":"06ff6b7ca3c72cc7cebb9fe70ea76b6f79b6226c4dc5adc4ebcc59220c23c9bf","created_at":1775036796,"tags":[],"content":"Test event kind 0 - should not require auth","sig":"d72d827742fdc86daf1389c8dad40efc56c22b9744ad23547ea0b0a37598e5c18ccdac20b9ef2e6adf46d7683bff19f2cbc11fb00a252903efc1076c8f613b38"} +2026-04-01 10:01:09 - Kind 0 event result: connecting to localhost:8888... ok. +{"kind":0,"id":"d87b69e8a2c8f6d98229b55cde3f0fe1b70c6cafd080e4462209849338eafc76","pubkey":"7c9b00749c966e50d18d4b807b44d4612bf9a8f67925fac3f00d21918b6e7f9c","created_at":1775052069,"tags":[],"content":"Test event kind 0 - should not require auth","sig":"e3222bd4882c881d039e6790a6a8d9f8e20a71d0f9d5089e98604acaa4feff7c1b1b2d9b4171d640da3bba955408a9391a0fd52334cc4a4165fa932e5163f9dd"} publishing to ws://localhost:8888... success. [SUCCESS] Kind 0 event accepted without authentication (correct) -2026-04-01 05:46:36 - Kind 3 event result: connecting to localhost:8888... ok. -{"kind":3,"id":"bf4155eb8b54bdd73a0b64365666caf886433634693cc3c4ab1d586187fc6bae","pubkey":"06ff6b7ca3c72cc7cebb9fe70ea76b6f79b6226c4dc5adc4ebcc59220c23c9bf","created_at":1775036796,"tags":[],"content":"Test event kind 3 - should not require auth","sig":"d27f654ccc9f95e76aacafcce7d42528545fa90b77d422a17301de6931883d8c00a0a84af56ab5d37a0f425140dbfc227458557f9050597beec42cacd0a15864"} +2026-04-01 10:01:09 - Kind 3 event result: connecting to localhost:8888... ok. +{"kind":3,"id":"ac0d45d7db2818bb5e674d4760711349928cf3d5dc8f1fc3717d9e7274e26fb0","pubkey":"7c9b00749c966e50d18d4b807b44d4612bf9a8f67925fac3f00d21918b6e7f9c","created_at":1775052069,"tags":[],"content":"Test event kind 3 - should not require auth","sig":"b65b317c81bc733ee2ff5921430b34d722e8d177147c5d1cddf14036573223582a3b7232810fc13169eb7a61ab3b99cd37ce13991673fce142d1ea10c702305e"} publishing to ws://localhost:8888... success. [SUCCESS] Kind 3 event accepted without authentication (correct) -2026-04-01 05:46:37 - Kind 7 event result: connecting to localhost:8888... ok. -{"kind":7,"id":"658e0caf1c4b52062363849d55312070005124e8a10202df285078222e523132","pubkey":"06ff6b7ca3c72cc7cebb9fe70ea76b6f79b6226c4dc5adc4ebcc59220c23c9bf","created_at":1775036797,"tags":[],"content":"Test event kind 7 - should not require auth","sig":"50361b3aa6b85747befe20e50e317d8f905128930cec0263494434ec9029089d880ff23cdf071c5b63b8417eff216658f1229c0f0dc5174a9a16a6d5fe38fd4d"} +2026-04-01 10:01:10 - Kind 7 event result: connecting to localhost:8888... ok. +{"kind":7,"id":"ad7e77948a4742abfca2d63c5bb748f3db3940c85841f521b3b7d634bf09955a","pubkey":"7c9b00749c966e50d18d4b807b44d4612bf9a8f67925fac3f00d21918b6e7f9c","created_at":1775052070,"tags":[],"content":"Test event kind 7 - should not require auth","sig":"8a5e5a03956cec04d56934314a0876fccbd221546432aaccdbc249b5f9cedc221219abf6e2856b98675de205afaa0e91ea642933ec6724576a40db43d17aa758"} publishing to ws://localhost:8888... success. [SUCCESS] Kind 7 event accepted without authentication (correct) [INFO] Kind-specific authentication test completed diff --git a/tests/test_results_20260401_095729.log b/tests/test_results_20260401_095729.log new file mode 100644 index 0000000..b562c09 --- /dev/null +++ b/tests/test_results_20260401_095729.log @@ -0,0 +1,733 @@ +2026-04-01 09:57:29 - ========================================== +2026-04-01 09:57:30 - C-Relay Comprehensive Test Suite Runner +2026-04-01 09:57:30 - ========================================== +2026-04-01 09:57:30 - Relay URL: ws://127.0.0.1:8888 +2026-04-01 09:57:30 - Log file: test_results_20260401_095729.log +2026-04-01 09:57:30 - Report file: test_report_20260401_095729.html +2026-04-01 09:57:30 - +2026-04-01 09:57:30 - Checking relay status at ws://127.0.0.1:8888... +2026-04-01 09:57:30 - \033[0;32m✓ Relay HTTP endpoint is accessible\033[0m +2026-04-01 09:57:30 - +2026-04-01 09:57:30 - Starting comprehensive test execution... +2026-04-01 09:57:30 - +2026-04-01 09:57:30 - \033[0;34m=== SECURITY TEST SUITES ===\033[0m +2026-04-01 09:57:30 - ========================================== +2026-04-01 09:57:30 - Running Test Suite: SQL Injection Tests +2026-04-01 09:57:30 - Description: Comprehensive SQL injection vulnerability testing +2026-04-01 09:57:30 - ========================================== +========================================== +C-Relay SQL Injection Test Suite +========================================== +Testing against relay at ws://127.0.0.1:8888 + +=== Basic Connectivity Test === +Testing Basic connectivity... PASSED - Valid query works + +=== Authors Filter SQL Injection Tests === +Testing Authors filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error) +Testing Authors filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (rejected with error) +Testing Authors filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error) +Testing Authors filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error) +Testing Authors filter with payload: admin'--... PASSED - SQL injection blocked (rejected with error) +Testing Authors filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error) +Testing Authors filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error) +Testing Authors filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error) +Testing Authors filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error) +Testing Authors filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error) +Testing Authors filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error) +Testing Authors filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error) +Testing Authors filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error) +Testing Authors filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error) +Testing Authors filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error) +Testing Authors filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error) +Testing Authors filter with payload: /*... PASSED - SQL injection blocked (rejected with error) +Testing Authors filter with payload: */... PASSED - SQL injection blocked (rejected with error) +Testing Authors filter with payload: /**/... PASSED - SQL injection blocked (rejected with error) +Testing Authors filter with payload: --... PASSED - SQL injection blocked (rejected with error) +Testing Authors filter with payload: #... PASSED - SQL injection blocked (rejected with error) +Testing Authors filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (rejected with error) +Testing Authors filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (rejected with error) +Testing Authors filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error) +Testing Authors filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error) +Testing Authors filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error) +Testing Authors filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error) +Testing Authors filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error) +Testing Authors filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error) +Testing Authors filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error) + +=== IDs Filter SQL Injection Tests === +Testing IDs filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error) +Testing IDs filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (rejected with error) +Testing IDs filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error) +Testing IDs filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error) +Testing IDs filter with payload: admin'--... PASSED - SQL injection blocked (rejected with error) +Testing IDs filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error) +Testing IDs filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error) +Testing IDs filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error) +Testing IDs filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error) +Testing IDs filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error) +Testing IDs filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error) +Testing IDs filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error) +Testing IDs filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error) +Testing IDs filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error) +Testing IDs filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error) +Testing IDs filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error) +Testing IDs filter with payload: /*... PASSED - SQL injection blocked (rejected with error) +Testing IDs filter with payload: */... PASSED - SQL injection blocked (rejected with error) +Testing IDs filter with payload: /**/... PASSED - SQL injection blocked (rejected with error) +Testing IDs filter with payload: --... PASSED - SQL injection blocked (rejected with error) +Testing IDs filter with payload: #... PASSED - SQL injection blocked (rejected with error) +Testing IDs filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (rejected with error) +Testing IDs filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (rejected with error) +Testing IDs filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error) +Testing IDs filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error) +Testing IDs filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error) +Testing IDs filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error) +Testing IDs filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error) +Testing IDs filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error) +Testing IDs filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error) + +=== Kinds Filter SQL Injection Tests === +Testing Kinds filter with string injection... PASSED - SQL injection blocked (rejected with error) +Testing Kinds filter with negative value... PASSED - SQL injection blocked (rejected with error) +Testing Kinds filter with very large value... PASSED - SQL injection blocked (rejected with error) + +=== Search Filter SQL Injection Tests === +Testing Search filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error) +Testing Search filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized) +Testing Search filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error) +Testing Search filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error) +Testing Search filter with payload: admin'--... PASSED - SQL injection blocked (rejected with error) +Testing Search filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error) +Testing Search filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error) +Testing Search filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error) +Testing Search filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error) +Testing Search filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error) +Testing Search filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error) +Testing Search filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error) +Testing Search filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error) +Testing Search filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error) +Testing Search filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error) +Testing Search filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error) +Testing Search filter with payload: /*... PASSED - SQL injection blocked (rejected with error) +Testing Search filter with payload: */... PASSED - SQL injection blocked (rejected with error) +Testing Search filter with payload: /**/... PASSED - SQL injection blocked (rejected with error) +Testing Search filter with payload: --... PASSED - SQL injection blocked (rejected with error) +Testing Search filter with payload: #... PASSED - SQL injection blocked (query sanitized) +Testing Search filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized) +Testing Search filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized) +Testing Search filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error) +Testing Search filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error) +Testing Search filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error) +Testing Search filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error) +Testing Search filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error) +Testing Search filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error) +Testing Search filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error) + +=== Tag Filter SQL Injection Tests === +Testing #e tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized) +Testing #e tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized) +Testing #e tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized) +Testing #e tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized) +Testing #e tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized) +Testing #e tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized) +Testing #e tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized) +Testing #e tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized) +Testing #e tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized) +Testing #e tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized) +Testing #e tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized) +Testing #e tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized) +Testing #e tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized) +Testing #e tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized) +Testing #e tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized) +Testing #e tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized) +Testing #e tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized) +Testing #e tag filter with payload: */... PASSED - SQL injection blocked (query sanitized) +Testing #e tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized) +Testing #e tag filter with payload: --... PASSED - SQL injection blocked (query sanitized) +Testing #e tag filter with payload: #... PASSED - SQL injection blocked (query sanitized) +Testing #e tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized) +Testing #e tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized) +Testing #e tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized) +Testing #e tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized) +Testing #e tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized) +Testing #e tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized) +Testing #e tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized) +Testing #e tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized) +Testing #e tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized) +Testing #p tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized) +Testing #p tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized) +Testing #p tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized) +Testing #p tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized) +Testing #p tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized) +Testing #p tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized) +Testing #p tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized) +Testing #p tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized) +Testing #p tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized) +Testing #p tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized) +Testing #p tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized) +Testing #p tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized) +Testing #p tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized) +Testing #p tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized) +Testing #p tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized) +Testing #p tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized) +Testing #p tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized) +Testing #p tag filter with payload: */... PASSED - SQL injection blocked (query sanitized) +Testing #p tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized) +Testing #p tag filter with payload: --... PASSED - SQL injection blocked (query sanitized) +Testing #p tag filter with payload: #... PASSED - SQL injection blocked (query sanitized) +Testing #p tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized) +Testing #p tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized) +Testing #p tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized) +Testing #p tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized) +Testing #p tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized) +Testing #p tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized) +Testing #p tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized) +Testing #p tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized) +Testing #p tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized) +Testing #t tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized) +Testing #t tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized) +Testing #t tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized) +Testing #t tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized) +Testing #t tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized) +Testing #t tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized) +Testing #t tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized) +Testing #t tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized) +Testing #t tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized) +Testing #t tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized) +Testing #t tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized) +Testing #t tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized) +Testing #t tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized) +Testing #t tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized) +Testing #t tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized) +Testing #t tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized) +Testing #t tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized) +Testing #t tag filter with payload: */... PASSED - SQL injection blocked (query sanitized) +Testing #t tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized) +Testing #t tag filter with payload: --... PASSED - SQL injection blocked (query sanitized) +Testing #t tag filter with payload: #... PASSED - SQL injection blocked (query sanitized) +Testing #t tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized) +Testing #t tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized) +Testing #t tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized) +Testing #t tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized) +Testing #t tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized) +Testing #t tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized) +Testing #t tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized) +Testing #t tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized) +Testing #t tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized) +Testing #r tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized) +Testing #r tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized) +Testing #r tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized) +Testing #r tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized) +Testing #r tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized) +Testing #r tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized) +Testing #r tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized) +Testing #r tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized) +Testing #r tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized) +Testing #r tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized) +Testing #r tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized) +Testing #r tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized) +Testing #r tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized) +Testing #r tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized) +Testing #r tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized) +Testing #r tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized) +Testing #r tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized) +Testing #r tag filter with payload: */... PASSED - SQL injection blocked (query sanitized) +Testing #r tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized) +Testing #r tag filter with payload: --... PASSED - SQL injection blocked (query sanitized) +Testing #r tag filter with payload: #... PASSED - SQL injection blocked (query sanitized) +Testing #r tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized) +Testing #r tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized) +Testing #r tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized) +Testing #r tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized) +Testing #r tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized) +Testing #r tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized) +Testing #r tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized) +Testing #r tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized) +Testing #r tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized) +Testing #d tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized) +Testing #d tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized) +Testing #d tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized) +Testing #d tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized) +Testing #d tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized) +Testing #d tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized) +Testing #d tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized) +Testing #d tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized) +Testing #d tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized) +Testing #d tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized) +Testing #d tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized) +Testing #d tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized) +Testing #d tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized) +Testing #d tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized) +Testing #d tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized) +Testing #d tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized) +Testing #d tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized) +Testing #d tag filter with payload: */... PASSED - SQL injection blocked (query sanitized) +Testing #d tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized) +Testing #d tag filter with payload: --... PASSED - SQL injection blocked (query sanitized) +Testing #d tag filter with payload: #... PASSED - SQL injection blocked (query sanitized) +Testing #d tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized) +Testing #d tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized) +Testing #d tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized) +Testing #d tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized) +Testing #d tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized) +Testing #d tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized) +Testing #d tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized) +Testing #d tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized) +Testing #d tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized) + +=== Timestamp Filter SQL Injection Tests === +Testing Since parameter injection... PASSED - SQL injection blocked (rejected with error) +Testing Until parameter injection... PASSED - SQL injection blocked (rejected with error) + +=== Limit Parameter SQL Injection Tests === +Testing Limit parameter injection... PASSED - SQL injection blocked (rejected with error) +Testing Limit with UNION... PASSED - SQL injection blocked (rejected with error) + +=== Complex Multi-Filter SQL Injection Tests === +Testing Multi-filter with authors injection... PASSED - SQL injection blocked (rejected with error) +Testing Multi-filter with search injection... PASSED - SQL injection blocked (rejected with error) +Testing Multi-filter with tag injection... PASSED - SQL injection blocked (query sanitized) + +=== COUNT Message SQL Injection Tests === +Testing COUNT with authors payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with search payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with authors payload: 1' OR '1'='1... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with search payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized) +Testing COUNT with authors payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with search payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with authors payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with search payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with authors payload: admin'--... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with search payload: admin'--... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with authors payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with search payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with authors payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with search payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with authors payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with search payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with authors payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with search payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with authors payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with search payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with authors payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with search payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with authors payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with search payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with authors payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with search payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with authors payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with search payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with authors payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with search payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with authors payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with search payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with authors payload: /*... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with search payload: /*... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with authors payload: */... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with search payload: */... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with authors payload: /**/... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with search payload: /**/... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with authors payload: --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with search payload: --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with authors payload: #... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with search payload: #... PASSED - SQL injection blocked (query sanitized) +Testing COUNT with authors payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with search payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized) +Testing COUNT with authors payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with search payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized) +Testing COUNT with authors payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with search payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with authors payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with search payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with authors payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with search payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with authors payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with search payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with authors payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with search payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with authors payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with search payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with authors payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error) +Testing COUNT with search payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error) + +=== Edge Case SQL Injection Tests === +Testing Empty string injection... PASSED - SQL injection blocked (rejected with error) +Testing Null byte injection... PASSED - SQL injection blocked (silently rejected) +Testing Unicode injection... PASSED - SQL injection blocked (rejected with error) +Testing Very long injection payload... PASSED - SQL injection blocked (rejected with error) + +=== Subscription ID SQL Injection Tests === +Testing Subscription ID injection... PASSED - SQL injection blocked (rejected with error) +Testing Subscription ID with quotes... PASSED - SQL injection blocked (silently rejected) + +=== CLOSE Message SQL Injection Tests === +Testing CLOSE with injection... PASSED - SQL injection blocked (rejected with error) + +=== Test Results === +Total tests: 318 +Passed: 318 +Failed: 0 +✓ All SQL injection tests passed! +The relay appears to be protected against SQL injection attacks. +2026-04-01 09:57:36 - \033[0;32m✓ SQL Injection Tests PASSED\033[0m (Duration: 6s) +2026-04-01 09:57:36 - ========================================== +2026-04-01 09:57:36 - Running Test Suite: Filter Validation Tests +2026-04-01 09:57:36 - Description: Input validation for REQ and COUNT messages +2026-04-01 09:57:36 - ========================================== +=== C-Relay Filter Validation Tests === +Testing against relay at ws://127.0.0.1:8888 + +Testing Valid REQ message... PASSED +Testing Valid COUNT message... PASSED + +=== Testing Filter Array Validation === +Testing Non-object filter... PASSED +Testing Too many filters... PASSED + +=== Testing Authors Validation === +Testing Invalid author type... PASSED +Testing Invalid author hex... PASSED +Testing Too many authors... PASSED + +=== Testing IDs Validation === +Testing Invalid ID type... PASSED +Testing Invalid ID hex... PASSED +Testing Too many IDs... PASSED + +=== Testing Kinds Validation === +Testing Invalid kind type... PASSED +Testing Negative kind... PASSED +Testing Too large kind... PASSED +Testing Too many kinds... PASSED + +=== Testing Timestamp Validation === +Testing Invalid since type... PASSED +Testing Negative since... PASSED +Testing Invalid until type... PASSED +Testing Negative until... PASSED + +=== Testing Limit Validation === +Testing Invalid limit type... PASSED +Testing Negative limit... PASSED +Testing Too large limit... PASSED + +=== Testing Search Validation === +Testing Invalid search type... PASSED +Testing Search too long... PASSED +Testing Search SQL injection... PASSED + +=== Testing Tag Filter Validation === +Testing Invalid tag filter type... PASSED +Testing Too many tag values... PASSED +Testing Tag value too long... PASSED + +=== Testing Rate Limiting === +Testing rate limiting with malformed requests... UNCERTAIN - Rate limiting may not have triggered (this could be normal) + +=== Test Results === +Total tests: 28 +Passed: 28 +Failed: 0 +All tests passed! +2026-04-01 09:57:39 - \033[0;32m✓ Filter Validation Tests PASSED\033[0m (Duration: 3s) +2026-04-01 09:57:39 - ========================================== +2026-04-01 09:57:39 - Running Test Suite: Subscription Validation Tests +2026-04-01 09:57:39 - Description: Subscription ID and message validation +2026-04-01 09:57:39 - ========================================== +Testing subscription ID validation fixes... +Testing malformed subscription IDs... +Empty ID test: Connection failed (expected) +Long ID test: Connection failed (expected) +Invalid chars test: Connection failed (expected) +NULL ID test: Connection failed (expected) +Valid ID test: Failed +Testing CLOSE message validation... +CLOSE empty ID test: Connection failed (expected) +CLOSE valid ID test: Failed +Subscription validation tests completed. +2026-04-01 09:57:39 - \033[0;32m✓ Subscription Validation Tests PASSED\033[0m (Duration: 0s) +2026-04-01 09:57:39 - ========================================== +2026-04-01 09:57:39 - Running Test Suite: Memory Corruption Tests +2026-04-01 09:57:39 - Description: Buffer overflow and memory safety testing +2026-04-01 09:57:39 - ========================================== +========================================== +C-Relay Memory Corruption Test Suite +========================================== +Testing against relay at ws://127.0.0.1:8888 +Note: These tests may cause the relay to crash if vulnerabilities exist + +=== Basic Connectivity Test === +Testing Basic connectivity... PASSED - No memory corruption detected + +=== Subscription ID Memory Corruption Tests === +Testing Empty subscription ID... UNCERTAIN - Expected error but got normal response +Testing Very long subscription ID (1KB)... UNCERTAIN - Expected error but got normal response +Testing Very long subscription ID (10KB)... UNCERTAIN - Expected error but got normal response +Testing Subscription ID with null bytes... UNCERTAIN - Expected error but got normal response +Testing Subscription ID with special chars... UNCERTAIN - Expected error but got normal response +Testing Unicode subscription ID... UNCERTAIN - Expected error but got normal response +Testing Subscription ID with path traversal... UNCERTAIN - Expected error but got normal response + +=== Filter Array Memory Corruption Tests === +Testing Too many filters (50)... UNCERTAIN - Expected error but got normal response + +=== Concurrent Access Memory Tests === +Testing Concurrent subscription creation... ["EVENT","concurrent_1775051860259483357",{"pubkey":"4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa","created_at":1775051826,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"791ebcdcbe58fd5153b9a1f8f39907b0fcc4d86fc189e35fc9b6365031339b53","sig":"4164e20b9169a57c8086a0430b81af9bcabf69082c1d0d9d869673f2f5b0a350fb0ce882f2a19f08b613a0872edb69a0f8d1ccba0675bacff16c19b7282080e6"}] +["EVENT","concurrent_1775051860259483357",{"pubkey":"4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa","created_at":1775051826,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"791ebcdcbe58fd5153b9a1f8f39907b0fcc4d86fc189e35fc9b6365031339b53","sig":"4164e20b9169a57c8086a0430b81af9bcabf69082c1d0d9d869673f2f5b0a350fb0ce882f2a19f08b613a0872edb69a0f8d1ccba0675bacff16c19b7282080e6"}] +["EVENT","concurrent_1775051860259483357",{"pubkey":"4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa","created_at":1775051826,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"791ebcdcbe58fd5153b9a1f8f39907b0fcc4d86fc189e35fc9b6365031339b53","sig":"4164e20b9169a57c8086a0430b81af9bcabf69082c1d0d9d869673f2f5b0a350fb0ce882f2a19f08b613a0872edb69a0f8d1ccba0675bacff16c19b7282080e6"}] +["EVENT","concurrent_1775051860259483357",{"pubkey":"4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa","created_at":1775051826,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"791ebcdcbe58fd5153b9a1f8f39907b0fcc4d86fc189e35fc9b6365031339b53","sig":"4164e20b9169a57c8086a0430b81af9bcabf69082c1d0d9d869673f2f5b0a350fb0ce882f2a19f08b613a0872edb69a0f8d1ccba0675bacff16c19b7282080e6"}] +["EVENT","concurrent_1775051860259483357",{"pubkey":"4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa","created_at":1775051826,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"791ebcdcbe58fd5153b9a1f8f39907b0fcc4d86fc189e35fc9b6365031339b53","sig":"4164e20b9169a57c8086a0430b81af9bcabf69082c1d0d9d869673f2f5b0a350fb0ce882f2a19f08b613a0872edb69a0f8d1ccba0675bacff16c19b7282080e6"}] +["EVENT","concurrent_1775051860259483357",{"pubkey":"4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa","created_at":1775051826,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"791ebcdcbe58fd5153b9a1f8f39907b0fcc4d86fc189e35fc9b6365031339b53","sig":"4164e20b9169a57c8086a0430b81af9bcabf69082c1d0d9d869673f2f5b0a350fb0ce882f2a19f08b613a0872edb69a0f8d1ccba0675bacff16c19b7282080e6"}] +["EVENT","concurrent_1775051860259483357",{"pubkey":"4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa","created_at":1775051826,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"791ebcdcbe58fd5153b9a1f8f39907b0fcc4d86fc189e35fc9b6365031339b53","sig":"4164e20b9169a57c8086a0430b81af9bcabf69082c1d0d9d869673f2f5b0a350fb0ce882f2a19f08b613a0872edb69a0f8d1ccba0675bacff16c19b7282080e6"}] +["EVENT","concurrent_1775051860259483357",{"pubkey":"4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa","created_at":1775051826,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"791ebcdcbe58fd5153b9a1f8f39907b0fcc4d86fc189e35fc9b6365031339b53","sig":"4164e20b9169a57c8086a0430b81af9bcabf69082c1d0d9d869673f2f5b0a350fb0ce882f2a19f08b613a0872edb69a0f8d1ccba0675bacff16c19b7282080e6"}] +["EVENT","concurrent_1775051860259483357",{"pubkey":"4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa","created_at":1775051826,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"791ebcdcbe58fd5153b9a1f8f39907b0fcc4d86fc189e35fc9b6365031339b53","sig":"4164e20b9169a57c8086a0430b81af9bcabf69082c1d0d9d869673f2f5b0a350fb0ce882f2a19f08b613a0872edb69a0f8d1ccba0675bacff16c19b7282080e6"}] +["EVENT","concurrent_1775051860259483357",{"pubkey":"4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa","created_at":1775051826,"kind":1,"tags":[],"content":"\nRelay Statistics\n━━━━━━━━━━━━━━━━━━━━\nDatabase Size 0.00 MB (4096 bytes)\nTotal Events 0\nActive Subscriptions 0\nOldest Event -\nNewest Event -\n\nEvent_Kind_Distribution:\n\nTime-based Statistics:\n0\tEvents in the last day\n0\tEvents in the last week\n0\tEvents in the last month\n\n\n","id":"791ebcdcbe58fd5153b9a1f8f39907b0fcc4d86fc189e35fc9b6365031339b53","sig":"4164e20b9169a57c8086a0430b81af9bcabf69082c1d0d9d869673f2f5b0a350fb0ce882f2a19f08b613a0872edb69a0f8d1ccba0675bacff16c19b7282080e6"}] +PASSED - Concurrent access handled safely +Testing Concurrent CLOSE operations... + + + + + + + + + +PASSED - Concurrent access handled safely + +=== Malformed JSON Memory Tests === +Testing Unclosed JSON object... UNCERTAIN - Expected error but got normal response +Testing Mismatched brackets... UNCERTAIN - Expected error but got normal response +Testing Extra closing brackets... UNCERTAIN - Expected error but got normal response +Testing Null bytes in JSON... UNCERTAIN - Expected error but got normal response + +=== Large Message Memory Tests === +Testing Very large filter array... UNCERTAIN - Expected error but got normal response +Testing Very long search term... UNCERTAIN - Expected error but got normal response + +=== Test Results === +Total tests: 17 +Passed: 17 +Failed: 0 +✓ All memory corruption tests passed! +The relay appears to handle memory safely. +2026-04-01 09:57:40 - \033[0;32m✓ Memory Corruption Tests PASSED\033[0m (Duration: 1s) +2026-04-01 09:57:40 - ========================================== +2026-04-01 09:57:40 - Running Test Suite: Input Validation Tests +2026-04-01 09:57:40 - Description: Comprehensive input boundary testing +2026-04-01 09:57:40 - ========================================== +========================================== +C-Relay Input Validation Test Suite +========================================== +Testing against relay at ws://127.0.0.1:8888 + +=== Basic Connectivity Test === +Testing Basic connectivity... PASSED - Input accepted correctly + +=== Message Type Validation === +Testing Invalid message type - string... PASSED - Invalid input properly rejected +Testing Invalid message type - number... PASSED - Invalid input properly rejected +Testing Invalid message type - null... PASSED - Invalid input properly rejected +Testing Invalid message type - object... PASSED - Invalid input properly rejected +Testing Empty message type... PASSED - Invalid input properly rejected +Testing Very long message type... PASSED - Invalid input properly rejected + +=== Message Structure Validation === +Testing Too few arguments... PASSED - Invalid input properly rejected +Testing Too many arguments... PASSED - Invalid input properly rejected +Testing Non-array message... PASSED - Invalid input properly rejected +Testing Empty array... PASSED - Invalid input properly rejected +Testing Nested arrays incorrectly... PASSED - Invalid input properly rejected + +=== Subscription ID Boundary Tests === +Testing Valid subscription ID... PASSED - Input accepted correctly +Testing Empty subscription ID... PASSED - Invalid input properly rejected +Testing Subscription ID with spaces... PASSED - Invalid input properly rejected +Testing Subscription ID with newlines... PASSED - Invalid input properly rejected +Testing Subscription ID with tabs... PASSED - Invalid input properly rejected +Testing Subscription ID with control chars... PASSED - Invalid input properly rejected +Testing Unicode subscription ID... PASSED - Invalid input properly rejected +Testing Very long subscription ID... PASSED - Invalid input properly rejected + +=== Filter Object Validation === +Testing Valid empty filter... PASSED - Input accepted correctly +Testing Non-object filter... PASSED - Invalid input properly rejected +Testing Null filter... PASSED - Invalid input properly rejected +Testing Array filter... PASSED - Invalid input properly rejected +Testing Filter with invalid keys... PASSED - Input accepted correctly + +=== Authors Field Validation === +Testing Valid authors array... PASSED - Input accepted correctly +Testing Empty authors array... PASSED - Input accepted correctly +Testing Non-array authors... PASSED - Invalid input properly rejected +Testing Invalid hex in authors... PASSED - Invalid input properly rejected +Testing Short pubkey in authors... PASSED - Invalid input properly rejected + +=== IDs Field Validation === +Testing Valid ids array... PASSED - Input accepted correctly +Testing Empty ids array... PASSED - Input accepted correctly +Testing Non-array ids... PASSED - Invalid input properly rejected + +=== Kinds Field Validation === +Testing Valid kinds array... PASSED - Input accepted correctly +Testing Empty kinds array... PASSED - Input accepted correctly +Testing Non-array kinds... PASSED - Invalid input properly rejected +Testing String in kinds... PASSED - Invalid input properly rejected + +=== Timestamp Field Validation === +Testing Valid since timestamp... PASSED - Input accepted correctly +Testing Valid until timestamp... PASSED - Input accepted correctly +Testing String since timestamp... PASSED - Invalid input properly rejected +Testing Negative timestamp... PASSED - Invalid input properly rejected + +=== Limit Field Validation === +Testing Valid limit... PASSED - Input accepted correctly +Testing Zero limit... PASSED - Input accepted correctly +Testing String limit... PASSED - Invalid input properly rejected +Testing Negative limit... PASSED - Invalid input properly rejected + +=== Multiple Filters === +Testing Two valid filters... PASSED - Input accepted correctly +Testing Many filters... PASSED - Input accepted correctly + +=== Test Results === +Total tests: 47 +Passed: 47 +Failed: 0 +✓ All input validation tests passed! +The relay properly validates input. +2026-04-01 09:57:41 - \033[0;32m✓ Input Validation Tests PASSED\033[0m (Duration: 1s) +2026-04-01 09:57:41 - +2026-04-01 09:57:41 - \033[0;34m=== PERFORMANCE TEST SUITES ===\033[0m +2026-04-01 09:57:41 - ========================================== +2026-04-01 09:57:41 - Running Test Suite: Subscription Limit Tests +2026-04-01 09:57:41 - Description: Subscription limit enforcement testing +2026-04-01 09:57:41 - ========================================== +=== Subscription Limit Test === +[INFO] Testing relay at: ws://127.0.0.1:8888 +[INFO] Note: This test assumes default subscription limits (max 25 per client) + +=== Test 1: Basic Connectivity === +[INFO] Testing basic WebSocket connection... +[PASS] Basic connectivity works + +=== Test 2: Subscription Limit Enforcement === +[INFO] Testing subscription limits by creating multiple subscriptions... +[INFO] Creating multiple subscriptions within a single connection... +[INFO] Hit subscription limit at subscription 2 +[PASS] Subscription limit enforcement working (limit hit after 1 subscriptions) + +=== Test Complete === +2026-04-01 09:57:42 - \033[0;32m✓ Subscription Limit Tests PASSED\033[0m (Duration: 1s) +2026-04-01 09:57:42 - ========================================== +2026-04-01 09:57:42 - Running Test Suite: Load Testing +2026-04-01 09:57:42 - Description: High concurrent connection testing +2026-04-01 09:57:42 - ========================================== +========================================== +C-Relay Load Testing Suite +========================================== +Testing against relay at ws://127.0.0.1:8888 + +=== Basic Connectivity Test === +✓ Relay is accessible + +========================================== +Load Test: Light Load Test +Description: Basic load test with moderate concurrent connections +Concurrent clients: 10 +Messages per client: 5 +========================================== +Launching 10 clients... +All clients completed. Processing results... + +=== Load Test Results === +Test duration: 1s +Total connections attempted: 10 +Successful connections: 10 +Failed connections: 0 +Connection success rate: 100% +Messages expected: 50 +Messages sent: 50 +Messages received: 100 +✓ EXCELLENT: High connection success rate + +Checking relay responsiveness... ✓ Relay is still responsive + +========================================== +Load Test: Medium Load Test +Description: Moderate load test with higher concurrency +Concurrent clients: 25 +Messages per client: 10 +========================================== +Launching 25 clients... +All clients completed. Processing results... + +=== Load Test Results === +Test duration: 4s +Total connections attempted: 35 +Successful connections: 25 +Failed connections: 0 +Connection success rate: 71% +Messages expected: 250 +Messages sent: 250 +Messages received: 500 +✗ POOR: Low connection success rate + +Checking relay responsiveness... ✓ Relay is still responsive + +========================================== +Load Test: Heavy Load Test +Description: Heavy load test with high concurrency +Concurrent clients: 50 +Messages per client: 20 +========================================== +Launching 50 clients... +All clients completed. Processing results... + +=== Load Test Results === +Test duration: 15s +Total connections attempted: 85 +Successful connections: 50 +Failed connections: 0 +Connection success rate: 58% +Messages expected: 1000 +Messages sent: 1000 +Messages received: 2000 +✗ POOR: Low connection success rate + +Checking relay responsiveness... ✓ Relay is still responsive + +========================================== +Load Test: Stress Test +Description: Maximum load test to find breaking point +Concurrent clients: 100 +Messages per client: 50 +========================================== +Launching 100 clients... +All clients completed. Processing results... + +=== Load Test Results === +Test duration: 64s +Total connections attempted: 185 +Successful connections: 100 +Failed connections: 0 +Connection success rate: 54% +Messages expected: 5000 +Messages sent: 5000 +Messages received: 7500 +✗ POOR: Low connection success rate + +Checking relay responsiveness... ✓ Relay is still responsive + +========================================== +Load Testing Complete +========================================== +All load tests completed. Check individual test results above. +If any tests failed, the relay may need optimization or have resource limits. +2026-04-01 09:59:07 - \033[0;32m✓ Load Testing PASSED\033[0m (Duration: 85s) +2026-04-01 09:59:07 - ========================================== +2026-04-01 09:59:07 - Running Test Suite: Stress Testing +2026-04-01 09:59:07 - Description: Resource usage and stability testing +2026-04-01 09:59:07 - ========================================== +2026-04-01 09:59:07 - \033[0;31mERROR: Test script stress_tests.sh not found\033[0m