474 lines
16 KiB
C
474 lines
16 KiB
C
/*
|
|
* NOSTR Core Library - NIP-011: Relay Information Document
|
|
*/
|
|
|
|
#include "nip011.h"
|
|
#include "../cjson/cJSON.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include "../nostr_core/nostr_common.h"
|
|
|
|
#ifndef DISABLE_NIP05 // NIP-11 uses the same HTTP infrastructure as NIP-05
|
|
|
|
#include <curl/curl.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
|
|
*/
|
|
static char* nip11_ws_to_http_url(const char* ws_url) {
|
|
if (!ws_url) {
|
|
return NULL;
|
|
}
|
|
|
|
size_t url_len = strlen(ws_url);
|
|
char* http_url = malloc(url_len + 10); // Extra space for protocol change
|
|
if (!http_url) {
|
|
return NULL;
|
|
}
|
|
|
|
// Convert ws:// to http:// and wss:// to https://
|
|
if (strncmp(ws_url, "ws://", 5) == 0) {
|
|
sprintf(http_url, "http://%s", ws_url + 5);
|
|
} else if (strncmp(ws_url, "wss://", 6) == 0) {
|
|
sprintf(http_url, "https://%s", ws_url + 6);
|
|
} else {
|
|
// Assume it's already HTTP(S) or add https:// as default
|
|
if (strncmp(ws_url, "http://", 7) == 0 || strncmp(ws_url, "https://", 8) == 0) {
|
|
strcpy(http_url, ws_url);
|
|
} else {
|
|
sprintf(http_url, "https://%s", ws_url);
|
|
}
|
|
}
|
|
|
|
return http_url;
|
|
}
|
|
|
|
/**
|
|
* Parse supported NIPs array from JSON
|
|
*/
|
|
static int nip11_parse_supported_nips(cJSON* nips_array, int** nips_out, size_t* count_out) {
|
|
if (!nips_array || !cJSON_IsArray(nips_array)) {
|
|
*nips_out = NULL;
|
|
*count_out = 0;
|
|
return NOSTR_SUCCESS;
|
|
}
|
|
|
|
int array_size = cJSON_GetArraySize(nips_array);
|
|
if (array_size == 0) {
|
|
*nips_out = NULL;
|
|
*count_out = 0;
|
|
return NOSTR_SUCCESS;
|
|
}
|
|
|
|
int* nips = malloc(array_size * sizeof(int));
|
|
if (!nips) {
|
|
return NOSTR_ERROR_MEMORY_FAILED;
|
|
}
|
|
|
|
int valid_count = 0;
|
|
for (int i = 0; i < array_size; i++) {
|
|
cJSON* nip_item = cJSON_GetArrayItem(nips_array, i);
|
|
if (nip_item && cJSON_IsNumber(nip_item)) {
|
|
nips[valid_count++] = (int)cJSON_GetNumberValue(nip_item);
|
|
}
|
|
}
|
|
|
|
if (valid_count == 0) {
|
|
free(nips);
|
|
*nips_out = NULL;
|
|
*count_out = 0;
|
|
} else {
|
|
*nips_out = nips;
|
|
*count_out = valid_count;
|
|
}
|
|
|
|
return NOSTR_SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* Parse string array from JSON
|
|
*/
|
|
static int nip11_parse_string_array(cJSON* json_array, char*** strings_out, size_t* count_out) {
|
|
if (!json_array || !cJSON_IsArray(json_array)) {
|
|
*strings_out = NULL;
|
|
*count_out = 0;
|
|
return NOSTR_SUCCESS;
|
|
}
|
|
|
|
int array_size = cJSON_GetArraySize(json_array);
|
|
if (array_size == 0) {
|
|
*strings_out = NULL;
|
|
*count_out = 0;
|
|
return NOSTR_SUCCESS;
|
|
}
|
|
|
|
char** strings = malloc(array_size * sizeof(char*));
|
|
if (!strings) {
|
|
return NOSTR_ERROR_MEMORY_FAILED;
|
|
}
|
|
|
|
int valid_count = 0;
|
|
for (int i = 0; i < array_size; i++) {
|
|
cJSON* string_item = cJSON_GetArrayItem(json_array, i);
|
|
if (string_item && cJSON_IsString(string_item)) {
|
|
const char* str_value = cJSON_GetStringValue(string_item);
|
|
if (str_value && strlen(str_value) > 0) {
|
|
strings[valid_count] = malloc(strlen(str_value) + 1);
|
|
if (strings[valid_count]) {
|
|
strcpy(strings[valid_count], str_value);
|
|
valid_count++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (valid_count == 0) {
|
|
free(strings);
|
|
*strings_out = NULL;
|
|
*count_out = 0;
|
|
} else {
|
|
*strings_out = strings;
|
|
*count_out = valid_count;
|
|
}
|
|
|
|
return NOSTR_SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* Helper to safely copy JSON string value
|
|
*/
|
|
static char* nip11_copy_json_string(cJSON* json_item) {
|
|
if (!json_item || !cJSON_IsString(json_item)) {
|
|
return NULL;
|
|
}
|
|
|
|
const char* str_value = cJSON_GetStringValue(json_item);
|
|
if (!str_value) {
|
|
return NULL;
|
|
}
|
|
|
|
char* copy = malloc(strlen(str_value) + 1);
|
|
if (copy) {
|
|
strcpy(copy, str_value);
|
|
}
|
|
|
|
return copy;
|
|
}
|
|
|
|
/**
|
|
* Parse NIP-11 relay information document from JSON
|
|
*/
|
|
static int nip11_parse_relay_info(const char* json_response, nostr_relay_info_t* info) {
|
|
if (!json_response || !info) {
|
|
return NOSTR_ERROR_INVALID_INPUT;
|
|
}
|
|
|
|
// Initialize structure
|
|
memset(info, 0, sizeof(nostr_relay_info_t));
|
|
|
|
// Parse JSON
|
|
cJSON* json = cJSON_Parse(json_response);
|
|
if (!json) {
|
|
return NOSTR_ERROR_NIP05_JSON_PARSE_FAILED;
|
|
}
|
|
|
|
// Parse basic information
|
|
info->basic.name = nip11_copy_json_string(cJSON_GetObjectItem(json, "name"));
|
|
info->basic.description = nip11_copy_json_string(cJSON_GetObjectItem(json, "description"));
|
|
info->basic.pubkey = nip11_copy_json_string(cJSON_GetObjectItem(json, "pubkey"));
|
|
info->basic.contact = nip11_copy_json_string(cJSON_GetObjectItem(json, "contact"));
|
|
info->basic.software = nip11_copy_json_string(cJSON_GetObjectItem(json, "software"));
|
|
info->basic.version = nip11_copy_json_string(cJSON_GetObjectItem(json, "version"));
|
|
|
|
// Parse supported NIPs
|
|
cJSON* supported_nips = cJSON_GetObjectItem(json, "supported_nips");
|
|
nip11_parse_supported_nips(supported_nips, &info->basic.supported_nips, &info->basic.supported_nips_count);
|
|
|
|
// Parse limitations (if present)
|
|
cJSON* limitation = cJSON_GetObjectItem(json, "limitation");
|
|
if (limitation && cJSON_IsObject(limitation)) {
|
|
info->has_limitations = 1;
|
|
|
|
cJSON* item;
|
|
item = cJSON_GetObjectItem(limitation, "max_message_length");
|
|
info->limitations.max_message_length = (item && cJSON_IsNumber(item)) ? (int)cJSON_GetNumberValue(item) : -1;
|
|
|
|
item = cJSON_GetObjectItem(limitation, "max_subscriptions");
|
|
info->limitations.max_subscriptions = (item && cJSON_IsNumber(item)) ? (int)cJSON_GetNumberValue(item) : -1;
|
|
|
|
item = cJSON_GetObjectItem(limitation, "max_filters");
|
|
info->limitations.max_filters = (item && cJSON_IsNumber(item)) ? (int)cJSON_GetNumberValue(item) : -1;
|
|
|
|
item = cJSON_GetObjectItem(limitation, "max_limit");
|
|
info->limitations.max_limit = (item && cJSON_IsNumber(item)) ? (int)cJSON_GetNumberValue(item) : -1;
|
|
|
|
item = cJSON_GetObjectItem(limitation, "max_subid_length");
|
|
info->limitations.max_subid_length = (item && cJSON_IsNumber(item)) ? (int)cJSON_GetNumberValue(item) : -1;
|
|
|
|
item = cJSON_GetObjectItem(limitation, "min_prefix");
|
|
info->limitations.min_prefix = (item && cJSON_IsNumber(item)) ? (int)cJSON_GetNumberValue(item) : -1;
|
|
|
|
item = cJSON_GetObjectItem(limitation, "max_event_tags");
|
|
info->limitations.max_event_tags = (item && cJSON_IsNumber(item)) ? (int)cJSON_GetNumberValue(item) : -1;
|
|
|
|
item = cJSON_GetObjectItem(limitation, "max_content_length");
|
|
info->limitations.max_content_length = (item && cJSON_IsNumber(item)) ? (int)cJSON_GetNumberValue(item) : -1;
|
|
|
|
item = cJSON_GetObjectItem(limitation, "min_pow_difficulty");
|
|
info->limitations.min_pow_difficulty = (item && cJSON_IsNumber(item)) ? (int)cJSON_GetNumberValue(item) : -1;
|
|
|
|
item = cJSON_GetObjectItem(limitation, "auth_required");
|
|
info->limitations.auth_required = (item && cJSON_IsBool(item)) ? (cJSON_IsTrue(item) ? 1 : 0) : -1;
|
|
|
|
item = cJSON_GetObjectItem(limitation, "payment_required");
|
|
info->limitations.payment_required = (item && cJSON_IsBool(item)) ? (cJSON_IsTrue(item) ? 1 : 0) : -1;
|
|
|
|
item = cJSON_GetObjectItem(limitation, "restricted_writes");
|
|
info->limitations.restricted_writes = (item && cJSON_IsBool(item)) ? (cJSON_IsTrue(item) ? 1 : 0) : -1;
|
|
|
|
item = cJSON_GetObjectItem(limitation, "created_at_lower_limit");
|
|
info->limitations.created_at_lower_limit = (item && cJSON_IsNumber(item)) ? (long)cJSON_GetNumberValue(item) : -1;
|
|
|
|
item = cJSON_GetObjectItem(limitation, "created_at_upper_limit");
|
|
info->limitations.created_at_upper_limit = (item && cJSON_IsNumber(item)) ? (long)cJSON_GetNumberValue(item) : -1;
|
|
}
|
|
|
|
// Parse relay countries
|
|
cJSON* relay_countries = cJSON_GetObjectItem(json, "relay_countries");
|
|
if (relay_countries && cJSON_IsArray(relay_countries)) {
|
|
info->has_content_limitations = 1;
|
|
nip11_parse_string_array(relay_countries, &info->content_limitations.relay_countries,
|
|
&info->content_limitations.relay_countries_count);
|
|
}
|
|
|
|
// Parse community preferences
|
|
cJSON* language_tags = cJSON_GetObjectItem(json, "language_tags");
|
|
cJSON* tags = cJSON_GetObjectItem(json, "tags");
|
|
cJSON* posting_policy = cJSON_GetObjectItem(json, "posting_policy");
|
|
|
|
if ((language_tags && cJSON_IsArray(language_tags)) ||
|
|
(tags && cJSON_IsArray(tags)) ||
|
|
(posting_policy && cJSON_IsString(posting_policy))) {
|
|
info->has_community_preferences = 1;
|
|
|
|
if (language_tags) {
|
|
nip11_parse_string_array(language_tags, &info->community_preferences.language_tags,
|
|
&info->community_preferences.language_tags_count);
|
|
}
|
|
|
|
if (tags) {
|
|
nip11_parse_string_array(tags, &info->community_preferences.tags,
|
|
&info->community_preferences.tags_count);
|
|
}
|
|
|
|
if (posting_policy) {
|
|
info->community_preferences.posting_policy = nip11_copy_json_string(posting_policy);
|
|
}
|
|
}
|
|
|
|
// Parse icon
|
|
cJSON* icon = cJSON_GetObjectItem(json, "icon");
|
|
if (icon && cJSON_IsString(icon)) {
|
|
info->has_icon = 1;
|
|
info->icon.icon = nip11_copy_json_string(icon);
|
|
}
|
|
|
|
cJSON_Delete(json);
|
|
return NOSTR_SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* Fetch relay information document from a relay URL
|
|
*/
|
|
int nostr_nip11_fetch_relay_info(const char* relay_url, nostr_relay_info_t** info_out, int timeout_seconds) {
|
|
if (!relay_url || !info_out) {
|
|
return NOSTR_ERROR_INVALID_INPUT;
|
|
}
|
|
|
|
// Convert WebSocket URL to HTTP URL
|
|
char* http_url = nip11_ws_to_http_url(relay_url);
|
|
if (!http_url) {
|
|
return NOSTR_ERROR_MEMORY_FAILED;
|
|
}
|
|
|
|
// Allocate info structure
|
|
nostr_relay_info_t* info = malloc(sizeof(nostr_relay_info_t));
|
|
if (!info) {
|
|
free(http_url);
|
|
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);
|
|
free(http_url);
|
|
|
|
if (res != CURLE_OK || response_code != 200) {
|
|
free(response.data);
|
|
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);
|
|
|
|
if (parse_result != NOSTR_SUCCESS) {
|
|
nostr_nip11_relay_info_free(info);
|
|
return parse_result;
|
|
}
|
|
|
|
*info_out = info;
|
|
return NOSTR_SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* Free relay information structure
|
|
*/
|
|
void nostr_nip11_relay_info_free(nostr_relay_info_t* info) {
|
|
if (!info) {
|
|
return;
|
|
}
|
|
|
|
// Free basic info strings
|
|
free(info->basic.name);
|
|
free(info->basic.description);
|
|
free(info->basic.pubkey);
|
|
free(info->basic.contact);
|
|
free(info->basic.software);
|
|
free(info->basic.version);
|
|
free(info->basic.supported_nips);
|
|
|
|
// Free content limitations
|
|
if (info->has_content_limitations) {
|
|
for (size_t i = 0; i < info->content_limitations.relay_countries_count; i++) {
|
|
free(info->content_limitations.relay_countries[i]);
|
|
}
|
|
free(info->content_limitations.relay_countries);
|
|
}
|
|
|
|
// Free community preferences
|
|
if (info->has_community_preferences) {
|
|
for (size_t i = 0; i < info->community_preferences.language_tags_count; i++) {
|
|
free(info->community_preferences.language_tags[i]);
|
|
}
|
|
free(info->community_preferences.language_tags);
|
|
|
|
for (size_t i = 0; i < info->community_preferences.tags_count; i++) {
|
|
free(info->community_preferences.tags[i]);
|
|
}
|
|
free(info->community_preferences.tags);
|
|
|
|
free(info->community_preferences.posting_policy);
|
|
}
|
|
|
|
// Free icon
|
|
if (info->has_icon) {
|
|
free(info->icon.icon);
|
|
}
|
|
|
|
free(info);
|
|
}
|
|
|
|
#else // DISABLE_NIP05
|
|
|
|
/**
|
|
* Stub implementations when NIP-05 is disabled at compile time
|
|
*/
|
|
int nostr_nip11_fetch_relay_info(const char* relay_url, nostr_relay_info_t** info_out, int timeout_seconds) {
|
|
(void)relay_url;
|
|
(void)info_out;
|
|
(void)timeout_seconds;
|
|
return NOSTR_ERROR_NETWORK_FAILED; // NIP-11 disabled at compile time
|
|
}
|
|
|
|
void nostr_nip11_relay_info_free(nostr_relay_info_t* info) {
|
|
(void)info;
|
|
// No-op when disabled
|
|
}
|
|
|
|
#endif // DISABLE_NIP05
|