From 392b8632923a624e0bbc6ec3a14c0be088c86538 Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Sun, 12 Apr 2026 15:51:27 -0400 Subject: [PATCH] Release v0.6.0: embedded ESP32 support docs and external esp32_send_kind4 example --- CMakeLists.txt | 61 ++++ README.md | 26 +- VERSION | 2 +- build.sh | 1 + increment_and_push.sh | 60 +++- nostr_core/crypto/nostr_secp256k1.c | 23 +- nostr_core/nip006.c | 20 +- nostr_core/nip019.c | 2 +- nostr_core/nostr_core.h | 6 +- nostr_core/nostr_platform.h | 20 ++ nostr_core/utils.c | 18 +- platform/esp32/nostr_http_esp32.c | 312 +++++++++++++++++ platform/esp32/nostr_platform_esp32.c | 12 + platform/esp32/nostr_websocket_esp32.c | 457 +++++++++++++++++++++++++ platform/linux.c | 28 ++ 15 files changed, 982 insertions(+), 66 deletions(-) create mode 100644 CMakeLists.txt create mode 100644 nostr_core/nostr_platform.h create mode 100644 platform/esp32/nostr_http_esp32.c create mode 100644 platform/esp32/nostr_platform_esp32.c create mode 100644 platform/esp32/nostr_websocket_esp32.c create mode 100644 platform/linux.c diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..c2588145 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,61 @@ +cmake_minimum_required(VERSION 3.16) + +if(ESP_PLATFORM) + idf_component_register( + SRCS + "nostr_core/nostr_common.c" + "nostr_core/nostr_log.c" + "nostr_core/nip001.c" + "nostr_core/nip004.c" + "nostr_core/nip006.c" + "nostr_core/nip019.c" + "nostr_core/utils.c" + "nostr_core/crypto/nostr_secp256k1.c" + "nostr_core/crypto/nostr_aes.c" + "nostr_core/crypto/nostr_chacha20.c" + "cjson/cJSON.c" + "platform/esp32/nostr_platform_esp32.c" + "platform/esp32/nostr_http_esp32.c" + "platform/esp32/nostr_websocket_esp32.c" + INCLUDE_DIRS + "." + "nostr_core" + "nostr_core/crypto" + "cjson" + "nostr_websocket" + REQUIRES + secp256k1 + esp_hw_support + esp_http_client + esp-tls + tcp_transport + mbedtls + ) + + target_compile_definitions(${COMPONENT_LIB} PRIVATE NOSTR_NO_FILESYSTEM=1) +else() + project(nostr_core_lib C) + + add_library(nostr_core STATIC + nostr_core/nostr_common.c + nostr_core/nostr_log.c + nostr_core/request_validator.c + nostr_core/nip001.c + nostr_core/nip006.c + nostr_core/nip019.c + nostr_core/utils.c + nostr_core/crypto/nostr_secp256k1.c + nostr_core/crypto/nostr_aes.c + nostr_core/crypto/nostr_chacha20.c + cjson/cJSON.c + platform/linux.c + ) + + target_include_directories(nostr_core PUBLIC + . + nostr_core + nostr_core/crypto + cjson + nostr_websocket + ) +endif() diff --git a/README.md b/README.md index 5554398d..16ce765c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A C library for NOSTR protocol implementation. Work in progress. -[![Version](https://img.shields.io/badge/version-0.5.12-blue.svg)](VERSION) +[![Version](https://img.shields.io/badge/version-0.6.0-blue.svg)](VERSION) [![License](https://img.shields.io/badge/license-MIT-green.svg)](#license) [![Build Status](https://img.shields.io/badge/build-passing-brightgreen.svg)](#building) @@ -508,6 +508,20 @@ int main(void) { - **Windows** (MinGW, MSYS2) - **Embedded Systems** (resource-constrained environments) +## 🔌 Embedded (ESP32) Support + +`nostr_core_lib` now includes a platform abstraction layer and ESP32-native implementations for core networking and entropy APIs, while preserving desktop compatibility. + +Implemented embedded capabilities include: +- platform random source abstraction via `nostr_platform_random()` +- ESP32 random provider using `esp_fill_random` +- ESP32 HTTP client implementation via `esp_http_client` +- ESP32 WebSocket/WSS transport via `esp_transport_ws` + TLS certificate bundle +- NIP-04 encrypted DM flow validated on-device against public relays + +Reference ESP-IDF example project (in this workspace): +- `../esp32_send_kind4/` — Wi-Fi connect + `wss://` relay connect + send two kind-4 encrypted events + print relay responses + ## 📄 Documentation - **[POOL_API.md](POOL_API.md)** - Relay pool API notes @@ -527,20 +541,22 @@ int main(void) { ## 📈 Version History -Current version: **0.5.12** +Current version: **0.6.0** The library uses automatic semantic versioning based on Git tags. Each build increments the patch version automatically. **Recent Developments:** -- **OpenSSL Migration**: Transitioned from mbedTLS to OpenSSL for improved compatibility +- **ESP32 Embedded Enablement**: Added PAL-based embedded support directly in `nostr_core_lib` +- **ESP32 HTTP + WSS**: Added ESP-IDF-backed HTTP and secure websocket transport implementations +- **NIP-04 On-Device Validation**: Verified encrypted kind-4 DM publish flow from ESP32 to live relays - **NIP-05 Support**: DNS-based internet identifier verification - **NIP-11 Support**: Relay information document fetching and parsing - **NIP-19 Support**: Bech32-encoded entities (nsec/npub) -- **Enhanced WebSocket**: OpenSSL-based TLS WebSocket communication - **Comprehensive Testing**: Extensive test suite and error handling **Version Timeline:** -- `v0.4.x` - Current development releases with relay pool and expanded NIP support +- `v0.6.x` - Embedded-capable releases with ESP32 PAL, HTTP, and WSS support +- `v0.4.x` - Development releases with relay pool and expanded NIP support - `v0.2.x` - Earlier OpenSSL-based development releases - `v0.1.x` - Initial development releases - Focus on core protocol implementation and OpenSSL-based crypto diff --git a/VERSION b/VERSION index 3afb327e..a918a2aa 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.5.16 +0.6.0 diff --git a/build.sh b/build.sh index 21024c29..4d901d9b 100755 --- a/build.sh +++ b/build.sh @@ -509,6 +509,7 @@ SOURCES="$SOURCES nostr_core/nostr_log.c" SOURCES="$SOURCES nostr_websocket/nostr_websocket_openssl.c" SOURCES="$SOURCES nostr_core/request_validator.c" SOURCES="$SOURCES nostr_core/nostr_http.c" +SOURCES="$SOURCES platform/linux.c" NIP_DESCRIPTIONS="" diff --git a/increment_and_push.sh b/increment_and_push.sh index a9a33636..075a2408 100755 --- a/increment_and_push.sh +++ b/increment_and_push.sh @@ -1,7 +1,9 @@ #!/bin/bash # increment_and_push.sh - Version increment and git automation script -# Usage: ./increment_and_push.sh "meaningful git comment" +# Usage: +# ./increment_and_push.sh "meaningful git comment" +# ./increment_and_push.sh --set-version vX.Y.Z "meaningful git comment" set -e # Exit on error @@ -65,16 +67,30 @@ if ! git rev-parse --git-dir > /dev/null 2>&1; then exit 1 fi -# Check if we have a commit message -if [ $# -eq 0 ]; then - print_error "Usage: $0 \"meaningful git comment\"" - echo "" - echo "Example: $0 \"Add enhanced subscription functionality\"" - echo "" - exit 1 -fi +TARGET_VERSION="" +COMMIT_MESSAGE="" -COMMIT_MESSAGE="$1" +# Parse arguments +if [ "$1" = "--set-version" ]; then + if [ $# -lt 3 ]; then + print_error "Usage: $0 --set-version vX.Y.Z \"meaningful git comment\"" + echo "" + echo "Example: $0 --set-version v0.6.0 \"Release v0.6.0 with ESP32 embedded support\"" + echo "" + exit 1 + fi + TARGET_VERSION="$2" + COMMIT_MESSAGE="$3" +else + if [ $# -eq 0 ]; then + print_error "Usage: $0 \"meaningful git comment\"" + echo "" + echo "Example: $0 \"Add enhanced subscription functionality\"" + echo "" + exit 1 + fi + COMMIT_MESSAGE="$1" +fi # Check if nostr_core.h exists if [ ! -f "nostr_core/nostr_core.h" ]; then @@ -103,21 +119,37 @@ fi print_info "Current version: $CURRENT_VERSION (Major: $VERSION_MAJOR, Minor: $VERSION_MINOR, Patch: $VERSION_PATCH)" -# Increment patch version -NEW_PATCH=$((VERSION_PATCH + 1)) -NEW_VERSION="v$VERSION_MAJOR.$VERSION_MINOR.$NEW_PATCH" +if [ -n "$TARGET_VERSION" ]; then + if [[ ! "$TARGET_VERSION" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then + print_error "Invalid target version format: $TARGET_VERSION (expected vX.Y.Z)" + exit 1 + fi + + NEW_VERSION="$TARGET_VERSION" + NEW_MAJOR="${BASH_REMATCH[1]}" + NEW_MINOR="${BASH_REMATCH[2]}" + NEW_PATCH="${BASH_REMATCH[3]}" +else + # Increment patch version + NEW_MAJOR="$VERSION_MAJOR" + NEW_MINOR="$VERSION_MINOR" + NEW_PATCH=$((VERSION_PATCH + 1)) + NEW_VERSION="v$NEW_MAJOR.$NEW_MINOR.$NEW_PATCH" +fi print_info "New version will be: $NEW_VERSION" # Update version in nostr_core.h sed -i "s/#define VERSION .*/#define VERSION \"$NEW_VERSION\"/" nostr_core/nostr_core.h +sed -i "s/#define VERSION_MAJOR .*/#define VERSION_MAJOR $NEW_MAJOR/" nostr_core/nostr_core.h +sed -i "s/#define VERSION_MINOR .*/#define VERSION_MINOR $NEW_MINOR/" nostr_core/nostr_core.h sed -i "s/#define VERSION_PATCH .*/#define VERSION_PATCH $NEW_PATCH/" nostr_core/nostr_core.h print_success "Updated version in nostr_core.h" # Check if VERSION file exists and update it if [ -f "VERSION" ]; then - echo "$VERSION_MAJOR.$VERSION_MINOR.$NEW_PATCH" > VERSION + echo "$NEW_MAJOR.$NEW_MINOR.$NEW_PATCH" > VERSION print_success "Updated VERSION file" fi diff --git a/nostr_core/crypto/nostr_secp256k1.c b/nostr_core/crypto/nostr_secp256k1.c index 7ac9a4fa..eebd28bc 100644 --- a/nostr_core/crypto/nostr_secp256k1.c +++ b/nostr_core/crypto/nostr_secp256k1.c @@ -3,9 +3,8 @@ #include #include #include -#include -#include #include +#include "../nostr_platform.h" /* * PRIVATE INTERNAL FUNCTIONS - NOT EXPORTED @@ -313,23 +312,9 @@ int nostr_secp256k1_get_random_bytes(unsigned char *buf, size_t len) { return 0; } - // Try to use /dev/urandom for good randomness - int fd = open("/dev/urandom", O_RDONLY); - if (fd >= 0) { - ssize_t result = read(fd, buf, len); - close(fd); - if (result == (ssize_t)len) { - return 1; - } + if (nostr_platform_random(buf, len) != 0) { + return 0; } - - // Fallback to a simple PRNG (not cryptographically secure, but better than nothing) - // In a real implementation, you'd want to use a proper CSPRNG - static unsigned long seed = 1; - for (size_t i = 0; i < len; i++) { - seed = seed * 1103515245 + 12345; - buf[i] = (unsigned char)(seed >> 16); - } - + return 1; } diff --git a/nostr_core/nip006.c b/nostr_core/nip006.c index 26672c8d..691fffcf 100644 --- a/nostr_core/nip006.c +++ b/nostr_core/nip006.c @@ -10,24 +10,17 @@ #include #include #include "../nostr_core/nostr_common.h" +#include "nostr_platform.h" int nostr_generate_keypair(unsigned char* private_key, unsigned char* public_key) { if (!private_key || !public_key) { return NOSTR_ERROR_INVALID_INPUT; } - // Generate random entropy using /dev/urandom - FILE* urandom = fopen("/dev/urandom", "rb"); - if (!urandom) { + if (nostr_platform_random(private_key, 32) != 0) { return NOSTR_ERROR_IO_FAILED; } - if (fread(private_key, 1, 32, urandom) != 32) { - fclose(urandom); - return NOSTR_ERROR_IO_FAILED; - } - fclose(urandom); - // Validate private key if (nostr_ec_private_key_verify(private_key) != 0) { return NOSTR_ERROR_CRYPTO_FAILED; @@ -50,17 +43,10 @@ int nostr_generate_mnemonic_and_keys(char* mnemonic, size_t mnemonic_size, // Generate entropy for 12-word mnemonic unsigned char entropy[16]; - FILE* urandom = fopen("/dev/urandom", "rb"); - if (!urandom) { + if (nostr_platform_random(entropy, sizeof(entropy)) != 0) { return NOSTR_ERROR_IO_FAILED; } - if (fread(entropy, 1, sizeof(entropy), urandom) != sizeof(entropy)) { - fclose(urandom); - return NOSTR_ERROR_IO_FAILED; - } - fclose(urandom); - // Generate mnemonic from entropy if (nostr_bip39_mnemonic_from_bytes(entropy, sizeof(entropy), mnemonic) != 0) { return NOSTR_ERROR_CRYPTO_FAILED; diff --git a/nostr_core/nip019.c b/nostr_core/nip019.c index a538ed7b..12ca5da5 100644 --- a/nostr_core/nip019.c +++ b/nostr_core/nip019.c @@ -180,7 +180,7 @@ nostr_input_type_t nostr_detect_input_type(const char* input) { if (len == 64) { int is_hex = 1; for (size_t i = 0; i < len; i++) { - if (!isxdigit(input[i])) { + if (!isxdigit((unsigned char)input[i])) { is_hex = 0; break; } diff --git a/nostr_core/nostr_core.h b/nostr_core/nostr_core.h index 19f3efdd..f65be93c 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.5.16" +#define VERSION "v0.6.0" #define VERSION_MAJOR 0 -#define VERSION_MINOR 5 -#define VERSION_PATCH 16 +#define VERSION_MINOR 6 +#define VERSION_PATCH 0 /* * NOSTR Core Library - Complete API Reference diff --git a/nostr_core/nostr_platform.h b/nostr_core/nostr_platform.h new file mode 100644 index 00000000..705c0866 --- /dev/null +++ b/nostr_core/nostr_platform.h @@ -0,0 +1,20 @@ +#ifndef NOSTR_PLATFORM_H +#define NOSTR_PLATFORM_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * Fill buffer with cryptographically secure random bytes. + * Returns 0 on success, -1 on failure. + */ +int nostr_platform_random(unsigned char *buf, size_t len); + +#ifdef __cplusplus +} +#endif + +#endif /* NOSTR_PLATFORM_H */ diff --git a/nostr_core/utils.c b/nostr_core/utils.c index 06545930..46b80ef2 100644 --- a/nostr_core/utils.c +++ b/nostr_core/utils.c @@ -433,32 +433,38 @@ int nostr_sha256_final(nostr_sha256_ctx_t* ctx, unsigned char* hash) { } int nostr_sha256_file_stream(const char* filename, unsigned char* hash) { +#ifndef NOSTR_NO_FILESYSTEM if (!filename || !hash) return -1; - + FILE* file = fopen(filename, "rb"); if (!file) return -1; - + nostr_sha256_ctx_t ctx; if (nostr_sha256_init(&ctx) != 0) { fclose(file); return -1; } - + // Process file in 4KB chunks for memory efficiency unsigned char buffer[4096]; size_t bytes_read; - + while ((bytes_read = fread(buffer, 1, sizeof(buffer), file)) > 0) { if (nostr_sha256_update(&ctx, buffer, bytes_read) != 0) { fclose(file); return -1; } } - + fclose(file); - + // Finalize and return result return nostr_sha256_final(&ctx, hash); +#else + (void)filename; + (void)hash; + return -1; +#endif } // ============================================================================= diff --git a/platform/esp32/nostr_http_esp32.c b/platform/esp32/nostr_http_esp32.c new file mode 100644 index 00000000..5ebbd066 --- /dev/null +++ b/platform/esp32/nostr_http_esp32.c @@ -0,0 +1,312 @@ +#include "../../nostr_core/nostr_http.h" + +#include "esp_http_client.h" + +#include +#include +#include + +typedef struct { + char* data; + size_t len; + size_t cap; + size_t max_bytes; + int truncated; +} nostr_http_buffer_t; + +static char g_ca_bundle_path[512] = {0}; + +static char* nostr_http_strdup_local(const char* s) { + if (!s) return NULL; + size_t n = strlen(s); + char* out = (char*)malloc(n + 1U); + if (!out) return NULL; + memcpy(out, s, n + 1U); + return out; +} + +static int append_bytes(nostr_http_buffer_t* b, const void* src, size_t n) { + if (!b || !src || n == 0) return 1; + + if (b->max_bytes > 0 && b->len >= b->max_bytes) { + b->truncated = 1; + return 1; + } + + size_t to_copy = n; + if (b->max_bytes > 0) { + size_t remaining = b->max_bytes - b->len; + if (to_copy > remaining) { + to_copy = remaining; + b->truncated = 1; + } + } + + if (b->len + to_copy + 1U > b->cap) { + size_t new_cap = b->cap == 0 ? 1024U : b->cap; + while (new_cap < b->len + to_copy + 1U) { + new_cap *= 2U; + } + char* p = (char*)realloc(b->data, new_cap); + if (!p) return 0; + b->data = p; + b->cap = new_cap; + } + + memcpy(b->data + b->len, src, to_copy); + b->len += to_copy; + b->data[b->len] = '\0'; + return 1; +} + +typedef struct { + nostr_http_buffer_t* body; + nostr_http_buffer_t* headers; + int capture_headers; +} nostr_http_event_ctx_t; + +static esp_err_t http_event_handler(esp_http_client_event_t* evt) { + if (!evt || !evt->user_data) return ESP_OK; + + nostr_http_event_ctx_t* ctx = (nostr_http_event_ctx_t*)evt->user_data; + + switch (evt->event_id) { + case HTTP_EVENT_ON_DATA: + if (evt->data && evt->data_len > 0 && ctx->body) { + if (!append_bytes(ctx->body, evt->data, (size_t)evt->data_len)) { + return ESP_FAIL; + } + } + break; + + case HTTP_EVENT_ON_HEADER: + if (ctx->capture_headers && ctx->headers && evt->header_key && evt->header_value) { + if (!append_bytes(ctx->headers, evt->header_key, strlen(evt->header_key)) || + !append_bytes(ctx->headers, ": ", 2) || + !append_bytes(ctx->headers, evt->header_value, strlen(evt->header_value)) || + !append_bytes(ctx->headers, "\r\n", 2)) { + return ESP_FAIL; + } + } + break; + + default: + break; + } + + return ESP_OK; +} + +void nostr_http_set_ca_bundle(const char* ca_bundle_path) { + if (!ca_bundle_path || ca_bundle_path[0] == '\0') { + g_ca_bundle_path[0] = '\0'; + return; + } + + strncpy(g_ca_bundle_path, ca_bundle_path, sizeof(g_ca_bundle_path) - 1); + g_ca_bundle_path[sizeof(g_ca_bundle_path) - 1] = '\0'; +} + +const char* nostr_http_detect_ca_bundle(void) { + if (g_ca_bundle_path[0] != '\0') { + return g_ca_bundle_path; + } + return NULL; +} + +int nostr_http_request(const nostr_http_request_t* req, nostr_http_response_t* resp) { + if (!req || !req->url || !resp) return NOSTR_ERROR_INVALID_INPUT; + + memset(resp, 0, sizeof(*resp)); + + const char* method = (req->method && req->method[0] != '\0') ? req->method : "GET"; + int timeout_ms = (req->timeout_seconds > 0 ? req->timeout_seconds : 30) * 1000; + + nostr_http_buffer_t body = {0}; + body.max_bytes = req->max_response_bytes; + + nostr_http_buffer_t headers = {0}; + + nostr_http_event_ctx_t evt_ctx = { + .body = &body, + .headers = &headers, + .capture_headers = req->capture_headers + }; + + esp_http_client_config_t cfg = { + .url = req->url, + .timeout_ms = timeout_ms, + .event_handler = http_event_handler, + .user_data = &evt_ctx, + .disable_auto_redirect = req->follow_redirects ? false : true, + .max_redirection_count = req->max_redirects > 0 ? req->max_redirects : 3, + }; + + esp_http_client_handle_t client = esp_http_client_init(&cfg); + if (!client) { + return NOSTR_ERROR_NETWORK_FAILED; + } + + if (req->user_agent && req->user_agent[0] != '\0') { + esp_http_client_set_header(client, "User-Agent", req->user_agent); + } else { + esp_http_client_set_header(client, "User-Agent", "nostr-core/1.0"); + } + + if (req->headers) { + for (const char** h = req->headers; *h; h++) { + const char* line = *h; + if (!line || line[0] == '\0') continue; + const char* colon = strchr(line, ':'); + if (!colon) continue; + + size_t key_len = (size_t)(colon - line); + if (key_len == 0) continue; + + char* key = (char*)malloc(key_len + 1U); + if (!key) { + esp_http_client_cleanup(client); + free(body.data); + free(headers.data); + return NOSTR_ERROR_MEMORY_FAILED; + } + + memcpy(key, line, key_len); + key[key_len] = '\0'; + + const char* value = colon + 1; + while (*value == ' ') value++; + + esp_http_client_set_header(client, key, value); + free(key); + } + } + + if (strcasecmp(method, "GET") == 0) { + esp_http_client_set_method(client, HTTP_METHOD_GET); + } else if (strcasecmp(method, "POST") == 0) { + esp_http_client_set_method(client, HTTP_METHOD_POST); + if (req->body && req->body_len > 0) { + esp_http_client_set_post_field(client, (const char*)req->body, (int)req->body_len); + } else { + esp_http_client_set_post_field(client, "", 0); + } + } else if (strcasecmp(method, "HEAD") == 0) { + esp_http_client_set_method(client, HTTP_METHOD_HEAD); + } else if (strcasecmp(method, "PUT") == 0) { + esp_http_client_set_method(client, HTTP_METHOD_PUT); + if (req->body && req->body_len > 0) { + esp_http_client_set_post_field(client, (const char*)req->body, (int)req->body_len); + } + } else if (strcasecmp(method, "DELETE") == 0) { + esp_http_client_set_method(client, HTTP_METHOD_DELETE); + } else { + esp_http_client_cleanup(client); + return NOSTR_ERROR_INVALID_INPUT; + } + + esp_err_t err = esp_http_client_perform(client); + if (err != ESP_OK) { + esp_http_client_cleanup(client); + free(body.data); + free(headers.data); + return NOSTR_ERROR_NETWORK_FAILED; + } + + resp->status_code = esp_http_client_get_status_code(client); + resp->body = body.data ? body.data : nostr_http_strdup_local(""); + resp->body_len = body.data ? body.len : 0; + resp->headers_raw = headers.data; + resp->truncated = body.truncated; + + char* ct = NULL; + char* ct_raw = NULL; + if (esp_http_client_get_header(client, "Content-Type", &ct_raw) == ESP_OK && ct_raw && ct_raw[0] != '\0') { + ct = nostr_http_strdup_local(ct_raw); + } + resp->content_type = ct; + + esp_http_client_cleanup(client); + + if (!resp->body) { + free(resp->headers_raw); + free(resp->content_type); + memset(resp, 0, sizeof(*resp)); + return NOSTR_ERROR_MEMORY_FAILED; + } + + return NOSTR_SUCCESS; +} + +void nostr_http_response_free(nostr_http_response_t* resp) { + if (!resp) return; + free(resp->body); + free(resp->content_type); + free(resp->headers_raw); + memset(resp, 0, sizeof(*resp)); +} + +int nostr_http_get(const char* url, int timeout_seconds, char** body_out, long* status_out) { + if (!url || !body_out) return NOSTR_ERROR_INVALID_INPUT; + + *body_out = NULL; + if (status_out) *status_out = 0; + + nostr_http_request_t req; + memset(&req, 0, sizeof(req)); + req.method = "GET"; + req.url = url; + req.timeout_seconds = timeout_seconds; + req.follow_redirects = 1; + req.max_redirects = 3; + + nostr_http_response_t resp; + int rc = nostr_http_request(&req, &resp); + if (rc != NOSTR_SUCCESS) return rc; + + if (status_out) *status_out = resp.status_code; + *body_out = resp.body; + free(resp.content_type); + free(resp.headers_raw); + return NOSTR_SUCCESS; +} + +int nostr_http_post_json(const char* url, + const char* json_body, + int timeout_seconds, + char** body_out, + long* status_out) { + if (!url || !body_out) return NOSTR_ERROR_INVALID_INPUT; + + *body_out = NULL; + if (status_out) *status_out = 0; + + const char* headers[] = { + "Accept: application/json", + "Content-Type: application/json", + NULL + }; + + const char* payload = json_body ? json_body : "{}"; + + nostr_http_request_t req; + memset(&req, 0, sizeof(req)); + req.method = "POST"; + req.url = url; + req.headers = headers; + req.body = (const unsigned char*)payload; + req.body_len = strlen(payload); + req.timeout_seconds = timeout_seconds; + req.follow_redirects = 1; + req.max_redirects = 3; + + nostr_http_response_t resp; + int rc = nostr_http_request(&req, &resp); + if (rc != NOSTR_SUCCESS) return rc; + + if (status_out) *status_out = resp.status_code; + *body_out = resp.body; + free(resp.content_type); + free(resp.headers_raw); + return NOSTR_SUCCESS; +} diff --git a/platform/esp32/nostr_platform_esp32.c b/platform/esp32/nostr_platform_esp32.c new file mode 100644 index 00000000..1192ce1b --- /dev/null +++ b/platform/esp32/nostr_platform_esp32.c @@ -0,0 +1,12 @@ +#include "../../nostr_core/nostr_platform.h" + +#include "esp_random.h" + +int nostr_platform_random(unsigned char *buf, size_t len) { + if (buf == NULL || len == 0) { + return -1; + } + + esp_fill_random(buf, len); + return 0; +} diff --git a/platform/esp32/nostr_websocket_esp32.c b/platform/esp32/nostr_websocket_esp32.c new file mode 100644 index 00000000..35599915 --- /dev/null +++ b/platform/esp32/nostr_websocket_esp32.c @@ -0,0 +1,457 @@ +#include "../../nostr_websocket/nostr_websocket_tls.h" + +#include "esp_crt_bundle.h" +#include "esp_err.h" +#include "esp_log.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +typedef struct { + char* host; + int port; + char* path; + int use_tls; +} ws_url_parts_t; + +struct nostr_ws_client { + esp_transport_list_handle_t list; + esp_transport_handle_t transport; + nostr_ws_state_t state; + int timeout_ms; + char* host; + int port; + char* path; +}; + +static void ws_free_url_parts(ws_url_parts_t* p) { + if (!p) return; + free(p->host); + free(p->path); + p->host = NULL; + p->path = NULL; + p->port = 0; + p->use_tls = 0; +} + +static int ws_parse_url(const char* url, ws_url_parts_t* out) { + if (!url || !out) return -1; + + memset(out, 0, sizeof(*out)); + + const char* p = NULL; + if (strncmp(url, "ws://", 5) == 0) { + out->use_tls = 0; + out->port = 80; + p = url + 5; + } else if (strncmp(url, "wss://", 6) == 0) { + out->use_tls = 1; + out->port = 443; + p = url + 6; + } else { + return -1; + } + + const char* slash = strchr(p, '/'); + const char* host_end = slash ? slash : (p + strlen(p)); + + const char* colon = NULL; + for (const char* it = p; it < host_end; ++it) { + if (*it == ':') { + colon = it; + break; + } + } + + size_t host_len; + if (colon) { + host_len = (size_t)(colon - p); + char portbuf[8] = {0}; + size_t port_len = (size_t)(host_end - colon - 1); + if (port_len == 0 || port_len >= sizeof(portbuf)) return -1; + memcpy(portbuf, colon + 1, port_len); + for (size_t i = 0; i < port_len; i++) { + if (!isdigit((unsigned char)portbuf[i])) return -1; + } + out->port = atoi(portbuf); + if (out->port <= 0 || out->port > 65535) return -1; + } else { + host_len = (size_t)(host_end - p); + } + + if (host_len == 0) return -1; + + out->host = (char*)malloc(host_len + 1U); + if (!out->host) return -1; + memcpy(out->host, p, host_len); + out->host[host_len] = '\0'; + + out->path = slash ? strdup(slash) : strdup("/"); + if (!out->path) { + ws_free_url_parts(out); + return -1; + } + + return 0; +} + +static void ws_client_destroy(struct nostr_ws_client* c) { + if (!c) return; + + if (c->transport) { + esp_transport_close(c->transport); + c->transport = NULL; + } + + if (c->list) { + esp_transport_list_destroy(c->list); + c->list = NULL; + } + + free(c->host); + free(c->path); + free(c); +} + +static esp_transport_handle_t ws_build_transport(const ws_url_parts_t* up, + esp_transport_list_handle_t list) { + if (!up || !list) return NULL; + + if (up->use_tls) { + esp_transport_handle_t ssl = esp_transport_ssl_init(); + if (!ssl) return NULL; + + esp_transport_ssl_crt_bundle_attach(ssl, esp_crt_bundle_attach); + esp_transport_set_default_port(ssl, 443); + if (esp_transport_list_add(list, ssl, "ssl") != ESP_OK) { + esp_transport_destroy(ssl); + return NULL; + } + + esp_transport_handle_t wss = esp_transport_ws_init(ssl); + if (!wss) return NULL; + + esp_transport_set_default_port(wss, up->port); + esp_transport_ws_set_path(wss, up->path); + if (esp_transport_ws_set_user_agent(wss, "nostr_core_lib/esp32") != ESP_OK) { + esp_transport_destroy(wss); + return NULL; + } + if (esp_transport_list_add(list, wss, "wss") != ESP_OK) { + esp_transport_destroy(wss); + return NULL; + } + return wss; + } + + esp_transport_handle_t tcp = esp_transport_tcp_init(); + if (!tcp) return NULL; + + esp_transport_set_default_port(tcp, 80); + if (esp_transport_list_add(list, tcp, "tcp") != ESP_OK) { + esp_transport_destroy(tcp); + return NULL; + } + + esp_transport_handle_t ws = esp_transport_ws_init(tcp); + if (!ws) return NULL; + + esp_transport_set_default_port(ws, up->port); + esp_transport_ws_set_path(ws, up->path); + if (esp_transport_ws_set_user_agent(ws, "nostr_core_lib/esp32") != ESP_OK) { + esp_transport_destroy(ws); + return NULL; + } + if (esp_transport_list_add(list, ws, "ws") != ESP_OK) { + esp_transport_destroy(ws); + return NULL; + } + + return ws; +} + +nostr_ws_client_t* nostr_ws_connect(const char* url) { + if (!url) return NULL; + + ws_url_parts_t up; + if (ws_parse_url(url, &up) != 0) return NULL; + + struct nostr_ws_client* c = (struct nostr_ws_client*)calloc(1, sizeof(struct nostr_ws_client)); + if (!c) { + ws_free_url_parts(&up); + return NULL; + } + + c->list = esp_transport_list_init(); + if (!c->list) { + ws_free_url_parts(&up); + free(c); + return NULL; + } + + c->transport = ws_build_transport(&up, c->list); + if (!c->transport) { + ws_free_url_parts(&up); + ws_client_destroy(c); + return NULL; + } + + c->timeout_ms = 30000; + c->state = NOSTR_WS_CONNECTING; + c->host = up.host; + c->port = up.port; + c->path = up.path; + + up.host = NULL; + up.path = NULL; + + int cr = esp_transport_connect(c->transport, c->host, c->port, c->timeout_ms); + if (cr < 0) { + c->state = NOSTR_WS_ERROR; + ws_client_destroy(c); + ws_free_url_parts(&up); + return NULL; + } + + int hs = esp_transport_ws_get_upgrade_request_status(c->transport); + if (hs != 101) { + c->state = NOSTR_WS_ERROR; + ws_client_destroy(c); + ws_free_url_parts(&up); + return NULL; + } + + c->state = NOSTR_WS_CONNECTED; + ws_free_url_parts(&up); + return c; +} + +int nostr_ws_close(nostr_ws_client_t* client) { + if (!client) return NOSTR_WS_ERROR_INVALID; + + if (client->state == NOSTR_WS_CONNECTED) { + (void)esp_transport_ws_send_raw(client->transport, + (ws_transport_opcodes_t)(WS_TRANSPORT_OPCODES_CLOSE | WS_TRANSPORT_OPCODES_FIN), + NULL, + 0, + client->timeout_ms); + } + + client->state = NOSTR_WS_CLOSED; + ws_client_destroy(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; + } + + int len = (int)strlen(message); + int wr = esp_transport_ws_send_raw(client->transport, + (ws_transport_opcodes_t)(WS_TRANSPORT_OPCODES_TEXT | WS_TRANSPORT_OPCODES_FIN), + message, + len, + client->timeout_ms); + return (wr == len) ? NOSTR_WS_SUCCESS : NOSTR_WS_ERROR_NETWORK; +} + +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; + if (client->state != NOSTR_WS_CONNECTED && client->state != NOSTR_WS_CLOSING) { + return NOSTR_WS_ERROR_INVALID; + } + + int tmo = (timeout_ms > 0) ? timeout_ms : client->timeout_ms; + int n = esp_transport_read(client->transport, buffer, (int)buffer_size - 1, tmo); + if (n < 0) { + return NOSTR_WS_ERROR_NETWORK; + } + if (n == 0) { + /* timeout or internally-handled control frame; not a hard network failure */ + buffer[0] = '\0'; + return 0; + } + + ws_transport_opcodes_t opcode = esp_transport_ws_get_read_opcode(client->transport); + + if (opcode == WS_TRANSPORT_OPCODES_TEXT || opcode == WS_TRANSPORT_OPCODES_BINARY) { + buffer[n] = '\0'; + return n; + } + + if (opcode == WS_TRANSPORT_OPCODES_PING) { + (void)esp_transport_ws_send_raw(client->transport, + (ws_transport_opcodes_t)(WS_TRANSPORT_OPCODES_PONG | WS_TRANSPORT_OPCODES_FIN), + buffer, + n, + tmo); + return nostr_ws_receive(client, buffer, buffer_size, timeout_ms); + } + + if (opcode == WS_TRANSPORT_OPCODES_PONG) { + buffer[n] = '\0'; + return n; + } + + if (opcode == WS_TRANSPORT_OPCODES_CLOSE) { + client->state = NOSTR_WS_CLOSING; + buffer[0] = '\0'; + return 0; + } + + 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; + } + + int wr = esp_transport_ws_send_raw(client->transport, + (ws_transport_opcodes_t)(WS_TRANSPORT_OPCODES_PING | WS_TRANSPORT_OPCODES_FIN), + "ping", + 4, + client->timeout_ms); + return (wr == 4) ? NOSTR_WS_SUCCESS : NOSTR_WS_ERROR_NETWORK; +} + +int nostr_ws_set_timeout(nostr_ws_client_t* client, int timeout_ms) { + if (!client || timeout_ms < 0) return NOSTR_WS_ERROR_INVALID; + client->timeout_ms = timeout_ms; + return NOSTR_WS_SUCCESS; +} + +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; + } + + cJSON* req_array = cJSON_CreateArray(); + cJSON_AddItemToArray(req_array, cJSON_CreateString("REQ")); + cJSON_AddItemToArray(req_array, cJSON_CreateString(subscription_id)); + + 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_PrintUnformatted(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; + } + + cJSON* event_array = cJSON_CreateArray(); + cJSON_AddItemToArray(event_array, cJSON_CreateString("EVENT")); + cJSON_AddItemToArray(event_array, cJSON_Duplicate(event, 1)); + + char* event_string = cJSON_PrintUnformatted(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; + } + + cJSON* close_array = cJSON_CreateArray(); + cJSON_AddItemToArray(close_array, cJSON_CreateString("CLOSE")); + cJSON_AddItemToArray(close_array, cJSON_CreateString(subscription_id)); + + char* close_string = cJSON_PrintUnformatted(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)) { + cJSON_Delete(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); + if (!*message_type) { + cJSON_Delete(json); + return NOSTR_WS_ERROR_MEMORY; + } + + *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"; + } +} diff --git a/platform/linux.c b/platform/linux.c new file mode 100644 index 00000000..af713ed7 --- /dev/null +++ b/platform/linux.c @@ -0,0 +1,28 @@ +#include "../nostr_core/nostr_platform.h" + +#include +#include + +int nostr_platform_random(unsigned char *buf, size_t len) { + if (buf == NULL || len == 0) { + return -1; + } + + int fd = open("/dev/urandom", O_RDONLY); + if (fd < 0) { + return -1; + } + + size_t total = 0; + while (total < len) { + ssize_t n = read(fd, buf + total, len - total); + if (n <= 0) { + close(fd); + return -1; + } + total += (size_t)n; + } + + close(fd); + return 0; +}