Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
693e21423e | ||
|
|
654bdd2186 | ||
|
|
13ad24b676 | ||
|
|
392b863292 | ||
|
|
8c5562edb0 | ||
|
|
ef8e68b952 | ||
|
|
25bdce7337 | ||
|
|
b3110218fc |
@@ -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()
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
A C library for NOSTR protocol implementation. Work in progress.
|
||||
|
||||
[](VERSION)
|
||||
[](VERSION)
|
||||
[](#license)
|
||||
[](#building)
|
||||
|
||||
@@ -12,11 +12,11 @@ A C library for NOSTR protocol implementation. Work in progress.
|
||||
### Core Protocol NIPs
|
||||
- [x] [NIP-01](nips/01.md) - Basic protocol flow - event creation, signing, and validation
|
||||
- [ ] [NIP-02](nips/02.md) - Contact List and Petnames
|
||||
- [ ] [NIP-03](nips/03.md) - OpenTimestamps Attestations for Events
|
||||
- [x] [NIP-03](nips/03.md) - OpenTimestamps Attestations for Events
|
||||
- [x] [NIP-04](nips/04.md) - Encrypted Direct Messages (legacy)
|
||||
- [x] [NIP-05](nips/05.md) - Mapping Nostr keys to DNS-based internet identifiers
|
||||
- [x] [NIP-06](nips/06.md) - Basic key derivation from mnemonic seed phrase
|
||||
- [ ] [NIP-07](nips/07.md) - `window.nostr` capability for web browsers
|
||||
- [-] [NIP-07](nips/07.md) - `window.nostr` capability for web browsers
|
||||
- [ ] [NIP-08](nips/08.md) - Handling Mentions
|
||||
- [ ] [NIP-09](nips/09.md) - Event Deletion
|
||||
- [ ] [NIP-10](nips/10.md) - Conventions for clients' use of `e` and `p` tags in text events
|
||||
@@ -62,7 +62,7 @@ A C library for NOSTR protocol implementation. Work in progress.
|
||||
- [ ] [NIP-52](nips/52.md) - Calendar Events
|
||||
- [ ] [NIP-53](nips/53.md) - Live Activities
|
||||
- [ ] [NIP-54](nips/54.md) - Wiki
|
||||
- [ ] [NIP-55](nips/55.md) - Android Signer Application
|
||||
- [-] [NIP-55](nips/55.md) - Android Signer Application
|
||||
- [ ] [NIP-56](nips/56.md) - Reporting
|
||||
- [ ] [NIP-57](nips/57.md) - Lightning Zaps
|
||||
- [ ] [NIP-58](nips/58.md) - Badges
|
||||
@@ -70,10 +70,10 @@ A C library for NOSTR protocol implementation. Work in progress.
|
||||
- [ ] [NIP-60](nips/60.md) - Cashu Wallet
|
||||
- [ ] [NIP-61](nips/61.md) - Nutzaps
|
||||
- [ ] [NIP-62](nips/62.md) - Log events
|
||||
- [ ] [NIP-64](nips/64.md) - Chess (PGN)
|
||||
- [-] [NIP-64](nips/64.md) - Chess (PGN)
|
||||
- [ ] [NIP-65](nips/65.md) - Relay List Metadata
|
||||
- [ ] [NIP-66](nips/66.md) - Relay Monitor
|
||||
- [ ] [NIP-68](nips/68.md) - Web badges
|
||||
- [-] [NIP-68](nips/68.md) - Web badges
|
||||
- [ ] [NIP-69](nips/69.md) - Peer-to-peer Order events
|
||||
- [ ] [NIP-70](nips/70.md) - Protected Events
|
||||
- [ ] [NIP-71](nips/71.md) - Video Events
|
||||
@@ -85,7 +85,7 @@ A C library for NOSTR protocol implementation. Work in progress.
|
||||
- [ ] [NIP-84](nips/84.md) - Highlights
|
||||
- [ ] [NIP-86](nips/86.md) - Relay Management API
|
||||
- [ ] [NIP-87](nips/87.md) - Relay List Recommendations
|
||||
- [ ] [NIP-88](nips/88.md) - Stella: A Stellar Relay
|
||||
- [-] [NIP-88](nips/88.md) - Stella: A Stellar Relay
|
||||
- [ ] [NIP-89](nips/89.md) - Recommended Application Handlers
|
||||
- [ ] [NIP-90](nips/90.md) - Data Vending Machines
|
||||
- [ ] [NIP-92](nips/92.md) - Media Attachments
|
||||
@@ -94,7 +94,7 @@ A C library for NOSTR protocol implementation. Work in progress.
|
||||
- [ ] [NIP-98](nips/98.md) - HTTP Auth
|
||||
- [ ] [NIP-99](nips/99.md) - Classified Listings
|
||||
|
||||
**Legend:** ✅ Fully Implemented | ⚠️ Partial Implementation | ❌ Not Implemented
|
||||
**Legend:** ✅ Fully Implemented | ⚠️ Partial Implementation | ❌ Not Implemented | ➖ Not Applicable
|
||||
|
||||
**Implementation Summary:** 13 of 96+ NIPs fully implemented (13.5%)
|
||||
|
||||
@@ -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.4.8**
|
||||
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
|
||||
@@ -580,7 +596,7 @@ gcc your_code.c ./libnostr_core_x64.a -lz -ldl -lpthread -lm -lssl -lcrypto -lcu
|
||||
### Getting Help
|
||||
|
||||
- Check the `examples/` directory for working code
|
||||
- Run `./build.sh test` to verify your environment
|
||||
- Run `./build.sh -t` to verify your environment
|
||||
- Review the comprehensive API documentation in `nostr_core/nostr_core.h`
|
||||
|
||||
## 📜 License
|
||||
|
||||
@@ -135,6 +135,7 @@ if [ "$HELP" = true ]; then
|
||||
echo ""
|
||||
echo "Available NIPs:"
|
||||
echo " 001 - Basic Protocol (event creation, signing)"
|
||||
echo " 003 - OpenTimestamps"
|
||||
echo " 004 - Encryption (legacy)"
|
||||
echo " 005 - DNS-based identifiers"
|
||||
echo " 006 - Key derivation from mnemonic"
|
||||
@@ -197,7 +198,7 @@ print_info "Auto-detecting needed NIPs from your source code..."
|
||||
NEEDED_NIPS=""
|
||||
if [ -n "$FORCE_NIPS" ]; then
|
||||
if [ "$FORCE_NIPS" = "all" ]; then
|
||||
NEEDED_NIPS="001 004 005 006 011 013 017 019 042 044 046 059 060 061"
|
||||
NEEDED_NIPS="001 003 004 005 006 011 013 017 019 021 042 044 046 059 060 061"
|
||||
print_info "Forced: Building all available NIPs"
|
||||
else
|
||||
# Convert comma-separated list to space-separated with 3-digit format
|
||||
@@ -216,7 +217,7 @@ else
|
||||
# Check for nostr_core.h (includes everything)
|
||||
if grep -q '#include[[:space:]]*["\<]nostr_core\.h["\>]' *.c 2>/dev/null; then
|
||||
print_info "Found #include \"nostr_core.h\" - building all NIPs"
|
||||
NEEDED_NIPS="001 004 005 006 011 013 019 042 044 046 059 060 061"
|
||||
NEEDED_NIPS="001 003 004 005 006 011 013 019 021 042 044 046 059 060 061"
|
||||
elif [ -n "$DETECTED" ]; then
|
||||
NEEDED_NIPS="$DETECTED"
|
||||
print_success "Auto-detected NIPs: $(echo $NEEDED_NIPS | tr ' ' ',')"
|
||||
@@ -234,7 +235,7 @@ fi
|
||||
|
||||
# If building tests or examples, include all NIPs to ensure compatibility
|
||||
if ([ "$BUILD_TESTS" = true ] || [ "$BUILD_EXAMPLES" = true ]) && [ -z "$FORCE_NIPS" ]; then
|
||||
NEEDED_NIPS="001 004 005 006 011 013 017 019 021 042 044 046 059 060 061"
|
||||
NEEDED_NIPS="001 003 004 005 006 011 013 017 019 021 042 044 046 059 060 061"
|
||||
print_info "Building tests/examples - including all available NIPs for compatibility"
|
||||
fi
|
||||
|
||||
@@ -499,6 +500,8 @@ detect_system_curl
|
||||
SOURCES="nostr_core/crypto/nostr_secp256k1.c"
|
||||
SOURCES="$SOURCES nostr_core/crypto/nostr_aes.c"
|
||||
SOURCES="$SOURCES nostr_core/crypto/nostr_chacha20.c"
|
||||
SOURCES="$SOURCES nostr_core/crypto/nostr_poly1305.c"
|
||||
SOURCES="$SOURCES nostr_core/crypto/nostr_chacha20poly1305.c"
|
||||
SOURCES="$SOURCES cjson/cJSON.c"
|
||||
SOURCES="$SOURCES nostr_core/utils.c"
|
||||
SOURCES="$SOURCES nostr_core/nostr_common.c"
|
||||
@@ -507,6 +510,8 @@ SOURCES="$SOURCES nostr_core/core_relay_pool.c"
|
||||
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=""
|
||||
|
||||
@@ -516,6 +521,7 @@ for nip in $NEEDED_NIPS; do
|
||||
SOURCES="$SOURCES $NIP_FILE"
|
||||
case $nip in
|
||||
001) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-001(Basic)" ;;
|
||||
003) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-003(OpenTimestamps)" ;;
|
||||
004) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-004(Encrypt)" ;;
|
||||
005) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-005(DNS)" ;;
|
||||
006) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-006(Keys)" ;;
|
||||
@@ -540,6 +546,8 @@ if echo "$NEEDED_NIPS" | grep -Eq '(^| )060( |$)|(^| )061( |$)'; then
|
||||
SOURCES="$SOURCES nostr_core/cashu_mint.c"
|
||||
fi
|
||||
|
||||
SOURCES="$SOURCES nostr_core/blossom_client.c"
|
||||
|
||||
# Build flags
|
||||
CFLAGS="-Wall -Wextra -std=c99 -fPIC -O2"
|
||||
CFLAGS="$CFLAGS -DENABLE_FILE_LOGGING -DENABLE_WEBSOCKET_LOGGING -DENABLE_DEBUG_LOGGING"
|
||||
|
||||
+46
-14
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,513 @@
|
||||
/*
|
||||
* Blossom HTTP Client
|
||||
*/
|
||||
|
||||
#include "blossom_client.h"
|
||||
|
||||
#include "../cjson/cJSON.h"
|
||||
#include "nip001.h"
|
||||
#include "nostr_http.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
static int is_hex64_local(const char* s) {
|
||||
if (!s || strlen(s) != 64) return 0;
|
||||
for (int i = 0; i < 64; i++) {
|
||||
char c = s[i];
|
||||
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void trim_trailing_slash_local(const char* in, char* out, size_t out_sz) {
|
||||
if (!in || !out || out_sz == 0) return;
|
||||
snprintf(out, out_sz, "%s", in);
|
||||
size_t n = strlen(out);
|
||||
while (n > 0 && out[n - 1] == '/') {
|
||||
out[n - 1] = '\0';
|
||||
n--;
|
||||
}
|
||||
}
|
||||
|
||||
static int build_blob_url_local(const char* server_url, const char* sha256_hex, char* out, size_t out_sz) {
|
||||
if (!server_url || !sha256_hex || !out || out_sz == 0) return -1;
|
||||
char base[512];
|
||||
trim_trailing_slash_local(server_url, base, sizeof(base));
|
||||
int n = snprintf(out, out_sz, "%s/%s", base, sha256_hex);
|
||||
if (n < 0 || (size_t)n >= out_sz) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_descriptor_local(const char* json_text, blossom_blob_descriptor_t* out) {
|
||||
if (!json_text || !out) return NOSTR_ERROR_INVALID_INPUT;
|
||||
cJSON* root = cJSON_Parse(json_text);
|
||||
if (!root) return NOSTR_ERROR_IO_FAILED;
|
||||
|
||||
cJSON* sha = cJSON_GetObjectItemCaseSensitive(root, "sha256");
|
||||
if (!sha) sha = cJSON_GetObjectItemCaseSensitive(root, "x");
|
||||
cJSON* url = cJSON_GetObjectItemCaseSensitive(root, "url");
|
||||
cJSON* size = cJSON_GetObjectItemCaseSensitive(root, "size");
|
||||
cJSON* ct = cJSON_GetObjectItemCaseSensitive(root, "content_type");
|
||||
if (!ct) ct = cJSON_GetObjectItemCaseSensitive(root, "m");
|
||||
cJSON* created = cJSON_GetObjectItemCaseSensitive(root, "created");
|
||||
if (!created) created = cJSON_GetObjectItemCaseSensitive(root, "created_at");
|
||||
|
||||
memset(out, 0, sizeof(*out));
|
||||
if (sha && cJSON_IsString(sha) && sha->valuestring) {
|
||||
snprintf(out->sha256, sizeof(out->sha256), "%s", sha->valuestring);
|
||||
}
|
||||
if (url && cJSON_IsString(url) && url->valuestring) {
|
||||
snprintf(out->url, sizeof(out->url), "%s", url->valuestring);
|
||||
}
|
||||
if (size && cJSON_IsNumber(size)) out->size = (long)cJSON_GetNumberValue(size);
|
||||
if (ct && cJSON_IsString(ct) && ct->valuestring) {
|
||||
snprintf(out->content_type, sizeof(out->content_type), "%s", ct->valuestring);
|
||||
}
|
||||
if (created && cJSON_IsNumber(created)) out->created = (long)cJSON_GetNumberValue(created);
|
||||
|
||||
cJSON_Delete(root);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
void blossom_set_ca_bundle(const char* ca_bundle_path) {
|
||||
nostr_http_set_ca_bundle(ca_bundle_path);
|
||||
}
|
||||
|
||||
char* blossom_create_auth_header(const unsigned char* private_key,
|
||||
const char* operation,
|
||||
const char* sha256_hex,
|
||||
int expiration_seconds) {
|
||||
if (!private_key || !operation || operation[0] == '\0') return NULL;
|
||||
if (sha256_hex && !is_hex64_local(sha256_hex)) return NULL;
|
||||
|
||||
cJSON* tags = cJSON_CreateArray();
|
||||
if (!tags) return NULL;
|
||||
|
||||
cJSON* t_tag = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(t_tag, cJSON_CreateString("t"));
|
||||
cJSON_AddItemToArray(t_tag, cJSON_CreateString(operation));
|
||||
cJSON_AddItemToArray(tags, t_tag);
|
||||
|
||||
if (sha256_hex) {
|
||||
cJSON* x_tag = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(x_tag, cJSON_CreateString("x"));
|
||||
cJSON_AddItemToArray(x_tag, cJSON_CreateString(sha256_hex));
|
||||
cJSON_AddItemToArray(tags, x_tag);
|
||||
}
|
||||
|
||||
int exp = expiration_seconds > 0 ? expiration_seconds : 300;
|
||||
long until = (long)time(NULL) + exp;
|
||||
char exp_buf[32];
|
||||
snprintf(exp_buf, sizeof(exp_buf), "%ld", until);
|
||||
cJSON* e_tag = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(e_tag, cJSON_CreateString("expiration"));
|
||||
cJSON_AddItemToArray(e_tag, cJSON_CreateString(exp_buf));
|
||||
cJSON_AddItemToArray(tags, e_tag);
|
||||
|
||||
cJSON* evt = nostr_create_and_sign_event(24242, "", tags, private_key, time(NULL));
|
||||
cJSON_Delete(tags);
|
||||
if (!evt) return NULL;
|
||||
|
||||
char* evt_json = cJSON_PrintUnformatted(evt);
|
||||
cJSON_Delete(evt);
|
||||
if (!evt_json) return NULL;
|
||||
|
||||
size_t evt_len = strlen(evt_json);
|
||||
size_t b64_cap = ((evt_len + 2U) / 3U) * 4U + 8U;
|
||||
char* b64 = (char*)malloc(b64_cap);
|
||||
if (!b64) {
|
||||
free(evt_json);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t n = base64_encode((const unsigned char*)evt_json, evt_len, b64, b64_cap);
|
||||
free(evt_json);
|
||||
if (n == 0) {
|
||||
free(b64);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t hdr_cap = n + 16U;
|
||||
char* header = (char*)malloc(hdr_cap);
|
||||
if (!header) {
|
||||
free(b64);
|
||||
return NULL;
|
||||
}
|
||||
snprintf(header, hdr_cap, "Nostr %s", b64);
|
||||
free(b64);
|
||||
return header;
|
||||
}
|
||||
|
||||
int blossom_upload(const char* server_url,
|
||||
const unsigned char* data,
|
||||
size_t data_len,
|
||||
const char* content_type,
|
||||
const unsigned char* private_key,
|
||||
const char* sha256_hex,
|
||||
int timeout_seconds,
|
||||
blossom_blob_descriptor_t* descriptor_out) {
|
||||
if (!server_url || !data || data_len == 0 || !descriptor_out) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
char hash_hex[65] = {0};
|
||||
if (sha256_hex) {
|
||||
if (!is_hex64_local(sha256_hex)) return NOSTR_ERROR_INVALID_INPUT;
|
||||
snprintf(hash_hex, sizeof(hash_hex), "%s", sha256_hex);
|
||||
} else {
|
||||
unsigned char hash[32];
|
||||
if (nostr_sha256(data, data_len, hash) != 0) return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
nostr_bytes_to_hex(hash, 32, hash_hex);
|
||||
}
|
||||
|
||||
char url[1024];
|
||||
char base[512];
|
||||
trim_trailing_slash_local(server_url, base, sizeof(base));
|
||||
snprintf(url, sizeof(url), "%s/upload", base);
|
||||
|
||||
char auth_buf[1024] = {0};
|
||||
char ctype_buf[256] = {0};
|
||||
const char* headers[4] = {"Accept: application/json", NULL, NULL, NULL};
|
||||
int h = 1;
|
||||
|
||||
if (content_type && content_type[0] != '\0') {
|
||||
snprintf(ctype_buf, sizeof(ctype_buf), "Content-Type: %s", content_type);
|
||||
headers[h++] = ctype_buf;
|
||||
}
|
||||
|
||||
char* auth = NULL;
|
||||
if (private_key) {
|
||||
auth = blossom_create_auth_header(private_key, "upload", hash_hex, 300);
|
||||
if (auth) {
|
||||
snprintf(auth_buf, sizeof(auth_buf), "Authorization: %s", auth);
|
||||
headers[h++] = auth_buf;
|
||||
}
|
||||
}
|
||||
headers[h] = NULL;
|
||||
|
||||
nostr_http_request_t req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.method = "PUT";
|
||||
req.url = url;
|
||||
req.headers = headers;
|
||||
req.body = data;
|
||||
req.body_len = data_len;
|
||||
req.timeout_seconds = timeout_seconds > 0 ? timeout_seconds : 20;
|
||||
req.follow_redirects = 1;
|
||||
req.max_redirects = 3;
|
||||
|
||||
nostr_http_response_t resp;
|
||||
int rc = nostr_http_request(&req, &resp);
|
||||
free(auth);
|
||||
if (rc != NOSTR_SUCCESS) return rc;
|
||||
|
||||
if (resp.status_code < 200 || resp.status_code >= 300) {
|
||||
nostr_http_response_free(&resp);
|
||||
return NOSTR_ERROR_NETWORK_FAILED;
|
||||
}
|
||||
|
||||
int prc = parse_descriptor_local(resp.body ? resp.body : "{}", descriptor_out);
|
||||
if (prc != NOSTR_SUCCESS) {
|
||||
memset(descriptor_out, 0, sizeof(*descriptor_out));
|
||||
snprintf(descriptor_out->sha256, sizeof(descriptor_out->sha256), "%s", hash_hex);
|
||||
descriptor_out->size = (long)data_len;
|
||||
if (content_type && content_type[0] != '\0') {
|
||||
snprintf(descriptor_out->content_type, sizeof(descriptor_out->content_type), "%s", content_type);
|
||||
}
|
||||
}
|
||||
|
||||
nostr_http_response_free(&resp);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
int blossom_upload_file(const char* server_url,
|
||||
const char* file_path,
|
||||
const char* content_type,
|
||||
const unsigned char* private_key,
|
||||
int timeout_seconds,
|
||||
blossom_blob_descriptor_t* descriptor_out) {
|
||||
if (!server_url || !file_path || !descriptor_out) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
FILE* fp = fopen(file_path, "rb");
|
||||
if (!fp) return NOSTR_ERROR_IO_FAILED;
|
||||
|
||||
if (fseek(fp, 0, SEEK_END) != 0) {
|
||||
fclose(fp);
|
||||
return NOSTR_ERROR_IO_FAILED;
|
||||
}
|
||||
long sz = ftell(fp);
|
||||
if (sz < 0) {
|
||||
fclose(fp);
|
||||
return NOSTR_ERROR_IO_FAILED;
|
||||
}
|
||||
if (fseek(fp, 0, SEEK_SET) != 0) {
|
||||
fclose(fp);
|
||||
return NOSTR_ERROR_IO_FAILED;
|
||||
}
|
||||
|
||||
unsigned char* buf = (unsigned char*)malloc((size_t)sz);
|
||||
if (!buf) {
|
||||
fclose(fp);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
size_t n = fread(buf, 1, (size_t)sz, fp);
|
||||
fclose(fp);
|
||||
if (n != (size_t)sz) {
|
||||
free(buf);
|
||||
return NOSTR_ERROR_IO_FAILED;
|
||||
}
|
||||
|
||||
int rc = blossom_upload(server_url, buf, n, content_type, private_key, NULL, timeout_seconds, descriptor_out);
|
||||
free(buf);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int blossom_download(const char* server_url,
|
||||
const char* sha256_hex,
|
||||
int timeout_seconds,
|
||||
size_t max_bytes,
|
||||
unsigned char** body_out,
|
||||
size_t* body_len_out,
|
||||
char* content_type_out,
|
||||
size_t content_type_out_size) {
|
||||
if (!server_url || !sha256_hex || !body_out || !body_len_out || !is_hex64_local(sha256_hex)) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
*body_out = NULL;
|
||||
*body_len_out = 0;
|
||||
if (content_type_out && content_type_out_size > 0) content_type_out[0] = '\0';
|
||||
|
||||
char url[1024];
|
||||
if (build_blob_url_local(server_url, sha256_hex, url, sizeof(url)) != 0) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
nostr_http_request_t req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.method = "GET";
|
||||
req.url = url;
|
||||
req.timeout_seconds = timeout_seconds > 0 ? timeout_seconds : 20;
|
||||
req.max_response_bytes = max_bytes;
|
||||
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 (resp.status_code < 200 || resp.status_code >= 300) {
|
||||
nostr_http_response_free(&resp);
|
||||
return NOSTR_ERROR_NETWORK_FAILED;
|
||||
}
|
||||
|
||||
unsigned char* out = (unsigned char*)malloc(resp.body_len > 0 ? resp.body_len : 1);
|
||||
if (!out) {
|
||||
nostr_http_response_free(&resp);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
if (resp.body_len > 0 && resp.body) memcpy(out, resp.body, resp.body_len);
|
||||
|
||||
*body_out = out;
|
||||
*body_len_out = resp.body_len;
|
||||
if (content_type_out && content_type_out_size > 0 && resp.content_type && resp.content_type[0] != '\0') {
|
||||
snprintf(content_type_out, content_type_out_size, "%s", resp.content_type);
|
||||
}
|
||||
|
||||
nostr_http_response_free(&resp);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
int blossom_download_to_file(const char* server_url,
|
||||
const char* sha256_hex,
|
||||
const char* output_path,
|
||||
int timeout_seconds,
|
||||
size_t max_bytes,
|
||||
blossom_blob_descriptor_t* descriptor_out) {
|
||||
if (!server_url || !sha256_hex || !output_path || !descriptor_out) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
unsigned char* body = NULL;
|
||||
size_t body_len = 0;
|
||||
char content_type[128] = {0};
|
||||
int rc = blossom_download(server_url, sha256_hex, timeout_seconds, max_bytes, &body, &body_len, content_type, sizeof(content_type));
|
||||
if (rc != NOSTR_SUCCESS) return rc;
|
||||
|
||||
FILE* fp = fopen(output_path, "wb");
|
||||
if (!fp) {
|
||||
free(body);
|
||||
return NOSTR_ERROR_IO_FAILED;
|
||||
}
|
||||
size_t wn = fwrite(body, 1, body_len, fp);
|
||||
fclose(fp);
|
||||
if (wn != body_len) {
|
||||
free(body);
|
||||
return NOSTR_ERROR_IO_FAILED;
|
||||
}
|
||||
|
||||
unsigned char hash[32];
|
||||
if (nostr_sha256(body, body_len, hash) != 0) {
|
||||
free(body);
|
||||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
}
|
||||
free(body);
|
||||
|
||||
char hash_hex[65];
|
||||
nostr_bytes_to_hex(hash, 32, hash_hex);
|
||||
|
||||
memset(descriptor_out, 0, sizeof(*descriptor_out));
|
||||
snprintf(descriptor_out->sha256, sizeof(descriptor_out->sha256), "%s", hash_hex);
|
||||
descriptor_out->size = (long)body_len;
|
||||
if (content_type[0] != '\0') snprintf(descriptor_out->content_type, sizeof(descriptor_out->content_type), "%s", content_type);
|
||||
build_blob_url_local(server_url, sha256_hex, descriptor_out->url, sizeof(descriptor_out->url));
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
int blossom_head(const char* server_url,
|
||||
const char* sha256_hex,
|
||||
int timeout_seconds,
|
||||
blossom_blob_descriptor_t* descriptor_out) {
|
||||
if (!server_url || !sha256_hex || !descriptor_out || !is_hex64_local(sha256_hex)) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
char url[1024];
|
||||
if (build_blob_url_local(server_url, sha256_hex, url, sizeof(url)) != 0) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
nostr_http_request_t req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.method = "HEAD";
|
||||
req.url = url;
|
||||
req.timeout_seconds = timeout_seconds > 0 ? timeout_seconds : 10;
|
||||
req.capture_headers = 1;
|
||||
|
||||
nostr_http_response_t resp;
|
||||
int rc = nostr_http_request(&req, &resp);
|
||||
if (rc != NOSTR_SUCCESS) return rc;
|
||||
|
||||
if (resp.status_code < 200 || resp.status_code >= 300) {
|
||||
nostr_http_response_free(&resp);
|
||||
return NOSTR_ERROR_NETWORK_FAILED;
|
||||
}
|
||||
|
||||
memset(descriptor_out, 0, sizeof(*descriptor_out));
|
||||
snprintf(descriptor_out->sha256, sizeof(descriptor_out->sha256), "%s", sha256_hex);
|
||||
if (build_blob_url_local(server_url, sha256_hex, descriptor_out->url, sizeof(descriptor_out->url)) != 0) {
|
||||
nostr_http_response_free(&resp);
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
if (resp.content_type && resp.content_type[0] != '\0') {
|
||||
snprintf(descriptor_out->content_type, sizeof(descriptor_out->content_type), "%s", resp.content_type);
|
||||
}
|
||||
|
||||
if (resp.headers_raw) {
|
||||
const char* key = "Content-Length:";
|
||||
char* p = strstr(resp.headers_raw, key);
|
||||
if (p) {
|
||||
p += strlen(key);
|
||||
while (*p == ' ' || *p == '\t') p++;
|
||||
descriptor_out->size = strtol(p, NULL, 10);
|
||||
}
|
||||
}
|
||||
|
||||
nostr_http_response_free(&resp);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
int blossom_delete(const char* server_url,
|
||||
const char* sha256_hex,
|
||||
const unsigned char* private_key,
|
||||
int timeout_seconds) {
|
||||
if (!server_url || !sha256_hex || !private_key || !is_hex64_local(sha256_hex)) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
char url[1024];
|
||||
if (build_blob_url_local(server_url, sha256_hex, url, sizeof(url)) != 0) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
char* auth = blossom_create_auth_header(private_key, "delete", sha256_hex, 300);
|
||||
if (!auth) return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
|
||||
char auth_header[1200];
|
||||
snprintf(auth_header, sizeof(auth_header), "Authorization: %s", auth);
|
||||
free(auth);
|
||||
|
||||
const char* headers[] = {"Accept: application/json", auth_header, NULL};
|
||||
|
||||
nostr_http_request_t req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.method = "DELETE";
|
||||
req.url = url;
|
||||
req.headers = headers;
|
||||
req.timeout_seconds = timeout_seconds > 0 ? timeout_seconds : 20;
|
||||
|
||||
nostr_http_response_t resp;
|
||||
int rc = nostr_http_request(&req, &resp);
|
||||
if (rc != NOSTR_SUCCESS) return rc;
|
||||
int ok = (resp.status_code >= 200 && resp.status_code < 300) ? NOSTR_SUCCESS : NOSTR_ERROR_NETWORK_FAILED;
|
||||
nostr_http_response_free(&resp);
|
||||
return ok;
|
||||
}
|
||||
|
||||
int blossom_list(const char* server_url,
|
||||
const char* pubkey_hex,
|
||||
int timeout_seconds,
|
||||
blossom_blob_descriptor_t** descriptors_out,
|
||||
int* count_out) {
|
||||
if (!server_url || !pubkey_hex || !descriptors_out || !count_out) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
*descriptors_out = NULL;
|
||||
*count_out = 0;
|
||||
|
||||
char base[512];
|
||||
trim_trailing_slash_local(server_url, base, sizeof(base));
|
||||
char url[1024];
|
||||
snprintf(url, sizeof(url), "%s/list/%s", base, pubkey_hex);
|
||||
|
||||
nostr_http_request_t req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.method = "GET";
|
||||
req.url = url;
|
||||
req.timeout_seconds = timeout_seconds > 0 ? timeout_seconds : 20;
|
||||
|
||||
nostr_http_response_t resp;
|
||||
int rc = nostr_http_request(&req, &resp);
|
||||
if (rc != NOSTR_SUCCESS) return rc;
|
||||
|
||||
if (resp.status_code < 200 || resp.status_code >= 300) {
|
||||
nostr_http_response_free(&resp);
|
||||
return NOSTR_ERROR_NETWORK_FAILED;
|
||||
}
|
||||
|
||||
cJSON* arr = cJSON_Parse(resp.body ? resp.body : "[]");
|
||||
if (!arr || !cJSON_IsArray(arr)) {
|
||||
cJSON_Delete(arr);
|
||||
nostr_http_response_free(&resp);
|
||||
return NOSTR_ERROR_IO_FAILED;
|
||||
}
|
||||
|
||||
int n = cJSON_GetArraySize(arr);
|
||||
blossom_blob_descriptor_t* out = (blossom_blob_descriptor_t*)calloc((size_t)n, sizeof(blossom_blob_descriptor_t));
|
||||
if (!out && n > 0) {
|
||||
cJSON_Delete(arr);
|
||||
nostr_http_response_free(&resp);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON* it = cJSON_GetArrayItem(arr, i);
|
||||
char* tmp = cJSON_PrintUnformatted(it);
|
||||
if (tmp) {
|
||||
(void)parse_descriptor_local(tmp, &out[i]);
|
||||
free(tmp);
|
||||
}
|
||||
}
|
||||
|
||||
*descriptors_out = out;
|
||||
*count_out = n;
|
||||
|
||||
cJSON_Delete(arr);
|
||||
nostr_http_response_free(&resp);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Blossom HTTP Client
|
||||
*/
|
||||
|
||||
#ifndef NOSTR_BLOSSOM_CLIENT_H
|
||||
#define NOSTR_BLOSSOM_CLIENT_H
|
||||
|
||||
#include "nostr_common.h"
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
char sha256[65];
|
||||
char url[512];
|
||||
long size;
|
||||
char content_type[128];
|
||||
long created;
|
||||
} blossom_blob_descriptor_t;
|
||||
|
||||
void blossom_set_ca_bundle(const char* ca_bundle_path);
|
||||
|
||||
char* blossom_create_auth_header(const unsigned char* private_key,
|
||||
const char* operation,
|
||||
const char* sha256_hex,
|
||||
int expiration_seconds);
|
||||
|
||||
int blossom_upload(const char* server_url,
|
||||
const unsigned char* data,
|
||||
size_t data_len,
|
||||
const char* content_type,
|
||||
const unsigned char* private_key,
|
||||
const char* sha256_hex,
|
||||
int timeout_seconds,
|
||||
blossom_blob_descriptor_t* descriptor_out);
|
||||
|
||||
int blossom_upload_file(const char* server_url,
|
||||
const char* file_path,
|
||||
const char* content_type,
|
||||
const unsigned char* private_key,
|
||||
int timeout_seconds,
|
||||
blossom_blob_descriptor_t* descriptor_out);
|
||||
|
||||
int blossom_download(const char* server_url,
|
||||
const char* sha256_hex,
|
||||
int timeout_seconds,
|
||||
size_t max_bytes,
|
||||
unsigned char** body_out,
|
||||
size_t* body_len_out,
|
||||
char* content_type_out,
|
||||
size_t content_type_out_size);
|
||||
|
||||
int blossom_download_to_file(const char* server_url,
|
||||
const char* sha256_hex,
|
||||
const char* output_path,
|
||||
int timeout_seconds,
|
||||
size_t max_bytes,
|
||||
blossom_blob_descriptor_t* descriptor_out);
|
||||
|
||||
int blossom_head(const char* server_url,
|
||||
const char* sha256_hex,
|
||||
int timeout_seconds,
|
||||
blossom_blob_descriptor_t* descriptor_out);
|
||||
|
||||
int blossom_delete(const char* server_url,
|
||||
const char* sha256_hex,
|
||||
const unsigned char* private_key,
|
||||
int timeout_seconds);
|
||||
|
||||
int blossom_list(const char* server_url,
|
||||
const char* pubkey_hex,
|
||||
int timeout_seconds,
|
||||
blossom_blob_descriptor_t** descriptors_out,
|
||||
int* count_out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NOSTR_BLOSSOM_CLIENT_H */
|
||||
+1226
-106
File diff suppressed because it is too large
Load Diff
+63
-1
@@ -17,11 +17,28 @@ extern "C" {
|
||||
#define CASHU_API_VERSION "v1"
|
||||
|
||||
typedef struct {
|
||||
char id[17];
|
||||
char id[NOSTR_CASHU_KEYSET_ID_HEX_SIZE];
|
||||
char unit[16];
|
||||
int active;
|
||||
} cashu_keyset_t;
|
||||
|
||||
typedef enum {
|
||||
CASHU_TOKEN_FORMAT_A = 0,
|
||||
CASHU_TOKEN_FORMAT_B = 1
|
||||
} cashu_token_format_t;
|
||||
|
||||
typedef struct {
|
||||
uint64_t amount;
|
||||
char pubkey[67];
|
||||
} cashu_amount_key_t;
|
||||
|
||||
typedef struct {
|
||||
char keyset_id[65];
|
||||
char unit[16];
|
||||
cashu_amount_key_t* keys;
|
||||
int key_count;
|
||||
} cashu_keyset_keys_t;
|
||||
|
||||
typedef struct {
|
||||
char* name;
|
||||
char* pubkey;
|
||||
@@ -106,12 +123,38 @@ typedef struct {
|
||||
int state_count;
|
||||
} cashu_checkstate_response_t;
|
||||
|
||||
typedef struct {
|
||||
char* mint_url;
|
||||
nostr_cashu_proof_t* proofs;
|
||||
int proof_count;
|
||||
} cashu_decoded_token_t;
|
||||
|
||||
void cashu_mint_set_ca_bundle(const char* ca_bundle_path);
|
||||
|
||||
int cashu_mint_get_info(const char* mint_url,
|
||||
cashu_mint_info_t* info_out,
|
||||
int timeout_seconds);
|
||||
|
||||
void cashu_mint_free_info(cashu_mint_info_t* info);
|
||||
|
||||
int cashu_mint_get_keysets(const char* mint_url,
|
||||
cashu_keyset_t** keysets_out,
|
||||
int* keyset_count_out,
|
||||
int timeout_seconds);
|
||||
|
||||
void cashu_mint_free_keysets(cashu_keyset_t* keysets);
|
||||
|
||||
int cashu_mint_get_keys(const char* mint_url,
|
||||
const char* keyset_id,
|
||||
cashu_keyset_keys_t* keys_out,
|
||||
int timeout_seconds);
|
||||
|
||||
void cashu_mint_free_keyset_keys(cashu_keyset_keys_t* keys);
|
||||
|
||||
int cashu_keyset_keys_find_pubkey_for_amount(const cashu_keyset_keys_t* keys,
|
||||
uint64_t amount,
|
||||
const char** pubkey_out);
|
||||
|
||||
int cashu_mint_request_mint_quote(const char* mint_url,
|
||||
uint64_t amount,
|
||||
const char* unit,
|
||||
@@ -203,6 +246,25 @@ int cashu_parse_checkstate_response(cJSON* response_body,
|
||||
|
||||
void cashu_mint_free_checkstate_response(cashu_checkstate_response_t* response);
|
||||
|
||||
int cashu_decode_token(const char* token_string,
|
||||
cashu_decoded_token_t* token_out);
|
||||
|
||||
int cashu_encode_token(const cashu_decoded_token_t* token,
|
||||
cashu_token_format_t format,
|
||||
char** token_string_out);
|
||||
|
||||
void cashu_free_decoded_token(cashu_decoded_token_t* token);
|
||||
|
||||
int cashu_blind_message(const char* secret,
|
||||
const char* mint_pubkey_hex,
|
||||
char out_B_hex[67],
|
||||
unsigned char out_r[32]);
|
||||
|
||||
int cashu_unblind_signature(const char* C_blinded_hex,
|
||||
const char* mint_pubkey_hex,
|
||||
const unsigned char r[32],
|
||||
char out_C_hex[67]);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
+135
-30
@@ -178,6 +178,9 @@ struct nostr_pool_subscription {
|
||||
|
||||
// Per-relay timeout tracking
|
||||
time_t* relay_last_activity; // Last activity time per relay
|
||||
|
||||
// Guard to ensure on_eose is emitted only once per subscription
|
||||
int eose_callback_fired;
|
||||
};
|
||||
|
||||
struct nostr_relay_pool {
|
||||
@@ -507,16 +510,32 @@ static void attempt_reconnect(relay_connection_t* relay) {
|
||||
relay->reconnect_attempts++;
|
||||
|
||||
if (ensure_relay_connection(relay) == 0) {
|
||||
// Success! Reset reconnection state
|
||||
relay->reconnect_attempts = 0;
|
||||
relay->next_reconnect_time = 0;
|
||||
// Success. Keep reconnect_attempts until the connection proves stable.
|
||||
// This prevents rapid connect->disconnect flapping from immediately resetting backoff.
|
||||
int reset_after_s = relay->pool->reconnect_config.reconnect_reset_stability_seconds;
|
||||
if (reset_after_s <= 0) {
|
||||
// Backward-compatible behavior: reset immediately when cooldown is disabled.
|
||||
relay->reconnect_attempts = 0;
|
||||
relay->next_reconnect_time = 0;
|
||||
} else {
|
||||
int delay_ms = calculate_reconnect_delay(relay);
|
||||
int delay_s = (delay_ms + 999) / 1000;
|
||||
if (delay_s < 1) {
|
||||
delay_s = 1;
|
||||
}
|
||||
relay->next_reconnect_time = time(NULL) + delay_s;
|
||||
}
|
||||
|
||||
// Restore subscriptions on reconnect
|
||||
restore_subscriptions_on_reconnect(relay);
|
||||
} else {
|
||||
// Failed - schedule next attempt with backoff
|
||||
int delay_ms = calculate_reconnect_delay(relay);
|
||||
relay->next_reconnect_time = time(NULL) + (delay_ms / 1000);
|
||||
int delay_s = (delay_ms + 999) / 1000;
|
||||
if (delay_s < 1) {
|
||||
delay_s = 1;
|
||||
}
|
||||
relay->next_reconnect_time = time(NULL) + delay_s;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -615,6 +634,7 @@ nostr_pool_reconnect_config_t* nostr_pool_reconnect_config_default(void) {
|
||||
.initial_reconnect_delay_ms = 1000,
|
||||
.max_reconnect_delay_ms = 30000,
|
||||
.reconnect_backoff_multiplier = 2,
|
||||
.reconnect_reset_stability_seconds = 30,
|
||||
.ping_interval_seconds = 30,
|
||||
.pong_timeout_seconds = 10
|
||||
};
|
||||
@@ -957,9 +977,9 @@ int nostr_pool_subscription_close(nostr_pool_subscription_t* subscription) {
|
||||
if (!subscription || subscription->closed) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
|
||||
subscription->closed = 1;
|
||||
|
||||
|
||||
// Send CLOSE messages to all relays
|
||||
for (int i = 0; i < subscription->relay_count; i++) {
|
||||
relay_connection_t* relay = find_relay_by_url(subscription->pool, subscription->relay_urls[i]);
|
||||
@@ -967,7 +987,7 @@ int nostr_pool_subscription_close(nostr_pool_subscription_t* subscription) {
|
||||
nostr_relay_send_close(relay->ws_client, subscription->subscription_id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Remove from pool
|
||||
nostr_relay_pool_t* pool = subscription->pool;
|
||||
for (int i = 0; i < pool->subscription_count; i++) {
|
||||
@@ -980,7 +1000,7 @@ int nostr_pool_subscription_close(nostr_pool_subscription_t* subscription) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Cleanup subscription
|
||||
cJSON_Delete(subscription->filter);
|
||||
for (int i = 0; i < subscription->relay_count; i++) {
|
||||
@@ -988,11 +1008,55 @@ int nostr_pool_subscription_close(nostr_pool_subscription_t* subscription) {
|
||||
}
|
||||
free(subscription->relay_urls);
|
||||
free(subscription->eose_received);
|
||||
free(subscription->relay_last_activity);
|
||||
if (subscription->collected_events) {
|
||||
for (int i = 0; i < subscription->collected_event_count; i++) {
|
||||
cJSON_Delete(subscription->collected_events[i]);
|
||||
}
|
||||
free(subscription->collected_events);
|
||||
}
|
||||
free(subscription);
|
||||
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
static int maybe_complete_subscription_eose(nostr_pool_subscription_t* sub) {
|
||||
if (!sub || sub->closed || sub->eose_callback_fired) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int all_eose = 1;
|
||||
for (int j = 0; j < sub->relay_count; j++) {
|
||||
if (!sub->eose_received[j]) {
|
||||
all_eose = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!all_eose) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
sub->eose_callback_fired = 1;
|
||||
|
||||
if (sub->on_eose) {
|
||||
// FIRST mode: no events collected, pass NULL/0
|
||||
if (sub->result_mode == NOSTR_POOL_EOSE_FIRST) {
|
||||
sub->on_eose(NULL, 0, sub->user_data);
|
||||
} else {
|
||||
// FULL_SET or MOST_RECENT: pass collected events
|
||||
sub->on_eose(sub->collected_events, sub->collected_event_count, sub->user_data);
|
||||
}
|
||||
}
|
||||
|
||||
if (sub->close_on_eose && !sub->closed) {
|
||||
nostr_pool_subscription_close(sub);
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Event processing
|
||||
static void process_relay_message(nostr_relay_pool_t* pool, relay_connection_t* relay, const char* message) {
|
||||
if (!pool || !relay || !message) {
|
||||
@@ -1032,6 +1096,15 @@ static void process_relay_message(nostr_relay_pool_t* pool, relay_connection_t*
|
||||
}
|
||||
|
||||
if (sub) {
|
||||
// Track per-relay activity for timeout fallback.
|
||||
time_t now = time(NULL);
|
||||
for (int j = 0; j < sub->relay_count; j++) {
|
||||
if (strcmp(sub->relay_urls[j], relay->url) == 0) {
|
||||
sub->relay_last_activity[j] = now;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for duplicate (per-subscription deduplication)
|
||||
int is_duplicate = 0;
|
||||
if (sub->enable_deduplication) {
|
||||
@@ -1099,32 +1172,15 @@ static void process_relay_message(nostr_relay_pool_t* pool, relay_connection_t*
|
||||
}
|
||||
}
|
||||
|
||||
// Check if all relays have sent EOSE
|
||||
int all_eose = 1;
|
||||
// Keep activity updated on explicit EOSE from this relay
|
||||
for (int j = 0; j < sub->relay_count; j++) {
|
||||
if (!sub->eose_received[j]) {
|
||||
all_eose = 0;
|
||||
if (strcmp(sub->relay_urls[j], relay->url) == 0) {
|
||||
sub->relay_last_activity[j] = time(NULL);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (all_eose) {
|
||||
if (sub->on_eose) {
|
||||
// Pass collected events based on result mode
|
||||
if (sub->result_mode == NOSTR_POOL_EOSE_FIRST) {
|
||||
// FIRST mode: no events collected, pass NULL/0
|
||||
sub->on_eose(NULL, 0, sub->user_data);
|
||||
} else {
|
||||
// FULL_SET or MOST_RECENT: pass collected events
|
||||
sub->on_eose(sub->collected_events, sub->collected_event_count, sub->user_data);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-close subscription if close_on_eose is enabled
|
||||
if (sub->close_on_eose && !sub->closed) {
|
||||
nostr_pool_subscription_close(sub);
|
||||
}
|
||||
}
|
||||
(void)maybe_complete_subscription_eose(sub);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1994,6 +2050,21 @@ int nostr_relay_pool_poll(nostr_relay_pool_t* pool, int timeout_ms) {
|
||||
|
||||
relay->status = NOSTR_POOL_RELAY_CONNECTED;
|
||||
|
||||
// If the relay remains connected long enough, clear reconnect backoff.
|
||||
if (relay->reconnect_attempts > 0) {
|
||||
int reset_after_s = pool->reconnect_config.reconnect_reset_stability_seconds;
|
||||
if (reset_after_s <= 0) {
|
||||
relay->reconnect_attempts = 0;
|
||||
relay->next_reconnect_time = 0;
|
||||
} else if (relay->connect_time > 0) {
|
||||
time_t now = time(NULL);
|
||||
if (now - relay->connect_time >= reset_after_s) {
|
||||
relay->reconnect_attempts = 0;
|
||||
relay->next_reconnect_time = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Connection health monitoring (ping/pong)
|
||||
check_connection_health(relay);
|
||||
|
||||
@@ -2018,5 +2089,39 @@ int nostr_relay_pool_poll(nostr_relay_pool_t* pool, int timeout_ms) {
|
||||
}
|
||||
}
|
||||
|
||||
// Timeout fallback for relays that never emit EOSE:
|
||||
// if no activity from a relay for relay_timeout_seconds, treat it as done.
|
||||
time_t now = time(NULL);
|
||||
for (int i = 0; i < pool->subscription_count; i++) {
|
||||
nostr_pool_subscription_t* sub = pool->subscriptions[i];
|
||||
if (!sub || sub->closed || sub->eose_callback_fired) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sub->relay_timeout_seconds <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int changed = 0;
|
||||
for (int j = 0; j < sub->relay_count; j++) {
|
||||
if (sub->eose_received[j]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sub->relay_last_activity &&
|
||||
now - sub->relay_last_activity[j] >= sub->relay_timeout_seconds) {
|
||||
sub->eose_received[j] = 1;
|
||||
changed = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
if (maybe_complete_subscription_eose(sub)) {
|
||||
// Subscription list may have shifted due to auto-close.
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return events_processed;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* nostr_chacha20poly1305.c - ChaCha20-Poly1305 AEAD implementation
|
||||
*
|
||||
* RFC 8439 Section 2.8
|
||||
*/
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
// ChaCha20 private API (provided by nostr_chacha20.c)
|
||||
int chacha20_block(const uint8_t key[32], uint32_t counter,
|
||||
const uint8_t nonce[12], uint8_t output[64]);
|
||||
int chacha20_encrypt(const uint8_t key[32], uint32_t counter,
|
||||
const uint8_t nonce[12], const uint8_t* input,
|
||||
uint8_t* output, size_t length);
|
||||
|
||||
// Poly1305 API (provided by nostr_poly1305.c)
|
||||
int nostr_poly1305_mac(const unsigned char key[32], const unsigned char *msg,
|
||||
size_t msg_len, unsigned char tag[16]);
|
||||
|
||||
static void store64_le(unsigned char out[8], uint64_t v) {
|
||||
out[0] = (unsigned char)(v & 0xff);
|
||||
out[1] = (unsigned char)((v >> 8) & 0xff);
|
||||
out[2] = (unsigned char)((v >> 16) & 0xff);
|
||||
out[3] = (unsigned char)((v >> 24) & 0xff);
|
||||
out[4] = (unsigned char)((v >> 32) & 0xff);
|
||||
out[5] = (unsigned char)((v >> 40) & 0xff);
|
||||
out[6] = (unsigned char)((v >> 48) & 0xff);
|
||||
out[7] = (unsigned char)((v >> 56) & 0xff);
|
||||
}
|
||||
|
||||
static int constant_time_tag_eq(const unsigned char a[16], const unsigned char b[16]) {
|
||||
unsigned char diff = 0;
|
||||
size_t i;
|
||||
for (i = 0; i < 16; i++) {
|
||||
diff |= (unsigned char)(a[i] ^ b[i]);
|
||||
}
|
||||
return diff == 0;
|
||||
}
|
||||
|
||||
static int build_poly1305_input(const unsigned char *aad, size_t aad_len,
|
||||
const unsigned char *ciphertext, size_t ct_len,
|
||||
unsigned char **out, size_t *out_len) {
|
||||
size_t aad_pad = (16 - (aad_len % 16)) % 16;
|
||||
size_t ct_pad = (16 - (ct_len % 16)) % 16;
|
||||
size_t total = aad_len + aad_pad + ct_len + ct_pad + 16; // len(aad)||len(ct)
|
||||
unsigned char *buf;
|
||||
size_t pos = 0;
|
||||
unsigned char len_block[16];
|
||||
|
||||
if (!out || !out_len) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
buf = (unsigned char *)malloc(total);
|
||||
if (!buf) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (aad_len > 0 && aad) {
|
||||
memcpy(buf + pos, aad, aad_len);
|
||||
pos += aad_len;
|
||||
}
|
||||
if (aad_pad > 0) {
|
||||
memset(buf + pos, 0, aad_pad);
|
||||
pos += aad_pad;
|
||||
}
|
||||
|
||||
if (ct_len > 0 && ciphertext) {
|
||||
memcpy(buf + pos, ciphertext, ct_len);
|
||||
pos += ct_len;
|
||||
}
|
||||
if (ct_pad > 0) {
|
||||
memset(buf + pos, 0, ct_pad);
|
||||
pos += ct_pad;
|
||||
}
|
||||
|
||||
store64_le(len_block, (uint64_t)aad_len);
|
||||
store64_le(len_block + 8, (uint64_t)ct_len);
|
||||
memcpy(buf + pos, len_block, sizeof(len_block));
|
||||
pos += sizeof(len_block);
|
||||
|
||||
*out = buf;
|
||||
*out_len = pos;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int nostr_chacha20poly1305_encrypt(const unsigned char key[32],
|
||||
const unsigned char nonce[12],
|
||||
const unsigned char *aad, size_t aad_len,
|
||||
const unsigned char *plaintext, size_t pt_len,
|
||||
unsigned char *ciphertext,
|
||||
unsigned char tag[16]) {
|
||||
unsigned char block0[64];
|
||||
unsigned char poly_key[32];
|
||||
unsigned char *poly_in = NULL;
|
||||
size_t poly_in_len = 0;
|
||||
int rc;
|
||||
|
||||
if (!key || !nonce || !ciphertext || !tag || (!plaintext && pt_len != 0) || (!aad && aad_len != 0)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (chacha20_block(key, 0, nonce, block0) != 0) {
|
||||
return -1;
|
||||
}
|
||||
memcpy(poly_key, block0, sizeof(poly_key));
|
||||
|
||||
if (pt_len > 0) {
|
||||
rc = chacha20_encrypt(key, 1, nonce, plaintext, ciphertext, pt_len);
|
||||
if (rc != 0) {
|
||||
memset(poly_key, 0, sizeof(poly_key));
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
rc = build_poly1305_input(aad, aad_len, ciphertext, pt_len, &poly_in, &poly_in_len);
|
||||
if (rc != 0) {
|
||||
memset(poly_key, 0, sizeof(poly_key));
|
||||
return -1;
|
||||
}
|
||||
|
||||
rc = nostr_poly1305_mac(poly_key, poly_in, poly_in_len, tag);
|
||||
|
||||
memset(poly_key, 0, sizeof(poly_key));
|
||||
memset(block0, 0, sizeof(block0));
|
||||
memset(poly_in, 0, poly_in_len);
|
||||
free(poly_in);
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
int nostr_chacha20poly1305_decrypt(const unsigned char key[32],
|
||||
const unsigned char nonce[12],
|
||||
const unsigned char *aad, size_t aad_len,
|
||||
const unsigned char *ciphertext, size_t ct_len,
|
||||
const unsigned char tag[16],
|
||||
unsigned char *plaintext) {
|
||||
unsigned char block0[64];
|
||||
unsigned char poly_key[32];
|
||||
unsigned char expected_tag[16];
|
||||
unsigned char *poly_in = NULL;
|
||||
size_t poly_in_len = 0;
|
||||
int rc;
|
||||
|
||||
if (!key || !nonce || !tag || !plaintext || (!ciphertext && ct_len != 0) || (!aad && aad_len != 0)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (chacha20_block(key, 0, nonce, block0) != 0) {
|
||||
return -1;
|
||||
}
|
||||
memcpy(poly_key, block0, sizeof(poly_key));
|
||||
|
||||
rc = build_poly1305_input(aad, aad_len, ciphertext, ct_len, &poly_in, &poly_in_len);
|
||||
if (rc != 0) {
|
||||
memset(poly_key, 0, sizeof(poly_key));
|
||||
return -1;
|
||||
}
|
||||
|
||||
rc = nostr_poly1305_mac(poly_key, poly_in, poly_in_len, expected_tag);
|
||||
if (rc != 0 || !constant_time_tag_eq(tag, expected_tag)) {
|
||||
memset(poly_key, 0, sizeof(poly_key));
|
||||
memset(block0, 0, sizeof(block0));
|
||||
memset(expected_tag, 0, sizeof(expected_tag));
|
||||
memset(poly_in, 0, poly_in_len);
|
||||
free(poly_in);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (ct_len > 0) {
|
||||
rc = chacha20_encrypt(key, 1, nonce, ciphertext, plaintext, ct_len);
|
||||
if (rc != 0) {
|
||||
memset(poly_key, 0, sizeof(poly_key));
|
||||
memset(block0, 0, sizeof(block0));
|
||||
memset(expected_tag, 0, sizeof(expected_tag));
|
||||
memset(poly_in, 0, poly_in_len);
|
||||
free(poly_in);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
memset(poly_key, 0, sizeof(poly_key));
|
||||
memset(block0, 0, sizeof(block0));
|
||||
memset(expected_tag, 0, sizeof(expected_tag));
|
||||
memset(poly_in, 0, poly_in_len);
|
||||
free(poly_in);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
/*
|
||||
* nostr_poly1305.c - Poly1305 message authentication code
|
||||
*
|
||||
* Public-domain style 32-bit implementation based on the poly1305-donna
|
||||
* approach, adapted for nostr_core_lib naming and conventions.
|
||||
*
|
||||
* RFC 8439 Section 2.5
|
||||
*/
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
uint32_t r0, r1, r2, r3, r4;
|
||||
uint32_t s1, s2, s3, s4;
|
||||
uint32_t h0, h1, h2, h3, h4;
|
||||
uint32_t pad0, pad1, pad2, pad3;
|
||||
size_t leftover;
|
||||
unsigned char buffer[16];
|
||||
unsigned char final;
|
||||
} nostr_poly1305_ctx_t;
|
||||
|
||||
static uint32_t u8to32_le(const unsigned char *p) {
|
||||
return ((uint32_t)p[0]) |
|
||||
((uint32_t)p[1] << 8) |
|
||||
((uint32_t)p[2] << 16) |
|
||||
((uint32_t)p[3] << 24);
|
||||
}
|
||||
|
||||
static void u32to8_le(unsigned char *p, uint32_t v) {
|
||||
p[0] = (unsigned char)(v & 0xff);
|
||||
p[1] = (unsigned char)((v >> 8) & 0xff);
|
||||
p[2] = (unsigned char)((v >> 16) & 0xff);
|
||||
p[3] = (unsigned char)((v >> 24) & 0xff);
|
||||
}
|
||||
|
||||
static void poly1305_blocks(nostr_poly1305_ctx_t *st, const unsigned char *m, size_t bytes) {
|
||||
const uint32_t r0 = st->r0;
|
||||
const uint32_t r1 = st->r1;
|
||||
const uint32_t r2 = st->r2;
|
||||
const uint32_t r3 = st->r3;
|
||||
const uint32_t r4 = st->r4;
|
||||
const uint32_t s1 = st->s1;
|
||||
const uint32_t s2 = st->s2;
|
||||
const uint32_t s3 = st->s3;
|
||||
const uint32_t s4 = st->s4;
|
||||
uint32_t h0 = st->h0;
|
||||
uint32_t h1 = st->h1;
|
||||
uint32_t h2 = st->h2;
|
||||
uint32_t h3 = st->h3;
|
||||
uint32_t h4 = st->h4;
|
||||
const uint32_t hibit = st->final ? 0 : (1U << 24);
|
||||
|
||||
while (bytes >= 16) {
|
||||
uint32_t t0 = u8to32_le(m + 0);
|
||||
uint32_t t1 = u8to32_le(m + 4);
|
||||
uint32_t t2 = u8to32_le(m + 8);
|
||||
uint32_t t3 = u8to32_le(m + 12);
|
||||
|
||||
uint64_t d0, d1, d2, d3, d4;
|
||||
uint32_t c;
|
||||
|
||||
h0 += ( t0 ) & 0x3ffffff;
|
||||
h1 += (((t1 << 6) | (t0 >> 26)) ) & 0x3ffffff;
|
||||
h2 += (((t2 << 12) | (t1 >> 20))) & 0x3ffffff;
|
||||
h3 += (((t3 << 18) | (t2 >> 14))) & 0x3ffffff;
|
||||
h4 += (( t3 >> 8) ) & 0x00ffffff;
|
||||
h4 += hibit;
|
||||
|
||||
d0 = ((uint64_t)h0 * r0) + ((uint64_t)h1 * s4) + ((uint64_t)h2 * s3) + ((uint64_t)h3 * s2) + ((uint64_t)h4 * s1);
|
||||
d1 = ((uint64_t)h0 * r1) + ((uint64_t)h1 * r0) + ((uint64_t)h2 * s4) + ((uint64_t)h3 * s3) + ((uint64_t)h4 * s2);
|
||||
d2 = ((uint64_t)h0 * r2) + ((uint64_t)h1 * r1) + ((uint64_t)h2 * r0) + ((uint64_t)h3 * s4) + ((uint64_t)h4 * s3);
|
||||
d3 = ((uint64_t)h0 * r3) + ((uint64_t)h1 * r2) + ((uint64_t)h2 * r1) + ((uint64_t)h3 * r0) + ((uint64_t)h4 * s4);
|
||||
d4 = ((uint64_t)h0 * r4) + ((uint64_t)h1 * r3) + ((uint64_t)h2 * r2) + ((uint64_t)h3 * r1) + ((uint64_t)h4 * r0);
|
||||
|
||||
c = (uint32_t)(d0 >> 26); h0 = (uint32_t)d0 & 0x3ffffff;
|
||||
d1 += c;
|
||||
c = (uint32_t)(d1 >> 26); h1 = (uint32_t)d1 & 0x3ffffff;
|
||||
d2 += c;
|
||||
c = (uint32_t)(d2 >> 26); h2 = (uint32_t)d2 & 0x3ffffff;
|
||||
d3 += c;
|
||||
c = (uint32_t)(d3 >> 26); h3 = (uint32_t)d3 & 0x3ffffff;
|
||||
d4 += c;
|
||||
c = (uint32_t)(d4 >> 26); h4 = (uint32_t)d4 & 0x3ffffff;
|
||||
h0 += c * 5;
|
||||
c = h0 >> 26; h0 &= 0x3ffffff;
|
||||
h1 += c;
|
||||
|
||||
m += 16;
|
||||
bytes -= 16;
|
||||
}
|
||||
|
||||
st->h0 = h0;
|
||||
st->h1 = h1;
|
||||
st->h2 = h2;
|
||||
st->h3 = h3;
|
||||
st->h4 = h4;
|
||||
}
|
||||
|
||||
void nostr_poly1305_init(nostr_poly1305_ctx_t *st, const unsigned char key[32]) {
|
||||
uint32_t t0, t1, t2, t3;
|
||||
|
||||
t0 = u8to32_le(key + 0);
|
||||
t1 = u8to32_le(key + 4);
|
||||
t2 = u8to32_le(key + 8);
|
||||
t3 = u8to32_le(key + 12);
|
||||
|
||||
st->r0 = ( t0 ) & 0x3ffffff;
|
||||
st->r1 = (((t1 << 6) | (t0 >> 26)) ) & 0x3ffff03;
|
||||
st->r2 = (((t2 << 12) | (t1 >> 20))) & 0x3ffc0ff;
|
||||
st->r3 = (((t3 << 18) | (t2 >> 14))) & 0x3f03fff;
|
||||
st->r4 = (( t3 >> 8) ) & 0x00fffff;
|
||||
|
||||
st->s1 = st->r1 * 5;
|
||||
st->s2 = st->r2 * 5;
|
||||
st->s3 = st->r3 * 5;
|
||||
st->s4 = st->r4 * 5;
|
||||
|
||||
st->h0 = 0;
|
||||
st->h1 = 0;
|
||||
st->h2 = 0;
|
||||
st->h3 = 0;
|
||||
st->h4 = 0;
|
||||
|
||||
st->pad0 = u8to32_le(key + 16);
|
||||
st->pad1 = u8to32_le(key + 20);
|
||||
st->pad2 = u8to32_le(key + 24);
|
||||
st->pad3 = u8to32_le(key + 28);
|
||||
|
||||
st->leftover = 0;
|
||||
st->final = 0;
|
||||
}
|
||||
|
||||
void nostr_poly1305_update(nostr_poly1305_ctx_t *st, const unsigned char *m, size_t bytes) {
|
||||
size_t i;
|
||||
|
||||
if (st->leftover) {
|
||||
size_t want = (size_t)(16 - st->leftover);
|
||||
if (want > bytes) {
|
||||
want = bytes;
|
||||
}
|
||||
for (i = 0; i < want; i++) {
|
||||
st->buffer[st->leftover + i] = m[i];
|
||||
}
|
||||
bytes -= want;
|
||||
m += want;
|
||||
st->leftover += want;
|
||||
if (st->leftover < 16) {
|
||||
return;
|
||||
}
|
||||
poly1305_blocks(st, st->buffer, 16);
|
||||
st->leftover = 0;
|
||||
}
|
||||
|
||||
if (bytes >= 16) {
|
||||
size_t want = bytes & ~(size_t)0xf;
|
||||
poly1305_blocks(st, m, want);
|
||||
m += want;
|
||||
bytes -= want;
|
||||
}
|
||||
|
||||
if (bytes) {
|
||||
for (i = 0; i < bytes; i++) {
|
||||
st->buffer[st->leftover + i] = m[i];
|
||||
}
|
||||
st->leftover += bytes;
|
||||
}
|
||||
}
|
||||
|
||||
void nostr_poly1305_final(nostr_poly1305_ctx_t *st, unsigned char tag[16]) {
|
||||
uint32_t h0, h1, h2, h3, h4, c;
|
||||
uint32_t g0, g1, g2, g3, g4;
|
||||
uint64_t f;
|
||||
uint32_t mask;
|
||||
|
||||
if (st->leftover) {
|
||||
size_t i = st->leftover;
|
||||
st->buffer[i++] = 1;
|
||||
for (; i < 16; i++) {
|
||||
st->buffer[i] = 0;
|
||||
}
|
||||
st->final = 1;
|
||||
poly1305_blocks(st, st->buffer, 16);
|
||||
}
|
||||
|
||||
h0 = st->h0;
|
||||
h1 = st->h1;
|
||||
h2 = st->h2;
|
||||
h3 = st->h3;
|
||||
h4 = st->h4;
|
||||
|
||||
c = h1 >> 26; h1 &= 0x3ffffff;
|
||||
h2 += c;
|
||||
c = h2 >> 26; h2 &= 0x3ffffff;
|
||||
h3 += c;
|
||||
c = h3 >> 26; h3 &= 0x3ffffff;
|
||||
h4 += c;
|
||||
c = h4 >> 26; h4 &= 0x3ffffff;
|
||||
h0 += c * 5;
|
||||
c = h0 >> 26; h0 &= 0x3ffffff;
|
||||
h1 += c;
|
||||
|
||||
g0 = h0 + 5;
|
||||
c = g0 >> 26; g0 &= 0x3ffffff;
|
||||
g1 = h1 + c;
|
||||
c = g1 >> 26; g1 &= 0x3ffffff;
|
||||
g2 = h2 + c;
|
||||
c = g2 >> 26; g2 &= 0x3ffffff;
|
||||
g3 = h3 + c;
|
||||
c = g3 >> 26; g3 &= 0x3ffffff;
|
||||
g4 = h4 + c - (1U << 26);
|
||||
|
||||
mask = (g4 >> 31) - 1U;
|
||||
g0 &= mask;
|
||||
g1 &= mask;
|
||||
g2 &= mask;
|
||||
g3 &= mask;
|
||||
g4 &= mask;
|
||||
mask = ~mask;
|
||||
h0 = (h0 & mask) | g0;
|
||||
h1 = (h1 & mask) | g1;
|
||||
h2 = (h2 & mask) | g2;
|
||||
h3 = (h3 & mask) | g3;
|
||||
h4 = (h4 & mask) | g4;
|
||||
|
||||
h0 = ((h0 ) | (h1 << 26)) & 0xffffffff;
|
||||
h1 = ((h1 >> 6 ) | (h2 << 20)) & 0xffffffff;
|
||||
h2 = ((h2 >> 12) | (h3 << 14)) & 0xffffffff;
|
||||
h3 = ((h3 >> 18) | (h4 << 8 )) & 0xffffffff;
|
||||
|
||||
f = (uint64_t)h0 + st->pad0;
|
||||
h0 = (uint32_t)f;
|
||||
f = (uint64_t)h1 + st->pad1 + (f >> 32);
|
||||
h1 = (uint32_t)f;
|
||||
f = (uint64_t)h2 + st->pad2 + (f >> 32);
|
||||
h2 = (uint32_t)f;
|
||||
f = (uint64_t)h3 + st->pad3 + (f >> 32);
|
||||
h3 = (uint32_t)f;
|
||||
|
||||
u32to8_le(tag + 0, h0);
|
||||
u32to8_le(tag + 4, h1);
|
||||
u32to8_le(tag + 8, h2);
|
||||
u32to8_le(tag + 12, h3);
|
||||
|
||||
memset(st, 0, sizeof(*st));
|
||||
}
|
||||
|
||||
int nostr_poly1305_mac(const unsigned char key[32], const unsigned char *msg,
|
||||
size_t msg_len, unsigned char tag[16]) {
|
||||
nostr_poly1305_ctx_t ctx;
|
||||
|
||||
if (!key || (!msg && msg_len != 0) || !tag) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
nostr_poly1305_init(&ctx, key);
|
||||
nostr_poly1305_update(&ctx, msg, msg_len);
|
||||
nostr_poly1305_final(&ctx, tag);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -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
|
||||
@@ -237,28 +236,85 @@ int nostr_secp256k1_ecdh(unsigned char *result, const nostr_secp256k1_pubkey *pu
|
||||
return secp256k1_ecdh(g_ctx, result, &internal_pubkey, seckey, hashfp, data);
|
||||
}
|
||||
|
||||
int nostr_secp256k1_ec_pubkey_tweak_mul(nostr_secp256k1_pubkey* pubkey, const unsigned char* tweak32) {
|
||||
if (g_ctx == NULL || pubkey == NULL || tweak32 == NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
secp256k1_pubkey internal_pubkey;
|
||||
memcpy(&internal_pubkey, pubkey->data, sizeof(secp256k1_pubkey));
|
||||
|
||||
if (!secp256k1_ec_pubkey_tweak_mul(g_ctx, &internal_pubkey, tweak32)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
memcpy(pubkey->data, &internal_pubkey, sizeof(secp256k1_pubkey));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int nostr_secp256k1_ec_pubkey_negate(nostr_secp256k1_pubkey* pubkey) {
|
||||
if (g_ctx == NULL || pubkey == NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
secp256k1_pubkey internal_pubkey;
|
||||
memcpy(&internal_pubkey, pubkey->data, sizeof(secp256k1_pubkey));
|
||||
|
||||
if (!secp256k1_ec_pubkey_negate(g_ctx, &internal_pubkey)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
memcpy(pubkey->data, &internal_pubkey, sizeof(secp256k1_pubkey));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int nostr_secp256k1_ec_pubkey_combine(nostr_secp256k1_pubkey* out,
|
||||
const nostr_secp256k1_pubkey* const* ins,
|
||||
size_t n) {
|
||||
if (g_ctx == NULL || out == NULL || ins == NULL || n == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
secp256k1_pubkey* parsed = (secp256k1_pubkey*)calloc(n, sizeof(secp256k1_pubkey));
|
||||
const secp256k1_pubkey** ptrs = (const secp256k1_pubkey**)calloc(n, sizeof(secp256k1_pubkey*));
|
||||
if (!parsed || !ptrs) {
|
||||
free(parsed);
|
||||
free(ptrs);
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
if (!ins[i]) {
|
||||
free(parsed);
|
||||
free(ptrs);
|
||||
return 0;
|
||||
}
|
||||
memcpy(&parsed[i], ins[i]->data, sizeof(secp256k1_pubkey));
|
||||
ptrs[i] = &parsed[i];
|
||||
}
|
||||
|
||||
secp256k1_pubkey combined;
|
||||
int ok = secp256k1_ec_pubkey_combine(g_ctx, &combined, ptrs, n);
|
||||
|
||||
free(parsed);
|
||||
free(ptrs);
|
||||
|
||||
if (!ok) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
memcpy(out->data, &combined, sizeof(secp256k1_pubkey));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int nostr_secp256k1_get_random_bytes(unsigned char *buf, size_t len) {
|
||||
if (buf == NULL || len == 0) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* NOSTR Core Library - NIP-03: OpenTimestamps Attestations for Events
|
||||
*/
|
||||
|
||||
#include "nip003.h"
|
||||
#include "nip001.h"
|
||||
#include "nostr_http.h"
|
||||
#include "utils.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
cJSON* nostr_nip03_create_proof_event(const char* target_event_id,
|
||||
int target_event_kind,
|
||||
const char* ots_data_base64,
|
||||
const char* relay_url,
|
||||
const unsigned char* private_key) {
|
||||
if (!target_event_id || !ots_data_base64 || !private_key) return NULL;
|
||||
|
||||
cJSON* tags = cJSON_CreateArray();
|
||||
|
||||
// ["e", <target-event-id>, <relay-url>]
|
||||
cJSON* e_tag = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(e_tag, cJSON_CreateString("e"));
|
||||
cJSON_AddItemToArray(e_tag, cJSON_CreateString(target_event_id));
|
||||
if (relay_url) {
|
||||
cJSON_AddItemToArray(e_tag, cJSON_CreateString(relay_url));
|
||||
}
|
||||
cJSON_AddItemToArray(tags, e_tag);
|
||||
|
||||
// ["k", "<target-event-kind>"]
|
||||
char kind_str[16];
|
||||
snprintf(kind_str, sizeof(kind_str), "%d", target_event_kind);
|
||||
cJSON* k_tag = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(k_tag, cJSON_CreateString("k"));
|
||||
cJSON_AddItemToArray(k_tag, cJSON_CreateString(kind_str));
|
||||
cJSON_AddItemToArray(tags, k_tag);
|
||||
|
||||
cJSON* event = nostr_create_and_sign_event(NOSTR_KIND_OT_PROOF, ots_data_base64, tags, private_key, 0);
|
||||
cJSON_Delete(tags);
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
char* nostr_nip03_request_timestamp(const char* event_id_hex,
|
||||
const char* calendar_url,
|
||||
int timeout_seconds) {
|
||||
if (!event_id_hex || !calendar_url) return NULL;
|
||||
|
||||
// Convert hex event ID to binary digest
|
||||
unsigned char digest[32];
|
||||
if (nostr_hex_to_bytes(event_id_hex, digest, 32) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// OTS calendar digest submission endpoint: POST /digest
|
||||
// The body is the raw 32-byte digest.
|
||||
|
||||
char full_url[512];
|
||||
snprintf(full_url, sizeof(full_url), "%s/digest", calendar_url);
|
||||
|
||||
nostr_http_request_t req = {0};
|
||||
req.method = "POST";
|
||||
req.url = full_url;
|
||||
req.body = digest;
|
||||
req.body_len = 32;
|
||||
req.timeout_seconds = timeout_seconds > 0 ? timeout_seconds : 10;
|
||||
req.follow_redirects = 1;
|
||||
req.max_redirects = 3;
|
||||
|
||||
const char* headers[] = {
|
||||
"Content-Type: application/octet-stream",
|
||||
"Accept: application/octet-stream",
|
||||
NULL
|
||||
};
|
||||
req.headers = headers;
|
||||
|
||||
nostr_http_response_t resp = {0};
|
||||
if (nostr_http_request(&req, &resp) != NOSTR_SUCCESS) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (resp.status_code != 200 || !resp.body || resp.body_len == 0) {
|
||||
nostr_http_response_free(&resp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// The response is the raw .ots file data.
|
||||
// We need to base64 encode it for the Nostr event content.
|
||||
size_t b64_size = ((resp.body_len + 2) / 3) * 4 + 1;
|
||||
char* b64 = (char*)malloc(b64_size);
|
||||
if (b64) {
|
||||
base64_encode((const unsigned char*)resp.body, resp.body_len, b64, b64_size);
|
||||
}
|
||||
|
||||
nostr_http_response_free(&resp);
|
||||
return b64;
|
||||
}
|
||||
|
||||
int nostr_nip03_is_proof_complete(const char* ots_data_base64) {
|
||||
if (!ots_data_base64) return -1;
|
||||
|
||||
size_t input_len = strlen(ots_data_base64);
|
||||
size_t max_decoded_len = (input_len / 4) * 3 + 3;
|
||||
unsigned char* data = (unsigned char*)malloc(max_decoded_len);
|
||||
if (!data) return -1;
|
||||
|
||||
size_t len = base64_decode(ots_data_base64, data);
|
||||
if (len == 0) {
|
||||
free(data);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Very simple heuristic for OTS file completeness:
|
||||
// A complete OTS file contains a Bitcoin attestation.
|
||||
// The Bitcoin attestation tag in OTS is 0x05 (Pending) vs 0x00 (Bitcoin).
|
||||
// This is a simplification; a real OTS parser would be better.
|
||||
// For now, we look for the Bitcoin block header attestation tag.
|
||||
|
||||
int complete = 0;
|
||||
// OTS files start with a specific header: "\x00OpenTimestamps\x00\x00\x03\x01"
|
||||
// We just check if it contains the Bitcoin attestation tag (0x00)
|
||||
// followed by the Bitcoin genesis block hash or similar markers.
|
||||
// Actually, the easiest way is to check if it's NOT just a pending attestation.
|
||||
|
||||
// For the purpose of this library, we'll assume if it's > 100 bytes it likely has an attestation.
|
||||
// Initial pending proofs are usually very small (~40-60 bytes).
|
||||
if (len > 150) {
|
||||
complete = 1;
|
||||
}
|
||||
|
||||
free(data);
|
||||
return complete;
|
||||
}
|
||||
|
||||
char* nostr_nip03_upgrade_proof(const char* ots_data_base64,
|
||||
const char* calendar_url,
|
||||
int timeout_seconds) {
|
||||
if (!ots_data_base64 || !calendar_url) return NULL;
|
||||
|
||||
size_t input_len = strlen(ots_data_base64);
|
||||
size_t max_decoded_len = (input_len / 4) * 3 + 3;
|
||||
unsigned char* data = (unsigned char*)malloc(max_decoded_len);
|
||||
if (!data) return NULL;
|
||||
|
||||
size_t len = base64_decode(ots_data_base64, data);
|
||||
if (len == 0) {
|
||||
free(data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// OTS upgrade endpoint: POST /upgrade
|
||||
// Body is the current .ots file content.
|
||||
|
||||
char full_url[512];
|
||||
snprintf(full_url, sizeof(full_url), "%s/upgrade", calendar_url);
|
||||
|
||||
nostr_http_request_t req = {0};
|
||||
req.method = "POST";
|
||||
req.url = full_url;
|
||||
req.body = data;
|
||||
req.body_len = len;
|
||||
req.timeout_seconds = timeout_seconds > 0 ? timeout_seconds : 10;
|
||||
req.follow_redirects = 1;
|
||||
req.max_redirects = 3;
|
||||
|
||||
const char* headers[] = {
|
||||
"Content-Type: application/octet-stream",
|
||||
"Accept: application/octet-stream",
|
||||
NULL
|
||||
};
|
||||
req.headers = headers;
|
||||
|
||||
nostr_http_response_t resp = {0};
|
||||
int ret = nostr_http_request(&req, &resp);
|
||||
free(data);
|
||||
|
||||
if (ret != NOSTR_SUCCESS || resp.status_code != 200 || !resp.body || resp.body_len == 0) {
|
||||
nostr_http_response_free(&resp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// If the response is the same length as input, it might not be upgraded yet
|
||||
if (resp.body_len <= len) {
|
||||
nostr_http_response_free(&resp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t b64_size = ((resp.body_len + 2) / 3) * 4 + 1;
|
||||
char* b64 = (char*)malloc(b64_size);
|
||||
if (b64) {
|
||||
base64_encode((const unsigned char*)resp.body, resp.body_len, b64, b64_size);
|
||||
}
|
||||
|
||||
nostr_http_response_free(&resp);
|
||||
return b64;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* NOSTR Core Library - NIP-03: OpenTimestamps Attestations for Events
|
||||
*/
|
||||
|
||||
#ifndef NOSTR_NIP03_H
|
||||
#define NOSTR_NIP03_H
|
||||
|
||||
#include "nostr_common.h"
|
||||
#include "cjson/cJSON.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define NOSTR_KIND_OT_PROOF 1040
|
||||
|
||||
/**
|
||||
* Create a NIP-03 OpenTimestamps proof event (kind 1040)
|
||||
*
|
||||
* @param target_event_id The ID of the event being timestamped (hex string)
|
||||
* @param target_event_kind The kind of the event being timestamped
|
||||
* @param ots_data_base64 Base64 encoded .ots file content
|
||||
* @param relay_url Optional relay URL where the target event can be found
|
||||
* @param private_key 32-byte private key for signing the proof event
|
||||
* @return cJSON* The signed proof event, or NULL on error
|
||||
*/
|
||||
cJSON* nostr_nip03_create_proof_event(const char* target_event_id,
|
||||
int target_event_kind,
|
||||
const char* ots_data_base64,
|
||||
const char* relay_url,
|
||||
const unsigned char* private_key);
|
||||
|
||||
/**
|
||||
* Request a timestamp for an event ID from an OpenTimestamps calendar
|
||||
*
|
||||
* This function sends the event ID (as a SHA256 digest) to an OTS calendar
|
||||
* and returns the initial .ots file data (base64 encoded).
|
||||
*
|
||||
* @param event_id_hex The event ID to timestamp (64-char hex string)
|
||||
* @param calendar_url The OTS calendar URL (e.g., "https://alice.btc.calendar.opentimestamps.org")
|
||||
* @param timeout_seconds Request timeout
|
||||
* @return char* Base64 encoded .ots data, or NULL on error (caller must free)
|
||||
*/
|
||||
char* nostr_nip03_request_timestamp(const char* event_id_hex,
|
||||
const char* calendar_url,
|
||||
int timeout_seconds);
|
||||
|
||||
/**
|
||||
* Check if an OTS proof is complete (has a Bitcoin attestation)
|
||||
*
|
||||
* @param ots_data_base64 Base64 encoded .ots file content
|
||||
* @return int 1 if complete, 0 if pending, -1 on error
|
||||
*/
|
||||
int nostr_nip03_is_proof_complete(const char* ots_data_base64);
|
||||
|
||||
/**
|
||||
* Upgrade an OTS proof by fetching missing attestations from a calendar
|
||||
*
|
||||
* @param ots_data_base64 Current base64 encoded .ots file content
|
||||
* @param calendar_url The OTS calendar URL
|
||||
* @param timeout_seconds Request timeout
|
||||
* @return char* New base64 encoded .ots data, or NULL on error/no change (caller must free)
|
||||
*/
|
||||
char* nostr_nip03_upgrade_proof(const char* ots_data_base64,
|
||||
const char* calendar_url,
|
||||
int timeout_seconds);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NOSTR_NIP03_H */
|
||||
+13
-79
@@ -12,46 +12,7 @@
|
||||
#include <ctype.h>
|
||||
|
||||
|
||||
#include <curl/curl.h>
|
||||
|
||||
|
||||
// Structure for HTTP response handling
|
||||
typedef struct {
|
||||
char* data;
|
||||
size_t size;
|
||||
size_t capacity;
|
||||
} nip05_http_response_t;
|
||||
|
||||
/**
|
||||
* Callback function for curl to write HTTP response data
|
||||
*/
|
||||
static size_t nip05_write_callback(void* contents, size_t size, size_t nmemb, void* userp) {
|
||||
nip05_http_response_t* response = (nip05_http_response_t*)userp;
|
||||
size_t total_size = size * nmemb;
|
||||
|
||||
// Check if we need to expand the buffer
|
||||
if (response->size + total_size >= response->capacity) {
|
||||
size_t new_capacity = response->capacity * 2;
|
||||
if (new_capacity < response->size + total_size + 1) {
|
||||
new_capacity = response->size + total_size + 1;
|
||||
}
|
||||
|
||||
char* new_data = realloc(response->data, new_capacity);
|
||||
if (!new_data) {
|
||||
return 0; // Out of memory
|
||||
}
|
||||
|
||||
response->data = new_data;
|
||||
response->capacity = new_capacity;
|
||||
}
|
||||
|
||||
// Append the new data
|
||||
memcpy(response->data + response->size, contents, total_size);
|
||||
response->size += total_size;
|
||||
response->data[response->size] = '\0';
|
||||
|
||||
return total_size;
|
||||
}
|
||||
#include "nostr_http.h"
|
||||
|
||||
/**
|
||||
* Parse and validate a NIP-05 identifier into local part and domain
|
||||
@@ -104,49 +65,22 @@ static int nip05_http_get(const char* url, int timeout_seconds, char** response_
|
||||
if (!url || !response_data) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
CURL* curl = curl_easy_init();
|
||||
if (!curl) {
|
||||
return NOSTR_ERROR_NETWORK_FAILED;
|
||||
}
|
||||
|
||||
nip05_http_response_t response = {0};
|
||||
response.capacity = 1024;
|
||||
response.data = malloc(response.capacity);
|
||||
if (!response.data) {
|
||||
curl_easy_cleanup(curl);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
response.data[0] = '\0';
|
||||
|
||||
// Set curl options with proper type casting to fix warnings
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, (curl_write_callback)nip05_write_callback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)(timeout_seconds > 0 ? timeout_seconds : NIP05_DEFAULT_TIMEOUT));
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L); // NIP-05 forbids redirects
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "nostr-core/1.0");
|
||||
|
||||
// Perform the request
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
long response_code = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
|
||||
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if (res != CURLE_OK) {
|
||||
free(response.data);
|
||||
|
||||
long status = 0;
|
||||
int rc = nostr_http_get(url,
|
||||
timeout_seconds > 0 ? timeout_seconds : NIP05_DEFAULT_TIMEOUT,
|
||||
response_data,
|
||||
&status);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
return NOSTR_ERROR_NIP05_HTTP_FAILED;
|
||||
}
|
||||
|
||||
if (response_code != 200) {
|
||||
free(response.data);
|
||||
|
||||
if (status != 200) {
|
||||
free(*response_data);
|
||||
*response_data = NULL;
|
||||
return NOSTR_ERROR_NIP05_HTTP_FAILED;
|
||||
}
|
||||
|
||||
*response_data = response.data;
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
+3
-17
@@ -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;
|
||||
|
||||
+25
-87
@@ -11,50 +11,13 @@
|
||||
|
||||
#ifndef DISABLE_NIP05 // NIP-11 uses the same HTTP infrastructure as NIP-05
|
||||
|
||||
#include <curl/curl.h>
|
||||
#include "nostr_http.h"
|
||||
|
||||
// Maximum sizes for NIP-11 operations
|
||||
#define NIP11_MAX_URL_SIZE 512
|
||||
#define NIP11_MAX_RESPONSE_SIZE 16384
|
||||
#define NIP11_DEFAULT_TIMEOUT 10
|
||||
|
||||
// Structure for HTTP response handling (same as NIP-05)
|
||||
typedef struct {
|
||||
char* data;
|
||||
size_t size;
|
||||
size_t capacity;
|
||||
} nip11_http_response_t;
|
||||
|
||||
/**
|
||||
* Callback function for curl to write HTTP response data
|
||||
*/
|
||||
static size_t nip11_write_callback(void* contents, size_t size, size_t nmemb, nip11_http_response_t* response) {
|
||||
size_t total_size = size * nmemb;
|
||||
|
||||
// Check if we need to expand the buffer
|
||||
if (response->size + total_size >= response->capacity) {
|
||||
size_t new_capacity = response->capacity * 2;
|
||||
if (new_capacity < response->size + total_size + 1) {
|
||||
new_capacity = response->size + total_size + 1;
|
||||
}
|
||||
|
||||
char* new_data = realloc(response->data, new_capacity);
|
||||
if (!new_data) {
|
||||
return 0; // Out of memory
|
||||
}
|
||||
|
||||
response->data = new_data;
|
||||
response->capacity = new_capacity;
|
||||
}
|
||||
|
||||
// Append the new data
|
||||
memcpy(response->data + response->size, contents, total_size);
|
||||
response->size += total_size;
|
||||
response->data[response->size] = '\0';
|
||||
|
||||
return total_size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert WebSocket URL to HTTP URL for NIP-11 document retrieval
|
||||
*/
|
||||
@@ -341,60 +304,35 @@ int nostr_nip11_fetch_relay_info(const char* relay_url, nostr_relay_info_t** inf
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
// Make HTTP request with NIP-11 required header
|
||||
CURL* curl = curl_easy_init();
|
||||
if (!curl) {
|
||||
free(http_url);
|
||||
free(info);
|
||||
return NOSTR_ERROR_NETWORK_FAILED;
|
||||
}
|
||||
|
||||
// Use the HTTP response structure
|
||||
nip11_http_response_t response = {0};
|
||||
response.capacity = 1024;
|
||||
response.data = malloc(response.capacity);
|
||||
if (!response.data) {
|
||||
curl_easy_cleanup(curl);
|
||||
free(http_url);
|
||||
free(info);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
response.data[0] = '\0';
|
||||
|
||||
// Set up headers for NIP-11
|
||||
struct curl_slist* headers = NULL;
|
||||
headers = curl_slist_append(headers, "Accept: application/nostr+json");
|
||||
|
||||
// Set curl options - use proper function pointer cast
|
||||
curl_easy_setopt(curl, CURLOPT_URL, http_url);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, (curl_write_callback)nip11_write_callback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)(timeout_seconds > 0 ? timeout_seconds : NIP11_DEFAULT_TIMEOUT));
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); // NIP-11 allows redirects
|
||||
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 3L);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "nostr-core/1.0");
|
||||
|
||||
// Perform the request
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
long response_code = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
|
||||
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
const char* headers[] = {
|
||||
"Accept: application/nostr+json",
|
||||
NULL
|
||||
};
|
||||
|
||||
nostr_http_request_t req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.method = "GET";
|
||||
req.url = http_url;
|
||||
req.headers = headers;
|
||||
req.timeout_seconds = timeout_seconds > 0 ? timeout_seconds : NIP11_DEFAULT_TIMEOUT;
|
||||
req.follow_redirects = 1;
|
||||
req.max_redirects = 3;
|
||||
|
||||
nostr_http_response_t response;
|
||||
int http_rc = nostr_http_request(&req, &response);
|
||||
free(http_url);
|
||||
|
||||
if (res != CURLE_OK || response_code != 200) {
|
||||
free(response.data);
|
||||
|
||||
if (http_rc != NOSTR_SUCCESS || response.status_code != 200) {
|
||||
if (http_rc == NOSTR_SUCCESS) {
|
||||
nostr_http_response_free(&response);
|
||||
}
|
||||
free(info);
|
||||
return NOSTR_ERROR_NIP05_HTTP_FAILED;
|
||||
}
|
||||
|
||||
|
||||
// Parse the relay information
|
||||
int parse_result = nip11_parse_relay_info(response.data, info);
|
||||
free(response.data);
|
||||
int parse_result = nip11_parse_relay_info(response.body, info);
|
||||
nostr_http_response_free(&response);
|
||||
|
||||
if (parse_result != NOSTR_SUCCESS) {
|
||||
nostr_nip11_relay_info_free(info);
|
||||
|
||||
@@ -354,6 +354,22 @@ cJSON* nostr_nip17_receive_dm(cJSON* gift_wrap,
|
||||
// Unseal the rumor
|
||||
cJSON* rumor = nostr_nip59_unseal_rumor(seal, sender_public_key, recipient_private_key);
|
||||
cJSON_Delete(seal); // Seal is no longer needed
|
||||
if (!rumor) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// NIP-17 safety check: seal pubkey must match rumor pubkey to prevent impersonation
|
||||
cJSON* rumor_pubkey_item = cJSON_GetObjectItem(rumor, "pubkey");
|
||||
if (!rumor_pubkey_item || !cJSON_IsString(rumor_pubkey_item)) {
|
||||
cJSON_Delete(rumor);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char* rumor_pubkey_hex = cJSON_GetStringValue(rumor_pubkey_item);
|
||||
if (!rumor_pubkey_hex || strcmp(sender_pubkey_hex, rumor_pubkey_hex) != 0) {
|
||||
cJSON_Delete(rumor);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return rumor;
|
||||
}
|
||||
|
||||
+1
-1
@@ -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;
|
||||
}
|
||||
|
||||
+86
-8
@@ -41,6 +41,39 @@ static time_t random_past_timestamp(long max_delay_sec) {
|
||||
return now - random_offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* NIP-44 padding calculation (mirrors nip044.c for output sizing)
|
||||
*/
|
||||
static size_t calc_nip44_padded_len(size_t unpadded_len) {
|
||||
if (unpadded_len <= 32) {
|
||||
return 32;
|
||||
}
|
||||
|
||||
size_t next_power = 1;
|
||||
while (next_power < unpadded_len) {
|
||||
next_power <<= 1;
|
||||
}
|
||||
|
||||
size_t chunk = (next_power <= 256) ? 32 : (next_power / 8);
|
||||
return chunk * ((unpadded_len - 1) / chunk + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate safe output buffer size for NIP-44 encrypted base64 payload.
|
||||
* Returns 0 if plaintext length exceeds NIP-44 max.
|
||||
*/
|
||||
static size_t calc_nip44_encrypted_b64_size(size_t plaintext_len) {
|
||||
if (plaintext_len > NOSTR_NIP44_MAX_PLAINTEXT_SIZE) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t padded_len = calc_nip44_padded_len(plaintext_len) + 2; // +2 for length prefix
|
||||
size_t payload_len = 1 + 32 + padded_len + 32; // version + nonce + ciphertext + mac
|
||||
size_t b64_len = ((payload_len + 2) / 3) * 4 + 1; // +1 for NUL terminator
|
||||
|
||||
return b64_len;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random private key for gift wrap
|
||||
*/
|
||||
@@ -160,18 +193,31 @@ cJSON* nostr_nip59_create_seal(cJSON* rumor, const unsigned char* sender_private
|
||||
}
|
||||
|
||||
// Encrypt the rumor using NIP-44
|
||||
char encrypted_content[4096]; // Should be large enough for most events
|
||||
size_t encrypted_size = calc_nip44_encrypted_b64_size(strlen(rumor_json));
|
||||
if (encrypted_size == 0) {
|
||||
free(rumor_json);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char* encrypted_content = malloc(encrypted_size);
|
||||
if (!encrypted_content) {
|
||||
free(rumor_json);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int encrypt_result = nostr_nip44_encrypt(sender_private_key, recipient_public_key,
|
||||
rumor_json, encrypted_content, sizeof(encrypted_content));
|
||||
rumor_json, encrypted_content, encrypted_size);
|
||||
free(rumor_json);
|
||||
|
||||
if (encrypt_result != NOSTR_SUCCESS) {
|
||||
free(encrypted_content);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get sender's public key
|
||||
unsigned char sender_public_key[32];
|
||||
if (nostr_ec_public_key_from_private_key(sender_private_key, sender_public_key) != 0) {
|
||||
free(encrypted_content);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -181,6 +227,7 @@ cJSON* nostr_nip59_create_seal(cJSON* rumor, const unsigned char* sender_private
|
||||
// Create seal event (kind 13)
|
||||
cJSON* seal = cJSON_CreateObject();
|
||||
if (!seal) {
|
||||
free(encrypted_content);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -191,6 +238,7 @@ cJSON* nostr_nip59_create_seal(cJSON* rumor, const unsigned char* sender_private
|
||||
cJSON_AddNumberToObject(seal, "kind", 13);
|
||||
cJSON_AddItemToObject(seal, "tags", cJSON_CreateArray()); // Empty tags array
|
||||
cJSON_AddStringToObject(seal, "content", encrypted_content);
|
||||
free(encrypted_content);
|
||||
|
||||
// Calculate event ID
|
||||
char event_id[65];
|
||||
@@ -261,13 +309,27 @@ cJSON* nostr_nip59_create_gift_wrap(cJSON* seal, const char* recipient_public_ke
|
||||
}
|
||||
|
||||
// Encrypt the seal using NIP-44
|
||||
char encrypted_content[8192]; // Larger buffer for nested encryption
|
||||
size_t encrypted_size = calc_nip44_encrypted_b64_size(strlen(seal_json));
|
||||
if (encrypted_size == 0) {
|
||||
memory_clear(random_private_key, 32);
|
||||
free(seal_json);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char* encrypted_content = malloc(encrypted_size);
|
||||
if (!encrypted_content) {
|
||||
memory_clear(random_private_key, 32);
|
||||
free(seal_json);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int encrypt_result = nostr_nip44_encrypt(random_private_key, recipient_public_key,
|
||||
seal_json, encrypted_content, sizeof(encrypted_content));
|
||||
seal_json, encrypted_content, encrypted_size);
|
||||
free(seal_json);
|
||||
|
||||
if (encrypt_result != NOSTR_SUCCESS) {
|
||||
memory_clear(random_private_key, 32);
|
||||
free(encrypted_content);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -275,6 +337,7 @@ cJSON* nostr_nip59_create_gift_wrap(cJSON* seal, const char* recipient_public_ke
|
||||
cJSON* gift_wrap = cJSON_CreateObject();
|
||||
if (!gift_wrap) {
|
||||
memory_clear(random_private_key, 32);
|
||||
free(encrypted_content);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -293,6 +356,7 @@ cJSON* nostr_nip59_create_gift_wrap(cJSON* seal, const char* recipient_public_ke
|
||||
cJSON_AddItemToObject(gift_wrap, "tags", tags);
|
||||
|
||||
cJSON_AddStringToObject(gift_wrap, "content", encrypted_content);
|
||||
free(encrypted_content);
|
||||
|
||||
// Calculate event ID
|
||||
char event_id[65];
|
||||
@@ -359,16 +423,23 @@ cJSON* nostr_nip59_unwrap_gift(cJSON* gift_wrap, const unsigned char* recipient_
|
||||
}
|
||||
|
||||
// Decrypt the content using NIP-44
|
||||
char decrypted_json[8192];
|
||||
size_t decrypted_size = strlen(encrypted_content) + 1;
|
||||
char* decrypted_json = malloc(decrypted_size);
|
||||
if (!decrypted_json) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int decrypt_result = nostr_nip44_decrypt(recipient_private_key, sender_public_key,
|
||||
encrypted_content, decrypted_json, sizeof(decrypted_json));
|
||||
encrypted_content, decrypted_json, decrypted_size);
|
||||
|
||||
if (decrypt_result != NOSTR_SUCCESS) {
|
||||
free(decrypted_json);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Parse the decrypted JSON as the seal event
|
||||
cJSON* seal = cJSON_Parse(decrypted_json);
|
||||
free(decrypted_json);
|
||||
if (!seal) {
|
||||
return NULL;
|
||||
}
|
||||
@@ -394,16 +465,23 @@ cJSON* nostr_nip59_unseal_rumor(cJSON* seal, const unsigned char* sender_public_
|
||||
const char* encrypted_content = cJSON_GetStringValue(content_item);
|
||||
|
||||
// Decrypt the content using NIP-44
|
||||
char decrypted_json[4096];
|
||||
size_t decrypted_size = strlen(encrypted_content) + 1;
|
||||
char* decrypted_json = malloc(decrypted_size);
|
||||
if (!decrypted_json) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int decrypt_result = nostr_nip44_decrypt(recipient_private_key, sender_public_key,
|
||||
encrypted_content, decrypted_json, sizeof(decrypted_json));
|
||||
encrypted_content, decrypted_json, decrypted_size);
|
||||
|
||||
if (decrypt_result != NOSTR_SUCCESS) {
|
||||
free(decrypted_json);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Parse the decrypted JSON as the rumor event
|
||||
cJSON* rumor = cJSON_Parse(decrypted_json);
|
||||
free(decrypted_json);
|
||||
if (!rumor) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ extern "C" {
|
||||
#define NOSTR_NIP60_HISTORY_KIND 7376
|
||||
#define NOSTR_NIP60_QUOTE_KIND 7374
|
||||
|
||||
#define NOSTR_CASHU_KEYSET_ID_HEX_SIZE 17
|
||||
#define NOSTR_CASHU_KEYSET_ID_HEX_SIZE 65
|
||||
#define NOSTR_CASHU_PUBKEY_HEX_SIZE 67
|
||||
#define NOSTR_CASHU_EVENT_ID_HEX_SIZE 65
|
||||
|
||||
|
||||
+15
-10
@@ -2,10 +2,10 @@
|
||||
#define NOSTR_CORE_H
|
||||
|
||||
// Version information (auto-updated by increment_and_push.sh)
|
||||
#define VERSION "v0.5.4"
|
||||
#define VERSION "v0.6.2"
|
||||
#define VERSION_MAJOR 0
|
||||
#define VERSION_MINOR 5
|
||||
#define VERSION_PATCH 4
|
||||
#define VERSION_MINOR 6
|
||||
#define VERSION_PATCH 2
|
||||
|
||||
/*
|
||||
* NOSTR Core Library - Complete API Reference
|
||||
@@ -177,6 +177,7 @@ extern "C" {
|
||||
|
||||
// NIP implementations
|
||||
#include "nip001.h" // Basic Protocol
|
||||
#include "nip003.h" // OpenTimestamps
|
||||
#include "nip004.h" // Encryption (legacy)
|
||||
#include "nip005.h" // DNS-based identifiers
|
||||
#include "nip006.h" // Key derivation from mnemonic
|
||||
@@ -193,6 +194,9 @@ extern "C" {
|
||||
#include "nip061.h" // Nutzaps
|
||||
#include "cashu_mint.h" // Cashu mint HTTP client
|
||||
|
||||
#include "nostr_http.h" // Shared HTTP client
|
||||
#include "blossom_client.h" // Blossom HTTP client
|
||||
|
||||
// Authentication and request validation system
|
||||
#include "request_validator.h" // Request validation and authentication rules
|
||||
|
||||
@@ -241,13 +245,14 @@ typedef struct nostr_pool_subscription nostr_pool_subscription_t;
|
||||
|
||||
// Reconnection configuration
|
||||
typedef struct {
|
||||
int enable_auto_reconnect; // 1 = enable, 0 = disable
|
||||
int max_reconnect_attempts; // Max attempts per relay
|
||||
int initial_reconnect_delay_ms; // Initial delay between attempts
|
||||
int max_reconnect_delay_ms; // Max delay (cap exponential backoff)
|
||||
int reconnect_backoff_multiplier; // Delay multiplier
|
||||
int ping_interval_seconds; // How often to ping (0 = disable)
|
||||
int pong_timeout_seconds; // How long to wait for pong before reconnecting
|
||||
int enable_auto_reconnect; // 1 = enable, 0 = disable
|
||||
int max_reconnect_attempts; // Max attempts per relay
|
||||
int initial_reconnect_delay_ms; // Initial delay between attempts
|
||||
int max_reconnect_delay_ms; // Max delay (cap exponential backoff)
|
||||
int reconnect_backoff_multiplier; // Delay multiplier
|
||||
int reconnect_reset_stability_seconds; // Connected time required before resetting reconnect_attempts
|
||||
int ping_interval_seconds; // How often to ping (0 = disable)
|
||||
int pong_timeout_seconds; // How long to wait for pong before reconnecting
|
||||
} nostr_pool_reconnect_config_t;
|
||||
|
||||
// Relay pool management functions
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
/*
|
||||
* NOSTR Core Library - Shared HTTP Client
|
||||
*/
|
||||
|
||||
#include "nostr_http.h"
|
||||
|
||||
#include <curl/curl.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <unistd.h>
|
||||
|
||||
typedef struct {
|
||||
char* data;
|
||||
size_t len;
|
||||
size_t cap;
|
||||
size_t max_bytes;
|
||||
int truncated;
|
||||
} nostr_http_buffer_t;
|
||||
|
||||
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 char g_ca_bundle_path[512] = {0};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
static size_t write_cb(void* contents, size_t size, size_t nmemb, void* userp) {
|
||||
nostr_http_buffer_t* rb = (nostr_http_buffer_t*)userp;
|
||||
size_t total = size * nmemb;
|
||||
if (!rb || total == 0) return total;
|
||||
if (!append_bytes(rb, contents, total)) return 0;
|
||||
return total;
|
||||
}
|
||||
|
||||
static size_t header_cb(void* contents, size_t size, size_t nmemb, void* userp) {
|
||||
nostr_http_buffer_t* hb = (nostr_http_buffer_t*)userp;
|
||||
size_t total = size * nmemb;
|
||||
if (!hb || total == 0) return total;
|
||||
if (!append_bytes(hb, contents, total)) return 0;
|
||||
return total;
|
||||
}
|
||||
|
||||
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) {
|
||||
const char* env = getenv("SSL_CERT_FILE");
|
||||
if (env && env[0] != '\0' && access(env, R_OK) == 0) {
|
||||
return env;
|
||||
}
|
||||
|
||||
static const char* candidates[] = {
|
||||
"/etc/ssl/certs/ca-certificates.crt",
|
||||
"/etc/ssl/cert.pem",
|
||||
"/etc/pki/tls/certs/ca-bundle.crt",
|
||||
"/etc/ssl/ca-bundle.pem"
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++) {
|
||||
if (access(candidates[i], R_OK) == 0) {
|
||||
return candidates[i];
|
||||
}
|
||||
}
|
||||
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));
|
||||
|
||||
CURL* curl = curl_easy_init();
|
||||
if (!curl) return NOSTR_ERROR_NETWORK_FAILED;
|
||||
|
||||
const char* method = (req->method && req->method[0] != '\0') ? req->method : "GET";
|
||||
int timeout = req->timeout_seconds > 0 ? req->timeout_seconds : 30;
|
||||
int follow = req->follow_redirects;
|
||||
if (follow != 0 && follow != 1) follow = 1;
|
||||
int max_redirs = req->max_redirects > 0 ? req->max_redirects : 3;
|
||||
const char* user_agent = (req->user_agent && req->user_agent[0] != '\0') ? req->user_agent : "nostr-core/1.0";
|
||||
|
||||
nostr_http_buffer_t body = {0};
|
||||
body.max_bytes = req->max_response_bytes;
|
||||
|
||||
nostr_http_buffer_t headers = {0};
|
||||
|
||||
struct curl_slist* header_list = NULL;
|
||||
if (req->headers) {
|
||||
for (const char** h = req->headers; *h; h++) {
|
||||
if ((*h)[0] != '\0') header_list = curl_slist_append(header_list, *h);
|
||||
}
|
||||
}
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, req->url);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)timeout);
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, (long)follow);
|
||||
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, (long)max_redirs);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &body);
|
||||
|
||||
if (req->capture_headers) {
|
||||
curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_cb);
|
||||
curl_easy_setopt(curl, CURLOPT_HEADERDATA, &headers);
|
||||
}
|
||||
|
||||
if (header_list) {
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header_list);
|
||||
}
|
||||
|
||||
if (g_ca_bundle_path[0] != '\0') {
|
||||
curl_easy_setopt(curl, CURLOPT_CAINFO, g_ca_bundle_path);
|
||||
}
|
||||
|
||||
if (strcasecmp(method, "GET") == 0) {
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
|
||||
} else if (strcasecmp(method, "POST") == 0) {
|
||||
curl_easy_setopt(curl, CURLOPT_POST, 1L);
|
||||
if (req->body && req->body_len > 0) {
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)req->body_len);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, (const char*)req->body);
|
||||
} else {
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "");
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, 0L);
|
||||
}
|
||||
// Disable Expect: 100-continue
|
||||
header_list = curl_slist_append(header_list, "Expect:");
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header_list);
|
||||
} else if (strcasecmp(method, "HEAD") == 0) {
|
||||
curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "HEAD");
|
||||
} else {
|
||||
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method);
|
||||
if (req->body && req->body_len > 0) {
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)req->body_len);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, (const char*)req->body);
|
||||
}
|
||||
}
|
||||
|
||||
CURLcode rc = curl_easy_perform(curl);
|
||||
long status = 0;
|
||||
char* content_type = NULL;
|
||||
char* content_type_copy = NULL;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
|
||||
curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &content_type);
|
||||
if (content_type && content_type[0] != '\0') {
|
||||
content_type_copy = nostr_http_strdup_local(content_type);
|
||||
}
|
||||
|
||||
if (header_list) curl_slist_free_all(header_list);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if (rc != CURLE_OK) {
|
||||
free(body.data);
|
||||
free(headers.data);
|
||||
free(content_type_copy);
|
||||
return NOSTR_ERROR_NETWORK_FAILED;
|
||||
}
|
||||
|
||||
resp->status_code = status;
|
||||
resp->body = body.data ? body.data : nostr_http_strdup_local("");
|
||||
resp->body_len = body.data ? body.len : 0;
|
||||
resp->headers_raw = headers.data;
|
||||
resp->content_type = content_type_copy;
|
||||
resp->truncated = body.truncated;
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* NOSTR Core Library - Shared HTTP Client
|
||||
*/
|
||||
|
||||
#ifndef NOSTR_HTTP_H
|
||||
#define NOSTR_HTTP_H
|
||||
|
||||
#include "nostr_common.h"
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
char* body;
|
||||
size_t body_len;
|
||||
long status_code;
|
||||
char* content_type;
|
||||
char* headers_raw;
|
||||
int truncated;
|
||||
} nostr_http_response_t;
|
||||
|
||||
typedef struct {
|
||||
const char* method;
|
||||
const char* url;
|
||||
const char** headers;
|
||||
const unsigned char* body;
|
||||
size_t body_len;
|
||||
int timeout_seconds;
|
||||
size_t max_response_bytes;
|
||||
int follow_redirects;
|
||||
int max_redirects;
|
||||
const char* user_agent;
|
||||
int capture_headers;
|
||||
} nostr_http_request_t;
|
||||
|
||||
void nostr_http_set_ca_bundle(const char* ca_bundle_path);
|
||||
const char* nostr_http_detect_ca_bundle(void);
|
||||
|
||||
int nostr_http_request(const nostr_http_request_t* req, nostr_http_response_t* resp);
|
||||
void nostr_http_response_free(nostr_http_response_t* resp);
|
||||
|
||||
int nostr_http_get(const char* url, int timeout_seconds, char** body_out, long* status_out);
|
||||
int nostr_http_post_json(const char* url,
|
||||
const char* json_body,
|
||||
int timeout_seconds,
|
||||
char** body_out,
|
||||
long* status_out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NOSTR_HTTP_H */
|
||||
@@ -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 */
|
||||
+12
-6
@@ -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
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* Blossom Live Client Integration Test
|
||||
*
|
||||
* Uploads a text file to a Blossom server, downloads it back,
|
||||
* and verifies the round-trip content.
|
||||
*
|
||||
* Required env:
|
||||
* BLOSSOM_TEST_PRIVKEY_HEX = 64-char hex private key
|
||||
*
|
||||
* Optional env:
|
||||
* BLOSSOM_TEST_SERVER = Blossom base URL (default: https://blossom.laantungir.net)
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "../nostr_core/nostr_core.h"
|
||||
|
||||
static int write_text_file(const char* path, const char* text) {
|
||||
if (!path || !text) return -1;
|
||||
FILE* fp = fopen(path, "wb");
|
||||
if (!fp) return -1;
|
||||
size_t len = strlen(text);
|
||||
size_t n = fwrite(text, 1, len, fp);
|
||||
fclose(fp);
|
||||
return (n == len) ? 0 : -1;
|
||||
}
|
||||
|
||||
static int read_text_file(const char* path, char** out_text, size_t* out_len) {
|
||||
if (!path || !out_text || !out_len) return -1;
|
||||
|
||||
*out_text = NULL;
|
||||
*out_len = 0;
|
||||
|
||||
FILE* fp = fopen(path, "rb");
|
||||
if (!fp) return -1;
|
||||
|
||||
if (fseek(fp, 0, SEEK_END) != 0) {
|
||||
fclose(fp);
|
||||
return -1;
|
||||
}
|
||||
|
||||
long sz = ftell(fp);
|
||||
if (sz < 0) {
|
||||
fclose(fp);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (fseek(fp, 0, SEEK_SET) != 0) {
|
||||
fclose(fp);
|
||||
return -1;
|
||||
}
|
||||
|
||||
char* buf = (char*)malloc((size_t)sz + 1U);
|
||||
if (!buf) {
|
||||
fclose(fp);
|
||||
return -1;
|
||||
}
|
||||
|
||||
size_t n = fread(buf, 1, (size_t)sz, fp);
|
||||
fclose(fp);
|
||||
if (n != (size_t)sz) {
|
||||
free(buf);
|
||||
return -1;
|
||||
}
|
||||
|
||||
buf[n] = '\0';
|
||||
*out_text = buf;
|
||||
*out_len = n;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int load_private_key_from_env(unsigned char out_privkey[32]) {
|
||||
const char* hex = getenv("BLOSSOM_TEST_PRIVKEY_HEX");
|
||||
if (!hex || hex[0] == '\0') {
|
||||
printf("[SKIP] BLOSSOM_TEST_PRIVKEY_HEX not set\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (strlen(hex) != 64) {
|
||||
printf("[SKIP] BLOSSOM_TEST_PRIVKEY_HEX must be 64 hex chars\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (nostr_hex_to_bytes(hex, out_privkey, 32) != 0) {
|
||||
printf("[SKIP] BLOSSOM_TEST_PRIVKEY_HEX is not valid hex\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
const char* server = getenv("BLOSSOM_TEST_SERVER");
|
||||
if (!server || server[0] == '\0') {
|
||||
server = "https://blossom.laantungir.net";
|
||||
}
|
||||
|
||||
printf("Blossom Live Client Integration Test\n");
|
||||
printf("====================================\n");
|
||||
printf("Server: %s\n", server);
|
||||
|
||||
const char* ca_bundle = nostr_http_detect_ca_bundle();
|
||||
if (ca_bundle && ca_bundle[0] != '\0') {
|
||||
nostr_http_set_ca_bundle(ca_bundle);
|
||||
}
|
||||
|
||||
unsigned char private_key[32] = {0};
|
||||
int key_rc = load_private_key_from_env(private_key);
|
||||
if (key_rc != 0) {
|
||||
return (key_rc == 1) ? 0 : 1;
|
||||
}
|
||||
|
||||
char input_text[1024];
|
||||
time_t now = time(NULL);
|
||||
int nw = snprintf(input_text,
|
||||
sizeof(input_text),
|
||||
"Didactyl Blossom live test\n"
|
||||
"timestamp=%ld\n"
|
||||
"server=%s\n"
|
||||
"message=Round-trip file upload/download validation.\n",
|
||||
(long)now,
|
||||
server);
|
||||
if (nw < 0 || (size_t)nw >= sizeof(input_text)) {
|
||||
printf("[FAIL] Unable to create input text payload\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char* input_path = "tests/.tmp_blossom_input.txt";
|
||||
const char* output_path = "tests/.tmp_blossom_output.txt";
|
||||
|
||||
if (write_text_file(input_path, input_text) != 0) {
|
||||
printf("[FAIL] Unable to write input file: %s\n", input_path);
|
||||
return 1;
|
||||
}
|
||||
|
||||
blossom_blob_descriptor_t uploaded;
|
||||
memset(&uploaded, 0, sizeof(uploaded));
|
||||
|
||||
int rc = blossom_upload_file(server,
|
||||
input_path,
|
||||
"text/plain; charset=utf-8",
|
||||
private_key,
|
||||
30,
|
||||
&uploaded);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
printf("[FAIL] blossom_upload_file rc=%d\n", rc);
|
||||
remove(input_path);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (uploaded.sha256[0] == '\0') {
|
||||
printf("[FAIL] Upload succeeded but returned empty sha256\n");
|
||||
remove(input_path);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("[INFO] Uploaded sha256: %s\n", uploaded.sha256);
|
||||
printf("[INFO] Uploaded url: %s\n", uploaded.url);
|
||||
|
||||
blossom_blob_descriptor_t downloaded;
|
||||
memset(&downloaded, 0, sizeof(downloaded));
|
||||
|
||||
rc = blossom_download_to_file(server,
|
||||
uploaded.sha256,
|
||||
output_path,
|
||||
30,
|
||||
1024U * 1024U,
|
||||
&downloaded);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
printf("[FAIL] blossom_download_to_file rc=%d\n", rc);
|
||||
remove(input_path);
|
||||
return 1;
|
||||
}
|
||||
|
||||
char* output_text = NULL;
|
||||
size_t output_len = 0;
|
||||
if (read_text_file(output_path, &output_text, &output_len) != 0) {
|
||||
printf("[FAIL] Unable to read output file: %s\n", output_path);
|
||||
remove(input_path);
|
||||
remove(output_path);
|
||||
return 1;
|
||||
}
|
||||
|
||||
size_t input_len = strlen(input_text);
|
||||
int same = (input_len == output_len) && (memcmp(input_text, output_text, input_len) == 0);
|
||||
if (!same) {
|
||||
printf("[FAIL] Round-trip mismatch: input_len=%zu output_len=%zu\n", input_len, output_len);
|
||||
free(output_text);
|
||||
remove(input_path);
|
||||
remove(output_path);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (strcmp(uploaded.sha256, downloaded.sha256) != 0) {
|
||||
printf("[FAIL] SHA mismatch: uploaded=%s downloaded=%s\n", uploaded.sha256, downloaded.sha256);
|
||||
free(output_text);
|
||||
remove(input_path);
|
||||
remove(output_path);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("[PASS] Upload/download round-trip verified\n");
|
||||
printf("[PASS] sha256=%s\n", uploaded.sha256);
|
||||
printf("[PASS] bytes=%zu\n", output_len);
|
||||
|
||||
free(output_text);
|
||||
remove(input_path);
|
||||
remove(output_path);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* blossom_client unit/integration tests using local mock Blossom server.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "../nostr_core/nostr_core.h"
|
||||
|
||||
static int tests_run = 0;
|
||||
static int tests_passed = 0;
|
||||
|
||||
#define TEST_ASSERT(cond, msg) do { \
|
||||
tests_run++; \
|
||||
if (cond) { tests_passed++; printf("✅ %s\n", msg); } \
|
||||
else { printf("❌ %s\n", msg); } \
|
||||
} while (0)
|
||||
|
||||
static const char* base_url(void) {
|
||||
const char* env = getenv("MOCK_BLOSSOM_BASE");
|
||||
return (env && env[0] != '\0') ? env : "http://127.0.0.1:18081";
|
||||
}
|
||||
|
||||
static void test_auth_header(void) {
|
||||
unsigned char priv[32];
|
||||
memset(priv, 1, sizeof(priv));
|
||||
|
||||
char sha[65];
|
||||
memset(sha, 'a', 64);
|
||||
sha[64] = '\0';
|
||||
|
||||
char* hdr = blossom_create_auth_header(priv, "upload", sha, 300);
|
||||
TEST_ASSERT(hdr != NULL, "blossom_create_auth_header returns non-null");
|
||||
TEST_ASSERT(hdr && strncmp(hdr, "Nostr ", 6) == 0, "blossom_create_auth_header prefix is 'Nostr '");
|
||||
free(hdr);
|
||||
}
|
||||
|
||||
static void test_invalid_inputs(void) {
|
||||
blossom_blob_descriptor_t d;
|
||||
memset(&d, 0, sizeof(d));
|
||||
|
||||
int rc = blossom_upload(NULL, (const unsigned char*)"x", 1, NULL, NULL, NULL, 5, &d);
|
||||
TEST_ASSERT(rc == NOSTR_ERROR_INVALID_INPUT, "blossom_upload rejects null server");
|
||||
|
||||
unsigned char* body = NULL;
|
||||
size_t body_len = 0;
|
||||
rc = blossom_download(base_url(), "not-a-sha", 5, 1024, &body, &body_len, NULL, 0);
|
||||
TEST_ASSERT(rc == NOSTR_ERROR_INVALID_INPUT, "blossom_download rejects invalid sha");
|
||||
|
||||
rc = blossom_head(base_url(), "not-a-sha", 5, &d);
|
||||
TEST_ASSERT(rc == NOSTR_ERROR_INVALID_INPUT, "blossom_head rejects invalid sha");
|
||||
|
||||
rc = blossom_delete(base_url(), "not-a-sha", (const unsigned char*)"x", 5);
|
||||
TEST_ASSERT(rc == NOSTR_ERROR_INVALID_INPUT, "blossom_delete rejects invalid sha");
|
||||
|
||||
blossom_blob_descriptor_t* items = NULL;
|
||||
int count = 0;
|
||||
rc = blossom_list(NULL, "abc", 5, &items, &count);
|
||||
TEST_ASSERT(rc == NOSTR_ERROR_INVALID_INPUT, "blossom_list rejects null server");
|
||||
}
|
||||
|
||||
static void test_round_trip_with_mock(void) {
|
||||
const char* server = base_url();
|
||||
const unsigned char payload[] = "blossom-client-test-payload";
|
||||
|
||||
blossom_blob_descriptor_t uploaded;
|
||||
memset(&uploaded, 0, sizeof(uploaded));
|
||||
|
||||
int rc = blossom_upload(server,
|
||||
payload,
|
||||
sizeof(payload) - 1,
|
||||
"text/plain",
|
||||
NULL,
|
||||
NULL,
|
||||
5,
|
||||
&uploaded);
|
||||
TEST_ASSERT(rc == NOSTR_SUCCESS, "blossom_upload mock success");
|
||||
TEST_ASSERT(uploaded.sha256[0] != '\0', "blossom_upload returns sha256");
|
||||
|
||||
unsigned char* body = NULL;
|
||||
size_t body_len = 0;
|
||||
char content_type[128] = {0};
|
||||
|
||||
rc = blossom_download(server,
|
||||
uploaded.sha256,
|
||||
5,
|
||||
4096,
|
||||
&body,
|
||||
&body_len,
|
||||
content_type,
|
||||
sizeof(content_type));
|
||||
TEST_ASSERT(rc == NOSTR_SUCCESS, "blossom_download mock success");
|
||||
TEST_ASSERT(body && body_len == sizeof(payload) - 1, "blossom_download length matches");
|
||||
TEST_ASSERT(body && memcmp(body, payload, body_len) == 0, "blossom_download payload matches");
|
||||
free(body);
|
||||
|
||||
blossom_blob_descriptor_t head;
|
||||
memset(&head, 0, sizeof(head));
|
||||
rc = blossom_head(server, uploaded.sha256, 5, &head);
|
||||
TEST_ASSERT(rc == NOSTR_SUCCESS, "blossom_head mock success");
|
||||
TEST_ASSERT(head.size == (long)(sizeof(payload) - 1), "blossom_head size matches");
|
||||
|
||||
blossom_blob_descriptor_t* list = NULL;
|
||||
int list_count = 0;
|
||||
rc = blossom_list(server, "dummy_pubkey", 5, &list, &list_count);
|
||||
TEST_ASSERT(rc == NOSTR_SUCCESS, "blossom_list mock success");
|
||||
TEST_ASSERT(list_count >= 1, "blossom_list returns at least one blob");
|
||||
free(list);
|
||||
|
||||
unsigned char fake_priv[32];
|
||||
memset(fake_priv, 2, sizeof(fake_priv));
|
||||
rc = blossom_delete(server, uploaded.sha256, fake_priv, 5);
|
||||
TEST_ASSERT(rc == NOSTR_SUCCESS, "blossom_delete mock success");
|
||||
|
||||
memset(&head, 0, sizeof(head));
|
||||
rc = blossom_head(server, uploaded.sha256, 5, &head);
|
||||
TEST_ASSERT(rc == NOSTR_ERROR_NETWORK_FAILED, "blossom_head after delete reports missing blob");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("blossom_client Tests\n");
|
||||
printf("====================\n");
|
||||
printf("Base URL: %s\n", base_url());
|
||||
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
printf("❌ nostr_init failed\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
test_auth_header();
|
||||
test_invalid_inputs();
|
||||
test_round_trip_with_mock();
|
||||
|
||||
nostr_cleanup();
|
||||
|
||||
printf("\nSummary: %d/%d passed\n", tests_passed, tests_run);
|
||||
return (tests_passed == tests_run) ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Blossom mock integration error-path tests.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "../nostr_core/blossom_client.h"
|
||||
#include "../nostr_core/nostr_common.h"
|
||||
|
||||
static int tests_run = 0;
|
||||
static int tests_passed = 0;
|
||||
|
||||
#define TEST_ASSERT(cond, msg) do { \
|
||||
tests_run++; \
|
||||
if (cond) { tests_passed++; printf("✅ %s\n", msg); } \
|
||||
else { printf("❌ %s\n", msg); } \
|
||||
} while (0)
|
||||
|
||||
static const char* base_url(void) {
|
||||
const char* env = getenv("MOCK_BLOSSOM_BASE");
|
||||
return (env && env[0] != '\0') ? env : "http://127.0.0.1:18081";
|
||||
}
|
||||
|
||||
static void make_error_server(char* out, size_t out_size, int status_code) {
|
||||
snprintf(out, out_size, "%s/blossom/error/%d", base_url(), status_code);
|
||||
}
|
||||
|
||||
static void test_forced_error_paths(void) {
|
||||
char server[512];
|
||||
char sha[65];
|
||||
memset(sha, 'a', 64);
|
||||
sha[64] = '\0';
|
||||
|
||||
unsigned char* body = NULL;
|
||||
size_t body_len = 0;
|
||||
|
||||
make_error_server(server, sizeof(server), 404);
|
||||
int rc = blossom_download(server, sha, 5, 1024, &body, &body_len, NULL, 0);
|
||||
TEST_ASSERT(rc == NOSTR_ERROR_NETWORK_FAILED, "blossom_download handles forced 404");
|
||||
|
||||
make_error_server(server, sizeof(server), 401);
|
||||
blossom_blob_descriptor_t d;
|
||||
memset(&d, 0, sizeof(d));
|
||||
rc = blossom_upload(server, (const unsigned char*)"x", 1, "text/plain", NULL, NULL, 5, &d);
|
||||
TEST_ASSERT(rc == NOSTR_ERROR_NETWORK_FAILED, "blossom_upload handles forced 401");
|
||||
|
||||
make_error_server(server, sizeof(server), 413);
|
||||
rc = blossom_upload(server, (const unsigned char*)"x", 1, "text/plain", NULL, NULL, 5, &d);
|
||||
TEST_ASSERT(rc == NOSTR_ERROR_NETWORK_FAILED, "blossom_upload handles forced 413");
|
||||
|
||||
make_error_server(server, sizeof(server), 500);
|
||||
rc = blossom_head(server, sha, 5, &d);
|
||||
TEST_ASSERT(rc == NOSTR_ERROR_NETWORK_FAILED, "blossom_head handles forced 500");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("blossom mock error-path tests\n");
|
||||
printf("=============================\n");
|
||||
printf("Base URL: %s\n", base_url());
|
||||
|
||||
test_forced_error_paths();
|
||||
|
||||
printf("\nSummary: %d/%d passed\n", tests_passed, tests_run);
|
||||
return (tests_passed == tests_run) ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
/*
|
||||
* ChaCha20-Poly1305 / Poly1305 Test Suite - RFC 8439 vectors
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
int nostr_poly1305_mac(const unsigned char key[32], const unsigned char *msg,
|
||||
size_t msg_len, unsigned char tag[16]);
|
||||
|
||||
int nostr_chacha20poly1305_encrypt(const unsigned char key[32],
|
||||
const unsigned char nonce[12],
|
||||
const unsigned char *aad, size_t aad_len,
|
||||
const unsigned char *plaintext, size_t pt_len,
|
||||
unsigned char *ciphertext,
|
||||
unsigned char tag[16]);
|
||||
|
||||
int nostr_chacha20poly1305_decrypt(const unsigned char key[32],
|
||||
const unsigned char nonce[12],
|
||||
const unsigned char *aad, size_t aad_len,
|
||||
const unsigned char *ciphertext, size_t ct_len,
|
||||
const unsigned char tag[16],
|
||||
unsigned char *plaintext);
|
||||
|
||||
static int hex_to_bytes(const char *hex, uint8_t *out, size_t out_len) {
|
||||
size_t i;
|
||||
if (!hex || !out) return -1;
|
||||
for (i = 0; i < out_len; i++) {
|
||||
if (sscanf(hex + (i * 2), "%2hhx", &out[i]) != 1) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int bytes_equal(const uint8_t *a, const uint8_t *b, size_t len) {
|
||||
return memcmp(a, b, len) == 0;
|
||||
}
|
||||
|
||||
static void print_hex(const uint8_t *buf, size_t len) {
|
||||
size_t i;
|
||||
for (i = 0; i < len; i++) {
|
||||
printf("%02x", buf[i]);
|
||||
}
|
||||
}
|
||||
|
||||
static int test_poly1305_rfc8439_2_5_2(void) {
|
||||
const char *key_hex =
|
||||
"85d6be7857556d337f4452fe42d506a8"
|
||||
"0103808afb0db2fd4abff6af4149f51b";
|
||||
const char *msg = "Cryptographic Forum Research Group";
|
||||
const char *expected_tag_hex = "a8061dc1305136c6c22b8baf0c0127a9";
|
||||
|
||||
uint8_t key[32];
|
||||
uint8_t tag[16];
|
||||
uint8_t expected[16];
|
||||
|
||||
printf("=== Poly1305 RFC 8439 §2.5.2 ===\n");
|
||||
|
||||
if (hex_to_bytes(key_hex, key, sizeof(key)) != 0 ||
|
||||
hex_to_bytes(expected_tag_hex, expected, sizeof(expected)) != 0) {
|
||||
printf("❌ hex parse failed\n\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (nostr_poly1305_mac(key, (const unsigned char *)msg, strlen(msg), tag) != 0) {
|
||||
printf("❌ nostr_poly1305_mac failed\n\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!bytes_equal(tag, expected, sizeof(tag))) {
|
||||
printf("❌ tag mismatch\nExpected: ");
|
||||
print_hex(expected, sizeof(expected));
|
||||
printf("\nGot: ");
|
||||
print_hex(tag, sizeof(tag));
|
||||
printf("\n\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
printf("✅ passed\n\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int test_aead_rfc8439_appendix_a_5(void) {
|
||||
const char *key_hex =
|
||||
"1c9240a5eb55d38af333888604f6b5f0"
|
||||
"473917c1402b80099dca5cbc207075c0";
|
||||
const char *nonce_hex = "000000000102030405060708";
|
||||
const char *aad_hex = "f33388860000000000004e91";
|
||||
const char *plaintext_hex =
|
||||
"496e7465726e65742d4472616674732061726520647261667420646f63756d656e74732076616c696420"
|
||||
"666f722061206d6178696d756d206f6620736978206d6f6e74687320616e64206d61792062652075706461"
|
||||
"7465642c207265706c616365642c206f72206f62736f6c65746564206279206f7468657220646f63756d65"
|
||||
"6e747320617420616e792074696d652e20497420697320696e617070726f70726961746520746f20757365"
|
||||
"20496e7465726e65742d447261667473206173207265666572656e6365206d6174657269616c206f722074"
|
||||
"6f2063697465207468656d206f74686572207468616e206173202fe2809c776f726b20696e2070726f6772"
|
||||
"6573732e2fe2809d";
|
||||
const char *expected_ct_hex =
|
||||
"64a0861575861af460f062c79be643bd"
|
||||
"5e805cfd345cf389f108670ac76c8cb2"
|
||||
"4c6cfc18755d43eea09ee94e382d26b0"
|
||||
"bdb7b73c321b0100d4f03b7f355894cf"
|
||||
"332f830e710b97ce98c8a84abd0b9481"
|
||||
"14ad176e008d33bd60f982b1ff37c855"
|
||||
"9797a06ef4f0ef61c186324e2b350638"
|
||||
"3606907b6a7c02b0f9f6157b53c867e4"
|
||||
"b9166c767b804d46a59b5216cde7a4e9"
|
||||
"9040c5a40433225ee282a1b0a06c523e"
|
||||
"af4534d7f83fa1155b0047718cbc546a"
|
||||
"0d072b04b3564eea1b422273f548271a"
|
||||
"0bb2316053fa76991955ebd63159434e"
|
||||
"cebb4e466dae5a1073a6727627097a10"
|
||||
"49e617d91d361094fa68f0ff77987130"
|
||||
"305beaba2eda04df997b714d6c6f2c29"
|
||||
"a6ad5cb4022b02709b";
|
||||
const char *expected_tag_hex = "eead9d67890cbb22392336fea1851f38";
|
||||
|
||||
uint8_t key[32], nonce[12], aad[12], expected_tag[16];
|
||||
uint8_t plaintext[1024], ciphertext[1024], decrypted[1024], expected_ct[1024];
|
||||
uint8_t tag[16];
|
||||
size_t pt_len = strlen(plaintext_hex) / 2;
|
||||
size_t aad_len = strlen(aad_hex) / 2;
|
||||
size_t ct_len = strlen(expected_ct_hex) / 2;
|
||||
|
||||
printf("=== ChaCha20-Poly1305 RFC 8439 Appendix A.5 ===\n");
|
||||
|
||||
if (hex_to_bytes(key_hex, key, sizeof(key)) != 0 ||
|
||||
hex_to_bytes(nonce_hex, nonce, sizeof(nonce)) != 0 ||
|
||||
hex_to_bytes(aad_hex, aad, aad_len) != 0 ||
|
||||
hex_to_bytes(plaintext_hex, plaintext, pt_len) != 0 ||
|
||||
hex_to_bytes(expected_ct_hex, expected_ct, ct_len) != 0 ||
|
||||
hex_to_bytes(expected_tag_hex, expected_tag, sizeof(expected_tag)) != 0) {
|
||||
printf("❌ hex parse failed\n\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (nostr_chacha20poly1305_encrypt(key, nonce, aad, aad_len,
|
||||
plaintext, pt_len, ciphertext, tag) != 0) {
|
||||
printf("❌ encrypt failed\n\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!bytes_equal(ciphertext, expected_ct, ct_len)) {
|
||||
size_t i;
|
||||
size_t mismatch_count = 0;
|
||||
printf("❌ ciphertext mismatch\n");
|
||||
for (i = 0; i < ct_len; i++) {
|
||||
if (ciphertext[i] != expected_ct[i]) {
|
||||
if (mismatch_count == 0) {
|
||||
size_t start = (i > 8) ? (i - 8) : 0;
|
||||
size_t end = (i + 8 < ct_len) ? (i + 8) : (ct_len - 1);
|
||||
size_t j;
|
||||
printf("first mismatch at byte %zu: got=%02x expected=%02x\n", i, ciphertext[i], expected_ct[i]);
|
||||
printf("context (got): ");
|
||||
for (j = start; j <= end; j++) printf("%02x", ciphertext[j]);
|
||||
printf("\ncontext (expected): ");
|
||||
for (j = start; j <= end; j++) printf("%02x", expected_ct[j]);
|
||||
printf("\n");
|
||||
}
|
||||
mismatch_count++;
|
||||
}
|
||||
}
|
||||
printf("total mismatched bytes: %zu\n\n", mismatch_count);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!bytes_equal(tag, expected_tag, sizeof(tag))) {
|
||||
printf("❌ tag mismatch\n\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (nostr_chacha20poly1305_decrypt(key, nonce, aad, aad_len,
|
||||
ciphertext, ct_len, tag, decrypted) != 0) {
|
||||
printf("❌ decrypt failed\n\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!bytes_equal(decrypted, plaintext, pt_len)) {
|
||||
printf("❌ decrypted plaintext mismatch\n\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
printf("✅ passed\n\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int test_aead_tamper_rejects(void) {
|
||||
uint8_t key[32] = {0};
|
||||
uint8_t nonce[12] = {0};
|
||||
uint8_t aad[8] = {1,2,3,4,5,6,7,8};
|
||||
uint8_t pt[32] = "hello chacha20-poly1305 world";
|
||||
uint8_t ct[64] = {0};
|
||||
uint8_t tag[16] = {0};
|
||||
uint8_t out[64] = {0};
|
||||
|
||||
printf("=== AEAD tamper rejection ===\n");
|
||||
|
||||
if (nostr_chacha20poly1305_encrypt(key, nonce, aad, sizeof(aad), pt, strlen((char *)pt), ct, tag) != 0) {
|
||||
printf("❌ encrypt failed\n\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
ct[0] ^= 0x01;
|
||||
if (nostr_chacha20poly1305_decrypt(key, nonce, aad, sizeof(aad), ct, strlen((char *)pt), tag, out) == 0) {
|
||||
printf("❌ tampered ciphertext accepted\n\n");
|
||||
return 0;
|
||||
}
|
||||
ct[0] ^= 0x01;
|
||||
|
||||
tag[0] ^= 0x80;
|
||||
if (nostr_chacha20poly1305_decrypt(key, nonce, aad, sizeof(aad), ct, strlen((char *)pt), tag, out) == 0) {
|
||||
printf("❌ tampered tag accepted\n\n");
|
||||
return 0;
|
||||
}
|
||||
tag[0] ^= 0x80;
|
||||
|
||||
aad[0] ^= 0x01;
|
||||
if (nostr_chacha20poly1305_decrypt(key, nonce, aad, sizeof(aad), ct, strlen((char *)pt), tag, out) == 0) {
|
||||
printf("❌ tampered AAD accepted\n\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
printf("✅ passed\n\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
int pass = 1;
|
||||
|
||||
pass &= test_poly1305_rfc8439_2_5_2();
|
||||
pass &= test_aead_rfc8439_appendix_a_5();
|
||||
pass &= test_aead_tamper_rejects();
|
||||
|
||||
if (pass) {
|
||||
printf("🎉 All ChaCha20-Poly1305/Poly1305 tests PASSED\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
printf("💥 Some tests FAILED\n");
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env python3
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
|
||||
HOST = "127.0.0.1"
|
||||
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 18081
|
||||
|
||||
STORE = {} # sha256 -> {bytes, content_type, created}
|
||||
|
||||
|
||||
def json_bytes(obj):
|
||||
return json.dumps(obj).encode("utf-8")
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
server_version = "mock-blossom/0.1"
|
||||
|
||||
def _write_json(self, code, obj):
|
||||
body = json_bytes(obj)
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def _read_body(self):
|
||||
length = int(self.headers.get("Content-Length", "0"))
|
||||
return self.rfile.read(length) if length > 0 else b""
|
||||
|
||||
def _maybe_error_prefix(self):
|
||||
# /blossom/error/<code>/...
|
||||
parts = [p for p in self.path.split("?")[0].split("/") if p]
|
||||
if len(parts) >= 3 and parts[0] == "blossom" and parts[1] == "error":
|
||||
try:
|
||||
code = int(parts[2])
|
||||
return code, parts[3:]
|
||||
except ValueError:
|
||||
return None, None
|
||||
return None, None
|
||||
|
||||
def do_HEAD(self):
|
||||
parsed = urlparse(self.path)
|
||||
path = parsed.path
|
||||
|
||||
code, remaining = self._maybe_error_prefix()
|
||||
if code is not None:
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", "0")
|
||||
self.end_headers()
|
||||
return
|
||||
|
||||
if path.startswith("/http/head"):
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", "0")
|
||||
self.send_header("X-Mock", "head-ok")
|
||||
self.end_headers()
|
||||
return
|
||||
|
||||
sha = path.lstrip("/")
|
||||
item = STORE.get(sha)
|
||||
if not item:
|
||||
self.send_response(404)
|
||||
self.send_header("Content-Length", "0")
|
||||
self.end_headers()
|
||||
return
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", item["content_type"])
|
||||
self.send_header("Content-Length", str(len(item["bytes"])))
|
||||
self.end_headers()
|
||||
|
||||
def do_GET(self):
|
||||
parsed = urlparse(self.path)
|
||||
path = parsed.path
|
||||
qs = parse_qs(parsed.query)
|
||||
|
||||
code, remaining = self._maybe_error_prefix()
|
||||
if code is not None:
|
||||
self._write_json(code, {"error": f"forced_{code}"})
|
||||
return
|
||||
|
||||
if path == "/health":
|
||||
self._write_json(200, {"ok": True})
|
||||
return
|
||||
|
||||
if path == "/http/get":
|
||||
self._write_json(200, {"ok": True, "method": "GET"})
|
||||
return
|
||||
|
||||
if path == "/http/slow":
|
||||
delay = int(qs.get("seconds", ["2"])[0])
|
||||
time.sleep(delay)
|
||||
self._write_json(200, {"ok": True, "delay": delay})
|
||||
return
|
||||
|
||||
if path == "/http/large":
|
||||
size = int(qs.get("size", ["4096"])[0])
|
||||
body = ("x" * size).encode("utf-8")
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/plain")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
return
|
||||
|
||||
if path.startswith("/list/"):
|
||||
out = []
|
||||
for sha, item in STORE.items():
|
||||
out.append({
|
||||
"sha256": sha,
|
||||
"url": f"http://{HOST}:{PORT}/{sha}",
|
||||
"size": len(item["bytes"]),
|
||||
"content_type": item["content_type"],
|
||||
"created": item["created"],
|
||||
})
|
||||
self._write_json(200, out)
|
||||
return
|
||||
|
||||
sha = path.lstrip("/")
|
||||
item = STORE.get(sha)
|
||||
if not item:
|
||||
self._write_json(404, {"error": "not_found"})
|
||||
return
|
||||
|
||||
body = item["bytes"]
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", item["content_type"])
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_POST(self):
|
||||
parsed = urlparse(self.path)
|
||||
path = parsed.path
|
||||
body = self._read_body()
|
||||
|
||||
code, remaining = self._maybe_error_prefix()
|
||||
if code is not None:
|
||||
self._write_json(code, {"error": f"forced_{code}"})
|
||||
return
|
||||
|
||||
if path == "/http/post":
|
||||
self._write_json(200, {
|
||||
"ok": True,
|
||||
"method": "POST",
|
||||
"content_type": self.headers.get("Content-Type", ""),
|
||||
"body": body.decode("utf-8", errors="replace"),
|
||||
})
|
||||
return
|
||||
|
||||
self._write_json(404, {"error": "not_found"})
|
||||
|
||||
def do_PUT(self):
|
||||
parsed = urlparse(self.path)
|
||||
path = parsed.path
|
||||
body = self._read_body()
|
||||
|
||||
code, remaining = self._maybe_error_prefix()
|
||||
if code is not None:
|
||||
self._write_json(code, {"error": f"forced_{code}"})
|
||||
return
|
||||
|
||||
if path == "/http/put":
|
||||
self._write_json(200, {
|
||||
"ok": True,
|
||||
"method": "PUT",
|
||||
"content_type": self.headers.get("Content-Type", ""),
|
||||
"body": body.decode("utf-8", errors="replace"),
|
||||
})
|
||||
return
|
||||
|
||||
if path == "/upload":
|
||||
sha = hashlib.sha256(body).hexdigest()
|
||||
ctype = self.headers.get("Content-Type", "application/octet-stream")
|
||||
created = int(time.time())
|
||||
STORE[sha] = {"bytes": body, "content_type": ctype, "created": created}
|
||||
self._write_json(200, {
|
||||
"sha256": sha,
|
||||
"url": f"http://{HOST}:{PORT}/{sha}",
|
||||
"size": len(body),
|
||||
"content_type": ctype,
|
||||
"created": created,
|
||||
})
|
||||
return
|
||||
|
||||
self._write_json(404, {"error": "not_found"})
|
||||
|
||||
def do_DELETE(self):
|
||||
parsed = urlparse(self.path)
|
||||
path = parsed.path
|
||||
|
||||
code, remaining = self._maybe_error_prefix()
|
||||
if code is not None:
|
||||
self._write_json(code, {"error": f"forced_{code}"})
|
||||
return
|
||||
|
||||
if path == "/http/delete":
|
||||
self._write_json(200, {"ok": True, "method": "DELETE"})
|
||||
return
|
||||
|
||||
sha = path.lstrip("/")
|
||||
if sha in STORE:
|
||||
del STORE[sha]
|
||||
self._write_json(200, {"deleted": True, "sha256": sha})
|
||||
else:
|
||||
self._write_json(404, {"error": "not_found"})
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
# keep test output clean
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
server = ThreadingHTTPServer((HOST, PORT), Handler)
|
||||
print(f"mock_blossom_server listening on http://{HOST}:{PORT}", flush=True)
|
||||
server.serve_forever()
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* NOSTR Core Library - NIP-03 Live Network Test
|
||||
*/
|
||||
|
||||
#include "nostr_core/nostr_core.h"
|
||||
#include "nostr_core/nip003.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
|
||||
#define CALENDAR_URL "https://alice.btc.calendar.opentimestamps.org"
|
||||
#define POLL_INTERVAL_SEC 60
|
||||
#define MAX_POLL_ATTEMPTS 60 // 1 hour total
|
||||
|
||||
void print_progress(int attempt, int complete, const char* ots_b64) {
|
||||
time_t now = time(NULL);
|
||||
char time_str[26];
|
||||
ctime_r(&now, time_str);
|
||||
time_str[24] = '\0';
|
||||
|
||||
printf("[%s] Attempt %d: Status = %s, Proof Size = %zu bytes\n",
|
||||
time_str, attempt, complete ? "✅ COMPLETE" : "⏳ PENDING",
|
||||
ots_b64 ? strlen(ots_b64) : 0);
|
||||
}
|
||||
|
||||
int main() {
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "Failed to initialize NOSTR library\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("🚀 Starting NIP-03 Live Network Test\n");
|
||||
printf("Using Calendar: %s\n\n", CALENDAR_URL);
|
||||
|
||||
// 1. Create a unique "file" (event ID) to stamp
|
||||
unsigned char private_key[32];
|
||||
unsigned char public_key[32];
|
||||
nostr_generate_keypair(private_key, public_key);
|
||||
|
||||
char content[128];
|
||||
snprintf(content, sizeof(content), "Live NIP-03 test at %ld", time(NULL));
|
||||
|
||||
cJSON* event = nostr_create_and_sign_event(1, content, NULL, private_key, 0);
|
||||
const char* event_id = cJSON_GetObjectItem(event, "id")->valuestring;
|
||||
printf("📄 Created test event: %s\n", event_id);
|
||||
|
||||
// 2. Submit to calendar
|
||||
printf("📤 Submitting to OpenTimestamps calendar...\n");
|
||||
char* ots_b64 = nostr_nip03_request_timestamp(event_id, CALENDAR_URL, 15);
|
||||
if (!ots_b64) {
|
||||
printf("❌ Failed to submit to calendar. Trying with curl to debug...\n");
|
||||
char cmd[1024];
|
||||
snprintf(cmd, sizeof(cmd), "curl -s -X POST --data-binary @- %s/digest <<EOF\n$(echo \"%s\" | xxd -r -p)\nEOF\n", CALENDAR_URL, event_id);
|
||||
printf("Running: %s\n", cmd);
|
||||
system(cmd);
|
||||
|
||||
cJSON_Delete(event);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
printf("✅ Initial proof received.\n\n");
|
||||
|
||||
// 3. Poll for completion
|
||||
int complete = 0;
|
||||
int attempts = 0;
|
||||
|
||||
while (!complete && attempts < MAX_POLL_ATTEMPTS) {
|
||||
attempts++;
|
||||
complete = nostr_nip03_is_proof_complete(ots_b64);
|
||||
print_progress(attempts, complete, ots_b64);
|
||||
|
||||
if (!complete) {
|
||||
printf(" (Waiting %d seconds for next poll...)\n", POLL_INTERVAL_SEC);
|
||||
sleep(POLL_INTERVAL_SEC);
|
||||
|
||||
// Try to upgrade
|
||||
char* upgraded = nostr_nip03_upgrade_proof(ots_b64, CALENDAR_URL, 15);
|
||||
if (upgraded) {
|
||||
free(ots_b64);
|
||||
ots_b64 = upgraded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (complete) {
|
||||
printf("\n🎉 SUCCESS! The proof is now complete with a Bitcoin attestation.\n");
|
||||
|
||||
// Create the final Nostr event
|
||||
cJSON* proof_event = nostr_nip03_create_proof_event(event_id, 1, ots_b64, NULL, private_key);
|
||||
char* json = cJSON_Print(proof_event);
|
||||
printf("\nFinal NIP-03 Event:\n%s\n", json);
|
||||
|
||||
free(json);
|
||||
cJSON_Delete(proof_event);
|
||||
} else {
|
||||
printf("\n❌ Test timed out after %d minutes. The proof is still pending.\n", attempts);
|
||||
}
|
||||
|
||||
free(ots_b64);
|
||||
cJSON_Delete(event);
|
||||
nostr_cleanup();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* NOSTR Core Library - NIP-03 Test
|
||||
*/
|
||||
|
||||
#include "nostr_core/nostr_core.h"
|
||||
#include "nostr_core/nip003.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
|
||||
void test_nip03_create_proof_event() {
|
||||
printf("🧪 Testing NIP-03 proof event creation...\n");
|
||||
|
||||
unsigned char private_key[32];
|
||||
unsigned char public_key[32];
|
||||
nostr_generate_keypair(private_key, public_key);
|
||||
|
||||
const char* target_id = "e71c6ea722987debdb60f81f9ea4f604b5ac0664120dd64fb9d23abc4ec7c323";
|
||||
int target_kind = 1;
|
||||
const char* ots_b64 = "base64encodedotsdata";
|
||||
const char* relay = "wss://relay.example.com";
|
||||
|
||||
cJSON* event = nostr_nip03_create_proof_event(target_id, target_kind, ots_b64, relay, private_key);
|
||||
assert(event != NULL);
|
||||
|
||||
// Verify kind
|
||||
cJSON* kind = cJSON_GetObjectItem(event, "kind");
|
||||
assert(kind != NULL && kind->valueint == 1040);
|
||||
|
||||
// Verify content
|
||||
cJSON* content = cJSON_GetObjectItem(event, "content");
|
||||
assert(content != NULL && strcmp(content->valuestring, ots_b64) == 0);
|
||||
|
||||
// Verify tags
|
||||
cJSON* tags = cJSON_GetObjectItem(event, "tags");
|
||||
assert(tags != NULL && cJSON_GetArraySize(tags) == 2);
|
||||
|
||||
cJSON* e_tag = cJSON_GetArrayItem(tags, 0);
|
||||
assert(strcmp(cJSON_GetArrayItem(e_tag, 0)->valuestring, "e") == 0);
|
||||
assert(strcmp(cJSON_GetArrayItem(e_tag, 1)->valuestring, target_id) == 0);
|
||||
assert(strcmp(cJSON_GetArrayItem(e_tag, 2)->valuestring, relay) == 0);
|
||||
|
||||
cJSON* k_tag = cJSON_GetArrayItem(tags, 1);
|
||||
assert(strcmp(cJSON_GetArrayItem(k_tag, 0)->valuestring, "k") == 0);
|
||||
assert(strcmp(cJSON_GetArrayItem(k_tag, 1)->valuestring, "1") == 0);
|
||||
|
||||
printf("✅ NIP-03 proof event creation test passed!\n");
|
||||
cJSON_Delete(event);
|
||||
}
|
||||
|
||||
int main() {
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "Failed to initialize NOSTR library\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
test_nip03_create_proof_event();
|
||||
|
||||
// Note: We don't test nostr_nip03_request_timestamp here as it requires a live OTS calendar
|
||||
// and network access, which might be flaky in a test environment.
|
||||
|
||||
nostr_cleanup();
|
||||
printf("\n🎉 All NIP-03 tests passed!\n");
|
||||
return 0;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* nostr_http unit tests using local mock server.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "../nostr_core/nostr_http.h"
|
||||
#include "../nostr_core/nostr_common.h"
|
||||
|
||||
static int tests_run = 0;
|
||||
static int tests_passed = 0;
|
||||
|
||||
#define TEST_ASSERT(cond, msg) do { \
|
||||
tests_run++; \
|
||||
if (cond) { tests_passed++; printf("✅ %s\n", msg); } \
|
||||
else { printf("❌ %s\n", msg); } \
|
||||
} while (0)
|
||||
|
||||
static const char* base_url(void) {
|
||||
const char* env = getenv("MOCK_BLOSSOM_BASE");
|
||||
return (env && env[0] != '\0') ? env : "http://127.0.0.1:18081";
|
||||
}
|
||||
|
||||
static void make_url(char* out, size_t out_size, const char* path) {
|
||||
snprintf(out, out_size, "%s%s", base_url(), path ? path : "");
|
||||
}
|
||||
|
||||
static void test_get_and_post_helpers(void) {
|
||||
char url[512];
|
||||
|
||||
make_url(url, sizeof(url), "/http/get");
|
||||
char* body = NULL;
|
||||
long status = 0;
|
||||
int rc = nostr_http_get(url, 5, &body, &status);
|
||||
TEST_ASSERT(rc == NOSTR_SUCCESS, "nostr_http_get transport success");
|
||||
TEST_ASSERT(status == 200, "nostr_http_get status=200");
|
||||
TEST_ASSERT(body && strstr(body, "\"method\": \"GET\""), "nostr_http_get response body contains method");
|
||||
free(body);
|
||||
|
||||
make_url(url, sizeof(url), "/http/post");
|
||||
body = NULL;
|
||||
status = 0;
|
||||
rc = nostr_http_post_json(url, "{\"hello\":\"world\"}", 5, &body, &status);
|
||||
TEST_ASSERT(rc == NOSTR_SUCCESS, "nostr_http_post_json transport success");
|
||||
TEST_ASSERT(status == 200, "nostr_http_post_json status=200");
|
||||
TEST_ASSERT(body && strstr(body, "\"method\": \"POST\""), "nostr_http_post_json response contains method");
|
||||
free(body);
|
||||
}
|
||||
|
||||
static void test_request_methods_and_headers(void) {
|
||||
char url[512];
|
||||
int rc;
|
||||
|
||||
nostr_http_request_t req;
|
||||
nostr_http_response_t resp;
|
||||
|
||||
make_url(url, sizeof(url), "/http/put");
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.method = "PUT";
|
||||
req.url = url;
|
||||
req.body = (const unsigned char*)"abc";
|
||||
req.body_len = 3;
|
||||
req.timeout_seconds = 5;
|
||||
rc = nostr_http_request(&req, &resp);
|
||||
TEST_ASSERT(rc == NOSTR_SUCCESS, "nostr_http_request PUT transport success");
|
||||
TEST_ASSERT(resp.status_code == 200, "nostr_http_request PUT status=200");
|
||||
TEST_ASSERT(resp.body && strstr(resp.body, "\"method\": \"PUT\""), "nostr_http_request PUT response contains method");
|
||||
nostr_http_response_free(&resp);
|
||||
|
||||
make_url(url, sizeof(url), "/http/delete");
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.method = "DELETE";
|
||||
req.url = url;
|
||||
req.timeout_seconds = 5;
|
||||
rc = nostr_http_request(&req, &resp);
|
||||
TEST_ASSERT(rc == NOSTR_SUCCESS, "nostr_http_request DELETE transport success");
|
||||
TEST_ASSERT(resp.status_code == 200, "nostr_http_request DELETE status=200");
|
||||
TEST_ASSERT(resp.body && strstr(resp.body, "\"method\": \"DELETE\""), "nostr_http_request DELETE response contains method");
|
||||
nostr_http_response_free(&resp);
|
||||
|
||||
make_url(url, sizeof(url), "/http/head");
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.method = "HEAD";
|
||||
req.url = url;
|
||||
req.timeout_seconds = 5;
|
||||
req.capture_headers = 1;
|
||||
rc = nostr_http_request(&req, &resp);
|
||||
TEST_ASSERT(rc == NOSTR_SUCCESS, "nostr_http_request HEAD transport success");
|
||||
TEST_ASSERT(resp.status_code == 200, "nostr_http_request HEAD status=200");
|
||||
TEST_ASSERT(resp.headers_raw != NULL, "nostr_http_request HEAD captured headers");
|
||||
nostr_http_response_free(&resp);
|
||||
}
|
||||
|
||||
static void test_timeout_and_max_response_bytes(void) {
|
||||
char url[512];
|
||||
int rc;
|
||||
|
||||
nostr_http_request_t req;
|
||||
nostr_http_response_t resp;
|
||||
|
||||
make_url(url, sizeof(url), "/http/slow?seconds=2");
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.method = "GET";
|
||||
req.url = url;
|
||||
req.timeout_seconds = 1;
|
||||
rc = nostr_http_request(&req, &resp);
|
||||
TEST_ASSERT(rc == NOSTR_ERROR_NETWORK_FAILED, "nostr_http_request timeout returns network error");
|
||||
|
||||
make_url(url, sizeof(url), "/http/large?size=4096");
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.method = "GET";
|
||||
req.url = url;
|
||||
req.timeout_seconds = 5;
|
||||
req.max_response_bytes = 128;
|
||||
rc = nostr_http_request(&req, &resp);
|
||||
TEST_ASSERT(rc == NOSTR_SUCCESS, "nostr_http_request large body transport success");
|
||||
TEST_ASSERT(resp.status_code == 200, "nostr_http_request large body status=200");
|
||||
TEST_ASSERT(resp.body_len <= 128, "nostr_http_request respects max_response_bytes");
|
||||
TEST_ASSERT(resp.truncated == 1, "nostr_http_request marks response as truncated");
|
||||
nostr_http_response_free(&resp);
|
||||
}
|
||||
|
||||
static void test_ca_bundle_helpers(void) {
|
||||
const char* detected = nostr_http_detect_ca_bundle();
|
||||
TEST_ASSERT(detected == NULL || detected[0] != '\0', "nostr_http_detect_ca_bundle callable");
|
||||
|
||||
nostr_http_set_ca_bundle("");
|
||||
TEST_ASSERT(1, "nostr_http_set_ca_bundle accepts empty string");
|
||||
|
||||
if (detected && detected[0] != '\0') {
|
||||
nostr_http_set_ca_bundle(detected);
|
||||
TEST_ASSERT(1, "nostr_http_set_ca_bundle accepts detected CA path");
|
||||
}
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("nostr_http Unit Tests\n");
|
||||
printf("=====================\n");
|
||||
printf("Base URL: %s\n", base_url());
|
||||
|
||||
test_get_and_post_helpers();
|
||||
test_request_methods_and_headers();
|
||||
test_timeout_and_max_response_bytes();
|
||||
test_ca_bundle_helpers();
|
||||
|
||||
printf("\nSummary: %d/%d passed\n", tests_passed, tests_run);
|
||||
return (tests_passed == tests_run) ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user