478 lines
18 KiB
C
478 lines
18 KiB
C
// NIP-11 Relay Information Document module
|
|
#define _GNU_SOURCE
|
|
#include <stdio.h>
|
|
#include "debug.h"
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <pthread.h>
|
|
#include <libwebsockets.h>
|
|
#include "../nostr_core_lib/cjson/cJSON.h"
|
|
#include "config.h"
|
|
|
|
|
|
// 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);
|
|
|
|
// NIP-11 relay information is now managed directly from config table
|
|
|
|
// 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() {
|
|
// NIP-11 relay information is now generated dynamically from config table
|
|
// No initialization needed - data is fetched directly from database when requested
|
|
}
|
|
|
|
// Clean up relay information JSON objects
|
|
void cleanup_relay_info() {
|
|
// NIP-11 relay information is now generated dynamically from config table
|
|
// No cleanup needed - data is fetched directly from database when requested
|
|
}
|
|
|
|
// Generate NIP-11 compliant JSON document
|
|
cJSON* generate_relay_info_json() {
|
|
cJSON* info = cJSON_CreateObject();
|
|
if (!info) {
|
|
DEBUG_ERROR("Failed to create relay info JSON object");
|
|
return NULL;
|
|
}
|
|
|
|
// Get all config values directly from database
|
|
const char* relay_name = get_config_value("relay_name");
|
|
const char* relay_description = get_config_value("relay_description");
|
|
const char* relay_banner = get_config_value("relay_banner");
|
|
const char* relay_icon = get_config_value("relay_icon");
|
|
const char* relay_pubkey = get_config_value("relay_pubkey");
|
|
const char* relay_contact = get_config_value("relay_contact");
|
|
const char* supported_nips_csv = get_config_value("supported_nips");
|
|
const char* relay_software = get_config_value("relay_software");
|
|
const char* relay_version = get_config_value("relay_version");
|
|
const char* privacy_policy = get_config_value("privacy_policy");
|
|
const char* terms_of_service = get_config_value("terms_of_service");
|
|
const char* posting_policy = get_config_value("posting_policy");
|
|
const char* language_tags_csv = get_config_value("language_tags");
|
|
const char* relay_countries_csv = get_config_value("relay_countries");
|
|
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 min_pow_difficulty = get_config_int("pow_min_difficulty", 0);
|
|
int admin_enabled = get_config_bool("admin_enabled", 0);
|
|
|
|
// Add basic relay information
|
|
if (relay_name && strlen(relay_name) > 0) {
|
|
cJSON_AddStringToObject(info, "name", relay_name);
|
|
free((char*)relay_name);
|
|
} else {
|
|
cJSON_AddStringToObject(info, "name", "C Nostr Relay");
|
|
}
|
|
|
|
if (relay_description && strlen(relay_description) > 0) {
|
|
cJSON_AddStringToObject(info, "description", relay_description);
|
|
free((char*)relay_description);
|
|
} else {
|
|
cJSON_AddStringToObject(info, "description", "A high-performance Nostr relay implemented in C with SQLite storage");
|
|
}
|
|
|
|
if (relay_banner && strlen(relay_banner) > 0) {
|
|
cJSON_AddStringToObject(info, "banner", relay_banner);
|
|
free((char*)relay_banner);
|
|
}
|
|
|
|
if (relay_icon && strlen(relay_icon) > 0) {
|
|
cJSON_AddStringToObject(info, "icon", relay_icon);
|
|
free((char*)relay_icon);
|
|
}
|
|
|
|
if (relay_pubkey && strlen(relay_pubkey) > 0) {
|
|
cJSON_AddStringToObject(info, "pubkey", relay_pubkey);
|
|
free((char*)relay_pubkey);
|
|
}
|
|
|
|
if (relay_contact && strlen(relay_contact) > 0) {
|
|
cJSON_AddStringToObject(info, "contact", relay_contact);
|
|
free((char*)relay_contact);
|
|
}
|
|
|
|
// Add supported NIPs
|
|
if (supported_nips_csv && strlen(supported_nips_csv) > 0) {
|
|
cJSON* supported_nips = parse_comma_separated_array(supported_nips_csv);
|
|
if (supported_nips) {
|
|
cJSON_AddItemToObject(info, "supported_nips", supported_nips);
|
|
}
|
|
free((char*)supported_nips_csv);
|
|
} else {
|
|
// Default supported NIPs
|
|
cJSON* supported_nips = cJSON_CreateArray();
|
|
if (supported_nips) {
|
|
cJSON_AddItemToArray(supported_nips, cJSON_CreateNumber(1)); // NIP-01: Basic protocol
|
|
cJSON_AddItemToArray(supported_nips, cJSON_CreateNumber(9)); // NIP-09: Event deletion
|
|
cJSON_AddItemToArray(supported_nips, cJSON_CreateNumber(11)); // NIP-11: Relay information
|
|
cJSON_AddItemToArray(supported_nips, cJSON_CreateNumber(13)); // NIP-13: Proof of Work
|
|
cJSON_AddItemToArray(supported_nips, cJSON_CreateNumber(15)); // NIP-15: EOSE
|
|
cJSON_AddItemToArray(supported_nips, cJSON_CreateNumber(20)); // NIP-20: Command results
|
|
cJSON_AddItemToArray(supported_nips, cJSON_CreateNumber(40)); // NIP-40: Expiration Timestamp
|
|
cJSON_AddItemToArray(supported_nips, cJSON_CreateNumber(42)); // NIP-42: Authentication
|
|
cJSON_AddItemToObject(info, "supported_nips", supported_nips);
|
|
}
|
|
}
|
|
|
|
// Add software information
|
|
if (relay_software && strlen(relay_software) > 0) {
|
|
cJSON_AddStringToObject(info, "software", relay_software);
|
|
free((char*)relay_software);
|
|
} else {
|
|
cJSON_AddStringToObject(info, "software", "https://git.laantungir.net/laantungir/c-relay.git");
|
|
}
|
|
|
|
if (relay_version && strlen(relay_version) > 0) {
|
|
cJSON_AddStringToObject(info, "version", relay_version);
|
|
free((char*)relay_version);
|
|
} else {
|
|
cJSON_AddStringToObject(info, "version", "0.2.0");
|
|
}
|
|
|
|
// Add policies
|
|
if (privacy_policy && strlen(privacy_policy) > 0) {
|
|
cJSON_AddStringToObject(info, "privacy_policy", privacy_policy);
|
|
free((char*)privacy_policy);
|
|
}
|
|
|
|
if (terms_of_service && strlen(terms_of_service) > 0) {
|
|
cJSON_AddStringToObject(info, "terms_of_service", terms_of_service);
|
|
free((char*)terms_of_service);
|
|
}
|
|
|
|
if (posting_policy && strlen(posting_policy) > 0) {
|
|
cJSON_AddStringToObject(info, "posting_policy", posting_policy);
|
|
free((char*)posting_policy);
|
|
}
|
|
|
|
// Add server limitations
|
|
cJSON* limitation = cJSON_CreateObject();
|
|
if (limitation) {
|
|
cJSON_AddNumberToObject(limitation, "max_message_length", max_message_length);
|
|
cJSON_AddNumberToObject(limitation, "max_subscriptions", max_subscriptions_per_client);
|
|
cJSON_AddNumberToObject(limitation, "max_limit", max_limit);
|
|
cJSON_AddNumberToObject(limitation, "max_subid_length", SUBSCRIPTION_ID_MAX_LENGTH);
|
|
cJSON_AddNumberToObject(limitation, "max_event_tags", max_event_tags);
|
|
cJSON_AddNumberToObject(limitation, "max_content_length", max_content_length);
|
|
cJSON_AddNumberToObject(limitation, "min_pow_difficulty", min_pow_difficulty);
|
|
cJSON_AddBoolToObject(limitation, "auth_required", admin_enabled ? cJSON_True : cJSON_False);
|
|
cJSON_AddBoolToObject(limitation, "payment_required", cJSON_False);
|
|
cJSON_AddBoolToObject(limitation, "restricted_writes", cJSON_False);
|
|
cJSON_AddNumberToObject(limitation, "created_at_lower_limit", 0);
|
|
cJSON_AddNumberToObject(limitation, "created_at_upper_limit", 2147483647);
|
|
cJSON_AddNumberToObject(limitation, "default_limit", default_limit);
|
|
cJSON_AddItemToObject(info, "limitation", limitation);
|
|
}
|
|
|
|
// Add retention policies (empty array for now)
|
|
cJSON* retention = cJSON_CreateArray();
|
|
if (retention) {
|
|
cJSON_AddItemToObject(info, "retention", retention);
|
|
}
|
|
|
|
// Add geographical and language information
|
|
if (relay_countries_csv && strlen(relay_countries_csv) > 0) {
|
|
cJSON* relay_countries = parse_comma_separated_array(relay_countries_csv);
|
|
if (relay_countries) {
|
|
cJSON_AddItemToObject(info, "relay_countries", relay_countries);
|
|
}
|
|
free((char*)relay_countries_csv);
|
|
} else {
|
|
cJSON* relay_countries = cJSON_CreateArray();
|
|
if (relay_countries) {
|
|
cJSON_AddItemToArray(relay_countries, cJSON_CreateString("*"));
|
|
cJSON_AddItemToObject(info, "relay_countries", relay_countries);
|
|
}
|
|
}
|
|
|
|
if (language_tags_csv && strlen(language_tags_csv) > 0) {
|
|
cJSON* language_tags = parse_comma_separated_array(language_tags_csv);
|
|
if (language_tags) {
|
|
cJSON_AddItemToObject(info, "language_tags", language_tags);
|
|
}
|
|
free((char*)language_tags_csv);
|
|
} else {
|
|
cJSON* language_tags = cJSON_CreateArray();
|
|
if (language_tags) {
|
|
cJSON_AddItemToArray(language_tags, cJSON_CreateString("*"));
|
|
cJSON_AddItemToObject(info, "language_tags", language_tags);
|
|
}
|
|
}
|
|
|
|
// Add content tags (empty array)
|
|
cJSON* tags = cJSON_CreateArray();
|
|
if (tags) {
|
|
cJSON_AddItemToObject(info, "tags", tags);
|
|
}
|
|
|
|
// Add payment information if configured
|
|
if (payments_url && strlen(payments_url) > 0) {
|
|
cJSON_AddStringToObject(info, "payments_url", payments_url);
|
|
free((char*)payments_url);
|
|
}
|
|
|
|
// Add fees (empty object - no payment required by default)
|
|
cJSON* fees = cJSON_CreateObject();
|
|
if (fees) {
|
|
cJSON_AddItemToObject(info, "fees", fees);
|
|
}
|
|
|
|
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) {
|
|
DEBUG_WARN("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) {
|
|
DEBUG_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) {
|
|
DEBUG_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) {
|
|
DEBUG_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;
|
|
}
|
|
|