diff --git a/nostr_websocket/EXPORT_GUIDE.md b/nostr_websocket/EXPORT_GUIDE.md index ae7cafe4..63318ec4 100644 --- a/nostr_websocket/EXPORT_GUIDE.md +++ b/nostr_websocket/EXPORT_GUIDE.md @@ -8,11 +8,11 @@ The NOSTR WebSocket library consists of these key files for export: ### Core Files (Required) - `nostr_websocket_tls.h` - Header with all function declarations and constants -- `nostr_websocket_mbedtls.c` - Main implementation using mbedTLS for SSL/TLS +- `nostr_websocket_openssl.c` - Main implementation using OpenSSL for SSL/TLS - `../cjson/cJSON.h` and `../cjson/cJSON.c` - JSON parsing (lightweight) ### Dependencies -- **mbedTLS** - For SSL/TLS support (wss:// connections) +- **OpenSSL** - For SSL/TLS support (wss:// connections) - libssl, libcrypto - **Standard C libraries** - socket, networking, etc. ## Quick Integration @@ -21,23 +21,29 @@ The NOSTR WebSocket library consists of these key files for export: ```bash # Copy the library files cp nostr_websocket_tls.h your_project/ -cp nostr_websocket_mbedtls.c your_project/ +cp nostr_websocket_openssl.c your_project/ cp ../cjson/cJSON.h your_project/ cp ../cjson/cJSON.c your_project/ ``` -### 2. Install mbedTLS Dependency +### 2. Install OpenSSL Dependency ```bash # Ubuntu/Debian -sudo apt-get install libmbedtls-dev +sudo apt-get install libssl-dev -# Or build from source (see mbedTLS documentation) +# CentOS/RHEL/Fedora +sudo yum install openssl-devel +# or +sudo dnf install openssl-devel + +# macOS (via Homebrew) +brew install openssl ``` ### 3. Compile Your Project ```bash -gcc -o my_nostr_app my_app.c nostr_websocket_mbedtls.c cJSON.c \ - -lmbedtls -lmbedx509 -lmbedcrypto -lm +gcc -o my_nostr_app my_app.c nostr_websocket_openssl.c cJSON.c \ + -lssl -lcrypto -lm ``` ## Basic Usage Example @@ -131,16 +137,17 @@ The library supports both TCP (`ws://`) and TLS (`wss://`) automatically based o ### Linux/Unix - Works out of the box with standard development tools -- Requires: gcc, mbedTLS development headers +- Requires: gcc, OpenSSL development headers (libssl-dev) -### Potential Windows Support +### Windows Support - Would need Winsock2 adaptations in transport layer -- mbedTLS is cross-platform compatible +- OpenSSL is widely available on Windows ### Embedded Systems - Lightweight design suitable for embedded use - Memory usage: ~4KB per client + message buffers - No dynamic allocations in hot paths +- OpenSSL provides optimized implementations for various architectures ## Library Design Benefits @@ -178,7 +185,21 @@ The library handles all WebSocket protocol details, SSL/TLS, and NOSTR message f - Based on WebSocket RFC 6455 - Implements NOSTR WebSocket conventions -- SSL/TLS via proven mbedTLS library +- SSL/TLS via industry-standard OpenSSL library - Tested with major NOSTR relays +## Migration Notes + +If you were previously using the mbedTLS version of this library: + +### Code Changes Required +- Update `#include` to reference `nostr_websocket_openssl.c` instead of `nostr_websocket_mbedtls.c` +- Change linking flags from `-lmbedtls -lmbedx509 -lmbedcrypto` to `-lssl -lcrypto` +- Install OpenSSL development headers instead of mbedTLS + +### API Compatibility +- All function signatures remain identical +- No changes to your application code required +- Same performance characteristics and behavior + This library provides a clean, efficient way to integrate NOSTR WebSocket functionality into any C project with minimal dependencies and maximum portability. diff --git a/nostr_websocket/Makefile b/nostr_websocket/Makefile index 1ac7e9f6..78e710a6 100644 --- a/nostr_websocket/Makefile +++ b/nostr_websocket/Makefile @@ -3,11 +3,11 @@ CC = gcc CFLAGS = -Wall -Wextra -std=c99 -O2 -INCLUDES = -I. -I.. -I../mbedtls/include -I../mbedtls/tf-psa-crypto/include -I../mbedtls/tf-psa-crypto/drivers/builtin/include -LIBS = -lm -L../mbedtls/library -lmbedtls -lmbedx509 -lmbedcrypto +INCLUDES = -I. -I.. -I../openssl-install/include +LIBS = -lm -L../openssl-install/lib64 -lssl -lcrypto # Source files -WEBSOCKET_SOURCES = nostr_websocket_mbedtls.c ../cjson/cJSON.c +WEBSOCKET_SOURCES = nostr_websocket_openssl.c ../cjson/cJSON.c WEBSOCKET_HEADERS = nostr_websocket_tls.h ../cjson/cJSON.h # Test programs @@ -21,7 +21,7 @@ WEBSOCKET_OBJECTS = $(WEBSOCKET_SOURCES:.c=.o) all: $(TEST_PROGRAMS) -# Test programs +# Test programs (if test file exists) test_5_events_clean: test_5_events_clean.o $(WEBSOCKET_OBJECTS) $(CC) -o $@ $^ $(LIBS) @@ -29,11 +29,16 @@ test_5_events_clean: test_5_events_clean.o $(WEBSOCKET_OBJECTS) %.o: %.c $(WEBSOCKET_HEADERS) $(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@ -# Test target -test: test_5_events_clean - @echo "๐Ÿงช Running WebSocket test..." - @echo "Press Ctrl-C to stop the test" - ./test_5_events_clean +# Test target (if test exists, otherwise use wss_test from tests/) +test: + @if [ -f test_5_events_clean ]; then \ + echo "๐Ÿงช Running local WebSocket test..."; \ + echo "Press Ctrl-C to stop the test"; \ + ./test_5_events_clean; \ + else \ + echo "๐Ÿงช Running WebSocket test from tests/ directory..."; \ + cd ../tests && make wss_test && ./wss_test; \ + fi # Clean build artifacts clean: @@ -46,18 +51,21 @@ info: @echo "==========================" @echo "Core files:" @echo " - nostr_websocket_tls.h (header)" - @echo " - nostr_websocket_mbedtls.c (implementation)" + @echo " - nostr_websocket_openssl.c (OpenSSL implementation)" @echo " - ../cjson/cJSON.h/c (JSON support)" @echo "" @echo "Dependencies:" - @echo " - mbedTLS (SSL/TLS support)" + @echo " - OpenSSL (SSL/TLS support)" @echo " - Standard C libraries" @echo "" @echo "Usage:" - @echo " make - Build test programs" + @echo " make - Build test programs (if available)" @echo " make test - Run WebSocket test" @echo " make clean - Clean build artifacts" @echo " make info - Show this information" + @echo "" + @echo "Note: This library is integrated into the main libnostr_core.a" + @echo "Use the tests in ../tests/ for comprehensive testing." # Help target help: info diff --git a/nostr_websocket/README.md b/nostr_websocket/README.md index df1f971b..5600fc09 100644 --- a/nostr_websocket/README.md +++ b/nostr_websocket/README.md @@ -5,7 +5,7 @@ A production-ready, lightweight WebSocket client library specifically designed f ## Features - โœ… **WebSocket RFC 6455 Compliant** - Full WebSocket protocol implementation -- โœ… **SSL/TLS Support** - Secure `wss://` connections via mbedTLS +- โœ… **SSL/TLS Support** - Secure `wss://` connections via OpenSSL - โœ… **NOSTR Protocol** - Built-in support for REQ, EVENT, CLOSE messages - โœ… **Production Ready** - Optimized performance and error handling - โœ… **Lightweight** - Minimal dependencies and memory footprint @@ -54,11 +54,11 @@ cJSON_Delete(filter); ### Core Components - **`nostr_websocket_tls.h`** - Public API header -- **`nostr_websocket_mbedtls.c`** - Main implementation (mbedTLS backend) +- **`nostr_websocket_openssl.c`** - Main implementation (OpenSSL backend) - **`../cjson/cJSON.h/c`** - JSON parsing support ### Dependencies -- **mbedTLS** - For SSL/TLS support +- **OpenSSL** - For SSL/TLS support (libssl, libcrypto) - **Standard C libraries** - POSIX sockets, etc. ## Installation in Other Projects @@ -121,14 +121,16 @@ make help # Show help ## Development History -This library evolved from the experimental WebSocket implementation in `../websocket_experiment/` and represents the production-ready, stable version suitable for integration into other projects. +This library evolved from the experimental WebSocket implementation and represents the production-ready, stable version suitable for integration into other projects. Key improvements made during development: +- **Migrated from mbedTLS to OpenSSL** - Better compatibility and performance - Fixed critical WebSocket frame parsing bugs - Optimized SSL/TLS performance - Reduced memory allocations - Enhanced error handling - Added comprehensive documentation +- Improved build system integration ## License diff --git a/nostr_websocket/nostr_websocket_mbedtls.c b/nostr_websocket/nostr_websocket_mbedtls.c deleted file mode 100644 index c0c6e0d9..00000000 --- a/nostr_websocket/nostr_websocket_mbedtls.c +++ /dev/null @@ -1,1046 +0,0 @@ -#define _GNU_SOURCE -#include "nostr_websocket_tls.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// mbedTLS headers -#include "mbedtls/ssl.h" -#include "mbedtls/entropy.h" -#include "mbedtls/ctr_drbg.h" -#include "mbedtls/net_sockets.h" -#include "mbedtls/error.h" -#include "mbedtls/debug.h" -#include "psa/crypto.h" - -// WebSocket magic string for handshake -#define WS_MAGIC_STRING "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" -#define WS_KEY_LEN 24 -#define MAX_HEADER_SIZE 4096 -#define MAX_FRAME_SIZE 65536 - -// Debug logging (conditional compilation) -#if defined(ENABLE_FILE_LOGGING) && defined(ENABLE_WEBSOCKET_LOGGING) -static FILE* debug_log_file = NULL; -static void debug_log_init(void); -static void debug_log_message(const char* direction, const char* host, int port, const char* message); -static const char* get_timestamp(void); -#endif - -// Transport layer abstraction -typedef struct { - int (*connect)(void* ctx, const char* host, int port); - int (*send)(void* ctx, const void* data, size_t len); - int (*recv)(void* ctx, void* data, size_t len, int timeout_ms); - int (*close)(void* ctx); - void (*cleanup)(void* ctx); -} transport_ops_t; - -// TCP transport context -typedef struct { - int socket_fd; -} tcp_transport_t; - -// TLS transport context (mbedTLS) -typedef struct { - int socket_fd; - mbedtls_ssl_context ssl; - mbedtls_ssl_config conf; - mbedtls_entropy_context entropy; - mbedtls_ctr_drbg_context ctr_drbg; - int ssl_connected; -} tls_transport_t; - -// WebSocket client structure -struct nostr_ws_client { - nostr_ws_state_t state; - char* host; - int port; - char* path; - int use_tls; - int timeout_ms; - char receive_buffer[MAX_FRAME_SIZE]; - size_t receive_buffer_pos; - - // Transport layer - transport_ops_t* transport; - union { - tcp_transport_t tcp; - tls_transport_t tls; - } transport_ctx; -}; - -// Transport layer implementations -static int tcp_connect(void* ctx, const char* host, int port); -static int tcp_send(void* ctx, const void* data, size_t len); -static int tcp_recv(void* ctx, void* data, size_t len, int timeout_ms); -static int tcp_close(void* ctx); -static void tcp_cleanup(void* ctx); - -static int tls_connect(void* ctx, const char* host, int port); -static int tls_send(void* ctx, const void* data, size_t len); -static int tls_recv(void* ctx, void* data, size_t len, int timeout_ms); -static int tls_close(void* ctx); -static void tls_cleanup(void* ctx); - -// Internal helper functions -static int ws_parse_url(const char* url, char** host, int* port, char** path, int* use_tls); -static int ws_create_handshake_key(char* key_out, size_t key_size); -static int ws_perform_handshake(nostr_ws_client_t* client, const char* key); -static int ws_send_frame(nostr_ws_client_t* client, ws_opcode_t opcode, const char* payload, size_t payload_len); -static int ws_receive_frame(nostr_ws_client_t* client, ws_opcode_t* opcode, char* payload, size_t* payload_len, int timeout_ms); -static void ws_mask_payload(char* payload, size_t len, uint32_t mask); -static uint32_t ws_generate_mask(void); - -// Transport layer vtables -static transport_ops_t tcp_transport_ops = { - .connect = tcp_connect, - .send = tcp_send, - .recv = tcp_recv, - .close = tcp_close, - .cleanup = tcp_cleanup -}; - -static transport_ops_t tls_transport_ops = { - .connect = tls_connect, - .send = tls_send, - .recv = tls_recv, - .close = tls_close, - .cleanup = tls_cleanup -}; - -// ============================================================================ -// Core WebSocket Functions -// ============================================================================ - -nostr_ws_client_t* nostr_ws_connect(const char* url) { - if (!url) return NULL; - - nostr_ws_client_t* client = calloc(1, sizeof(nostr_ws_client_t)); - if (!client) return NULL; - - client->state = NOSTR_WS_CONNECTING; - client->timeout_ms = 30000; // 30 second default timeout - - // Parse URL and determine if TLS is needed - if (ws_parse_url(url, &client->host, &client->port, &client->path, &client->use_tls) != 0) { - free(client); - return NULL; - } - - // URL parsed successfully - - // Set up transport layer - if (client->use_tls) { - client->transport = &tls_transport_ops; - memset(&client->transport_ctx.tls, 0, sizeof(client->transport_ctx.tls)); - client->transport_ctx.tls.socket_fd = -1; - } else { - client->transport = &tcp_transport_ops; - client->transport_ctx.tcp.socket_fd = -1; - } - - // Connect to server - if (client->transport->connect(&client->transport_ctx, client->host, client->port) != 0) { - free(client->host); - free(client->path); - free(client); - return NULL; - } - - // Perform WebSocket handshake - char handshake_key[WS_KEY_LEN + 1]; - if (ws_create_handshake_key(handshake_key, sizeof(handshake_key)) != 0) { - client->transport->close(&client->transport_ctx); - client->transport->cleanup(&client->transport_ctx); - free(client->host); - free(client->path); - free(client); - return NULL; - } - - // Perform WebSocket handshake (silently) - if (ws_perform_handshake(client, handshake_key) != 0) { - client->transport->close(&client->transport_ctx); - client->transport->cleanup(&client->transport_ctx); - free(client->host); - free(client->path); - free(client); - return NULL; - } - - client->state = NOSTR_WS_CONNECTED; - return client; -} - -int nostr_ws_close(nostr_ws_client_t* client) { - if (!client) return NOSTR_WS_ERROR_INVALID; - - if (client->state == NOSTR_WS_CONNECTED) { - // Send close frame - ws_send_frame(client, WS_OPCODE_CLOSE, NULL, 0); - client->state = NOSTR_WS_CLOSING; - } - - client->transport->close(&client->transport_ctx); - client->transport->cleanup(&client->transport_ctx); - - free(client->host); - free(client->path); - client->state = NOSTR_WS_CLOSED; - free(client); - - return NOSTR_WS_SUCCESS; -} - -nostr_ws_state_t nostr_ws_get_state(nostr_ws_client_t* client) { - if (!client) return NOSTR_WS_ERROR; - return client->state; -} - -int nostr_ws_send_text(nostr_ws_client_t* client, const char* message) { - if (!client || !message || client->state != NOSTR_WS_CONNECTED) { - return NOSTR_WS_ERROR_INVALID; - } - - return ws_send_frame(client, WS_OPCODE_TEXT, message, strlen(message)); -} - -int nostr_ws_receive(nostr_ws_client_t* client, char* buffer, size_t buffer_size, int timeout_ms) { - if (!client || !buffer || buffer_size == 0) { - return NOSTR_WS_ERROR_INVALID; - } - - // Allow receiving even when in CLOSING state to capture final messages - if (client->state != NOSTR_WS_CONNECTED && client->state != NOSTR_WS_CLOSING) { - return NOSTR_WS_ERROR_INVALID; - } - - ws_opcode_t opcode; - size_t payload_len = buffer_size - 1; // Leave space for null terminator - - int result = ws_receive_frame(client, &opcode, buffer, &payload_len, timeout_ms); - if (result < 0) return result; - - // Handle different frame types - switch (opcode) { - case WS_OPCODE_TEXT: - buffer[payload_len] = '\0'; // Null terminate text - return (int)payload_len; - - case WS_OPCODE_PING: - // Respond with pong - ws_send_frame(client, WS_OPCODE_PONG, buffer, payload_len); - // Continue receiving - return nostr_ws_receive(client, buffer, buffer_size, timeout_ms); - - case WS_OPCODE_PONG: - // Return pong frames with special prefix to distinguish from text - // Format: "__PONG__" + payload - const char* pong_prefix = "__PONG__"; - size_t prefix_len = strlen(pong_prefix); - - if (payload_len + prefix_len < buffer_size - 1) { - memmove(buffer + prefix_len, buffer, payload_len); - memcpy(buffer, pong_prefix, prefix_len); - buffer[payload_len + prefix_len] = '\0'; - return (int)(payload_len + prefix_len); - } else { - // Not enough space for prefix, just return payload - buffer[payload_len] = '\0'; - return (int)payload_len; - } - - case WS_OPCODE_CLOSE: - client->state = NOSTR_WS_CLOSING; - buffer[payload_len] = '\0'; // Null terminate - - - return (int)payload_len; // Return the close message content - - default: - return NOSTR_WS_ERROR_PROTOCOL; - } -} - -int nostr_ws_ping(nostr_ws_client_t* client) { - if (!client || client->state != NOSTR_WS_CONNECTED) { - return NOSTR_WS_ERROR_INVALID; - } - - return ws_send_frame(client, WS_OPCODE_PING, "ping", 4); -} - -int nostr_ws_set_timeout(nostr_ws_client_t* client, int timeout_ms) { - if (!client) return NOSTR_WS_ERROR_INVALID; - client->timeout_ms = timeout_ms; - return NOSTR_WS_SUCCESS; -} - -// ============================================================================ -// NOSTR-Specific Helper Functions (same as original) -// ============================================================================ - -int nostr_relay_send_req(nostr_ws_client_t* client, const char* subscription_id, cJSON* filters) { - if (!client || !subscription_id || !filters) { - return NOSTR_WS_ERROR_INVALID; - } - - // Create REQ message: ["REQ", subscription_id, ...filters] - cJSON* req_array = cJSON_CreateArray(); - cJSON_AddItemToArray(req_array, cJSON_CreateString("REQ")); - cJSON_AddItemToArray(req_array, cJSON_CreateString(subscription_id)); - - // Add filters - create a copy to avoid double-free - if (cJSON_IsArray(filters)) { - cJSON* filter; - cJSON_ArrayForEach(filter, filters) { - cJSON_AddItemToArray(req_array, cJSON_Duplicate(filter, 1)); - } - } else { - cJSON_AddItemToArray(req_array, cJSON_Duplicate(filters, 1)); - } - - char* req_string = cJSON_Print(req_array); - if (!req_string) { - cJSON_Delete(req_array); - return NOSTR_WS_ERROR_MEMORY; - } - - int result = nostr_ws_send_text(client, req_string); - - free(req_string); - cJSON_Delete(req_array); - - return result; -} - -int nostr_relay_send_event(nostr_ws_client_t* client, cJSON* event) { - if (!client || !event) { - return NOSTR_WS_ERROR_INVALID; - } - - // Create EVENT message: ["EVENT", event] - duplicate event to avoid double-free - cJSON* event_array = cJSON_CreateArray(); - cJSON_AddItemToArray(event_array, cJSON_CreateString("EVENT")); - cJSON_AddItemToArray(event_array, cJSON_Duplicate(event, 1)); - - char* event_string = cJSON_Print(event_array); - if (!event_string) { - cJSON_Delete(event_array); - return NOSTR_WS_ERROR_MEMORY; - } - - int result = nostr_ws_send_text(client, event_string); - - free(event_string); - cJSON_Delete(event_array); - - return result; -} - -int nostr_relay_send_close(nostr_ws_client_t* client, const char* subscription_id) { - if (!client || !subscription_id) { - return NOSTR_WS_ERROR_INVALID; - } - - // Create CLOSE message: ["CLOSE", subscription_id] - cJSON* close_array = cJSON_CreateArray(); - cJSON_AddItemToArray(close_array, cJSON_CreateString("CLOSE")); - cJSON_AddItemToArray(close_array, cJSON_CreateString(subscription_id)); - - char* close_string = cJSON_Print(close_array); - if (!close_string) { - cJSON_Delete(close_array); - return NOSTR_WS_ERROR_MEMORY; - } - - int result = nostr_ws_send_text(client, close_string); - - free(close_string); - cJSON_Delete(close_array); - - return result; -} - -int nostr_parse_relay_message(const char* message, char** message_type, cJSON** parsed_json) { - if (!message || !message_type || !parsed_json) { - return NOSTR_WS_ERROR_INVALID; - } - - *message_type = NULL; - *parsed_json = NULL; - - cJSON* json = cJSON_Parse(message); - if (!json || !cJSON_IsArray(json)) { - return NOSTR_WS_ERROR_PROTOCOL; - } - - cJSON* type_item = cJSON_GetArrayItem(json, 0); - if (!type_item || !cJSON_IsString(type_item)) { - cJSON_Delete(json); - return NOSTR_WS_ERROR_PROTOCOL; - } - - *message_type = strdup(type_item->valuestring); - *parsed_json = json; - - return NOSTR_WS_SUCCESS; -} - -const char* nostr_ws_strerror(int error_code) { - switch (error_code) { - case NOSTR_WS_SUCCESS: return "Success"; - case NOSTR_WS_ERROR_INVALID: return "Invalid parameter"; - case NOSTR_WS_ERROR_NETWORK: return "Network error"; - case NOSTR_WS_ERROR_PROTOCOL: return "Protocol error"; - case NOSTR_WS_ERROR_MEMORY: return "Memory allocation error"; - case NOSTR_WS_ERROR_TLS: return "TLS error"; - default: return "Unknown error"; - } -} - -// ============================================================================ -// TCP Transport Implementation -// ============================================================================ - -static int tcp_connect(void* ctx, const char* host, int port) { - tcp_transport_t* tcp = (tcp_transport_t*)ctx; - - // Create socket - tcp->socket_fd = socket(AF_INET, SOCK_STREAM, 0); - if (tcp->socket_fd < 0) { - return -1; - } - - // Resolve hostname - struct hostent* he = gethostbyname(host); - if (!he) { - close(tcp->socket_fd); - tcp->socket_fd = -1; - return -1; - } - - // Set up address - struct sockaddr_in addr; - memset(&addr, 0, sizeof(addr)); - addr.sin_family = AF_INET; - addr.sin_port = htons(port); - memcpy(&addr.sin_addr, he->h_addr_list[0], he->h_length); - - // Connect - if (connect(tcp->socket_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { - close(tcp->socket_fd); - tcp->socket_fd = -1; - return -1; - } - - return 0; -} - -static int tcp_send(void* ctx, const void* data, size_t len) { - tcp_transport_t* tcp = (tcp_transport_t*)ctx; - return send(tcp->socket_fd, data, len, MSG_NOSIGNAL); -} - -static int tcp_recv(void* ctx, void* data, size_t len, int timeout_ms) { - tcp_transport_t* tcp = (tcp_transport_t*)ctx; - - if (timeout_ms > 0) { - fd_set readfds; - struct timeval tv; - - FD_ZERO(&readfds); - FD_SET(tcp->socket_fd, &readfds); - - tv.tv_sec = timeout_ms / 1000; - tv.tv_usec = (timeout_ms % 1000) * 1000; - - int result = select(tcp->socket_fd + 1, &readfds, NULL, NULL, &tv); - if (result <= 0) return -1; - } - - return recv(tcp->socket_fd, data, len, 0); -} - -static int tcp_close(void* ctx) { - tcp_transport_t* tcp = (tcp_transport_t*)ctx; - if (tcp->socket_fd >= 0) { - close(tcp->socket_fd); - tcp->socket_fd = -1; - } - return 0; -} - -static void tcp_cleanup(void* ctx) { - tcp_close(ctx); -} - -// ============================================================================ -// TLS Transport Implementation (mbedTLS) -// ============================================================================ - -// Custom certificate verification callback (accept all certificates for NOSTR) -static int verify_cert(void *data, mbedtls_x509_crt *crt, int depth, uint32_t *flags) { - (void)data; (void)crt; (void)depth; - *flags = 0; // Clear all verification flags (accept any certificate) - return 0; -} - -// Custom send function for mbedTLS -static int mbedtls_send(void *ctx, const unsigned char *buf, size_t len) { - int sock_fd = *((int*)ctx); - ssize_t sent = send(sock_fd, buf, len, MSG_NOSIGNAL); - if (sent < 0) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { - return MBEDTLS_ERR_SSL_WANT_WRITE; - } - return MBEDTLS_ERR_NET_SEND_FAILED; - } - return sent; -} - -// Custom receive function for mbedTLS -static int mbedtls_recv(void *ctx, unsigned char *buf, size_t len) { - int sock_fd = *((int*)ctx); - ssize_t received = recv(sock_fd, buf, len, 0); - if (received < 0) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { - return MBEDTLS_ERR_SSL_WANT_READ; - } - return MBEDTLS_ERR_NET_RECV_FAILED; - } - if (received == 0) { - return MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY; - } - return received; -} - -static int tls_connect(void* ctx, const char* host, int port) { - tls_transport_t* tls = (tls_transport_t*)ctx; - int ret; - - // First establish TCP connection - tcp_transport_t tcp_ctx = { .socket_fd = -1 }; - if (tcp_connect(&tcp_ctx, host, port) != 0) { - return -1; - } - tls->socket_fd = tcp_ctx.socket_fd; - - // Initialize PSA crypto (required for this version of mbedTLS) - psa_status_t psa_status = psa_crypto_init(); - if (psa_status != PSA_SUCCESS) { - goto cleanup; - } - - // Initialize mbedTLS structures - mbedtls_ssl_init(&tls->ssl); - mbedtls_ssl_config_init(&tls->conf); - mbedtls_entropy_init(&tls->entropy); - mbedtls_ctr_drbg_init(&tls->ctr_drbg); - - // Seed the random number generator - const char *pers = "nostr_ws_client"; - if ((ret = mbedtls_ctr_drbg_seed(&tls->ctr_drbg, mbedtls_entropy_func, &tls->entropy, - (const unsigned char *)pers, strlen(pers))) != 0) { - goto cleanup; - } - - // Set up SSL configuration - if ((ret = mbedtls_ssl_config_defaults(&tls->conf, - MBEDTLS_SSL_IS_CLIENT, - MBEDTLS_SSL_TRANSPORT_STREAM, - MBEDTLS_SSL_PRESET_DEFAULT)) != 0) { - goto cleanup; - } - - // RNG is configured automatically through mbedtls_ssl_config_defaults - // The CTR_DRBG we initialized should be used automatically - - // Disable certificate verification for NOSTR (we only need encryption) - mbedtls_ssl_conf_authmode(&tls->conf, MBEDTLS_SSL_VERIFY_NONE); - mbedtls_ssl_conf_verify(&tls->conf, verify_cert, NULL); - - // Set up SSL context - if ((ret = mbedtls_ssl_setup(&tls->ssl, &tls->conf)) != 0) { - goto cleanup; - } - - // Set hostname for SNI - if ((ret = mbedtls_ssl_set_hostname(&tls->ssl, host)) != 0) { - goto cleanup; - } - - // Set I/O functions - mbedtls_ssl_set_bio(&tls->ssl, &tls->socket_fd, mbedtls_send, mbedtls_recv, NULL); - - // Perform TLS handshake - while ((ret = mbedtls_ssl_handshake(&tls->ssl)) != 0) { - if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) { - goto cleanup; - } - } - - tls->ssl_connected = 1; - return 0; - -cleanup: - mbedtls_ssl_free(&tls->ssl); - mbedtls_ssl_config_free(&tls->conf); - mbedtls_ctr_drbg_free(&tls->ctr_drbg); - mbedtls_entropy_free(&tls->entropy); - close(tls->socket_fd); - tls->socket_fd = -1; - return -1; -} - -static int tls_send(void* ctx, const void* data, size_t len) { - tls_transport_t* tls = (tls_transport_t*)ctx; - if (!tls->ssl_connected) return -1; - - int ret = mbedtls_ssl_write(&tls->ssl, (const unsigned char*)data, len); - if (ret < 0) { - if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) { - return -1; // Would block - } - return -1; - } - return ret; -} - -static int tls_recv(void* ctx, void* data, size_t len, int timeout_ms) { - tls_transport_t* tls = (tls_transport_t*)ctx; - if (!tls->ssl_connected) { - return -1; - } - - // Check if mbedTLS has pending data first - this avoids unnecessary select() calls - size_t pending = mbedtls_ssl_get_bytes_avail(&tls->ssl); - if (pending == 0 && timeout_ms > 0) { - // Only use select() if no data is pending in SSL buffers - fd_set readfds; - struct timeval tv; - - FD_ZERO(&readfds); - FD_SET(tls->socket_fd, &readfds); - - tv.tv_sec = timeout_ms / 1000; - tv.tv_usec = (timeout_ms % 1000) * 1000; - - int result = select(tls->socket_fd + 1, &readfds, NULL, NULL, &tv); - if (result < 0) { - return -1; - } else if (result == 0) { - return -1; // Timeout - } - } - - // Retry loop for SSL reads that want more data - int attempts = 0; - const int max_attempts = 10; - - while (attempts < max_attempts) { - int ret = mbedtls_ssl_read(&tls->ssl, (unsigned char*)data, len); - - if (ret >= 0) { - return ret; // Success or clean close - } - - if (ret == MBEDTLS_ERR_SSL_WANT_READ) { - // Check if more data is available on socket - fd_set readfds; - struct timeval tv = {0, 100000}; // 100ms timeout - - FD_ZERO(&readfds); - FD_SET(tls->socket_fd, &readfds); - - int select_result = select(tls->socket_fd + 1, &readfds, NULL, NULL, &tv); - if (select_result <= 0) { - return -1; - } - - attempts++; - continue; // Retry the SSL read - - } else if (ret == MBEDTLS_ERR_SSL_WANT_WRITE) { - return -1; - - } else if (ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) { - return 0; // Clean close - - } else if (ret == -0x7b00) { // MBEDTLS_ERR_SSL_RECEIVED_NEW_SESSION_TICKET - attempts++; - continue; // This is normal, keep trying to read application data - - } else { - return -1; - } - } - return -1; -} - -static int tls_close(void* ctx) { - tls_transport_t* tls = (tls_transport_t*)ctx; - - if (tls->ssl_connected) { - mbedtls_ssl_close_notify(&tls->ssl); - tls->ssl_connected = 0; - } - - if (tls->socket_fd >= 0) { - close(tls->socket_fd); - tls->socket_fd = -1; - } - - return 0; -} - -static void tls_cleanup(void* ctx) { - tls_transport_t* tls = (tls_transport_t*)ctx; - - tls_close(ctx); - - mbedtls_ssl_free(&tls->ssl); - mbedtls_ssl_config_free(&tls->conf); - mbedtls_ctr_drbg_free(&tls->ctr_drbg); - mbedtls_entropy_free(&tls->entropy); -} - -// ============================================================================ -// Internal Helper Functions -// ============================================================================ - -static int ws_parse_url(const char* url, char** host, int* port, char** path, int* use_tls) { - if (!url || !host || !port || !path || !use_tls) return -1; - - *host = NULL; - *path = NULL; - *use_tls = 0; - - // Check protocol - if (strncmp(url, "ws://", 5) == 0) { - *use_tls = 0; - url += 5; - *port = 80; - } else if (strncmp(url, "wss://", 6) == 0) { - *use_tls = 1; - url += 6; - *port = 443; - } else { - return -1; - } - - // Find path separator - const char* path_start = strchr(url, '/'); - if (path_start) { - *path = strdup(path_start); - } else { - *path = strdup("/"); - } - - // Extract host and port - const char* port_start = strchr(url, ':'); - if (port_start && (!path_start || port_start < path_start)) { - // Port specified - size_t host_len = port_start - url; - *host = strndup(url, host_len); - *port = atoi(port_start + 1); - } else { - // No port specified - size_t host_len = path_start ? (size_t)(path_start - url) : strlen(url); - *host = strndup(url, host_len); - } - - return 0; -} - -static int ws_create_handshake_key(char* key_out, size_t key_size) { - if (key_size < WS_KEY_LEN + 1) return -1; - - // Generate random 16 bytes - unsigned char random_bytes[16]; - for (int i = 0; i < 16; i++) { - random_bytes[i] = rand() & 0xFF; - } - - // Base64 encode - static const char base64_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - - for (int i = 0; i < 16; i += 3) { - int b = (random_bytes[i] << 16) | - (i + 1 < 16 ? random_bytes[i + 1] << 8 : 0) | - (i + 2 < 16 ? random_bytes[i + 2] : 0); - - key_out[(i / 3) * 4 + 0] = base64_chars[(b >> 18) & 0x3F]; - key_out[(i / 3) * 4 + 1] = base64_chars[(b >> 12) & 0x3F]; - key_out[(i / 3) * 4 + 2] = base64_chars[(b >> 6) & 0x3F]; - key_out[(i / 3) * 4 + 3] = base64_chars[b & 0x3F]; - } - - key_out[WS_KEY_LEN] = '\0'; - return 0; -} - -static int ws_perform_handshake(nostr_ws_client_t* client, const char* key) { - // Build HTTP upgrade request - char request[MAX_HEADER_SIZE]; - int len = snprintf(request, sizeof(request), - "GET %s HTTP/1.1\r\n" - "Host: %s:%d\r\n" - "Upgrade: websocket\r\n" - "Connection: Upgrade\r\n" - "Sec-WebSocket-Key: %s\r\n" - "Sec-WebSocket-Version: 13\r\n" - "\r\n", - client->path, client->host, client->port, key); - - if ((size_t)len >= sizeof(request)) { - return -1; - } - - // Send handshake request - int sent = client->transport->send(&client->transport_ctx, request, len); - if (sent != len) { - return -1; - } - - // Small delay to allow server to process - usleep(100000); // 100ms - - // Read response - char response[MAX_HEADER_SIZE]; - int total_received = 0; - - while ((size_t)total_received < sizeof(response) - 1) { - int received = client->transport->recv(&client->transport_ctx, - response + total_received, - sizeof(response) - total_received - 1, - client->timeout_ms); - if (received <= 0) { - return -1; - } - - total_received += received; - response[total_received] = '\0'; - - // Check if we have complete headers - if (strstr(response, "\r\n\r\n")) break; - } - - // Check if response starts with the correct HTTP status line - if (strncmp(response, "HTTP/1.1 101 Switching Protocols\r\n", 34) != 0) { - return -1; - } - - if (!strstr(response, "Upgrade: websocket") && !strstr(response, "upgrade: websocket")) { - return -1; - } - - return 0; -} - -static int ws_send_frame(nostr_ws_client_t* client, ws_opcode_t opcode, const char* payload, size_t payload_len) { - if (!client) return -1; - - char frame[MAX_FRAME_SIZE]; - size_t frame_len = 0; - - - // First byte: FIN=1, opcode - frame[0] = 0x80 | (opcode & 0x0F); - frame_len = 1; - - // Payload length and masking - uint32_t mask = ws_generate_mask(); - - if (payload_len < 126) { - frame[1] = 0x80 | (payload_len & 0x7F); // MASK=1, length - frame_len = 2; - } else if (payload_len < 65536) { - frame[1] = 0x80 | 126; // MASK=1, length=126 - frame[2] = (payload_len >> 8) & 0xFF; - frame[3] = payload_len & 0xFF; - frame_len = 4; - } else { - // Should not happen with our MAX_FRAME_SIZE - return -1; - } - - // Add mask - frame[frame_len++] = (mask >> 24) & 0xFF; - frame[frame_len++] = (mask >> 16) & 0xFF; - frame[frame_len++] = (mask >> 8) & 0xFF; - frame[frame_len++] = mask & 0xFF; - - // Add payload (masked) - if (payload && payload_len > 0) { - memcpy(frame + frame_len, payload, payload_len); - ws_mask_payload(frame + frame_len, payload_len, mask); - frame_len += payload_len; - } - - // Log outgoing message to debug.log -#if defined(ENABLE_FILE_LOGGING) && defined(ENABLE_WEBSOCKET_LOGGING) - if (opcode == WS_OPCODE_TEXT && payload && payload_len > 0) { - debug_log_message("SEND", client->host, client->port, payload); - } -#endif - - // Send frame - int result = client->transport->send(&client->transport_ctx, frame, frame_len); - - return result == (int)frame_len ? 0 : -1; -} - -static int ws_receive_frame(nostr_ws_client_t* client, ws_opcode_t* opcode, char* payload, size_t* payload_len, int timeout_ms) { - if (!client || !opcode || !payload || !payload_len) return -1; - - char header[14]; // Max header size - size_t header_len = 2; // Minimum header size - - // Read basic header - int header_result = client->transport->recv(&client->transport_ctx, header, 2, timeout_ms); - - if (header_result != 2) { - return -1; - } - - // Parse header - *opcode = (ws_opcode_t)(header[0] & 0x0F); - uint8_t masked = (header[1] & 0x80) != 0; - uint64_t len = header[1] & 0x7F; - - // Extended length - if (len == 126) { - if (client->transport->recv(&client->transport_ctx, header + 2, 2, timeout_ms) != 2) { - return -1; - } - len = ((uint16_t)header[2] << 8) | (uint8_t)header[3]; - header_len = 4; - } else if (len == 127) { - if (client->transport->recv(&client->transport_ctx, header + 2, 8, timeout_ms) != 8) { - return -1; - } - // For simplicity, we don't support 64-bit lengths - len = ((uint32_t)header[6] << 24) | ((uint32_t)header[7] << 16) | - ((uint32_t)header[8] << 8) | header[9]; - header_len = 10; - } - - // Check payload length - if (len > *payload_len) { - return -1; - } - - // Read mask (if present) - uint32_t mask = 0; - if (masked) { - if (client->transport->recv(&client->transport_ctx, header + header_len, 4, timeout_ms) != 4) { - return -1; - } - mask = ((uint32_t)header[header_len] << 24) | - ((uint32_t)header[header_len + 1] << 16) | - ((uint32_t)header[header_len + 2] << 8) | - header[header_len + 3]; - header_len += 4; - } - - // Read payload - if (len > 0) { - size_t received = 0; - while (received < len) { - int chunk = client->transport->recv(&client->transport_ctx, - payload + received, - len - received, - timeout_ms); - if (chunk <= 0) return -1; - received += chunk; - } - - // Unmask payload if needed - if (masked) { - ws_mask_payload(payload, len, mask); - } - - // Log incoming text messages to debug.log -#if defined(ENABLE_FILE_LOGGING) && defined(ENABLE_WEBSOCKET_LOGGING) - if (*opcode == WS_OPCODE_TEXT && len > 0) { - // Null terminate for logging - char temp_payload[len + 1]; - memcpy(temp_payload, payload, len); - temp_payload[len] = '\0'; - debug_log_message("RECV", client->host, client->port, temp_payload); - } -#endif - } - - *payload_len = len; - return 0; -} - -static void ws_mask_payload(char* payload, size_t len, uint32_t mask) { - unsigned char mask_bytes[4] = { - (mask >> 24) & 0xFF, - (mask >> 16) & 0xFF, - (mask >> 8) & 0xFF, - mask & 0xFF - }; - - for (size_t i = 0; i < len; i++) { - payload[i] ^= mask_bytes[i % 4]; - } -} - -static uint32_t ws_generate_mask(void) { - return ((uint32_t)rand() << 16) | ((uint32_t)rand() & 0xFFFF); -} - -// ============================================================================ -// Debug Logging Functions -// ============================================================================ - -#if defined(ENABLE_FILE_LOGGING) && defined(ENABLE_WEBSOCKET_LOGGING) -static void debug_log_init(void) { - if (!debug_log_file) { - debug_log_file = fopen("debug.log", "a"); - if (debug_log_file) { - fprintf(debug_log_file, "\n=== NOSTR WebSocket Debug Log Started ===\n"); - fflush(debug_log_file); - } - } -} - - -static const char* get_timestamp(void) { - static char timestamp[32]; - struct timespec ts; - struct tm *timeinfo; - - clock_gettime(CLOCK_REALTIME, &ts); - timeinfo = localtime(&ts.tv_sec); - - // Format: HH:MM:SS.mmm (with milliseconds) - strftime(timestamp, sizeof(timestamp), "%H:%M:%S", timeinfo); - snprintf(timestamp + 8, sizeof(timestamp) - 8, ".%03ld", ts.tv_nsec / 1000000); - - return timestamp; -} - -static void debug_log_message(const char* direction, const char* host, int port, const char* message) { - debug_log_init(); - - if (debug_log_file) { - fprintf(debug_log_file, "[%s] %s %s:%d: %s\n", - get_timestamp(), direction, host, port, message); - fflush(debug_log_file); - } -} -#endif