/* * NOSTR Core Library - NIP-011: Relay Information Document */ #include "nip011.h" #include "../cjson/cJSON.h" #include #include #include #include "../nostr_core/nostr_common.h" #ifndef DISABLE_NIP05 // NIP-11 uses the same HTTP infrastructure as NIP-05 #include // 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