v0.4.8 - Implement web server functionality for embedded admin interface - serve HTML/CSS/JS from /api/ endpoint with proper MIME types, CORS headers, and performance optimizations

This commit is contained in:
Your Name
2025-10-04 12:45:35 -04:00
parent 36c9c84047
commit 64b418a551
28 changed files with 10635 additions and 4289 deletions

165
src/api.c Normal file
View File

@@ -0,0 +1,165 @@
// Define _GNU_SOURCE to ensure all POSIX features are available
#define _GNU_SOURCE
// API module for serving embedded web content
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <libwebsockets.h>
#include "api.h"
#include "embedded_web_content.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);
// Handle HTTP request for embedded files (assumes GET)
int handle_embedded_file_request(struct lws* wsi, const char* requested_uri) {
log_info("Handling embedded file request");
const char* file_path;
// Handle /api requests
char temp_path[256];
if (strcmp(requested_uri, "/api") == 0) {
// /api -> serve index.html
file_path = "/";
} else if (strncmp(requested_uri, "/api/", 5) == 0) {
// Extract file path from /api/ prefix and add leading slash for lookup
snprintf(temp_path, sizeof(temp_path), "/%s", requested_uri + 5); // Add leading slash
file_path = temp_path;
} else {
log_warning("Embedded file request without /api prefix");
lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL);
return -1;
}
// Get embedded file
embedded_file_t* file = get_embedded_file(file_path);
if (!file) {
log_warning("Embedded file not found");
lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL);
return -1;
}
// Allocate session data
struct embedded_file_session_data* session_data = malloc(sizeof(struct embedded_file_session_data));
if (!session_data) {
log_error("Failed to allocate embedded file session data");
return -1;
}
session_data->type = 1; // Embedded file
session_data->data = file->data;
session_data->size = file->size;
session_data->content_type = file->content_type;
session_data->headers_sent = 0;
session_data->body_sent = 0;
// Store session data
lws_set_wsi_user(wsi, session_data);
// Prepare HTTP response headers
unsigned char buf[LWS_PRE + 1024];
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_OK, &p, end)) {
free(session_data);
return -1;
}
if (lws_add_http_header_by_token(wsi, WSI_TOKEN_HTTP_CONTENT_TYPE, (unsigned char*)file->content_type, strlen(file->content_type), &p, end)) {
free(session_data);
return -1;
}
if (lws_add_http_header_content_length(wsi, file->size, &p, end)) {
free(session_data);
return -1;
}
// Add CORS headers (same as NIP-11 for consistency)
if (lws_add_http_header_by_name(wsi, (unsigned char*)"access-control-allow-origin:", (unsigned char*)"*", 1, &p, end)) {
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);
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);
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);
return -1;
}
if (lws_finalize_http_header(wsi, &p, end)) {
free(session_data);
return -1;
}
// Write headers
if (lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS) < 0) {
free(session_data);
return -1;
}
session_data->headers_sent = 1;
// Request callback for body transmission
lws_callback_on_writable(wsi);
log_success("Embedded file headers sent, body transmission scheduled");
return 0;
}
// Handle HTTP_WRITEABLE for embedded files
int handle_embedded_file_writeable(struct lws* wsi) {
struct embedded_file_session_data* session_data = (struct embedded_file_session_data*)lws_wsi_user(wsi);
if (!session_data || session_data->headers_sent == 0 || session_data->body_sent == 1) {
return 0;
}
// Allocate buffer for data transmission
unsigned char *buf = malloc(LWS_PRE + session_data->size);
if (!buf) {
log_error("Failed to allocate buffer for embedded file transmission");
free(session_data);
lws_set_wsi_user(wsi, NULL);
return -1;
}
// Copy data to buffer
memcpy(buf + LWS_PRE, session_data->data, session_data->size);
// Write data
int write_result = lws_write(wsi, buf + LWS_PRE, session_data->size, LWS_WRITE_HTTP);
// Free the transmission buffer
free(buf);
if (write_result < 0) {
log_error("Failed to write embedded file data");
free(session_data);
lws_set_wsi_user(wsi, NULL);
return -1;
}
// Mark as sent and clean up
session_data->body_sent = 1;
free(session_data);
lws_set_wsi_user(wsi, NULL);
log_success("Embedded file served successfully");
return 0;
}

20
src/api.h Normal file
View File

@@ -0,0 +1,20 @@
// API module for serving embedded web content
#ifndef API_H
#define API_H
#include <libwebsockets.h>
// Embedded file session data structure for managing buffer lifetime
struct embedded_file_session_data {
int type; // 1 for embedded file
const unsigned char* data;
size_t size;
const char* content_type;
int headers_sent;
int body_sent;
};
// Handle HTTP request for embedded API files
int handle_embedded_file_request(struct lws* wsi, const char* requested_uri);
#endif // API_H

View File

@@ -65,6 +65,9 @@ extern void log_error(const char* message);
int populate_default_config_values(void);
int process_admin_config_event(cJSON* event, char* error_message, size_t error_size);
void invalidate_config_cache(void);
// Forward declaration for relay info initialization
void init_relay_info(void);
int add_auth_rule_from_config(const char* rule_type, const char* pattern_type,
const char* pattern_value, const char* action);
int remove_auth_rule_from_config(const char* rule_type, const char* pattern_type,
@@ -80,6 +83,7 @@ const char* get_first_tag_name(cJSON* event);
const char* get_tag_value(cJSON* event, const char* tag_name, int value_index);
int parse_auth_query_parameters(cJSON* event, char** query_type, char** pattern_value);
int handle_config_update_unified(cJSON* event, char* error_message, size_t error_size, struct lws* wsi);
int handle_stats_query_unified(cJSON* event, char* error_message, size_t error_size, struct lws* wsi);
// Current configuration cache
@@ -215,32 +219,44 @@ static int refresh_unified_cache_from_table(void) {
return -1;
}
// Clear cache
memset(&g_unified_cache, 0, sizeof(g_unified_cache));
g_unified_cache.cache_lock = (pthread_mutex_t)PTHREAD_MUTEX_INITIALIZER;
log_info("Refreshing unified configuration cache from database");
// Lock the cache for update (don't memset entire cache to avoid wiping relay_info)
log_info("DEBUG: Acquiring cache lock for refresh");
pthread_mutex_lock(&g_unified_cache.cache_lock);
log_info("DEBUG: Cache lock acquired");
// Load critical config values from table
log_info("DEBUG: Loading admin_pubkey from table");
const char* admin_pubkey = get_config_value_from_table("admin_pubkey");
if (admin_pubkey) {
log_info("DEBUG: Setting admin_pubkey in cache");
strncpy(g_unified_cache.admin_pubkey, admin_pubkey, sizeof(g_unified_cache.admin_pubkey) - 1);
g_unified_cache.admin_pubkey[sizeof(g_unified_cache.admin_pubkey) - 1] = '\0';
free((char*)admin_pubkey);
}
log_info("DEBUG: Loading relay_pubkey from table");
const char* relay_pubkey = get_config_value_from_table("relay_pubkey");
if (relay_pubkey) {
log_info("DEBUG: Setting relay_pubkey in cache");
strncpy(g_unified_cache.relay_pubkey, relay_pubkey, sizeof(g_unified_cache.relay_pubkey) - 1);
g_unified_cache.relay_pubkey[sizeof(g_unified_cache.relay_pubkey) - 1] = '\0';
free((char*)relay_pubkey);
}
// Load auth-related config
const char* auth_required = get_config_value_from_table("auth_required");
g_unified_cache.auth_required = (auth_required && strcmp(auth_required, "true") == 0) ? 1 : 0;
if (auth_required) free((char*)auth_required);
const char* admin_enabled = get_config_value_from_table("admin_enabled");
g_unified_cache.admin_enabled = (admin_enabled && strcmp(admin_enabled, "true") == 0) ? 1 : 0;
if (admin_enabled) free((char*)admin_enabled);
const char* max_file_size = get_config_value_from_table("max_file_size");
g_unified_cache.max_file_size = max_file_size ? atol(max_file_size) : 104857600; // 100MB default
if (max_file_size) free((char*)max_file_size);
const char* nip42_mode = get_config_value_from_table("nip42_mode");
if (nip42_mode) {
@@ -251,38 +267,122 @@ static int refresh_unified_cache_from_table(void) {
} else {
g_unified_cache.nip42_mode = 1; // Optional/enabled
}
free((char*)nip42_mode);
} else {
g_unified_cache.nip42_mode = 1; // Default to optional/enabled
}
const char* challenge_timeout = get_config_value_from_table("nip42_challenge_timeout");
g_unified_cache.nip42_challenge_timeout = challenge_timeout ? atoi(challenge_timeout) : 600;
if (challenge_timeout) free((char*)challenge_timeout);
const char* time_tolerance = get_config_value_from_table("nip42_time_tolerance");
g_unified_cache.nip42_time_tolerance = time_tolerance ? atoi(time_tolerance) : 300;
if (time_tolerance) free((char*)time_tolerance);
// Load NIP-70 protected events config
const char* nip70_enabled = get_config_value_from_table("nip70_protected_events_enabled");
g_unified_cache.nip70_protected_events_enabled = (nip70_enabled && strcmp(nip70_enabled, "true") == 0) ? 1 : 0;
if (nip70_enabled) free((char*)nip70_enabled);
// Load NIP-11 relay info fields directly into cache (avoid circular dependency)
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);
g_unified_cache.relay_info.name[sizeof(g_unified_cache.relay_info.name) - 1] = '\0';
free((char*)relay_name);
}
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);
g_unified_cache.relay_info.description[sizeof(g_unified_cache.relay_info.description) - 1] = '\0';
free((char*)relay_description);
}
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);
g_unified_cache.relay_info.contact[sizeof(g_unified_cache.relay_info.contact) - 1] = '\0';
free((char*)relay_contact);
}
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);
g_unified_cache.relay_info.software[sizeof(g_unified_cache.relay_info.software) - 1] = '\0';
free((char*)relay_software);
}
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);
g_unified_cache.relay_info.version[sizeof(g_unified_cache.relay_info.version) - 1] = '\0';
free((char*)relay_version);
}
const char* supported_nips = get_config_value_from_table("supported_nips");
if (supported_nips) {
strncpy(g_unified_cache.relay_info.supported_nips_str, supported_nips, sizeof(g_unified_cache.relay_info.supported_nips_str) - 1);
g_unified_cache.relay_info.supported_nips_str[sizeof(g_unified_cache.relay_info.supported_nips_str) - 1] = '\0';
free((char*)supported_nips);
}
const char* language_tags = get_config_value_from_table("language_tags");
if (language_tags) {
strncpy(g_unified_cache.relay_info.language_tags_str, language_tags, sizeof(g_unified_cache.relay_info.language_tags_str) - 1);
g_unified_cache.relay_info.language_tags_str[sizeof(g_unified_cache.relay_info.language_tags_str) - 1] = '\0';
free((char*)language_tags);
}
const char* relay_countries = get_config_value_from_table("relay_countries");
if (relay_countries) {
strncpy(g_unified_cache.relay_info.relay_countries_str, relay_countries, sizeof(g_unified_cache.relay_info.relay_countries_str) - 1);
g_unified_cache.relay_info.relay_countries_str[sizeof(g_unified_cache.relay_info.relay_countries_str) - 1] = '\0';
free((char*)relay_countries);
}
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);
g_unified_cache.relay_info.posting_policy[sizeof(g_unified_cache.relay_info.posting_policy) - 1] = '\0';
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);
g_unified_cache.relay_info.payments_url[sizeof(g_unified_cache.relay_info.payments_url) - 1] = '\0';
free((char*)payments_url);
}
// Set cache expiration
log_info("DEBUG: Setting cache expiration and validity");
int cache_timeout = get_cache_timeout();
g_unified_cache.cache_expires = time(NULL) + cache_timeout;
g_unified_cache.cache_valid = 1;
log_info("DEBUG: Releasing cache lock");
pthread_mutex_unlock(&g_unified_cache.cache_lock);
log_info("Unified configuration cache refreshed from database");
return 0;
}
// Get admin pubkey from cache (with automatic refresh)
const char* get_admin_pubkey_cached(void) {
// First check without holding lock: whether we need refresh
pthread_mutex_lock(&g_unified_cache.cache_lock);
// Check cache validity
if (!g_unified_cache.cache_valid || time(NULL) > g_unified_cache.cache_expires) {
int need_refresh = (!g_unified_cache.cache_valid || time(NULL) > g_unified_cache.cache_expires);
pthread_mutex_unlock(&g_unified_cache.cache_lock);
if (need_refresh) {
// Perform refresh, which locks internally
refresh_unified_cache_from_table();
}
// Now read under lock
pthread_mutex_lock(&g_unified_cache.cache_lock);
const char* result = g_unified_cache.admin_pubkey[0] ? g_unified_cache.admin_pubkey : NULL;
pthread_mutex_unlock(&g_unified_cache.cache_lock);
return result;
@@ -290,13 +390,18 @@ const char* get_admin_pubkey_cached(void) {
// Get relay pubkey from cache (with automatic refresh)
const char* get_relay_pubkey_cached(void) {
// First check without holding lock: whether we need refresh
pthread_mutex_lock(&g_unified_cache.cache_lock);
// Check cache validity
if (!g_unified_cache.cache_valid || time(NULL) > g_unified_cache.cache_expires) {
int need_refresh = (!g_unified_cache.cache_valid || time(NULL) > g_unified_cache.cache_expires);
pthread_mutex_unlock(&g_unified_cache.cache_lock);
if (need_refresh) {
// Perform refresh, which locks internally
refresh_unified_cache_from_table();
}
// Now read under lock
pthread_mutex_lock(&g_unified_cache.cache_lock);
const char* result = g_unified_cache.relay_pubkey[0] ? g_unified_cache.relay_pubkey : NULL;
pthread_mutex_unlock(&g_unified_cache.cache_lock);
return result;
@@ -696,23 +801,22 @@ int init_configuration_system(const char* config_dir_override, const char* confi
// Initialize unified cache with proper structure initialization
pthread_mutex_lock(&g_unified_cache.cache_lock);
// Clear the entire cache structure
memset(&g_unified_cache, 0, sizeof(g_unified_cache));
// Reinitialize the mutex after memset
g_unified_cache.cache_lock = (pthread_mutex_t)PTHREAD_MUTEX_INITIALIZER;
// Initialize basic cache state
// Initialize basic cache state (do not memset entire struct to avoid corrupting mutex)
g_unified_cache.cache_valid = 0;
g_unified_cache.cache_expires = 0;
// Clear string fields
memset(g_unified_cache.admin_pubkey, 0, sizeof(g_unified_cache.admin_pubkey));
memset(g_unified_cache.relay_pubkey, 0, sizeof(g_unified_cache.relay_pubkey));
memset(&g_unified_cache.relay_info, 0, sizeof(g_unified_cache.relay_info));
// Initialize relay_info structure with default values
strncpy(g_unified_cache.relay_info.software, "https://git.laantungir.net/laantungir/c-relay.git",
sizeof(g_unified_cache.relay_info.software) - 1);
strncpy(g_unified_cache.relay_info.version, "0.2.0",
sizeof(g_unified_cache.relay_info.version) - 1);
// Initialize pow_config structure with defaults
g_unified_cache.pow_config.enabled = 1;
g_unified_cache.pow_config.min_pow_difficulty = 0;
@@ -721,14 +825,23 @@ int init_configuration_system(const char* config_dir_override, const char* confi
g_unified_cache.pow_config.reject_lower_targets = 0;
g_unified_cache.pow_config.strict_format = 0;
g_unified_cache.pow_config.anti_spam_mode = 0;
// Initialize expiration_config structure with defaults
g_unified_cache.expiration_config.enabled = 1;
g_unified_cache.expiration_config.strict_mode = 1;
g_unified_cache.expiration_config.filter_responses = 1;
g_unified_cache.expiration_config.delete_expired = 0;
g_unified_cache.expiration_config.grace_period = 1;
// Initialize other scalar fields
g_unified_cache.auth_required = 0;
g_unified_cache.admin_enabled = 0;
g_unified_cache.max_file_size = 0;
g_unified_cache.nip42_mode = 0;
g_unified_cache.nip42_challenge_timeout = 0;
g_unified_cache.nip42_time_tolerance = 0;
g_unified_cache.nip70_protected_events_enabled = 0;
pthread_mutex_unlock(&g_unified_cache.cache_lock);
log_success("Event-based configuration system initialized with unified cache structures");
@@ -750,34 +863,53 @@ void cleanup_configuration_system(void) {
// Clear unified cache with proper cleanup of JSON objects
pthread_mutex_lock(&g_unified_cache.cache_lock);
// Clean up relay_info JSON objects if they exist
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.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.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.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;
}
// Clear the entire cache structure
memset(&g_unified_cache, 0, sizeof(g_unified_cache));
g_unified_cache.cache_lock = (pthread_mutex_t)PTHREAD_MUTEX_INITIALIZER;
// Clear cache fields individually (do not memset entire struct to avoid corrupting mutex)
g_unified_cache.cache_valid = 0;
g_unified_cache.cache_expires = 0;
memset(g_unified_cache.admin_pubkey, 0, sizeof(g_unified_cache.admin_pubkey));
memset(g_unified_cache.relay_pubkey, 0, sizeof(g_unified_cache.relay_pubkey));
memset(&g_unified_cache.relay_info, 0, sizeof(g_unified_cache.relay_info));
g_unified_cache.auth_required = 0;
g_unified_cache.admin_enabled = 0;
g_unified_cache.max_file_size = 0;
g_unified_cache.nip42_mode = 0;
g_unified_cache.nip42_challenge_timeout = 0;
g_unified_cache.nip42_time_tolerance = 0;
g_unified_cache.nip70_protected_events_enabled = 0;
memset(&g_unified_cache.pow_config, 0, sizeof(g_unified_cache.pow_config));
memset(&g_unified_cache.expiration_config, 0, sizeof(g_unified_cache.expiration_config));
pthread_mutex_unlock(&g_unified_cache.cache_lock);
log_success("Configuration system cleaned up with proper JSON cleanup");
}
@@ -1938,71 +2070,8 @@ const char* get_config_value_from_table(const char* key) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
const char* value = (char*)sqlite3_column_text(stmt, 0);
if (value) {
// For NIP-11 fields, store in cache buffers but return dynamically allocated strings for consistency
if (strcmp(key, "relay_name") == 0) {
pthread_mutex_lock(&g_unified_cache.cache_lock);
strncpy(g_unified_cache.relay_info.name, value, sizeof(g_unified_cache.relay_info.name) - 1);
g_unified_cache.relay_info.name[sizeof(g_unified_cache.relay_info.name) - 1] = '\0';
result = strdup(value); // Return dynamically allocated copy
pthread_mutex_unlock(&g_unified_cache.cache_lock);
} else if (strcmp(key, "relay_description") == 0) {
pthread_mutex_lock(&g_unified_cache.cache_lock);
strncpy(g_unified_cache.relay_info.description, value, sizeof(g_unified_cache.relay_info.description) - 1);
g_unified_cache.relay_info.description[sizeof(g_unified_cache.relay_info.description) - 1] = '\0';
result = strdup(value); // Return dynamically allocated copy
pthread_mutex_unlock(&g_unified_cache.cache_lock);
} else if (strcmp(key, "relay_contact") == 0) {
pthread_mutex_lock(&g_unified_cache.cache_lock);
strncpy(g_unified_cache.relay_info.contact, value, sizeof(g_unified_cache.relay_info.contact) - 1);
g_unified_cache.relay_info.contact[sizeof(g_unified_cache.relay_info.contact) - 1] = '\0';
result = strdup(value); // Return dynamically allocated copy
pthread_mutex_unlock(&g_unified_cache.cache_lock);
} else if (strcmp(key, "relay_software") == 0) {
pthread_mutex_lock(&g_unified_cache.cache_lock);
strncpy(g_unified_cache.relay_info.software, value, sizeof(g_unified_cache.relay_info.software) - 1);
g_unified_cache.relay_info.software[sizeof(g_unified_cache.relay_info.software) - 1] = '\0';
result = strdup(value); // Return dynamically allocated copy
pthread_mutex_unlock(&g_unified_cache.cache_lock);
} else if (strcmp(key, "relay_version") == 0) {
pthread_mutex_lock(&g_unified_cache.cache_lock);
strncpy(g_unified_cache.relay_info.version, value, sizeof(g_unified_cache.relay_info.version) - 1);
g_unified_cache.relay_info.version[sizeof(g_unified_cache.relay_info.version) - 1] = '\0';
result = strdup(value); // Return dynamically allocated copy
pthread_mutex_unlock(&g_unified_cache.cache_lock);
} else if (strcmp(key, "supported_nips") == 0) {
pthread_mutex_lock(&g_unified_cache.cache_lock);
strncpy(g_unified_cache.relay_info.supported_nips_str, value, sizeof(g_unified_cache.relay_info.supported_nips_str) - 1);
g_unified_cache.relay_info.supported_nips_str[sizeof(g_unified_cache.relay_info.supported_nips_str) - 1] = '\0';
result = strdup(value); // Return dynamically allocated copy
pthread_mutex_unlock(&g_unified_cache.cache_lock);
} else if (strcmp(key, "language_tags") == 0) {
pthread_mutex_lock(&g_unified_cache.cache_lock);
strncpy(g_unified_cache.relay_info.language_tags_str, value, sizeof(g_unified_cache.relay_info.language_tags_str) - 1);
g_unified_cache.relay_info.language_tags_str[sizeof(g_unified_cache.relay_info.language_tags_str) - 1] = '\0';
result = strdup(value); // Return dynamically allocated copy
pthread_mutex_unlock(&g_unified_cache.cache_lock);
} else if (strcmp(key, "relay_countries") == 0) {
pthread_mutex_lock(&g_unified_cache.cache_lock);
strncpy(g_unified_cache.relay_info.relay_countries_str, value, sizeof(g_unified_cache.relay_info.relay_countries_str) - 1);
g_unified_cache.relay_info.relay_countries_str[sizeof(g_unified_cache.relay_info.relay_countries_str) - 1] = '\0';
result = strdup(value); // Return dynamically allocated copy
pthread_mutex_unlock(&g_unified_cache.cache_lock);
} else if (strcmp(key, "posting_policy") == 0) {
pthread_mutex_lock(&g_unified_cache.cache_lock);
strncpy(g_unified_cache.relay_info.posting_policy, value, sizeof(g_unified_cache.relay_info.posting_policy) - 1);
g_unified_cache.relay_info.posting_policy[sizeof(g_unified_cache.relay_info.posting_policy) - 1] = '\0';
result = strdup(value); // Return dynamically allocated copy
pthread_mutex_unlock(&g_unified_cache.cache_lock);
} else if (strcmp(key, "payments_url") == 0) {
pthread_mutex_lock(&g_unified_cache.cache_lock);
strncpy(g_unified_cache.relay_info.payments_url, value, sizeof(g_unified_cache.relay_info.payments_url) - 1);
g_unified_cache.relay_info.payments_url[sizeof(g_unified_cache.relay_info.payments_url) - 1] = '\0';
result = strdup(value); // Return dynamically allocated copy
pthread_mutex_unlock(&g_unified_cache.cache_lock);
} else {
// For other keys, return a dynamically allocated string to prevent buffer reuse
result = strdup(value);
}
// Return a dynamically allocated string to prevent buffer reuse
result = strdup(value);
}
}
@@ -3032,6 +3101,10 @@ int handle_kind_23456_unified(cJSON* event, char* error_message, size_t error_si
printf(" Command: %s\n", command);
return handle_system_command_unified(event, command, error_message, error_size, wsi);
}
else if (strcmp(action_type, "stats_query") == 0) {
log_info("DEBUG: Routing to stats_query handler");
return handle_stats_query_unified(event, error_message, error_size, wsi);
}
else if (strcmp(action_type, "whitelist") == 0 || strcmp(action_type, "blacklist") == 0) {
log_info("DEBUG: Routing to auth rule modification handler");
printf(" Rule type: %s\n", action_type);
@@ -3708,6 +3781,131 @@ int handle_auth_rule_modification_unified(cJSON* event, char* error_message, siz
return -1;
}
}
// Unified stats query handler
int handle_stats_query_unified(cJSON* event, char* error_message, size_t error_size, struct lws* wsi) {
// Suppress unused parameter warning
(void)wsi;
if (!g_db) {
snprintf(error_message, error_size, "database not available");
return -1;
}
log_info("Processing unified stats query");
printf(" Query type: stats_query\n");
// Build response with database statistics
cJSON* response = cJSON_CreateObject();
cJSON_AddStringToObject(response, "query_type", "stats_query");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
// Get database file size
struct stat db_stat;
long long db_size = 0;
if (stat(g_database_path, &db_stat) == 0) {
db_size = db_stat.st_size;
}
cJSON_AddNumberToObject(response, "database_size_bytes", db_size);
// Query total events count
sqlite3_stmt* stmt;
if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM events", -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
cJSON_AddNumberToObject(response, "total_events", sqlite3_column_int64(stmt, 0));
}
sqlite3_finalize(stmt);
}
// Query event kinds distribution
cJSON* event_kinds = cJSON_CreateArray();
if (sqlite3_prepare_v2(g_db, "SELECT kind, count, percentage FROM event_kinds_view ORDER BY count DESC", -1, &stmt, NULL) == SQLITE_OK) {
while (sqlite3_step(stmt) == SQLITE_ROW) {
cJSON* kind_obj = cJSON_CreateObject();
cJSON_AddNumberToObject(kind_obj, "kind", sqlite3_column_int(stmt, 0));
cJSON_AddNumberToObject(kind_obj, "count", sqlite3_column_int64(stmt, 1));
cJSON_AddNumberToObject(kind_obj, "percentage", sqlite3_column_double(stmt, 2));
cJSON_AddItemToArray(event_kinds, kind_obj);
}
sqlite3_finalize(stmt);
}
cJSON_AddItemToObject(response, "event_kinds", event_kinds);
// Query time-based statistics
cJSON* time_stats = cJSON_CreateObject();
if (sqlite3_prepare_v2(g_db, "SELECT total_events, events_24h, events_7d, events_30d FROM time_stats_view", -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
cJSON_AddNumberToObject(time_stats, "total", sqlite3_column_int64(stmt, 0));
cJSON_AddNumberToObject(time_stats, "last_24h", sqlite3_column_int64(stmt, 1));
cJSON_AddNumberToObject(time_stats, "last_7d", sqlite3_column_int64(stmt, 2));
cJSON_AddNumberToObject(time_stats, "last_30d", sqlite3_column_int64(stmt, 3));
}
sqlite3_finalize(stmt);
}
cJSON_AddItemToObject(response, "time_stats", time_stats);
// Query top pubkeys
cJSON* top_pubkeys = cJSON_CreateArray();
if (sqlite3_prepare_v2(g_db, "SELECT pubkey, event_count, percentage FROM top_pubkeys_view ORDER BY event_count DESC LIMIT 10", -1, &stmt, NULL) == SQLITE_OK) {
while (sqlite3_step(stmt) == SQLITE_ROW) {
cJSON* pubkey_obj = cJSON_CreateObject();
const char* pubkey = (const char*)sqlite3_column_text(stmt, 0);
cJSON_AddStringToObject(pubkey_obj, "pubkey", pubkey ? pubkey : "");
cJSON_AddNumberToObject(pubkey_obj, "event_count", sqlite3_column_int64(stmt, 1));
cJSON_AddNumberToObject(pubkey_obj, "percentage", sqlite3_column_double(stmt, 2));
cJSON_AddItemToArray(top_pubkeys, pubkey_obj);
}
sqlite3_finalize(stmt);
}
cJSON_AddItemToObject(response, "top_pubkeys", top_pubkeys);
// Get database creation timestamp (oldest event)
if (sqlite3_prepare_v2(g_db, "SELECT MIN(created_at) FROM events", -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
sqlite3_int64 oldest_timestamp = sqlite3_column_int64(stmt, 0);
if (oldest_timestamp > 0) {
cJSON_AddNumberToObject(response, "database_created_at", (double)oldest_timestamp);
}
}
sqlite3_finalize(stmt);
}
// Get latest event timestamp
if (sqlite3_prepare_v2(g_db, "SELECT MAX(created_at) FROM events", -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
sqlite3_int64 latest_timestamp = sqlite3_column_int64(stmt, 0);
if (latest_timestamp > 0) {
cJSON_AddNumberToObject(response, "latest_event_at", (double)latest_timestamp);
}
}
sqlite3_finalize(stmt);
}
printf("=== Database Statistics ===\n");
printf("Database size: %lld bytes\n", db_size);
printf("Event kinds: %d\n", cJSON_GetArraySize(event_kinds));
printf("Top pubkeys: %d\n", cJSON_GetArraySize(top_pubkeys));
// Get admin pubkey from event for response
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
const char* admin_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : NULL;
if (!admin_pubkey) {
cJSON_Delete(response);
snprintf(error_message, error_size, "missing admin pubkey for response");
return -1;
}
// Send response as signed kind 23457 event
if (send_admin_response_event(response, admin_pubkey, wsi) == 0) {
log_success("Stats query completed successfully with signed response");
cJSON_Delete(response);
return 0;
}
cJSON_Delete(response);
snprintf(error_message, error_size, "failed to send stats query response");
return -1;
}
// Unified config update handler - handles multiple config objects in single atomic command
int handle_config_update_unified(cJSON* event, char* error_message, size_t error_size, struct lws* wsi) {
// Suppress unused parameter warning

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,19 @@
// Auto-generated embedded web content header
// Do not edit manually - generated by embed_web_files.sh
#ifndef EMBEDDED_WEB_CONTENT_H
#define EMBEDDED_WEB_CONTENT_H
#include <stddef.h>
// Embedded file lookup function
typedef struct {
const char *path;
const unsigned char *data;
size_t size;
const char *content_type;
} embedded_file_t;
embedded_file_t *get_embedded_file(const char *path);
#endif // EMBEDDED_WEB_CONTENT_H

View File

@@ -352,9 +352,175 @@ cJSON* generate_relay_info_json() {
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();
log_info("NIP-11 relay_info rebuilt directly from config table");
}
// Add basic relay information
if (strlen(g_unified_cache.relay_info.name) > 0) {
cJSON_AddStringToObject(info, "name", g_unified_cache.relay_info.name);
@@ -435,6 +601,7 @@ cJSON* generate_relay_info_json() {
// 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;
@@ -504,7 +671,7 @@ int handle_nip11_http_request(struct lws* wsi, const char* accept_header) {
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];
@@ -527,8 +694,11 @@ int handle_nip11_http_request(struct lws* wsi, const char* accept_header) {
lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS);
return -1;
}
size_t json_len = strlen(json_string);
log_info("Generated NIP-11 JSON");
printf(" JSON length: %zu bytes\n", json_len);
printf(" JSON preview: %.100s%s\n", json_string, json_len > 100 ? "..." : "");
// Allocate session data to manage buffer lifetime across callbacks
struct nip11_session_data* session_data = malloc(sizeof(struct nip11_session_data));
@@ -539,6 +709,7 @@ int handle_nip11_http_request(struct lws* wsi, const char* accept_header) {
}
// 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;
@@ -594,6 +765,13 @@ int handle_nip11_http_request(struct lws* wsi, const char* accept_header) {
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)) {

View File

@@ -297,6 +297,65 @@ WHERE event_type = 'created'\n\
AND subscription_id NOT IN (\n\
SELECT subscription_id FROM subscription_events\n\
WHERE event_type IN ('closed', 'expired', 'disconnected')\n\
);";
);\n\
\n\
-- Database Statistics Views for Admin API\n\
-- Event kinds distribution view\n\
CREATE VIEW event_kinds_view AS\n\
SELECT\n\
kind,\n\
COUNT(*) as count,\n\
ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM events), 2) as percentage\n\
FROM events\n\
GROUP BY kind\n\
ORDER BY count DESC;\n\
\n\
-- Top pubkeys by event count view\n\
CREATE VIEW top_pubkeys_view AS\n\
SELECT\n\
pubkey,\n\
COUNT(*) as event_count,\n\
ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM events), 2) as percentage\n\
FROM events\n\
GROUP BY pubkey\n\
ORDER BY event_count DESC\n\
LIMIT 10;\n\
\n\
-- Time-based statistics view\n\
CREATE VIEW time_stats_view AS\n\
SELECT\n\
'total' as period,\n\
COUNT(*) as total_events,\n\
COUNT(DISTINCT pubkey) as unique_pubkeys,\n\
MIN(created_at) as oldest_event,\n\
MAX(created_at) as newest_event\n\
FROM events\n\
UNION ALL\n\
SELECT\n\
'24h' as period,\n\
COUNT(*) as total_events,\n\
COUNT(DISTINCT pubkey) as unique_pubkeys,\n\
MIN(created_at) as oldest_event,\n\
MAX(created_at) as newest_event\n\
FROM events\n\
WHERE created_at >= (strftime('%s', 'now') - 86400)\n\
UNION ALL\n\
SELECT\n\
'7d' as period,\n\
COUNT(*) as total_events,\n\
COUNT(DISTINCT pubkey) as unique_pubkeys,\n\
MIN(created_at) as oldest_event,\n\
MAX(created_at) as newest_event\n\
FROM events\n\
WHERE created_at >= (strftime('%s', 'now') - 604800)\n\
UNION ALL\n\
SELECT\n\
'30d' as period,\n\
COUNT(*) as total_events,\n\
COUNT(DISTINCT pubkey) as unique_pubkeys,\n\
MIN(created_at) as oldest_event,\n\
MAX(created_at) as newest_event\n\
FROM events\n\
WHERE created_at >= (strftime('%s', 'now') - 2592000);";
#endif /* SQL_SCHEMA_H */

View File

@@ -26,6 +26,8 @@
#include "sql_schema.h" // Embedded database schema
#include "websockets.h" // WebSocket structures and constants
#include "subscriptions.h" // Subscription structures and functions
#include "embedded_web_content.h" // Embedded web content
#include "api.h" // API for embedded files
// Forward declarations for logging functions
void log_info(const char* message);
@@ -47,6 +49,9 @@ void handle_nip42_auth_challenge_response(struct lws* wsi, struct per_session_da
// Forward declarations for NIP-11 relay information handling
int handle_nip11_http_request(struct lws* wsi, const char* accept_header);
// Forward declarations for embedded file handling
int handle_embedded_file_writeable(struct lws* wsi);
// Forward declarations for database functions
int store_event(cJSON* event);
@@ -105,80 +110,132 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
switch (reason) {
case LWS_CALLBACK_HTTP:
// Handle NIP-11 relay information requests (HTTP GET to root path)
// Handle HTTP requests
{
char *requested_uri = (char *)in;
log_info("HTTP request received");
// Check if this is an OPTIONS request
char method[16] = {0};
int method_len = lws_hdr_copy(wsi, method, sizeof(method) - 1, WSI_TOKEN_GET_URI);
if (method_len > 0) {
method[method_len] = '\0';
if (strcmp(method, "OPTIONS") == 0) {
// Handle OPTIONS request 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];
if (lws_add_http_header_status(wsi, HTTP_STATUS_OK, &p, end)) return -1;
if (lws_add_http_header_by_name(wsi, (unsigned char*)"access-control-allow-origin:", (unsigned char*)"*", 1, &p, end)) return -1;
if (lws_add_http_header_by_name(wsi, (unsigned char*)"access-control-allow-headers:", (unsigned char*)"content-type, accept", 20, &p, end)) return -1;
if (lws_add_http_header_by_name(wsi, (unsigned char*)"access-control-allow-methods:", (unsigned char*)"GET, OPTIONS", 12, &p, end)) return -1;
if (lws_add_http_header_by_name(wsi, (unsigned char*)"connection:", (unsigned char*)"close", 5, &p, end)) return -1;
if (lws_finalize_http_header(wsi, &p, end)) return -1;
if (lws_write(wsi, start, p - start, LWS_WRITE_HTTP_HEADERS) < 0) return -1;
return 0;
}
}
// Check if this is a GET request to the root path
if (strcmp(requested_uri, "/") == 0) {
// Get Accept header
char accept_header[256] = {0};
int header_len = lws_hdr_copy(wsi, accept_header, sizeof(accept_header) - 1, WSI_TOKEN_HTTP_ACCEPT);
if (header_len > 0) {
accept_header[header_len] = '\0';
// Handle NIP-11 request
if (handle_nip11_http_request(wsi, accept_header) == 0) {
return 0; // Successfully handled
// Check if this is a NIP-11 request
int is_nip11_request = (strstr(accept_header, "application/nostr+json") != NULL);
if (is_nip11_request) {
// Handle NIP-11 request
if (handle_nip11_http_request(wsi, accept_header) == 0) {
return 0; // Successfully handled
}
}
} else {
log_warning("HTTP request without Accept header");
}
// Return 404 for other requests
// Root path without NIP-11 Accept header - return 404
lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL);
return -1;
}
// Return 404 for non-root paths
// Check for embedded API files
if (handle_embedded_file_request(wsi, requested_uri) == 0) {
return 0; // Successfully handled
}
// Return 404 for other paths
lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL);
return -1;
}
case LWS_CALLBACK_HTTP_WRITEABLE:
// Handle NIP-11 HTTP body transmission with proper buffer management
// Handle HTTP body transmission for NIP-11 or embedded files
{
struct nip11_session_data* session_data = (struct nip11_session_data*)lws_wsi_user(wsi);
if (session_data && session_data->headers_sent && !session_data->body_sent) {
// Allocate buffer for JSON body transmission
unsigned char *json_buf = malloc(LWS_PRE + session_data->json_length);
if (!json_buf) {
log_error("Failed to allocate buffer for NIP-11 body transmission");
// Clean up session data
free(session_data->json_buffer);
free(session_data);
lws_set_wsi_user(wsi, NULL);
return -1;
void* user_data = lws_wsi_user(wsi);
char debug_msg[256];
snprintf(debug_msg, sizeof(debug_msg), "HTTP_WRITEABLE: user_data=%p", user_data);
log_info(debug_msg);
if (user_data) {
int type = *(int*)user_data;
if (type == 0) {
// NIP-11
struct nip11_session_data* session_data = (struct nip11_session_data*)user_data;
snprintf(debug_msg, sizeof(debug_msg), "NIP-11: session_data=%p, type=%d, json_length=%zu, headers_sent=%d, body_sent=%d",
session_data, session_data->type, session_data->json_length, session_data->headers_sent, session_data->body_sent);
log_info(debug_msg);
if (session_data->headers_sent && !session_data->body_sent) {
snprintf(debug_msg, sizeof(debug_msg), "NIP-11: Attempting to send body, json_length=%zu", session_data->json_length);
log_info(debug_msg);
// Allocate buffer for JSON body transmission (no LWS_PRE needed for body)
unsigned char *json_buf = malloc(session_data->json_length);
if (!json_buf) {
log_error("Failed to allocate buffer for NIP-11 body transmission");
// Clean up session data
free(session_data->json_buffer);
free(session_data);
lws_set_wsi_user(wsi, NULL);
return -1;
}
log_info("NIP-11: Buffer allocated successfully");
// Copy JSON data to buffer
memcpy(json_buf, session_data->json_buffer, session_data->json_length);
// Write JSON body
int write_result = lws_write(wsi, json_buf, session_data->json_length, LWS_WRITE_HTTP);
// Free the transmission buffer immediately (it's been copied by libwebsockets)
free(json_buf);
if (write_result < 0) {
log_error("Failed to write NIP-11 JSON body");
// Clean up session data
free(session_data->json_buffer);
free(session_data);
lws_set_wsi_user(wsi, NULL);
return -1;
}
// Mark body as sent and clean up session data
session_data->body_sent = 1;
free(session_data->json_buffer);
free(session_data);
lws_set_wsi_user(wsi, NULL);
log_success("NIP-11 relay information served successfully");
return 0; // Close connection after successful transmission
}
} else if (type == 1) {
// Embedded file
return handle_embedded_file_writeable(wsi);
}
// Copy JSON data to buffer
memcpy(json_buf + LWS_PRE, session_data->json_buffer, session_data->json_length);
// Write JSON body
int write_result = lws_write(wsi, json_buf + LWS_PRE, session_data->json_length, LWS_WRITE_HTTP);
// Free the transmission buffer immediately (it's been copied by libwebsockets)
free(json_buf);
if (write_result < 0) {
log_error("Failed to write NIP-11 JSON body");
// Clean up session data
free(session_data->json_buffer);
free(session_data);
lws_set_wsi_user(wsi, NULL);
return -1;
}
// Mark body as sent and clean up session data
session_data->body_sent = 1;
free(session_data->json_buffer);
free(session_data);
lws_set_wsi_user(wsi, NULL);
log_success("NIP-11 relay information served successfully");
return 0; // Close connection after successful transmission
}
}
break;

View File

@@ -34,6 +34,7 @@ struct per_session_data {
// 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;