// NIP-11 Relay Information Document module #define _GNU_SOURCE #include #include #include #include #include #include "../nostr_core_lib/cjson/cJSON.h" #include "config.h" // Forward declarations for logging functions void log_info(const char* message); void log_success(const char* message); void log_error(const char* message); void log_warning(const char* message); // Forward declarations for configuration functions const char* get_config_value(const char* key); int get_config_int(const char* key, int default_value); int get_config_bool(const char* key, int default_value); // Forward declarations for global cache access extern unified_config_cache_t g_unified_cache; // Forward declarations for constants (defined in config.h and other headers) #define HTTP_STATUS_OK 200 #define HTTP_STATUS_NOT_ACCEPTABLE 406 #define HTTP_STATUS_INTERNAL_SERVER_ERROR 500 ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // NIP-11 RELAY INFORMATION DOCUMENT ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // Helper function to parse comma-separated string into cJSON array cJSON* parse_comma_separated_array(const char* csv_string) { if (!csv_string || strlen(csv_string) == 0) { return cJSON_CreateArray(); } cJSON* array = cJSON_CreateArray(); if (!array) { return NULL; } char* csv_copy = strdup(csv_string); if (!csv_copy) { cJSON_Delete(array); return NULL; } char* token = strtok(csv_copy, ","); while (token) { // Trim whitespace while (*token == ' ') token++; char* end = token + strlen(token) - 1; while (end > token && *end == ' ') *end-- = '\0'; if (strlen(token) > 0) { // Try to parse as number first (for supported_nips) char* endptr; long num = strtol(token, &endptr, 10); if (*endptr == '\0') { // It's a number cJSON_AddItemToArray(array, cJSON_CreateNumber(num)); } else { // It's a string cJSON_AddItemToArray(array, cJSON_CreateString(token)); } } token = strtok(NULL, ","); } free(csv_copy); return array; } // Initialize relay information using configuration system void init_relay_info() { // Get all config values first (without holding mutex to avoid deadlock) // Note: These may be dynamically allocated strings that need to be freed const char* relay_name = get_config_value("relay_name"); const char* relay_description = get_config_value("relay_description"); const char* relay_software = get_config_value("relay_software"); const char* relay_version = get_config_value("relay_version"); const char* relay_contact = get_config_value("relay_contact"); const char* relay_pubkey = get_config_value("relay_pubkey"); const char* supported_nips_csv = get_config_value("supported_nips"); const char* language_tags_csv = get_config_value("language_tags"); const char* relay_countries_csv = get_config_value("relay_countries"); const char* posting_policy = get_config_value("posting_policy"); const char* payments_url = get_config_value("payments_url"); // Get config values for limitations int max_message_length = get_config_int("max_message_length", 16384); int max_subscriptions_per_client = get_config_int("max_subscriptions_per_client", 20); int max_limit = get_config_int("max_limit", 5000); int max_event_tags = get_config_int("max_event_tags", 100); int max_content_length = get_config_int("max_content_length", 8196); int default_limit = get_config_int("default_limit", 500); int admin_enabled = get_config_bool("admin_enabled", 0); pthread_mutex_lock(&g_unified_cache.cache_lock); // Update relay information fields if (relay_name) { strncpy(g_unified_cache.relay_info.name, relay_name, sizeof(g_unified_cache.relay_info.name) - 1); free((char*)relay_name); // Free dynamically allocated string } else { strncpy(g_unified_cache.relay_info.name, "C Nostr Relay", sizeof(g_unified_cache.relay_info.name) - 1); } if (relay_description) { strncpy(g_unified_cache.relay_info.description, relay_description, sizeof(g_unified_cache.relay_info.description) - 1); free((char*)relay_description); // Free dynamically allocated string } else { strncpy(g_unified_cache.relay_info.description, "A high-performance Nostr relay implemented in C with SQLite storage", sizeof(g_unified_cache.relay_info.description) - 1); } if (relay_software) { strncpy(g_unified_cache.relay_info.software, relay_software, sizeof(g_unified_cache.relay_info.software) - 1); free((char*)relay_software); // Free dynamically allocated string } else { strncpy(g_unified_cache.relay_info.software, "https://git.laantungir.net/laantungir/c-relay.git", sizeof(g_unified_cache.relay_info.software) - 1); } if (relay_version) { strncpy(g_unified_cache.relay_info.version, relay_version, sizeof(g_unified_cache.relay_info.version) - 1); free((char*)relay_version); // Free dynamically allocated string } else { strncpy(g_unified_cache.relay_info.version, "0.2.0", sizeof(g_unified_cache.relay_info.version) - 1); } if (relay_contact) { strncpy(g_unified_cache.relay_info.contact, relay_contact, sizeof(g_unified_cache.relay_info.contact) - 1); free((char*)relay_contact); // Free dynamically allocated string } if (relay_pubkey) { strncpy(g_unified_cache.relay_info.pubkey, relay_pubkey, sizeof(g_unified_cache.relay_info.pubkey) - 1); free((char*)relay_pubkey); // Free dynamically allocated string } if (posting_policy) { strncpy(g_unified_cache.relay_info.posting_policy, posting_policy, sizeof(g_unified_cache.relay_info.posting_policy) - 1); free((char*)posting_policy); // Free dynamically allocated string } if (payments_url) { strncpy(g_unified_cache.relay_info.payments_url, payments_url, sizeof(g_unified_cache.relay_info.payments_url) - 1); free((char*)payments_url); // Free dynamically allocated string } // Initialize supported NIPs array from config if (supported_nips_csv) { g_unified_cache.relay_info.supported_nips = parse_comma_separated_array(supported_nips_csv); free((char*)supported_nips_csv); // Free dynamically allocated string } else { // Fallback to default supported NIPs g_unified_cache.relay_info.supported_nips = cJSON_CreateArray(); if (g_unified_cache.relay_info.supported_nips) { cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(1)); // NIP-01: Basic protocol cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(9)); // NIP-09: Event deletion cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(11)); // NIP-11: Relay information cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(13)); // NIP-13: Proof of Work cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(15)); // NIP-15: EOSE cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(20)); // NIP-20: Command results cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(40)); // NIP-40: Expiration Timestamp cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(42)); // NIP-42: Authentication } } // Initialize server limitations using configuration g_unified_cache.relay_info.limitation = cJSON_CreateObject(); if (g_unified_cache.relay_info.limitation) { cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_message_length", max_message_length); cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_subscriptions", max_subscriptions_per_client); cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_limit", max_limit); cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_subid_length", SUBSCRIPTION_ID_MAX_LENGTH); cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_event_tags", max_event_tags); cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_content_length", max_content_length); cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "min_pow_difficulty", g_unified_cache.pow_config.min_pow_difficulty); cJSON_AddBoolToObject(g_unified_cache.relay_info.limitation, "auth_required", admin_enabled ? cJSON_True : cJSON_False); cJSON_AddBoolToObject(g_unified_cache.relay_info.limitation, "payment_required", cJSON_False); cJSON_AddBoolToObject(g_unified_cache.relay_info.limitation, "restricted_writes", cJSON_False); cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "created_at_lower_limit", 0); cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "created_at_upper_limit", 2147483647); cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "default_limit", default_limit); } // Initialize empty retention policies (can be configured later) g_unified_cache.relay_info.retention = cJSON_CreateArray(); // Initialize language tags from config if (language_tags_csv) { g_unified_cache.relay_info.language_tags = parse_comma_separated_array(language_tags_csv); free((char*)language_tags_csv); // Free dynamically allocated string } else { // Fallback to global g_unified_cache.relay_info.language_tags = cJSON_CreateArray(); if (g_unified_cache.relay_info.language_tags) { cJSON_AddItemToArray(g_unified_cache.relay_info.language_tags, cJSON_CreateString("*")); } } // Initialize relay countries from config if (relay_countries_csv) { g_unified_cache.relay_info.relay_countries = parse_comma_separated_array(relay_countries_csv); free((char*)relay_countries_csv); // Free dynamically allocated string } else { // Fallback to global g_unified_cache.relay_info.relay_countries = cJSON_CreateArray(); if (g_unified_cache.relay_info.relay_countries) { cJSON_AddItemToArray(g_unified_cache.relay_info.relay_countries, cJSON_CreateString("*")); } } // Initialize content tags as empty array g_unified_cache.relay_info.tags = cJSON_CreateArray(); // Initialize fees as empty object (no payment required by default) g_unified_cache.relay_info.fees = cJSON_CreateObject(); pthread_mutex_unlock(&g_unified_cache.cache_lock); log_success("Relay information initialized with default values"); } // Clean up relay information JSON objects void cleanup_relay_info() { pthread_mutex_lock(&g_unified_cache.cache_lock); if (g_unified_cache.relay_info.supported_nips) { cJSON_Delete(g_unified_cache.relay_info.supported_nips); g_unified_cache.relay_info.supported_nips = NULL; } if (g_unified_cache.relay_info.limitation) { cJSON_Delete(g_unified_cache.relay_info.limitation); g_unified_cache.relay_info.limitation = NULL; } if (g_unified_cache.relay_info.retention) { cJSON_Delete(g_unified_cache.relay_info.retention); g_unified_cache.relay_info.retention = NULL; } if (g_unified_cache.relay_info.language_tags) { cJSON_Delete(g_unified_cache.relay_info.language_tags); g_unified_cache.relay_info.language_tags = NULL; } if (g_unified_cache.relay_info.relay_countries) { cJSON_Delete(g_unified_cache.relay_info.relay_countries); g_unified_cache.relay_info.relay_countries = NULL; } if (g_unified_cache.relay_info.tags) { cJSON_Delete(g_unified_cache.relay_info.tags); g_unified_cache.relay_info.tags = NULL; } if (g_unified_cache.relay_info.fees) { cJSON_Delete(g_unified_cache.relay_info.fees); g_unified_cache.relay_info.fees = NULL; } pthread_mutex_unlock(&g_unified_cache.cache_lock); } // Generate NIP-11 compliant JSON document cJSON* generate_relay_info_json() { cJSON* info = cJSON_CreateObject(); if (!info) { log_error("Failed to create relay info JSON object"); return NULL; } pthread_mutex_lock(&g_unified_cache.cache_lock); // Defensive reinit: if relay_info appears empty (cache refresh wiped it), rebuild it directly from table if (strlen(g_unified_cache.relay_info.name) == 0 && strlen(g_unified_cache.relay_info.description) == 0 && strlen(g_unified_cache.relay_info.software) == 0) { log_warning("NIP-11 relay_info appears empty, rebuilding directly from config table"); // Rebuild relay_info directly from config table to avoid circular cache dependency // Get values directly from table (similar to init_relay_info but without cache calls) const char* relay_name = get_config_value_from_table("relay_name"); if (relay_name) { strncpy(g_unified_cache.relay_info.name, relay_name, sizeof(g_unified_cache.relay_info.name) - 1); free((char*)relay_name); } else { strncpy(g_unified_cache.relay_info.name, "C Nostr Relay", sizeof(g_unified_cache.relay_info.name) - 1); } const char* relay_description = get_config_value_from_table("relay_description"); if (relay_description) { strncpy(g_unified_cache.relay_info.description, relay_description, sizeof(g_unified_cache.relay_info.description) - 1); free((char*)relay_description); } else { strncpy(g_unified_cache.relay_info.description, "A high-performance Nostr relay implemented in C with SQLite storage", sizeof(g_unified_cache.relay_info.description) - 1); } const char* relay_software = get_config_value_from_table("relay_software"); if (relay_software) { strncpy(g_unified_cache.relay_info.software, relay_software, sizeof(g_unified_cache.relay_info.software) - 1); free((char*)relay_software); } else { strncpy(g_unified_cache.relay_info.software, "https://git.laantungir.net/laantungir/c-relay.git", sizeof(g_unified_cache.relay_info.software) - 1); } const char* relay_version = get_config_value_from_table("relay_version"); if (relay_version) { strncpy(g_unified_cache.relay_info.version, relay_version, sizeof(g_unified_cache.relay_info.version) - 1); free((char*)relay_version); } else { strncpy(g_unified_cache.relay_info.version, "0.2.0", sizeof(g_unified_cache.relay_info.version) - 1); } const char* relay_contact = get_config_value_from_table("relay_contact"); if (relay_contact) { strncpy(g_unified_cache.relay_info.contact, relay_contact, sizeof(g_unified_cache.relay_info.contact) - 1); free((char*)relay_contact); } const char* relay_pubkey = get_config_value_from_table("relay_pubkey"); if (relay_pubkey) { strncpy(g_unified_cache.relay_info.pubkey, relay_pubkey, sizeof(g_unified_cache.relay_info.pubkey) - 1); free((char*)relay_pubkey); } const char* posting_policy = get_config_value_from_table("posting_policy"); if (posting_policy) { strncpy(g_unified_cache.relay_info.posting_policy, posting_policy, sizeof(g_unified_cache.relay_info.posting_policy) - 1); free((char*)posting_policy); } const char* payments_url = get_config_value_from_table("payments_url"); if (payments_url) { strncpy(g_unified_cache.relay_info.payments_url, payments_url, sizeof(g_unified_cache.relay_info.payments_url) - 1); free((char*)payments_url); } // Rebuild supported_nips array const char* supported_nips_csv = get_config_value_from_table("supported_nips"); if (supported_nips_csv) { g_unified_cache.relay_info.supported_nips = parse_comma_separated_array(supported_nips_csv); free((char*)supported_nips_csv); } else { g_unified_cache.relay_info.supported_nips = cJSON_CreateArray(); if (g_unified_cache.relay_info.supported_nips) { cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(1)); cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(9)); cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(11)); cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(13)); cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(15)); cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(20)); cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(40)); cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(42)); } } // Rebuild limitation object int max_message_length = 16384; const char* max_msg_str = get_config_value_from_table("max_message_length"); if (max_msg_str) { max_message_length = atoi(max_msg_str); free((char*)max_msg_str); } int max_subscriptions_per_client = 20; const char* max_subs_str = get_config_value_from_table("max_subscriptions_per_client"); if (max_subs_str) { max_subscriptions_per_client = atoi(max_subs_str); free((char*)max_subs_str); } int max_limit = 5000; const char* max_limit_str = get_config_value_from_table("max_limit"); if (max_limit_str) { max_limit = atoi(max_limit_str); free((char*)max_limit_str); } int max_event_tags = 100; const char* max_tags_str = get_config_value_from_table("max_event_tags"); if (max_tags_str) { max_event_tags = atoi(max_tags_str); free((char*)max_tags_str); } int max_content_length = 8196; const char* max_content_str = get_config_value_from_table("max_content_length"); if (max_content_str) { max_content_length = atoi(max_content_str); free((char*)max_content_str); } int default_limit = 500; const char* default_limit_str = get_config_value_from_table("default_limit"); if (default_limit_str) { default_limit = atoi(default_limit_str); free((char*)default_limit_str); } int admin_enabled = 0; const char* admin_enabled_str = get_config_value_from_table("admin_enabled"); if (admin_enabled_str) { admin_enabled = (strcmp(admin_enabled_str, "true") == 0) ? 1 : 0; free((char*)admin_enabled_str); } g_unified_cache.relay_info.limitation = cJSON_CreateObject(); if (g_unified_cache.relay_info.limitation) { cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_message_length", max_message_length); cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_subscriptions", max_subscriptions_per_client); cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_limit", max_limit); cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_subid_length", SUBSCRIPTION_ID_MAX_LENGTH); cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_event_tags", max_event_tags); cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_content_length", max_content_length); cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "min_pow_difficulty", g_unified_cache.pow_config.min_pow_difficulty); cJSON_AddBoolToObject(g_unified_cache.relay_info.limitation, "auth_required", admin_enabled ? cJSON_True : cJSON_False); cJSON_AddBoolToObject(g_unified_cache.relay_info.limitation, "payment_required", cJSON_False); cJSON_AddBoolToObject(g_unified_cache.relay_info.limitation, "restricted_writes", cJSON_False); cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "created_at_lower_limit", 0); cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "created_at_upper_limit", 2147483647); cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "default_limit", default_limit); } // Rebuild other arrays (empty for now) g_unified_cache.relay_info.retention = cJSON_CreateArray(); g_unified_cache.relay_info.language_tags = cJSON_CreateArray(); if (g_unified_cache.relay_info.language_tags) { cJSON_AddItemToArray(g_unified_cache.relay_info.language_tags, cJSON_CreateString("*")); } g_unified_cache.relay_info.relay_countries = cJSON_CreateArray(); if (g_unified_cache.relay_info.relay_countries) { cJSON_AddItemToArray(g_unified_cache.relay_info.relay_countries, cJSON_CreateString("*")); } g_unified_cache.relay_info.tags = cJSON_CreateArray(); g_unified_cache.relay_info.fees = cJSON_CreateObject(); } // Add basic relay information if (strlen(g_unified_cache.relay_info.name) > 0) { cJSON_AddStringToObject(info, "name", g_unified_cache.relay_info.name); } if (strlen(g_unified_cache.relay_info.description) > 0) { cJSON_AddStringToObject(info, "description", g_unified_cache.relay_info.description); } if (strlen(g_unified_cache.relay_info.banner) > 0) { cJSON_AddStringToObject(info, "banner", g_unified_cache.relay_info.banner); } if (strlen(g_unified_cache.relay_info.icon) > 0) { cJSON_AddStringToObject(info, "icon", g_unified_cache.relay_info.icon); } if (strlen(g_unified_cache.relay_info.pubkey) > 0) { cJSON_AddStringToObject(info, "pubkey", g_unified_cache.relay_info.pubkey); } if (strlen(g_unified_cache.relay_info.contact) > 0) { cJSON_AddStringToObject(info, "contact", g_unified_cache.relay_info.contact); } // Add supported NIPs if (g_unified_cache.relay_info.supported_nips) { cJSON_AddItemToObject(info, "supported_nips", cJSON_Duplicate(g_unified_cache.relay_info.supported_nips, 1)); } // Add software information if (strlen(g_unified_cache.relay_info.software) > 0) { cJSON_AddStringToObject(info, "software", g_unified_cache.relay_info.software); } if (strlen(g_unified_cache.relay_info.version) > 0) { cJSON_AddStringToObject(info, "version", g_unified_cache.relay_info.version); } // Add policies if (strlen(g_unified_cache.relay_info.privacy_policy) > 0) { cJSON_AddStringToObject(info, "privacy_policy", g_unified_cache.relay_info.privacy_policy); } if (strlen(g_unified_cache.relay_info.terms_of_service) > 0) { cJSON_AddStringToObject(info, "terms_of_service", g_unified_cache.relay_info.terms_of_service); } if (strlen(g_unified_cache.relay_info.posting_policy) > 0) { cJSON_AddStringToObject(info, "posting_policy", g_unified_cache.relay_info.posting_policy); } // Add server limitations if (g_unified_cache.relay_info.limitation) { cJSON_AddItemToObject(info, "limitation", cJSON_Duplicate(g_unified_cache.relay_info.limitation, 1)); } // Add retention policies if configured if (g_unified_cache.relay_info.retention && cJSON_GetArraySize(g_unified_cache.relay_info.retention) > 0) { cJSON_AddItemToObject(info, "retention", cJSON_Duplicate(g_unified_cache.relay_info.retention, 1)); } // Add geographical and language information if (g_unified_cache.relay_info.relay_countries) { cJSON_AddItemToObject(info, "relay_countries", cJSON_Duplicate(g_unified_cache.relay_info.relay_countries, 1)); } if (g_unified_cache.relay_info.language_tags) { cJSON_AddItemToObject(info, "language_tags", cJSON_Duplicate(g_unified_cache.relay_info.language_tags, 1)); } if (g_unified_cache.relay_info.tags && cJSON_GetArraySize(g_unified_cache.relay_info.tags) > 0) { cJSON_AddItemToObject(info, "tags", cJSON_Duplicate(g_unified_cache.relay_info.tags, 1)); } // Add payment information if configured if (strlen(g_unified_cache.relay_info.payments_url) > 0) { cJSON_AddStringToObject(info, "payments_url", g_unified_cache.relay_info.payments_url); } if (g_unified_cache.relay_info.fees && cJSON_GetObjectItem(g_unified_cache.relay_info.fees, "admission")) { cJSON_AddItemToObject(info, "fees", cJSON_Duplicate(g_unified_cache.relay_info.fees, 1)); } pthread_mutex_unlock(&g_unified_cache.cache_lock); return info; } // NIP-11 HTTP session data structure for managing buffer lifetime struct nip11_session_data { int type; // 0 for NIP-11 char* json_buffer; size_t json_length; int headers_sent; int body_sent; }; // Handle NIP-11 HTTP request with proper asynchronous buffer management int handle_nip11_http_request(struct lws* wsi, const char* accept_header) { // Check if client accepts application/nostr+json int accepts_nostr_json = 0; if (accept_header) { if (strstr(accept_header, "application/nostr+json") != NULL) { accepts_nostr_json = 1; } } if (!accepts_nostr_json) { log_warning("HTTP request without proper Accept header for NIP-11"); // Return 406 Not Acceptable unsigned char buf[LWS_PRE + 256]; unsigned char *p = &buf[LWS_PRE]; unsigned char *start = p; unsigned char *end = &buf[sizeof(buf) - 1]; if (lws_add_http_header_status(wsi, HTTP_STATUS_NOT_ACCEPTABLE, &p, end)) { return -1; } if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE, (unsigned char*)"text/plain", 10, &p, end)) { return -1; } if (lws_add_http_header_content_length(wsi, 0, &p, end)) { return -1; } if (lws_finalize_http_header(wsi, &p, end)) { return -1; } lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS); return -1; // Close connection } // Generate relay information JSON cJSON* info_json = generate_relay_info_json(); if (!info_json) { log_error("Failed to generate relay info JSON"); unsigned char buf[LWS_PRE + 256]; unsigned char *p = &buf[LWS_PRE]; unsigned char *start = p; unsigned char *end = &buf[sizeof(buf) - 1]; if (lws_add_http_header_status(wsi, HTTP_STATUS_INTERNAL_SERVER_ERROR, &p, end)) { return -1; } if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE, (unsigned char*)"text/plain", 10, &p, end)) { return -1; } if (lws_add_http_header_content_length(wsi, 0, &p, end)) { return -1; } if (lws_finalize_http_header(wsi, &p, end)) { return -1; } lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS); return -1; } char* json_string = cJSON_Print(info_json); cJSON_Delete(info_json); if (!json_string) { log_error("Failed to serialize relay info JSON"); unsigned char buf[LWS_PRE + 256]; unsigned char *p = &buf[LWS_PRE]; unsigned char *start = p; unsigned char *end = &buf[sizeof(buf) - 1]; if (lws_add_http_header_status(wsi, HTTP_STATUS_INTERNAL_SERVER_ERROR, &p, end)) { return -1; } if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE, (unsigned char*)"text/plain", 10, &p, end)) { return -1; } if (lws_add_http_header_content_length(wsi, 0, &p, end)) { return -1; } if (lws_finalize_http_header(wsi, &p, end)) { return -1; } lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS); return -1; } size_t json_len = strlen(json_string); // Allocate session data to manage buffer lifetime across callbacks struct nip11_session_data* session_data = malloc(sizeof(struct nip11_session_data)); if (!session_data) { log_error("Failed to allocate NIP-11 session data"); free(json_string); return -1; } // Store JSON buffer in session data for asynchronous handling session_data->type = 0; // NIP-11 session_data->json_buffer = json_string; session_data->json_length = json_len; session_data->headers_sent = 0; session_data->body_sent = 0; // Store session data in WSI user data for callback access lws_set_wsi_user(wsi, session_data); // Prepare HTTP response with CORS headers unsigned char buf[LWS_PRE + 1024]; unsigned char *p = &buf[LWS_PRE]; unsigned char *start = p; unsigned char *end = &buf[sizeof(buf) - 1]; // Add status if (lws_add_http_header_status(wsi, HTTP_STATUS_OK, &p, end)) { free(session_data->json_buffer); free(session_data); return -1; } // Add content type if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE, (unsigned char*)"application/nostr+json", 22, &p, end)) { free(session_data->json_buffer); free(session_data); return -1; } // Add content length if (lws_add_http_header_content_length(wsi, json_len, &p, end)) { free(session_data->json_buffer); free(session_data); return -1; } // Add CORS headers as required by NIP-11 if (lws_add_http_header_by_name(wsi, (unsigned char*)"access-control-allow-origin:", (unsigned char*)"*", 1, &p, end)) { free(session_data->json_buffer); free(session_data); return -1; } if (lws_add_http_header_by_name(wsi, (unsigned char*)"access-control-allow-headers:", (unsigned char*)"content-type, accept", 20, &p, end)) { free(session_data->json_buffer); free(session_data); return -1; } if (lws_add_http_header_by_name(wsi, (unsigned char*)"access-control-allow-methods:", (unsigned char*)"GET, OPTIONS", 12, &p, end)) { free(session_data->json_buffer); free(session_data); return -1; } // Add Connection: close to ensure connection closes after response if (lws_add_http_header_by_name(wsi, (unsigned char*)"connection:", (unsigned char*)"close", 5, &p, end)) { free(session_data->json_buffer); free(session_data); return -1; } // Finalize headers if (lws_finalize_http_header(wsi, &p, end)) { free(session_data->json_buffer); free(session_data); return -1; } // Write headers if (lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS) < 0) { free(session_data->json_buffer); free(session_data); return -1; } session_data->headers_sent = 1; // Request callback for body transmission lws_callback_on_writable(wsi); return 0; }