From 4a53455190a018aa431bb6dc8e094e0189bb833c Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Tue, 28 Jul 2026 12:21:21 -0400 Subject: [PATCH] Fix TLS read buffer in ws client: buffer excess SSL_read bytes to prevent loss of multiple WebSocket frames in a single TLS record --- VERSION | 2 +- nostr_core/nostr_core.h | 15 ++++++- nostr_websocket/nostr_websocket_openssl.c | 53 +++++++++++++++++++---- 3 files changed, 58 insertions(+), 12 deletions(-) diff --git a/VERSION b/VERSION index 04e84f89..45a346db 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.6.10 +0.6.11 diff --git a/nostr_core/nostr_core.h b/nostr_core/nostr_core.h index 3bcc9a21..5c306cce 100644 --- a/nostr_core/nostr_core.h +++ b/nostr_core/nostr_core.h @@ -2,10 +2,10 @@ #define NOSTR_CORE_H // Version information (auto-updated by increment_and_push.sh) -#define VERSION "v0.6.10" +#define VERSION "v0.6.11" #define VERSION_MAJOR 0 #define VERSION_MINOR 6 -#define VERSION_PATCH 10 +#define VERSION_PATCH 11 /* * NOSTR Core Library - Complete API Reference @@ -304,6 +304,17 @@ cJSON** nostr_relay_pool_query_sync( cJSON* filter, int* event_count, int timeout_ms); +// Same as above but also reports whether all relays sent EOSE (vs timed out). +// out_got_eose is set to 1 if all connected relays sent EOSE, 0 if any timed +// out. May be NULL if the caller doesn't care. +cJSON** nostr_relay_pool_query_sync_with_eose( + nostr_relay_pool_t* pool, + const char** relay_urls, + int relay_count, + cJSON* filter, + int* event_count, + int timeout_ms, + int* out_got_eose); cJSON* nostr_relay_pool_get_event( nostr_relay_pool_t* pool, const char** relay_urls, diff --git a/nostr_websocket/nostr_websocket_openssl.c b/nostr_websocket/nostr_websocket_openssl.c index bf365e56..8a748e06 100644 --- a/nostr_websocket/nostr_websocket_openssl.c +++ b/nostr_websocket/nostr_websocket_openssl.c @@ -57,6 +57,15 @@ typedef struct { SSL_CTX* ssl_ctx; SSL* ssl; int ssl_connected; + /* Internal read buffer: SSL_read may return more bytes than the + * caller requested (a full TLS record at once). Without buffering + * the excess, those bytes are lost — which breaks ws_receive_frame + * when it asks for a 2-byte header but SSL_read returns a full + * event message. This caused the caching backfill to only receive + * 1 event per query from TLS relays. */ + char read_buf[65536]; + size_t read_buf_len; /* total bytes in read_buf */ + size_t read_buf_pos; /* offset of next byte to consume */ } tls_transport_t; // WebSocket client structure @@ -708,10 +717,27 @@ static int tls_recv(void* ctx, void* data, size_t len, int timeout_ms) { if (!tls->ssl_connected || !tls->ssl) { return -1; } - - // Always use select() to ensure proper blocking behavior - // If SSL has pending data, use zero timeout to return immediately - // Otherwise use the full timeout to block until data arrives + + /* Serve from the internal read buffer first. SSL_read may have + * returned more bytes than the caller requested on a previous call + * (a full TLS record), and we buffered the excess. Without this, + * ws_receive_frame loses the extra bytes when it asks for a 2-byte + * header but SSL_read returns a full event message. */ + if (tls->read_buf_pos < tls->read_buf_len) { + size_t avail = tls->read_buf_len - tls->read_buf_pos; + size_t to_copy = (avail < len) ? avail : len; + memcpy(data, tls->read_buf + tls->read_buf_pos, to_copy); + tls->read_buf_pos += to_copy; + if (tls->read_buf_pos >= tls->read_buf_len) { + tls->read_buf_len = 0; + tls->read_buf_pos = 0; + } + return (int)to_copy; + } + + /* Buffer is empty — refill it with a single SSL_read, then serve + * the requested bytes from it. This ensures excess bytes are + * preserved for subsequent tls_recv calls. */ if (timeout_ms > 0) { fd_set readfds; struct timeval tv; @@ -719,7 +745,6 @@ static int tls_recv(void* ctx, void* data, size_t len, int timeout_ms) { FD_ZERO(&readfds); FD_SET(tls->socket_fd, &readfds); - // If SSL has buffered data, use zero timeout; otherwise use full timeout if (SSL_pending(tls->ssl) > 0) { tv.tv_sec = 0; tv.tv_usec = 0; @@ -741,16 +766,26 @@ static int tls_recv(void* ctx, void* data, size_t len, int timeout_ms) { const int max_attempts = 10; while (attempts < max_attempts) { - int ret = SSL_read(tls->ssl, data, len); + /* Read into the internal buffer (up to its capacity), then copy + * the requested number of bytes to the caller's output. */ + int ret = SSL_read(tls->ssl, tls->read_buf, sizeof(tls->read_buf)); if (ret > 0) { - return ret; // Success + tls->read_buf_len = (size_t)ret; + tls->read_buf_pos = 0; + size_t to_copy = (tls->read_buf_len < len) ? tls->read_buf_len : len; + memcpy(data, tls->read_buf, to_copy); + tls->read_buf_pos = to_copy; + if (tls->read_buf_pos >= tls->read_buf_len) { + tls->read_buf_len = 0; + tls->read_buf_pos = 0; + } + return (int)to_copy; } int err = SSL_get_error(tls->ssl, ret); if (err == SSL_ERROR_WANT_READ) { - // Check if more data is available on socket fd_set readfds; struct timeval tv = {0, 100000}; // 100ms timeout @@ -763,7 +798,7 @@ static int tls_recv(void* ctx, void* data, size_t len, int timeout_ms) { } attempts++; - continue; // Retry the SSL read + continue; } else if (err == SSL_ERROR_WANT_WRITE) { return -1;