Release v0.6.0: embedded ESP32 support docs and external esp32_send_kind4 example

This commit is contained in:
Laan Tungir
2026-04-12 15:51:27 -04:00
parent 8c5562edb0
commit 392b863292
15 changed files with 982 additions and 66 deletions

61
CMakeLists.txt Normal file
View File

@@ -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()

View File

@@ -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

View File

@@ -1 +1 @@
0.5.16
0.6.0

View File

@@ -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=""

View File

@@ -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

View File

@@ -3,9 +3,8 @@
#include <secp256k1_ecdh.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <stddef.h>
#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;
}

View File

@@ -10,24 +10,17 @@
#include <ctype.h>
#include <time.h>
#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;

View File

@@ -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;
}

View File

@@ -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

View File

@@ -0,0 +1,20 @@
#ifndef NOSTR_PLATFORM_H
#define NOSTR_PLATFORM_H
#include <stddef.h>
#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 */

View File

@@ -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
}
// =============================================================================

View File

@@ -0,0 +1,312 @@
#include "../../nostr_core/nostr_http.h"
#include "esp_http_client.h"
#include <stdlib.h>
#include <string.h>
#include <strings.h>
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;
}

View File

@@ -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;
}

View File

@@ -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 <ctype.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <esp_transport.h>
#include <esp_transport_ssl.h>
#include <esp_transport_tcp.h>
#include <esp_transport_ws.h>
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";
}
}

28
platform/linux.c Normal file
View File

@@ -0,0 +1,28 @@
#include "../nostr_core/nostr_platform.h"
#include <fcntl.h>
#include <unistd.h>
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;
}