added blossom client functionality. Unified curl
This commit is contained in:
3
build.sh
3
build.sh
@@ -507,6 +507,7 @@ 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"
|
||||
|
||||
NIP_DESCRIPTIONS=""
|
||||
|
||||
@@ -540,6 +541,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"
|
||||
|
||||
513
nostr_core/blossom_client.c
Normal file
513
nostr_core/blossom_client.c
Normal file
@@ -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;
|
||||
}
|
||||
82
nostr_core/blossom_client.h
Normal file
82
nostr_core/blossom_client.h
Normal file
@@ -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 */
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "cashu_mint.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
#include "utils.h"
|
||||
#include "nostr_http.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
@@ -26,22 +27,8 @@ int nostr_secp256k1_ec_pubkey_combine(nostr_secp256k1_pubkey* out,
|
||||
const nostr_secp256k1_pubkey* const* ins,
|
||||
size_t n);
|
||||
|
||||
#include <curl/curl.h>
|
||||
|
||||
typedef struct {
|
||||
char* data;
|
||||
size_t size;
|
||||
size_t capacity;
|
||||
} cashu_http_response_t;
|
||||
|
||||
static char g_cashu_ca_bundle_path[512] = {0};
|
||||
|
||||
void cashu_mint_set_ca_bundle(const char* ca_bundle_path) {
|
||||
if (!ca_bundle_path || ca_bundle_path[0] == '\0') {
|
||||
g_cashu_ca_bundle_path[0] = '\0';
|
||||
return;
|
||||
}
|
||||
snprintf(g_cashu_ca_bundle_path, sizeof(g_cashu_ca_bundle_path), "%s", ca_bundle_path);
|
||||
nostr_http_set_ca_bundle(ca_bundle_path);
|
||||
}
|
||||
|
||||
static char* cashu_strdup(const char* s) {
|
||||
@@ -61,30 +48,6 @@ static int cashu_is_hex_string_len(const char* s, size_t expected_len) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
static size_t cashu_write_callback(void* contents, size_t size, size_t nmemb, void* userp) {
|
||||
cashu_http_response_t* response = (cashu_http_response_t*)userp;
|
||||
size_t total_size = size * nmemb;
|
||||
|
||||
if (response->size + total_size + 1 > response->capacity) {
|
||||
size_t new_capacity = response->capacity ? response->capacity * 2 : 1024;
|
||||
while (new_capacity < response->size + total_size + 1) {
|
||||
new_capacity *= 2;
|
||||
}
|
||||
|
||||
char* new_data = (char*)realloc(response->data, new_capacity);
|
||||
if (!new_data) return 0;
|
||||
|
||||
response->data = new_data;
|
||||
response->capacity = new_capacity;
|
||||
}
|
||||
|
||||
memcpy(response->data + response->size, contents, total_size);
|
||||
response->size += total_size;
|
||||
response->data[response->size] = '\0';
|
||||
|
||||
return total_size;
|
||||
}
|
||||
|
||||
static int cashu_http_json_request(const char* method,
|
||||
const char* url,
|
||||
const char* body,
|
||||
@@ -98,58 +61,32 @@ static int cashu_http_json_request(const char* method,
|
||||
*response_out = NULL;
|
||||
if (status_out) *status_out = 0;
|
||||
|
||||
CURL* curl = curl_easy_init();
|
||||
if (!curl) {
|
||||
return NOSTR_ERROR_NETWORK_FAILED;
|
||||
}
|
||||
|
||||
cashu_http_response_t response = {0};
|
||||
response.capacity = 1024;
|
||||
response.data = (char*)malloc(response.capacity);
|
||||
if (!response.data) {
|
||||
curl_easy_cleanup(curl);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
response.data[0] = '\0';
|
||||
|
||||
struct curl_slist* headers = NULL;
|
||||
headers = curl_slist_append(headers, "Accept: application/json");
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, (curl_write_callback)cashu_write_callback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)(timeout_seconds > 0 ? timeout_seconds : 10));
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
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");
|
||||
if (g_cashu_ca_bundle_path[0] != '\0') {
|
||||
curl_easy_setopt(curl, CURLOPT_CAINFO, g_cashu_ca_bundle_path);
|
||||
}
|
||||
int rc = NOSTR_ERROR_CASHU_HTTP_FAILED;
|
||||
long status = 0;
|
||||
|
||||
if (strcmp(method, "POST") == 0) {
|
||||
curl_easy_setopt(curl, CURLOPT_POST, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body ? body : "{}");
|
||||
rc = nostr_http_post_json(url,
|
||||
body ? body : "{}",
|
||||
timeout_seconds > 0 ? timeout_seconds : 10,
|
||||
response_out,
|
||||
&status);
|
||||
} else {
|
||||
rc = nostr_http_get(url,
|
||||
timeout_seconds > 0 ? timeout_seconds : 10,
|
||||
response_out,
|
||||
&status);
|
||||
}
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
long response_code = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
|
||||
if (status_out) *status_out = status;
|
||||
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if (status_out) *status_out = response_code;
|
||||
|
||||
if (res != CURLE_OK) {
|
||||
free(response.data);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
if (*response_out) {
|
||||
free(*response_out);
|
||||
*response_out = NULL;
|
||||
}
|
||||
return NOSTR_ERROR_CASHU_HTTP_FAILED;
|
||||
}
|
||||
|
||||
*response_out = response.data;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -193,6 +193,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
|
||||
|
||||
|
||||
289
nostr_core/nostr_http.c
Normal file
289
nostr_core/nostr_http.c
Normal file
@@ -0,0 +1,289 @@
|
||||
/*
|
||||
* 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_POSTFIELDS, req->body);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)req->body_len);
|
||||
}
|
||||
} 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_POSTFIELDS, req->body);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)req->body_len);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
55
nostr_core/nostr_http.h
Normal file
55
nostr_core/nostr_http.h
Normal file
@@ -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 */
|
||||
BIN
tests/blossom_client_live_test
Executable file
BIN
tests/blossom_client_live_test
Executable file
Binary file not shown.
213
tests/blossom_client_live_test.c
Normal file
213
tests/blossom_client_live_test.c
Normal file
@@ -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;
|
||||
}
|
||||
BIN
tests/blossom_client_test
Executable file
BIN
tests/blossom_client_test
Executable file
Binary file not shown.
139
tests/blossom_client_test.c
Normal file
139
tests/blossom_client_test.c
Normal file
@@ -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;
|
||||
}
|
||||
BIN
tests/blossom_mock_error_test
Executable file
BIN
tests/blossom_mock_error_test
Executable file
Binary file not shown.
67
tests/blossom_mock_error_test.c
Normal file
67
tests/blossom_mock_error_test.c
Normal file
@@ -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;
|
||||
}
|
||||
222
tests/mock_blossom_server.py
Normal file
222
tests/mock_blossom_server.py
Normal file
@@ -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()
|
||||
BIN
tests/nostr_http_test
Executable file
BIN
tests/nostr_http_test
Executable file
Binary file not shown.
150
tests/nostr_http_test.c
Normal file
150
tests/nostr_http_test.c
Normal file
@@ -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