v0.1.12 - Successfully integrated websocket client, and program connects to relays and publishes kinds 0 and 10002

This commit is contained in:
Your Name
2025-12-10 14:28:38 -04:00
parent deec021933
commit 6592c37c6e
21 changed files with 4277 additions and 1082 deletions

3
.gitmodules vendored
View File

@@ -1,6 +1,3 @@
[submodule "blossom"] [submodule "blossom"]
path = blossom path = blossom
url = ssh://git@git.laantungir.net:222/laantungir/blossom.git url = ssh://git@git.laantungir.net:222/laantungir/blossom.git
[submodule "nostr_core_lib"]
path = nostr_core_lib
url = ssh://git@git.laantungir.net:222/laantungir/nostr_core_lib.git

View File

@@ -1,16 +1,20 @@
# Ginxsom Blossom Server Makefile # Ginxsom Blossom Server Makefile
CC = gcc CC = gcc
CFLAGS = -Wall -Wextra -std=gnu99 -O2 -Inostr_core_lib/nostr_core -Inostr_core_lib/cjson $(shell pkg-config --cflags libwebsockets) CFLAGS = -Wall -Wextra -std=gnu99 -O2 -Inostr_core_lib/nostr_core -Inostr_core_lib/cjson
LIBS = -lfcgi -lsqlite3 nostr_core_lib/libnostr_core_x64.a -lz -ldl -lpthread -lm -L/usr/local/lib -lsecp256k1 -lssl -lcrypto -lcurl $(shell pkg-config --libs libwebsockets) LIBS = -lfcgi -lsqlite3 nostr_core_lib/libnostr_core_x64.a -lz -ldl -lpthread -lm -L/usr/local/lib -lsecp256k1 -lssl -lcrypto -lcurl
SRCDIR = src SRCDIR = src
BUILDDIR = build BUILDDIR = build
TARGET = $(BUILDDIR)/ginxsom-fcgi TARGET = $(BUILDDIR)/ginxsom-fcgi
# Source files # Source files
SOURCES = $(SRCDIR)/main.c $(SRCDIR)/admin_api.c $(SRCDIR)/admin_auth.c $(SRCDIR)/admin_event.c $(SRCDIR)/admin_websocket.c $(SRCDIR)/admin_handlers.c $(SRCDIR)/bud04.c $(SRCDIR)/bud06.c $(SRCDIR)/bud08.c $(SRCDIR)/bud09.c $(SRCDIR)/request_validator.c SOURCES = $(SRCDIR)/main.c $(SRCDIR)/admin_api.c $(SRCDIR)/admin_auth.c $(SRCDIR)/admin_event.c $(SRCDIR)/admin_handlers.c $(SRCDIR)/bud04.c $(SRCDIR)/bud06.c $(SRCDIR)/bud08.c $(SRCDIR)/bud09.c $(SRCDIR)/request_validator.c $(SRCDIR)/relay_client.c
OBJECTS = $(SOURCES:$(SRCDIR)/%.c=$(BUILDDIR)/%.o) OBJECTS = $(SOURCES:$(SRCDIR)/%.c=$(BUILDDIR)/%.o)
# Add core_relay_pool.c from nostr_core_lib
POOL_SRC = nostr_core_lib/nostr_core/core_relay_pool.c
POOL_OBJ = $(BUILDDIR)/core_relay_pool.o
# Default target # Default target
all: $(TARGET) all: $(TARGET)
@@ -22,9 +26,13 @@ $(BUILDDIR):
$(BUILDDIR)/%.o: $(SRCDIR)/%.c | $(BUILDDIR) $(BUILDDIR)/%.o: $(SRCDIR)/%.c | $(BUILDDIR)
$(CC) $(CFLAGS) -c $< -o $@ $(CC) $(CFLAGS) -c $< -o $@
# Compile core_relay_pool.o (needs src/ for request_validator.h)
$(POOL_OBJ): $(POOL_SRC) | $(BUILDDIR)
$(CC) $(CFLAGS) -I$(SRCDIR) -c $< -o $@
# Link final executable # Link final executable
$(TARGET): $(OBJECTS) $(TARGET): $(OBJECTS) $(POOL_OBJ)
$(CC) $(OBJECTS) $(LIBS) -o $@ $(CC) $(OBJECTS) $(POOL_OBJ) $(LIBS) -o $@
# Clean build files # Clean build files
clean: clean:

View File

@@ -41,7 +41,10 @@ INSERT OR IGNORE INTO config (key, value, description) VALUES
('admin_enabled', 'true', 'Whether admin API is enabled'), ('admin_enabled', 'true', 'Whether admin API is enabled'),
('nip42_require_auth', 'false', 'Enable NIP-42 challenge/response authentication'), ('nip42_require_auth', 'false', 'Enable NIP-42 challenge/response authentication'),
('nip42_challenge_timeout', '600', 'NIP-42 challenge timeout in seconds'), ('nip42_challenge_timeout', '600', 'NIP-42 challenge timeout in seconds'),
('nip42_time_tolerance', '300', 'NIP-42 timestamp tolerance in seconds'); ('nip42_time_tolerance', '300', 'NIP-42 timestamp tolerance in seconds'),
('enable_relay_connect', 'true', 'Enable Nostr relay client connections'),
('kind_0_content', '{"name":"Ginxsom Blossom Server","about":"A Blossom media server for storing and serving files on Nostr","picture":"","nip05":""}', 'Kind 0 profile metadata content (JSON)'),
('kind_10002_tags', '["wss://relay.laantungir.net"]', 'Kind 10002 relay list - JSON array of relay URLs');
-- Authentication rules table for whitelist/blacklist functionality -- Authentication rules table for whitelist/blacklist functionality
CREATE TABLE IF NOT EXISTS auth_rules ( CREATE TABLE IF NOT EXISTS auth_rules (

Binary file not shown.

BIN
build/core_relay_pool.o Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
build/relay_client.o Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -2,7 +2,8 @@
# Comprehensive Blossom Protocol Implementation # Comprehensive Blossom Protocol Implementation
# Main context - specify error log here to override system default # Main context - specify error log here to override system default
error_log logs/nginx/error.log info; # Set to warn level to capture FastCGI stderr messages
error_log logs/nginx/error.log warn;
pid logs/nginx/nginx.pid; pid logs/nginx/nginx.pid;
events { events {
@@ -219,25 +220,6 @@ http {
fastcgi_param HTTP_AUTHORIZATION $http_authorization; fastcgi_param HTTP_AUTHORIZATION $http_authorization;
} }
# WebSocket Admin endpoint (/admin) - Nostr Kind 23456/23457 events
location /admin {
proxy_pass http://127.0.0.1:9442;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket timeouts
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
# Disable buffering for WebSocket
proxy_buffering off;
}
# Admin API endpoints (/api/*) # Admin API endpoints (/api/*)
location /api/ { location /api/ {
if ($request_method !~ ^(GET|PUT|POST)$) { if ($request_method !~ ^(GET|PUT|POST)$) {
@@ -589,25 +571,6 @@ http {
fastcgi_param HTTP_AUTHORIZATION $http_authorization; fastcgi_param HTTP_AUTHORIZATION $http_authorization;
} }
# WebSocket Admin endpoint (/admin) - Nostr Kind 23456/23457 events
location /admin {
proxy_pass http://127.0.0.1:9442;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket timeouts
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
# Disable buffering for WebSocket
proxy_buffering off;
}
# Admin API endpoints (/api/*) # Admin API endpoints (/api/*)
location /api/ { location /api/ {
if ($request_method !~ ^(GET|PUT|POST)$) { if ($request_method !~ ^(GET|PUT|POST)$) {

1639
debug.log Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Submodule nostr_core_lib deleted from 7d7c3eafe8

View File

@@ -1,541 +0,0 @@
/*
* Ginxsom Admin WebSocket Server
* Handles WebSocket connections for Kind 23456/23457 admin commands
* Based on c-relay's WebSocket implementation using libwebsockets
*/
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <cjson/cJSON.h>
#include <sqlite3.h>
#include <libwebsockets.h>
#include "ginxsom.h"
// Forward declarations from admin_event.c
extern char g_db_path[];
extern int nostr_hex_to_bytes(const char* hex, unsigned char* bytes, size_t bytes_len);
extern int nostr_nip44_decrypt(const unsigned char* recipient_private_key,
const unsigned char* sender_public_key,
const char* encrypted_data,
char* output,
size_t output_size);
extern int nostr_nip44_encrypt(const unsigned char* sender_private_key,
const unsigned char* recipient_public_key,
const char* plaintext,
char* output,
size_t output_size);
extern cJSON* nostr_create_and_sign_event(int kind, const char* content, cJSON* tags,
const unsigned char* private_key, time_t created_at);
// Per-session data for each WebSocket connection
struct per_session_data {
char admin_pubkey[65];
int authenticated;
unsigned char pending_response[LWS_PRE + 131072];
size_t pending_response_len;
};
// Global WebSocket context
static struct lws_context *ws_context = NULL;
static volatile int force_exit = 0;
// Function prototypes
static int get_server_privkey(unsigned char* privkey_bytes);
static int get_server_pubkey(char* pubkey_hex, size_t size);
static int handle_config_query_command(cJSON* response_data);
static int process_admin_event(struct lws *wsi, struct per_session_data *pss, const char *json_str);
/**
* WebSocket protocol callback
*/
static int callback_admin_protocol(struct lws *wsi, enum lws_callback_reasons reason,
void *user, void *in, size_t len) {
struct per_session_data *pss = (struct per_session_data *)user;
switch (reason) {
case LWS_CALLBACK_ESTABLISHED:
fprintf(stderr, "[WebSocket] New connection established\n");
fflush(stderr);
memset(pss, 0, sizeof(*pss));
pss->authenticated = 0;
break;
case LWS_CALLBACK_RECEIVE:
fprintf(stderr, "[WebSocket] Received %zu bytes\n", len);
fflush(stderr);
// Null-terminate the received data
char *json_str = malloc(len + 1);
if (!json_str) {
fprintf(stderr, "[WebSocket] Memory allocation failed\n");
fflush(stderr);
return -1;
}
memcpy(json_str, in, len);
json_str[len] = '\0';
// Process the admin event
int result = process_admin_event(wsi, pss, json_str);
free(json_str);
if (result == 0 && pss->pending_response_len > 0) {
// Request callback to send response
lws_callback_on_writable(wsi);
}
break;
case LWS_CALLBACK_SERVER_WRITEABLE:
if (pss->pending_response_len > 0) {
fprintf(stderr, "[WebSocket] Sending %zu bytes\n", pss->pending_response_len - LWS_PRE);
fflush(stderr);
int written = lws_write(wsi,
&pss->pending_response[LWS_PRE],
pss->pending_response_len - LWS_PRE,
LWS_WRITE_TEXT);
if (written < 0) {
fprintf(stderr, "[WebSocket] Write failed\n");
fflush(stderr);
return -1;
}
pss->pending_response_len = 0;
}
break;
case LWS_CALLBACK_CLOSED:
fprintf(stderr, "[WebSocket] Connection closed\n");
fflush(stderr);
break;
default:
break;
}
return 0;
}
/**
* WebSocket protocols
*/
static struct lws_protocols protocols[] = {
{
"nostr-admin",
callback_admin_protocol,
sizeof(struct per_session_data),
131072, // rx buffer size
0, NULL, 0
},
{ NULL, NULL, 0, 0, 0, NULL, 0 } // terminator
};
/**
* Process Kind 23456 admin event received via WebSocket
*/
static int process_admin_event(struct lws *wsi __attribute__((unused)), struct per_session_data *pss, const char *json_str) {
// Parse event JSON
cJSON *event = cJSON_Parse(json_str);
if (!event) {
fprintf(stderr, "[WebSocket] Invalid JSON\n");
fflush(stderr);
return -1;
}
// Verify it's Kind 23456
cJSON *kind_obj = cJSON_GetObjectItem(event, "kind");
if (!kind_obj || !cJSON_IsNumber(kind_obj) ||
(int)cJSON_GetNumberValue(kind_obj) != 23456) {
fprintf(stderr, "[WebSocket] Not a Kind 23456 event\n");
fflush(stderr);
cJSON_Delete(event);
return -1;
}
// Get event ID for response correlation
cJSON *id_obj = cJSON_GetObjectItem(event, "id");
if (!id_obj || !cJSON_IsString(id_obj)) {
fprintf(stderr, "[WebSocket] Event missing id\n");
fflush(stderr);
cJSON_Delete(event);
return -1;
}
const char *request_id = cJSON_GetStringValue(id_obj);
// Get admin pubkey from event
cJSON *pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
if (!pubkey_obj || !cJSON_IsString(pubkey_obj)) {
fprintf(stderr, "[WebSocket] Event missing pubkey\n");
fflush(stderr);
cJSON_Delete(event);
return -1;
}
const char *admin_pubkey = cJSON_GetStringValue(pubkey_obj);
// Verify admin pubkey
if (!verify_admin_pubkey(admin_pubkey)) {
fprintf(stderr, "[WebSocket] Not authorized as admin: %s\n", admin_pubkey);
fflush(stderr);
cJSON_Delete(event);
return -1;
}
// Store admin pubkey in session
strncpy(pss->admin_pubkey, admin_pubkey, sizeof(pss->admin_pubkey) - 1);
pss->authenticated = 1;
// Get encrypted content
cJSON *content_obj = cJSON_GetObjectItem(event, "content");
if (!content_obj || !cJSON_IsString(content_obj)) {
fprintf(stderr, "[WebSocket] Event missing content\n");
fflush(stderr);
cJSON_Delete(event);
return -1;
}
const char *encrypted_content = cJSON_GetStringValue(content_obj);
// Get server private key for decryption
unsigned char server_privkey[32];
if (get_server_privkey(server_privkey) != 0) {
fprintf(stderr, "[WebSocket] Failed to get server private key\n");
fflush(stderr);
cJSON_Delete(event);
return -1;
}
// Convert admin pubkey to bytes
unsigned char admin_pubkey_bytes[32];
if (nostr_hex_to_bytes(admin_pubkey, admin_pubkey_bytes, 32) != 0) {
fprintf(stderr, "[WebSocket] Invalid admin pubkey format\n");
fflush(stderr);
cJSON_Delete(event);
return -1;
}
// Decrypt content using NIP-44
char decrypted_content[8192];
const char *content_to_parse = encrypted_content;
// Check if content is already plaintext JSON (starts with '[')
if (encrypted_content[0] != '[') {
int decrypt_result = nostr_nip44_decrypt(
server_privkey,
admin_pubkey_bytes,
encrypted_content,
decrypted_content,
sizeof(decrypted_content)
);
if (decrypt_result != 0) {
fprintf(stderr, "[WebSocket] Failed to decrypt content\n");
fflush(stderr);
cJSON_Delete(event);
return -1;
}
content_to_parse = decrypted_content;
}
// Parse command array
cJSON *command_array = cJSON_Parse(content_to_parse);
if (!command_array || !cJSON_IsArray(command_array)) {
fprintf(stderr, "[WebSocket] Decrypted content is not a valid command array\n");
fflush(stderr);
cJSON_Delete(event);
return -1;
}
// Get command type
cJSON *command_type = cJSON_GetArrayItem(command_array, 0);
if (!command_type || !cJSON_IsString(command_type)) {
fprintf(stderr, "[WebSocket] Invalid command format\n");
fflush(stderr);
cJSON_Delete(command_array);
cJSON_Delete(event);
return -1;
}
const char *cmd = cJSON_GetStringValue(command_type);
fprintf(stderr, "[WebSocket] Processing command: %s\n", cmd);
fflush(stderr);
// Create response data object
cJSON *response_data = cJSON_CreateObject();
cJSON_AddStringToObject(response_data, "query_type", cmd);
cJSON_AddNumberToObject(response_data, "timestamp", (double)time(NULL));
// Handle command
int result = -1;
if (strcmp(cmd, "config_query") == 0) {
result = handle_config_query_command(response_data);
} else {
cJSON_AddStringToObject(response_data, "status", "error");
cJSON_AddStringToObject(response_data, "error", "Unknown command");
}
cJSON_Delete(command_array);
cJSON_Delete(event);
if (result == 0) {
// Get server keys
char server_pubkey[65];
if (get_server_pubkey(server_pubkey, sizeof(server_pubkey)) != 0) {
fprintf(stderr, "[WebSocket] Failed to get server pubkey\n");
fflush(stderr);
cJSON_Delete(response_data);
return -1;
}
// Convert response data to JSON string
char *response_json = cJSON_PrintUnformatted(response_data);
cJSON_Delete(response_data);
if (!response_json) {
fprintf(stderr, "[WebSocket] Failed to serialize response\n");
fflush(stderr);
return -1;
}
// Encrypt response using NIP-44
char encrypted_response[131072];
int encrypt_result = nostr_nip44_encrypt(
server_privkey,
admin_pubkey_bytes,
response_json,
encrypted_response,
sizeof(encrypted_response)
);
free(response_json);
if (encrypt_result != 0) {
fprintf(stderr, "[WebSocket] Failed to encrypt response\n");
fflush(stderr);
return -1;
}
// Create Kind 23457 response event
cJSON *tags = cJSON_CreateArray();
// p tag for admin
cJSON *p_tag = cJSON_CreateArray();
cJSON_AddItemToArray(p_tag, cJSON_CreateString("p"));
cJSON_AddItemToArray(p_tag, cJSON_CreateString(admin_pubkey));
cJSON_AddItemToArray(tags, p_tag);
// e tag for request correlation
cJSON *e_tag = cJSON_CreateArray();
cJSON_AddItemToArray(e_tag, cJSON_CreateString("e"));
cJSON_AddItemToArray(e_tag, cJSON_CreateString(request_id));
cJSON_AddItemToArray(tags, e_tag);
// Sign the event
cJSON *signed_event = nostr_create_and_sign_event(
23457,
encrypted_response,
tags,
server_privkey,
time(NULL)
);
if (!signed_event) {
fprintf(stderr, "[WebSocket] Failed to sign response event\n");
fflush(stderr);
return -1;
}
// Serialize event to JSON
char *event_json = cJSON_PrintUnformatted(signed_event);
cJSON_Delete(signed_event);
if (!event_json) {
fprintf(stderr, "[WebSocket] Failed to serialize event\n");
fflush(stderr);
return -1;
}
// Store response in session for sending
size_t json_len = strlen(event_json);
if (json_len + LWS_PRE < sizeof(pss->pending_response)) {
memcpy(&pss->pending_response[LWS_PRE], event_json, json_len);
pss->pending_response_len = LWS_PRE + json_len;
fprintf(stderr, "[WebSocket] Response prepared (%zu bytes)\n", json_len);
fflush(stderr);
} else {
fprintf(stderr, "[WebSocket] Response too large\n");
fflush(stderr);
}
free(event_json);
return 0;
} else {
cJSON_Delete(response_data);
return -1;
}
}
/**
* Get server private key from database
*/
static int get_server_privkey(unsigned char* privkey_bytes) {
sqlite3 *db;
int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc != SQLITE_OK) {
return -1;
}
sqlite3_stmt *stmt;
const char *sql = "SELECT seckey FROM blossom_seckey LIMIT 1";
int result = -1;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
const char *privkey_hex = (const char*)sqlite3_column_text(stmt, 0);
if (privkey_hex && nostr_hex_to_bytes(privkey_hex, privkey_bytes, 32) == 0) {
result = 0;
}
}
sqlite3_finalize(stmt);
}
sqlite3_close(db);
return result;
}
/**
* Get server public key from database
*/
static int get_server_pubkey(char* pubkey_hex, size_t size) {
sqlite3 *db;
int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc != SQLITE_OK) {
return -1;
}
sqlite3_stmt *stmt;
const char *sql = "SELECT value FROM config WHERE key = 'blossom_pubkey'";
int result = -1;
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
const char *pubkey = (const char*)sqlite3_column_text(stmt, 0);
if (pubkey) {
strncpy(pubkey_hex, pubkey, size - 1);
pubkey_hex[size - 1] = '\0';
result = 0;
}
}
sqlite3_finalize(stmt);
}
sqlite3_close(db);
return result;
}
/**
* Handle config_query command
*/
static int handle_config_query_command(cJSON* response_data) {
sqlite3 *db;
int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc != SQLITE_OK) {
cJSON_AddStringToObject(response_data, "status", "error");
cJSON_AddStringToObject(response_data, "error", "Database error");
return -1;
}
cJSON_AddStringToObject(response_data, "status", "success");
cJSON *data = cJSON_CreateObject();
// Query all config settings
sqlite3_stmt *stmt;
const char *sql = "SELECT key, value FROM config ORDER BY key";
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
while (sqlite3_step(stmt) == SQLITE_ROW) {
const char *key = (const char*)sqlite3_column_text(stmt, 0);
const char *value = (const char*)sqlite3_column_text(stmt, 1);
if (key && value) {
cJSON_AddStringToObject(data, key, value);
}
}
sqlite3_finalize(stmt);
}
cJSON_AddItemToObject(response_data, "data", data);
sqlite3_close(db);
return 0;
}
/**
* WebSocket server thread
*/
void* admin_websocket_thread(void* arg) {
int port = *(int*)arg;
struct lws_context_creation_info info;
memset(&info, 0, sizeof(info));
info.port = port;
info.iface = "127.0.0.1"; // Force IPv4 binding for localhost compatibility
info.protocols = protocols;
info.gid = -1;
info.uid = -1;
info.options = LWS_SERVER_OPTION_VALIDATE_UTF8 | LWS_SERVER_OPTION_DISABLE_IPV6;
fprintf(stderr, "[WebSocket] Starting admin WebSocket server on 127.0.0.1:%d (IPv4 only)\n", port);
fflush(stderr);
ws_context = lws_create_context(&info);
if (!ws_context) {
fprintf(stderr, "[WebSocket] Failed to create context\n");
fflush(stderr);
return NULL;
}
fprintf(stderr, "[WebSocket] Server started successfully\n");
fflush(stderr);
// Service loop
while (!force_exit) {
lws_service(ws_context, 50);
}
lws_context_destroy(ws_context);
fprintf(stderr, "[WebSocket] Server stopped\n");
fflush(stderr);
return NULL;
}
/**
* Start admin WebSocket server
*/
int start_admin_websocket_server(int port) {
static int server_port;
server_port = port;
pthread_t thread;
int result = pthread_create(&thread, NULL, admin_websocket_thread, &server_port);
if (result != 0) {
fprintf(stderr, "[WebSocket] Failed to create thread: %d\n", result);
fflush(stderr);
return -1;
}
pthread_detach(thread);
fprintf(stderr, "[WebSocket] Thread started\n");
fflush(stderr);
return 0;
}
/**
* Stop admin WebSocket server
*/
void stop_admin_websocket_server(void) {
force_exit = 1;
}

View File

@@ -10,8 +10,8 @@
// Version information (auto-updated by build system) // Version information (auto-updated by build system)
#define VERSION_MAJOR 0 #define VERSION_MAJOR 0
#define VERSION_MINOR 1 #define VERSION_MINOR 1
#define VERSION_PATCH 11 #define VERSION_PATCH 12
#define VERSION "v0.1.11" #define VERSION "v0.1.12"
#include <stddef.h> #include <stddef.h>
#include <stdint.h> #include <stdint.h>
@@ -250,6 +250,16 @@ void send_json_response(int status_code, const char* json_content);
// Logging utilities // Logging utilities
void log_request(const char* method, const char* uri, const char* auth_status, int status_code); void log_request(const char* method, const char* uri, const char* auth_status, int status_code);
// Centralized application logging (writes to logs/app/app.log)
typedef enum {
LOG_DEBUG = 0,
LOG_INFO = 1,
LOG_WARN = 2,
LOG_ERROR = 3
} log_level_t;
void app_log(log_level_t level, const char* format, ...);
// SHA-256 validation helper (used by multiple BUDs) // SHA-256 validation helper (used by multiple BUDs)
int validate_sha256_format(const char* sha256); int validate_sha256_format(const char* sha256);
@@ -283,10 +293,6 @@ void send_json_response(int status, const char* json_content);
void send_json_error(int status, const char* error, const char* message); void send_json_error(int status, const char* error, const char* message);
int parse_query_params(const char* query_string, char params[][256], int max_params); int parse_query_params(const char* query_string, char params[][256], int max_params);
// Admin WebSocket server functions
int start_admin_websocket_server(int port);
void stop_admin_websocket_server(void);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

View File

@@ -5,11 +5,13 @@
#define _GNU_SOURCE #define _GNU_SOURCE
#include "ginxsom.h" #include "ginxsom.h"
#include "relay_client.h"
#include "../nostr_core_lib/nostr_core/nostr_common.h" #include "../nostr_core_lib/nostr_core/nostr_common.h"
#include "../nostr_core_lib/nostr_core/utils.h" #include "../nostr_core_lib/nostr_core/utils.h"
#include <getopt.h> #include <getopt.h>
#include <curl/curl.h> #include <curl/curl.h>
#include <sqlite3.h> #include <sqlite3.h>
#include <stdarg.h>
#include <stdint.h> #include <stdint.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
@@ -19,7 +21,43 @@
#include <time.h> #include <time.h>
#include <unistd.h> #include <unistd.h>
// Debug macros removed // Centralized logging system (declaration in ginxsom.h)
void app_log(log_level_t level, const char *format, ...) {
FILE *log_file = fopen("logs/app/app.log", "a");
if (!log_file) {
return; // Silently fail if we can't open log file
}
// Get timestamp
time_t now = time(NULL);
struct tm *tm_info = localtime(&now);
char timestamp[64];
strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", tm_info);
// Get log level string
const char *level_str;
switch (level) {
case LOG_DEBUG: level_str = "DEBUG"; break;
case LOG_INFO: level_str = "INFO"; break;
case LOG_WARN: level_str = "WARN"; break;
case LOG_ERROR: level_str = "ERROR"; break;
default: level_str = "UNKNOWN"; break;
}
// Write log prefix with timestamp, PID, and level
fprintf(log_file, "[%s] [PID:%d] [%s] ", timestamp, getpid(), level_str);
// Write formatted message
va_list args;
va_start(args, format);
vfprintf(log_file, format, args);
va_end(args);
// Ensure newline
fprintf(log_file, "\n");
fclose(log_file);
}
#define MAX_SHA256_LEN 65 #define MAX_SHA256_LEN 65
#define MAX_PATH_LEN 4096 #define MAX_PATH_LEN 4096
@@ -196,7 +234,10 @@ int initialize_database(const char *db_path) {
" ('admin_enabled', 'true', 'Whether admin API is enabled')," " ('admin_enabled', 'true', 'Whether admin API is enabled'),"
" ('nip42_require_auth', 'false', 'Enable NIP-42 challenge/response authentication')," " ('nip42_require_auth', 'false', 'Enable NIP-42 challenge/response authentication'),"
" ('nip42_challenge_timeout', '600', 'NIP-42 challenge timeout in seconds')," " ('nip42_challenge_timeout', '600', 'NIP-42 challenge timeout in seconds'),"
" ('nip42_time_tolerance', '300', 'NIP-42 timestamp tolerance in seconds');"; " ('nip42_time_tolerance', '300', 'NIP-42 timestamp tolerance in seconds'),"
" ('enable_relay_connect', 'true', 'Enable connection to Nostr relays'),"
" ('kind_0_content', '{\"name\":\"Ginxsom Blossom Server\",\"about\":\"A Nostr-enabled Blossom media server\",\"picture\":\"\"}', 'JSON content for Kind 0 profile event'),"
" ('kind_10002_tags', '[\"wss://relay.laantungir.net\"]', 'JSON array of relay URLs for Kind 10002');";
rc = sqlite3_exec(db, insert_config, NULL, NULL, &err_msg); rc = sqlite3_exec(db, insert_config, NULL, NULL, &err_msg);
if (rc != SQLITE_OK) { if (rc != SQLITE_OK) {
@@ -1830,18 +1871,9 @@ void handle_auth_challenge_request(void) {
///////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
// Redirect stderr to log file BEFORE any other operations // Initialize application logging
// This is necessary because spawn-fcgi doesn't preserve stderr redirections app_log(LOG_INFO, "=== Ginxsom FastCGI Application Starting ===");
FILE *stderr_log = freopen("logs/app/stderr.log", "a", stderr); app_log(LOG_INFO, "Process ID: %d", getpid());
if (!stderr_log) {
// If redirection fails, continue anyway but log to original stderr
perror("Warning: Failed to redirect stderr to log file");
}
// Set stderr to unbuffered mode so all fprintf(stderr, ...) calls flush immediately
setvbuf(stderr, NULL, _IONBF, 0);
fprintf(stderr, "DEBUG: main() started\n");
// Parse command line arguments // Parse command line arguments
int use_test_keys = 0; int use_test_keys = 0;
@@ -1901,17 +1933,16 @@ int main(int argc, char *argv[]) {
} }
} }
fprintf(stderr, "STARTUP: Using storage directory: %s\n", g_storage_dir); app_log(LOG_INFO, "Storage directory: %s", g_storage_dir);
// CRITICAL: Initialize nostr crypto system BEFORE key operations // CRITICAL: Initialize nostr crypto system BEFORE key operations
fprintf(stderr, "STARTUP: Initializing nostr crypto system...\r\n"); app_log(LOG_INFO, "Initializing nostr crypto system...");
int crypto_init_result = nostr_crypto_init(); int crypto_init_result = nostr_crypto_init();
fprintf(stderr, "CRYPTO INIT RESULT: %d\r\n", crypto_init_result);
if (crypto_init_result != 0) { if (crypto_init_result != 0) {
fprintf(stderr, "FATAL ERROR: Failed to initialize nostr crypto system\r\n"); app_log(LOG_ERROR, "Failed to initialize nostr crypto system (result: %d)", crypto_init_result);
return 1; return 1;
} }
fprintf(stderr, "STARTUP: nostr crypto system initialized successfully\r\n"); app_log(LOG_INFO, "Nostr crypto system initialized successfully");
// ======================================================================== // ========================================================================
// DATABASE AND KEY INITIALIZATION - 5 SCENARIOS // DATABASE AND KEY INITIALIZATION - 5 SCENARIOS
@@ -1919,12 +1950,12 @@ int main(int argc, char *argv[]) {
// Scenario 4: Test Mode (--test-keys) // Scenario 4: Test Mode (--test-keys)
if (use_test_keys) { if (use_test_keys) {
fprintf(stderr, "\n=== SCENARIO 4: TEST MODE ===\n"); app_log(LOG_INFO, "=== SCENARIO 4: TEST MODE ===");
// Load test keys from .test_keys file // Load test keys from .test_keys file
FILE *keys_file = fopen(".test_keys", "r"); FILE *keys_file = fopen(".test_keys", "r");
if (!keys_file) { if (!keys_file) {
fprintf(stderr, "ERROR: Cannot open .test_keys file\n"); app_log(LOG_ERROR, "Cannot open .test_keys file");
return 1; return 1;
} }
@@ -1944,38 +1975,38 @@ int main(int argc, char *argv[]) {
if (end && (end - start) == 64) { if (end && (end - start) == 64) {
strncpy(test_server_privkey, start, 64); strncpy(test_server_privkey, start, 64);
test_server_privkey[64] = '\0'; test_server_privkey[64] = '\0';
fprintf(stderr, "TEST MODE: Parsed SERVER_PRIVKEY: %s\n", test_server_privkey); app_log(LOG_DEBUG, "Parsed SERVER_PRIVKEY from .test_keys");
} else { } else {
fprintf(stderr, "TEST MODE: Failed to parse SERVER_PRIVKEY (length: %ld)\n", end ? (long)(end - start) : -1L); app_log(LOG_ERROR, "Failed to parse SERVER_PRIVKEY (length: %ld)", end ? (long)(end - start) : -1L);
} }
} }
} }
fclose(keys_file); fclose(keys_file);
fprintf(stderr, "TEST MODE: Loaded keys from .test_keys\n"); app_log(LOG_INFO, "Loaded keys from .test_keys");
fprintf(stderr, "TEST MODE: Admin pubkey: %s\n", g_admin_pubkey); app_log(LOG_INFO, "Admin pubkey: %s", g_admin_pubkey);
// Derive pubkey from test privkey // Derive pubkey from test privkey
if (derive_pubkey_from_privkey(test_server_privkey, g_blossom_pubkey) != 0) { if (derive_pubkey_from_privkey(test_server_privkey, g_blossom_pubkey) != 0) {
fprintf(stderr, "ERROR: Failed to derive pubkey from test privkey\n"); app_log(LOG_ERROR, "Failed to derive pubkey from test privkey");
return 1; return 1;
} }
fprintf(stderr, "TEST MODE: Server pubkey: %s\n", g_blossom_pubkey); app_log(LOG_INFO, "Server pubkey: %s", g_blossom_pubkey);
// Set database path based on test pubkey // Set database path based on test pubkey
if (set_db_path_from_pubkey(g_blossom_pubkey) != 0) { if (set_db_path_from_pubkey(g_blossom_pubkey) != 0) {
fprintf(stderr, "ERROR: Failed to set database path\n"); app_log(LOG_ERROR, "Failed to set database path");
return 1; return 1;
} }
// Test mode ALWAYS overwrites database for clean testing // Test mode ALWAYS overwrites database for clean testing
fprintf(stderr, "TEST MODE: Creating/overwriting database: %s\n", g_db_path); app_log(LOG_INFO, "Creating/overwriting test database: %s", g_db_path);
unlink(g_db_path); // Remove if exists unlink(g_db_path); // Remove if exists
// Initialize new database // Initialize new database
if (initialize_database(g_db_path) != 0) { if (initialize_database(g_db_path) != 0) {
fprintf(stderr, "ERROR: Failed to initialize test database\n"); app_log(LOG_ERROR, "Failed to initialize test database");
return 1; return 1;
} }
@@ -1984,7 +2015,7 @@ int main(int argc, char *argv[]) {
g_blossom_seckey[64] = '\0'; g_blossom_seckey[64] = '\0';
if (store_blossom_private_key(test_server_privkey) != 0) { if (store_blossom_private_key(test_server_privkey) != 0) {
fprintf(stderr, "ERROR: Failed to store test private key\n"); app_log(LOG_ERROR, "Failed to store test private key");
return 1; return 1;
} }
@@ -2016,12 +2047,12 @@ int main(int argc, char *argv[]) {
sqlite3_close(db); sqlite3_close(db);
} }
fprintf(stderr, "TEST MODE: Database initialized successfully\n"); app_log(LOG_INFO, "Test database initialized successfully");
} }
// Scenario 3: Keys Specified (--server-privkey) // Scenario 3: Keys Specified (--server-privkey)
else if (test_server_privkey[0] != '\0') { else if (test_server_privkey[0] != '\0') {
fprintf(stderr, "\n=== SCENARIO 3: KEYS SPECIFIED ===\n"); app_log(LOG_INFO, "=== SCENARIO 3: KEYS SPECIFIED ===");
// Derive pubkey from provided privkey // Derive pubkey from provided privkey
if (derive_pubkey_from_privkey(test_server_privkey, g_blossom_pubkey) != 0) { if (derive_pubkey_from_privkey(test_server_privkey, g_blossom_pubkey) != 0) {
@@ -2171,21 +2202,18 @@ int main(int argc, char *argv[]) {
// END DATABASE AND KEY INITIALIZATION // END DATABASE AND KEY INITIALIZATION
// ======================================================================== // ========================================================================
fprintf(stderr, "\n=== FINAL CONFIGURATION ===\n"); app_log(LOG_INFO, "=== FINAL CONFIGURATION ===");
fprintf(stderr, "Database path: %s\n", g_db_path); app_log(LOG_INFO, "Database path: %s", g_db_path);
fprintf(stderr, "Storage directory: %s\n", g_storage_dir); app_log(LOG_INFO, "Storage directory: %s", g_storage_dir);
fprintf(stderr, "Server pubkey: %s\n", g_blossom_pubkey); app_log(LOG_INFO, "Server pubkey: %s", g_blossom_pubkey);
if (strlen(g_admin_pubkey) > 0) { if (strlen(g_admin_pubkey) > 0) {
fprintf(stderr, "Admin pubkey: %s\n", g_admin_pubkey); app_log(LOG_INFO, "Admin pubkey: %s", g_admin_pubkey);
} }
fprintf(stderr, "===========================\n\n"); app_log(LOG_INFO, "===========================");
fflush(stderr);
// If --generate-keys was specified, exit after key generation // If --generate-keys was specified, exit after key generation
if (g_generate_keys) { if (g_generate_keys) {
fprintf(stderr, "Key generation completed, exiting.\n"); app_log(LOG_INFO, "Key generation completed, exiting");
fflush(stderr);
return 0; return 0;
} }
@@ -2206,51 +2234,48 @@ if (!config_loaded /* && !initialize_server_config() */) {
} }
// Initialize request validator system // Initialize request validator system
fprintf(stderr, "STARTUP: Initializing request validator system...\r\n"); app_log(LOG_INFO, "Initializing request validator system...");
int validator_init_result = int validator_init_result =
ginxsom_request_validator_init(g_db_path, "ginxsom"); ginxsom_request_validator_init(g_db_path, "ginxsom");
fprintf(stderr, "MAIN: validator init return code: %d\r\n",
validator_init_result);
if (validator_init_result != NOSTR_SUCCESS) { if (validator_init_result != NOSTR_SUCCESS) {
fprintf(stderr, app_log(LOG_ERROR, "Failed to initialize request validator system (result: %d)", validator_init_result);
"FATAL ERROR: Failed to initialize request validator system\r\n");
return 1; return 1;
} }
fprintf(stderr, app_log(LOG_INFO, "Request validator system initialized successfully");
"STARTUP: Request validator system initialized successfully\r\n");
fflush(stderr);
// Start WebSocket admin server if enabled // Initialize relay client system
sqlite3 *db; app_log(LOG_INFO, "Initializing relay client system...");
sqlite3_stmt *stmt; int relay_init_result = relay_client_init(g_db_path);
int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL); if (relay_init_result != 0) {
if (rc == SQLITE_OK) { app_log(LOG_WARN, "Failed to initialize relay client system (result: %d)", relay_init_result);
const char *sql = "SELECT value FROM config WHERE key = 'admin_enabled'"; app_log(LOG_WARN, "Continuing without relay client functionality");
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
const char *admin_enabled = (const char *)sqlite3_column_text(stmt, 0);
if (admin_enabled && (strcmp(admin_enabled, "true") == 0 || strcmp(admin_enabled, "1") == 0)) {
fprintf(stderr, "STARTUP: Starting WebSocket admin server on port 9442...\n");
if (start_admin_websocket_server(9442) == 0) {
fprintf(stderr, "STARTUP: WebSocket admin server started successfully\n");
} else { } else {
fprintf(stderr, "WARNING: Failed to start WebSocket admin server\n"); app_log(LOG_INFO, "Relay client system initialized successfully");
}
// Start relay connections (this will check enable_relay_connect config)
app_log(LOG_INFO, "Starting relay client connections...");
int relay_start_result = relay_client_start();
if (relay_start_result != 0) {
app_log(LOG_WARN, "Failed to start relay client (result: %d)", relay_start_result);
app_log(LOG_WARN, "Relay client disabled - check configuration");
} else { } else {
fprintf(stderr, "STARTUP: Admin interface disabled in config\n"); app_log(LOG_INFO, "Relay client started successfully");
} }
} }
sqlite3_finalize(stmt);
}
sqlite3_close(db);
}
///////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////
// THIS IS WHERE THE REQUESTS ENTER THE FastCGI // THIS IS WHERE THE REQUESTS ENTER THE FastCGI
///////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////
app_log(LOG_INFO, "FastCGI request loop starting - ready to accept requests");
int first_request = 1;
while (FCGI_Accept() >= 0) { while (FCGI_Accept() >= 0) {
// Test stderr capture on first request
if (first_request) {
fprintf(stderr, "FCGI: First request received - testing nginx stderr capture\n");
fflush(stderr);
first_request = 0;
}
const char *request_method = getenv("REQUEST_METHOD"); const char *request_method = getenv("REQUEST_METHOD");
const char *request_uri = getenv("REQUEST_URI"); const char *request_uri = getenv("REQUEST_URI");
const char *auth_header = getenv("HTTP_AUTHORIZATION"); const char *auth_header = getenv("HTTP_AUTHORIZATION");

784
src/relay_client.c Normal file
View File

@@ -0,0 +1,784 @@
/*
* Ginxsom Relay Client Implementation
*
* Manages connections to Nostr relays, publishes events, and subscribes to admin commands.
*/
#include "relay_client.h"
#include "../nostr_core_lib/nostr_core/nostr_core.h"
#include <sqlite3.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <time.h>
// Forward declare app_log to avoid including ginxsom.h (which has typedef conflicts)
typedef enum {
LOG_DEBUG = 0,
LOG_INFO = 1,
LOG_WARN = 2,
LOG_ERROR = 3
} log_level_t;
void app_log(log_level_t level, const char* format, ...);
// Maximum number of relays to connect to
#define MAX_RELAYS 10
// Reconnection settings
#define RECONNECT_DELAY_SECONDS 30
#define MAX_RECONNECT_ATTEMPTS 5
// Global state
static struct {
int enabled;
int initialized;
int running;
char db_path[512];
nostr_relay_pool_t* pool;
char** relay_urls;
int relay_count;
nostr_pool_subscription_t* admin_subscription;
pthread_t management_thread;
pthread_mutex_t state_mutex;
} g_relay_state = {0};
// External globals from main.c
extern char g_blossom_seckey[65];
extern char g_blossom_pubkey[65];
extern char g_admin_pubkey[65];
// Forward declarations
static void *relay_management_thread(void *arg);
static int load_config_from_db(void);
static int parse_relay_urls(const char *json_array);
static int subscribe_to_admin_commands(void);
static void on_publish_response(const char* relay_url, const char* event_id, int success, const char* message, void* user_data);
static void on_admin_command_event(cJSON* event, const char* relay_url, void* user_data);
static void on_admin_subscription_eose(cJSON** events, int event_count, void* user_data);
// Initialize relay client system
int relay_client_init(const char *db_path) {
if (g_relay_state.initialized) {
app_log(LOG_WARN, "Relay client already initialized");
return 0;
}
app_log(LOG_INFO, "Initializing relay client system...");
// Store database path
strncpy(g_relay_state.db_path, db_path, sizeof(g_relay_state.db_path) - 1);
// Initialize mutex
if (pthread_mutex_init(&g_relay_state.state_mutex, NULL) != 0) {
app_log(LOG_ERROR, "Failed to initialize relay state mutex");
return -1;
}
// Load configuration from database
if (load_config_from_db() != 0) {
app_log(LOG_ERROR, "Failed to load relay configuration from database");
pthread_mutex_destroy(&g_relay_state.state_mutex);
return -1;
}
// Create relay pool if enabled
if (g_relay_state.enabled) {
// Use default reconnection config (don't free - it's a static structure)
nostr_pool_reconnect_config_t* config = nostr_pool_reconnect_config_default();
g_relay_state.pool = nostr_relay_pool_create(config);
if (!g_relay_state.pool) {
app_log(LOG_ERROR, "Failed to create relay pool");
pthread_mutex_destroy(&g_relay_state.state_mutex);
return -1;
}
// Add all relays to pool
for (int i = 0; i < g_relay_state.relay_count; i++) {
if (nostr_relay_pool_add_relay(g_relay_state.pool, g_relay_state.relay_urls[i]) != NOSTR_SUCCESS) {
app_log(LOG_WARN, "Failed to add relay to pool: %s", g_relay_state.relay_urls[i]);
}
}
// Trigger initial connection attempts by creating a dummy subscription
// This forces ensure_relay_connection() to be called for each relay
app_log(LOG_INFO, "Initiating relay connections...");
cJSON* dummy_filter = cJSON_CreateObject();
cJSON* kinds = cJSON_CreateArray();
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(0)); // Kind 0 (will match nothing)
cJSON_AddItemToObject(dummy_filter, "kinds", kinds);
cJSON_AddNumberToObject(dummy_filter, "limit", 0); // Limit 0 = no results
nostr_pool_subscription_t* dummy_sub = nostr_relay_pool_subscribe(
g_relay_state.pool,
(const char**)g_relay_state.relay_urls,
g_relay_state.relay_count,
dummy_filter,
NULL, // No event callback
NULL, // No EOSE callback
NULL, // No user data
1, // close_on_eose
1, // enable_deduplication
NOSTR_POOL_EOSE_FIRST, // result_mode
30, // relay_timeout_seconds
30 // eose_timeout_seconds
);
cJSON_Delete(dummy_filter);
// Immediately close the dummy subscription
if (dummy_sub) {
nostr_pool_subscription_close(dummy_sub);
app_log(LOG_INFO, "Connection attempts initiated for %d relays", g_relay_state.relay_count);
} else {
app_log(LOG_WARN, "Failed to initiate connection attempts");
}
}
g_relay_state.initialized = 1;
app_log(LOG_INFO, "Relay client initialized (enabled: %d, relays: %d)",
g_relay_state.enabled, g_relay_state.relay_count);
return 0;
}
// Load configuration from database
static int load_config_from_db(void) {
sqlite3 *db;
sqlite3_stmt *stmt;
int rc;
rc = sqlite3_open_v2(g_relay_state.db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc != SQLITE_OK) {
app_log(LOG_ERROR, "Cannot open database: %s", sqlite3_errmsg(db));
return -1;
}
// Load enable_relay_connect
const char *sql = "SELECT value FROM config WHERE key = ?";
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
app_log(LOG_ERROR, "Failed to prepare statement: %s", sqlite3_errmsg(db));
sqlite3_close(db);
return -1;
}
sqlite3_bind_text(stmt, 1, "enable_relay_connect", -1, SQLITE_STATIC);
rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
const char *value = (const char *)sqlite3_column_text(stmt, 0);
g_relay_state.enabled = (strcmp(value, "true") == 0 || strcmp(value, "1") == 0);
} else {
g_relay_state.enabled = 0;
}
sqlite3_finalize(stmt);
// If not enabled, skip loading relay URLs
if (!g_relay_state.enabled) {
sqlite3_close(db);
return 0;
}
// Load kind_10002_tags (relay URLs)
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
app_log(LOG_ERROR, "Failed to prepare statement: %s", sqlite3_errmsg(db));
sqlite3_close(db);
return -1;
}
sqlite3_bind_text(stmt, 1, "kind_10002_tags", -1, SQLITE_STATIC);
rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
const char *json_array = (const char *)sqlite3_column_text(stmt, 0);
if (parse_relay_urls(json_array) != 0) {
app_log(LOG_ERROR, "Failed to parse relay URLs from config");
sqlite3_finalize(stmt);
sqlite3_close(db);
return -1;
}
} else {
app_log(LOG_WARN, "No relay URLs configured in kind_10002_tags");
}
sqlite3_finalize(stmt);
sqlite3_close(db);
return 0;
}
// Parse relay URLs from JSON array
static int parse_relay_urls(const char *json_array) {
cJSON *root = cJSON_Parse(json_array);
if (!root || !cJSON_IsArray(root)) {
app_log(LOG_ERROR, "Invalid JSON array for relay URLs");
if (root) cJSON_Delete(root);
return -1;
}
int count = cJSON_GetArraySize(root);
if (count > MAX_RELAYS) {
app_log(LOG_WARN, "Too many relays configured (%d), limiting to %d", count, MAX_RELAYS);
count = MAX_RELAYS;
}
// Allocate relay URLs array
g_relay_state.relay_urls = malloc(count * sizeof(char*));
if (!g_relay_state.relay_urls) {
cJSON_Delete(root);
return -1;
}
g_relay_state.relay_count = 0;
for (int i = 0; i < count; i++) {
cJSON *item = cJSON_GetArrayItem(root, i);
if (cJSON_IsString(item) && item->valuestring) {
g_relay_state.relay_urls[g_relay_state.relay_count] = strdup(item->valuestring);
if (!g_relay_state.relay_urls[g_relay_state.relay_count]) {
// Cleanup on failure
for (int j = 0; j < g_relay_state.relay_count; j++) {
free(g_relay_state.relay_urls[j]);
}
free(g_relay_state.relay_urls);
cJSON_Delete(root);
return -1;
}
g_relay_state.relay_count++;
}
}
cJSON_Delete(root);
app_log(LOG_INFO, "Parsed %d relay URLs from configuration", g_relay_state.relay_count);
return 0;
}
// Start relay connections
int relay_client_start(void) {
if (!g_relay_state.initialized) {
app_log(LOG_ERROR, "Relay client not initialized");
return -1;
}
if (!g_relay_state.enabled) {
app_log(LOG_INFO, "Relay client disabled in configuration");
return 0;
}
if (g_relay_state.running) {
app_log(LOG_WARN, "Relay client already running");
return 0;
}
app_log(LOG_INFO, "Starting relay client...");
// Start management thread
g_relay_state.running = 1;
if (pthread_create(&g_relay_state.management_thread, NULL, relay_management_thread, NULL) != 0) {
app_log(LOG_ERROR, "Failed to create relay management thread");
g_relay_state.running = 0;
return -1;
}
app_log(LOG_INFO, "Relay client started successfully");
return 0;
}
// Relay management thread
static void *relay_management_thread(void *arg) {
(void)arg;
app_log(LOG_INFO, "Relay management thread started");
// Wait for at least one relay to connect (max 30 seconds)
int connected = 0;
for (int i = 0; i < 30 && !connected; i++) {
sleep(1);
// Poll to process connection attempts
nostr_relay_pool_poll(g_relay_state.pool, 100);
// Check if any relay is connected
for (int j = 0; j < g_relay_state.relay_count; j++) {
nostr_pool_relay_status_t status = nostr_relay_pool_get_relay_status(
g_relay_state.pool,
g_relay_state.relay_urls[j]
);
if (status == NOSTR_POOL_RELAY_CONNECTED) {
connected = 1;
app_log(LOG_INFO, "Relay connected: %s", g_relay_state.relay_urls[j]);
break;
}
}
}
if (!connected) {
app_log(LOG_WARN, "No relays connected after 30 seconds, continuing anyway");
}
// Publish initial events
relay_client_publish_kind0();
relay_client_publish_kind10002();
// Subscribe to admin commands
subscribe_to_admin_commands();
// Main loop: poll the relay pool for incoming messages
while (g_relay_state.running) {
// Poll with 1000ms timeout
int events_processed = nostr_relay_pool_poll(g_relay_state.pool, 1000);
if (events_processed < 0) {
app_log(LOG_ERROR, "Error polling relay pool");
sleep(1);
}
// Pool handles all connection management, reconnection, and message processing
}
app_log(LOG_INFO, "Relay management thread stopping");
return NULL;
}
// Stop relay connections
void relay_client_stop(void) {
if (!g_relay_state.running) {
return;
}
app_log(LOG_INFO, "Stopping relay client...");
g_relay_state.running = 0;
// Wait for management thread to finish
pthread_join(g_relay_state.management_thread, NULL);
// Close admin subscription
if (g_relay_state.admin_subscription) {
nostr_pool_subscription_close(g_relay_state.admin_subscription);
g_relay_state.admin_subscription = NULL;
}
// Destroy relay pool (automatically disconnects all relays)
if (g_relay_state.pool) {
nostr_relay_pool_destroy(g_relay_state.pool);
g_relay_state.pool = NULL;
}
// Free relay URLs
if (g_relay_state.relay_urls) {
for (int i = 0; i < g_relay_state.relay_count; i++) {
free(g_relay_state.relay_urls[i]);
}
free(g_relay_state.relay_urls);
g_relay_state.relay_urls = NULL;
}
pthread_mutex_destroy(&g_relay_state.state_mutex);
app_log(LOG_INFO, "Relay client stopped");
}
// Check if relay client is enabled
int relay_client_is_enabled(void) {
return g_relay_state.enabled;
}
// Publish Kind 0 profile event
int relay_client_publish_kind0(void) {
if (!g_relay_state.enabled || !g_relay_state.running || !g_relay_state.pool) {
return -1;
}
app_log(LOG_INFO, "Publishing Kind 0 profile event...");
// Load kind_0_content from database
sqlite3 *db;
sqlite3_stmt *stmt;
int rc;
rc = sqlite3_open_v2(g_relay_state.db_path, &db, SQLITE_OPEN_READONLY, NULL);
if (rc != SQLITE_OK) {
app_log(LOG_ERROR, "Cannot open database: %s", sqlite3_errmsg(db));
return -1;
}
const char *sql = "SELECT value FROM config WHERE key = 'kind_0_content'";
rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
app_log(LOG_ERROR, "Failed to prepare statement: %s", sqlite3_errmsg(db));
sqlite3_close(db);
return -1;
}
rc = sqlite3_step(stmt);
if (rc != SQLITE_ROW) {
app_log(LOG_WARN, "No kind_0_content found in config");
sqlite3_finalize(stmt);
sqlite3_close(db);
return -1;
}
const char *content = (const char *)sqlite3_column_text(stmt, 0);
// Convert private key from hex to bytes
unsigned char privkey_bytes[32];
if (nostr_hex_to_bytes(g_blossom_seckey, privkey_bytes, 32) != 0) {
app_log(LOG_ERROR, "Failed to convert private key from hex");
sqlite3_finalize(stmt);
sqlite3_close(db);
return -1;
}
// Create and sign Kind 0 event using nostr_core_lib
cJSON* event = nostr_create_and_sign_event(
0, // kind
content, // content
NULL, // tags (empty for Kind 0)
privkey_bytes, // private key
time(NULL) // created_at
);
sqlite3_finalize(stmt);
sqlite3_close(db);
if (!event) {
app_log(LOG_ERROR, "Failed to create Kind 0 event");
return -1;
}
// Publish to all relays using async pool API
int result = nostr_relay_pool_publish_async(
g_relay_state.pool,
(const char**)g_relay_state.relay_urls,
g_relay_state.relay_count,
event,
on_publish_response,
(void*)"Kind 0" // user_data to identify event type
);
cJSON_Delete(event);
if (result == 0) {
app_log(LOG_INFO, "Kind 0 profile event publish initiated");
return 0;
} else {
app_log(LOG_ERROR, "Failed to initiate Kind 0 profile event publish");
return -1;
}
}
// Publish Kind 10002 relay list event
int relay_client_publish_kind10002(void) {
if (!g_relay_state.enabled || !g_relay_state.running || !g_relay_state.pool) {
return -1;
}
app_log(LOG_INFO, "Publishing Kind 10002 relay list event...");
// Build tags array from configured relays
cJSON* tags = cJSON_CreateArray();
for (int i = 0; i < g_relay_state.relay_count; i++) {
cJSON* tag = cJSON_CreateArray();
cJSON_AddItemToArray(tag, cJSON_CreateString("r"));
cJSON_AddItemToArray(tag, cJSON_CreateString(g_relay_state.relay_urls[i]));
cJSON_AddItemToArray(tags, tag);
}
// Convert private key from hex to bytes
unsigned char privkey_bytes[32];
if (nostr_hex_to_bytes(g_blossom_seckey, privkey_bytes, 32) != 0) {
app_log(LOG_ERROR, "Failed to convert private key from hex");
cJSON_Delete(tags);
return -1;
}
// Create and sign Kind 10002 event
cJSON* event = nostr_create_and_sign_event(
10002, // kind
"", // content (empty for Kind 10002)
tags, // tags
privkey_bytes, // private key
time(NULL) // created_at
);
cJSON_Delete(tags);
if (!event) {
app_log(LOG_ERROR, "Failed to create Kind 10002 event");
return -1;
}
// Publish to all relays using async pool API
int result = nostr_relay_pool_publish_async(
g_relay_state.pool,
(const char**)g_relay_state.relay_urls,
g_relay_state.relay_count,
event,
on_publish_response,
(void*)"Kind 10002" // user_data to identify event type
);
cJSON_Delete(event);
if (result == 0) {
app_log(LOG_INFO, "Kind 10002 relay list event publish initiated");
return 0;
} else {
app_log(LOG_ERROR, "Failed to initiate Kind 10002 relay list event publish");
return -1;
}
}
// Send Kind 23457 admin response event
int relay_client_send_admin_response(const char *recipient_pubkey, const char *response_content) {
if (!g_relay_state.enabled || !g_relay_state.running || !g_relay_state.pool) {
return -1;
}
if (!recipient_pubkey || !response_content) {
return -1;
}
app_log(LOG_INFO, "Sending Kind 23457 admin response to %s", recipient_pubkey);
// TODO: Encrypt response_content using NIP-44
// For now, use plaintext (stub implementation)
const char *encrypted_content = response_content;
// Build tags array
cJSON* tags = cJSON_CreateArray();
cJSON* p_tag = cJSON_CreateArray();
cJSON_AddItemToArray(p_tag, cJSON_CreateString("p"));
cJSON_AddItemToArray(p_tag, cJSON_CreateString(recipient_pubkey));
cJSON_AddItemToArray(tags, p_tag);
// Convert private key from hex to bytes
unsigned char privkey_bytes[32];
if (nostr_hex_to_bytes(g_blossom_seckey, privkey_bytes, 32) != 0) {
app_log(LOG_ERROR, "Failed to convert private key from hex");
cJSON_Delete(tags);
return -1;
}
// Create and sign Kind 23457 event
cJSON* event = nostr_create_and_sign_event(
23457, // kind
encrypted_content, // content
tags, // tags
privkey_bytes, // private key
time(NULL) // created_at
);
cJSON_Delete(tags);
if (!event) {
app_log(LOG_ERROR, "Failed to create Kind 23457 event");
return -1;
}
// Publish to all relays using async pool API
int result = nostr_relay_pool_publish_async(
g_relay_state.pool,
(const char**)g_relay_state.relay_urls,
g_relay_state.relay_count,
event,
on_publish_response,
(void*)"Kind 23457" // user_data to identify event type
);
cJSON_Delete(event);
if (result == 0) {
app_log(LOG_INFO, "Kind 23457 admin response publish initiated");
return 0;
} else {
app_log(LOG_ERROR, "Failed to initiate Kind 23457 admin response publish");
return -1;
}
}
// Callback for publish responses
static void on_publish_response(const char* relay_url, const char* event_id, int success, const char* message, void* user_data) {
const char* event_type = (const char*)user_data;
if (success) {
app_log(LOG_INFO, "%s event published successfully to %s (ID: %s)",
event_type, relay_url, event_id);
} else {
app_log(LOG_WARN, "%s event rejected by %s: %s",
event_type, relay_url, message ? message : "unknown error");
}
}
// Callback for received Kind 23456 admin command events
static void on_admin_command_event(cJSON* event, const char* relay_url, void* user_data) {
(void)user_data;
app_log(LOG_INFO, "Received Kind 23456 admin command from relay: %s", relay_url);
// Extract event fields
cJSON* kind_json = cJSON_GetObjectItem(event, "kind");
cJSON* pubkey_json = cJSON_GetObjectItem(event, "pubkey");
cJSON* content_json = cJSON_GetObjectItem(event, "content");
cJSON* id_json = cJSON_GetObjectItem(event, "id");
if (!kind_json || !pubkey_json || !content_json || !id_json) {
app_log(LOG_ERROR, "Invalid event structure");
return;
}
int kind = cJSON_GetNumberValue(kind_json);
const char* sender_pubkey = cJSON_GetStringValue(pubkey_json);
const char* encrypted_content = cJSON_GetStringValue(content_json);
const char* event_id = cJSON_GetStringValue(id_json);
if (kind != 23456) {
app_log(LOG_WARN, "Unexpected event kind: %d", kind);
return;
}
// Verify sender is admin
if (strcmp(sender_pubkey, g_admin_pubkey) != 0) {
app_log(LOG_WARN, "Ignoring command from non-admin pubkey: %s", sender_pubkey);
return;
}
app_log(LOG_INFO, "Processing admin command (event ID: %s)", event_id);
// TODO: Decrypt content using NIP-44
// For now, log the encrypted content
app_log(LOG_DEBUG, "Encrypted command content: %s", encrypted_content);
// TODO: Parse and execute command
// TODO: Send response using relay_client_send_admin_response()
}
// Callback for EOSE (End Of Stored Events) - new signature
static void on_admin_subscription_eose(cJSON** events, int event_count, void* user_data) {
(void)events;
(void)event_count;
(void)user_data;
app_log(LOG_INFO, "Received EOSE for admin command subscription");
}
// Subscribe to admin commands (Kind 23456)
static int subscribe_to_admin_commands(void) {
if (!g_relay_state.pool) {
return -1;
}
app_log(LOG_INFO, "Subscribing to Kind 23456 admin commands...");
// Create subscription filter for Kind 23456 events addressed to us
cJSON* filter = cJSON_CreateObject();
cJSON* kinds = cJSON_CreateArray();
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(23456));
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON* p_tags = cJSON_CreateArray();
cJSON_AddItemToArray(p_tags, cJSON_CreateString(g_blossom_pubkey));
cJSON_AddItemToObject(filter, "#p", p_tags);
cJSON_AddNumberToObject(filter, "since", (double)time(NULL));
// Subscribe using pool with new API signature
g_relay_state.admin_subscription = nostr_relay_pool_subscribe(
g_relay_state.pool,
(const char**)g_relay_state.relay_urls,
g_relay_state.relay_count,
filter,
on_admin_command_event,
on_admin_subscription_eose,
NULL, // user_data
0, // close_on_eose (keep subscription open)
1, // enable_deduplication
NOSTR_POOL_EOSE_FULL_SET, // result_mode
30, // relay_timeout_seconds
30 // eose_timeout_seconds
);
cJSON_Delete(filter);
if (!g_relay_state.admin_subscription) {
app_log(LOG_ERROR, "Failed to create admin command subscription");
return -1;
}
app_log(LOG_INFO, "Successfully subscribed to admin commands");
return 0;
}
// Get current relay connection status
char *relay_client_get_status(void) {
if (!g_relay_state.pool) {
return strdup("[]");
}
cJSON *root = cJSON_CreateArray();
pthread_mutex_lock(&g_relay_state.state_mutex);
for (int i = 0; i < g_relay_state.relay_count; i++) {
cJSON *relay_obj = cJSON_CreateObject();
cJSON_AddStringToObject(relay_obj, "url", g_relay_state.relay_urls[i]);
// Get status from pool
nostr_pool_relay_status_t status = nostr_relay_pool_get_relay_status(
g_relay_state.pool,
g_relay_state.relay_urls[i]
);
const char *state_str;
switch (status) {
case NOSTR_POOL_RELAY_CONNECTED: state_str = "connected"; break;
case NOSTR_POOL_RELAY_CONNECTING: state_str = "connecting"; break;
case NOSTR_POOL_RELAY_ERROR: state_str = "error"; break;
default: state_str = "disconnected"; break;
}
cJSON_AddStringToObject(relay_obj, "state", state_str);
// Get statistics from pool
const nostr_relay_stats_t* stats = nostr_relay_pool_get_relay_stats(
g_relay_state.pool,
g_relay_state.relay_urls[i]
);
if (stats) {
cJSON_AddNumberToObject(relay_obj, "events_received", stats->events_received);
cJSON_AddNumberToObject(relay_obj, "events_published", stats->events_published);
cJSON_AddNumberToObject(relay_obj, "connection_attempts", stats->connection_attempts);
cJSON_AddNumberToObject(relay_obj, "connection_failures", stats->connection_failures);
if (stats->query_latency_avg > 0) {
cJSON_AddNumberToObject(relay_obj, "query_latency_ms", stats->query_latency_avg);
}
}
cJSON_AddItemToArray(root, relay_obj);
}
pthread_mutex_unlock(&g_relay_state.state_mutex);
char *json_str = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
return json_str;
}
// Force reconnection to all relays
int relay_client_reconnect(void) {
if (!g_relay_state.enabled || !g_relay_state.running || !g_relay_state.pool) {
return -1;
}
app_log(LOG_INFO, "Forcing reconnection to all relays...");
// Remove and re-add all relays to force reconnection
pthread_mutex_lock(&g_relay_state.state_mutex);
for (int i = 0; i < g_relay_state.relay_count; i++) {
nostr_relay_pool_remove_relay(g_relay_state.pool, g_relay_state.relay_urls[i]);
nostr_relay_pool_add_relay(g_relay_state.pool, g_relay_state.relay_urls[i]);
}
pthread_mutex_unlock(&g_relay_state.state_mutex);
app_log(LOG_INFO, "Reconnection initiated for all relays");
return 0;
}

78
src/relay_client.h Normal file
View File

@@ -0,0 +1,78 @@
/*
* Ginxsom Relay Client - Nostr Relay Connection Manager
*
* This module enables Ginxsom to act as a Nostr client, connecting to relays
* to publish events (Kind 0, Kind 10002) and subscribe to admin commands (Kind 23456).
*/
#ifndef RELAY_CLIENT_H
#define RELAY_CLIENT_H
#include <stddef.h>
#include <time.h>
// Connection states for relay tracking
typedef enum {
RELAY_STATE_DISCONNECTED = 0,
RELAY_STATE_CONNECTING = 1,
RELAY_STATE_CONNECTED = 2,
RELAY_STATE_ERROR = 3
} relay_state_t;
// Relay connection info (in-memory only)
typedef struct {
char url[256];
relay_state_t state;
int reconnect_attempts;
time_t last_connect_attempt;
time_t connected_since;
} relay_info_t;
// Initialize relay client system
// Loads configuration from database and prepares for connections
// Returns: 0 on success, -1 on error
int relay_client_init(const char *db_path);
// Start relay connections
// Connects to all relays specified in kind_10002_tags config
// Publishes Kind 0 and Kind 10002 events after successful connection
// Returns: 0 on success, -1 on error
int relay_client_start(void);
// Stop relay connections and cleanup
// Gracefully disconnects from all relays and stops background thread
void relay_client_stop(void);
// Check if relay client is enabled
// Returns: 1 if enabled, 0 if disabled
int relay_client_is_enabled(void);
// Publish Kind 0 profile event to all connected relays
// Uses kind_0_content from config database
// Returns: 0 on success, -1 on error
int relay_client_publish_kind0(void);
// Publish Kind 10002 relay list event to all connected relays
// Uses kind_10002_tags from config database
// Returns: 0 on success, -1 on error
int relay_client_publish_kind10002(void);
// Send Kind 23457 admin response event
// Encrypts content using NIP-44 and publishes to all connected relays
// Parameters:
// - recipient_pubkey: Admin's public key (recipient)
// - response_content: JSON response content to encrypt
// Returns: 0 on success, -1 on error
int relay_client_send_admin_response(const char *recipient_pubkey, const char *response_content);
// Get current relay connection status
// Returns JSON string with relay status (caller must free)
// Format: [{"url": "wss://...", "state": "connected", "connected_since": 1234567890}, ...]
char *relay_client_get_status(void);
// Force reconnection to all relays
// Disconnects and reconnects to all configured relays
// Returns: 0 on success, -1 on error
int relay_client_reconnect(void);
#endif // RELAY_CLIENT_H

View File

@@ -529,7 +529,7 @@ int nostr_validate_unified_request(const nostr_unified_request_t *request,
"VALIDATOR_DEBUG: STEP 10 FAILED - NIP-42 requires request_url and " "VALIDATOR_DEBUG: STEP 10 FAILED - NIP-42 requires request_url and "
"challenge (from event tags)\n"); "challenge (from event tags)\n");
result->valid = 0; result->valid = 0;
result->error_code = NOSTR_ERROR_NIP42_NOT_CONFIGURED; result->error_code = NOSTR_ERROR_NIP42_INVALID_CHALLENGE;
strcpy(result->reason, "NIP-42 authentication requires request_url and challenge in event tags"); strcpy(result->reason, "NIP-42 authentication requires request_url and challenge in event tags");
cJSON_Delete(event); cJSON_Delete(event);
return NOSTR_SUCCESS; return NOSTR_SUCCESS;
@@ -549,15 +549,12 @@ int nostr_validate_unified_request(const nostr_unified_request_t *request,
// Map specific NIP-42 error codes to detailed error messages // Map specific NIP-42 error codes to detailed error messages
switch (nip42_result) { switch (nip42_result) {
case NOSTR_ERROR_NIP42_CHALLENGE_NOT_FOUND: case NOSTR_ERROR_NIP42_INVALID_CHALLENGE:
strcpy(result->reason, "Challenge not found or has been used. Request a new challenge from /auth endpoint."); strcpy(result->reason, "Challenge not found or invalid. Request a new challenge from /auth endpoint.");
break; break;
case NOSTR_ERROR_NIP42_CHALLENGE_EXPIRED: case NOSTR_ERROR_NIP42_CHALLENGE_EXPIRED:
strcpy(result->reason, "Challenge has expired. Request a new challenge from /auth endpoint."); strcpy(result->reason, "Challenge has expired. Request a new challenge from /auth endpoint.");
break; break;
case NOSTR_ERROR_NIP42_INVALID_CHALLENGE:
strcpy(result->reason, "Invalid challenge format. Challenge must be a valid hex string.");
break;
case NOSTR_ERROR_NIP42_URL_MISMATCH: case NOSTR_ERROR_NIP42_URL_MISMATCH:
strcpy(result->reason, "Relay URL in auth event does not match server. Use 'ginxsom' as relay value."); strcpy(result->reason, "Relay URL in auth event does not match server. Use 'ginxsom' as relay value.");
break; break;
@@ -576,12 +573,6 @@ int nostr_validate_unified_request(const nostr_unified_request_t *request,
case NOSTR_ERROR_EVENT_INVALID_TAGS: case NOSTR_ERROR_EVENT_INVALID_TAGS:
strcpy(result->reason, "Required tags missing. Auth event must include 'relay' and 'expiration' tags."); strcpy(result->reason, "Required tags missing. Auth event must include 'relay' and 'expiration' tags.");
break; break;
case NOSTR_ERROR_NIP42_INVALID_RELAY_URL:
strcpy(result->reason, "Invalid relay URL in tags. Use 'ginxsom' as the relay identifier.");
break;
case NOSTR_ERROR_NIP42_NOT_CONFIGURED:
strcpy(result->reason, "NIP-42 authentication not properly configured on server.");
break;
default: default:
snprintf(result->reason, sizeof(result->reason), snprintf(result->reason, sizeof(result->reason),
"NIP-42 authentication failed (error code: %d). Check event structure and signature.", "NIP-42 authentication failed (error code: %d). Check event structure and signature.",
@@ -1907,7 +1898,7 @@ static int validate_challenge(const char *challenge_id) {
} }
validator_debug_log("NIP-42: Challenge not found\n"); validator_debug_log("NIP-42: Challenge not found\n");
return NOSTR_ERROR_NIP42_CHALLENGE_NOT_FOUND; return NOSTR_ERROR_NIP42_INVALID_CHALLENGE;
} }
/** /**

View File

@@ -1,397 +0,0 @@
#!/bin/bash
# Ginxsom WebSocket Admin Test Script
# Tests Kind 23456/23457 admin command system over WebSocket with NIP-44 encryption
#
# Prerequisites:
# - websocat: WebSocket client (https://github.com/vi/websocat)
# - nak: Nostr Army Knife (https://github.com/fiatjaf/nak)
# - jq: JSON processor
# - Server running with test keys from .test_keys
set -e
# Configuration
WEBSOCKET_URL="wss://localhost:9443/admin" # Secure WebSocket via nginx HTTPS
WEBSOCKET_HTTP_URL="ws://localhost:9001/admin" # Non-secure WebSocket via nginx HTTP
WEBSOCKET_DIRECT_URL="ws://localhost:9442" # Direct connection to WebSocket server (port 9442)
TEST_KEYS_FILE=".test_keys"
TIMEOUT=10 # WebSocket connection timeout in seconds
# Load test keys
if [[ ! -f "$TEST_KEYS_FILE" ]]; then
echo "ERROR: $TEST_KEYS_FILE not found"
echo "Run the server with --test-keys to generate test keys"
exit 1
fi
source "$TEST_KEYS_FILE"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# Helper functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_debug() {
echo -e "${CYAN}[DEBUG]${NC} $1"
}
check_dependencies() {
log_info "Checking dependencies..."
for cmd in websocat nak jq; do
if ! command -v $cmd &> /dev/null; then
log_error "$cmd is not installed"
case $cmd in
websocat)
echo "Install from: https://github.com/vi/websocat"
echo " cargo install websocat"
;;
nak)
echo "Install from: https://github.com/fiatjaf/nak"
echo " go install github.com/fiatjaf/nak@latest"
;;
jq)
echo "Install jq for JSON processing"
echo " apt-get install jq # Debian/Ubuntu"
;;
esac
exit 1
fi
done
log_success "All dependencies found"
log_info " websocat: $(websocat --version 2>&1 | head -n1)"
log_info " nak: $(nak --version 2>&1 | head -n1)"
log_info " jq: $(jq --version 2>&1)"
}
# Test basic WebSocket connection
test_websocket_connection() {
local url="$1"
log_info "=== Testing WebSocket Connection ==="
log_info "Connecting to: $url"
# For wss:// connections, add --insecure flag to skip certificate verification
local websocat_opts=""
if [[ "$url" == wss://* ]]; then
websocat_opts="--insecure"
log_debug "Using --insecure flag for self-signed certificate"
fi
# Try to connect and send a ping
local result=$(timeout $TIMEOUT websocat $websocat_opts -n1 "$url" <<< '{"test":"ping"}' 2>&1 || echo "TIMEOUT")
if [[ "$result" == "TIMEOUT" ]]; then
log_error "Connection timeout after ${TIMEOUT}s"
return 1
elif [[ -z "$result" ]]; then
log_warning "Connected but no response (this may be normal for WebSocket)"
return 0
else
log_success "Connection established"
log_debug "Response: $result"
return 0
fi
}
# Create NIP-44 encrypted admin command event (Kind 23456)
create_admin_command_event() {
local command="$1"
local expiration=$(($(date +%s) + 3600)) # 1 hour from now
log_info "Creating Kind 23456 admin command event..."
log_info "Command: $command"
# Content is a JSON array of commands
local content="[\"$command\"]"
# Create event with nak
# Kind 23456 = admin command
# Tags: p = server pubkey, expiration
local event=$(nak event -k 23456 \
-c "$content" \
--tag p="$SERVER_PUBKEY" \
--tag expiration="$expiration" \
--sec "$ADMIN_PRIVKEY" 2>&1)
if [[ $? -ne 0 ]]; then
log_error "Failed to create event with nak"
log_error "$event"
return 1
fi
echo "$event"
}
# Send admin command via WebSocket and wait for response
send_websocket_admin_command() {
local command="$1"
local url="$2"
log_info "=== Testing Admin Command via WebSocket: $command ==="
# Create Kind 23456 event
local event=$(create_admin_command_event "$command")
if [[ -z "$event" ]]; then
log_error "Failed to create admin event"
return 1
fi
log_success "Event created successfully"
log_debug "Event JSON:"
echo "$event" | jq -C . 2>/dev/null || echo "$event"
echo ""
# Send to WebSocket server and wait for response
log_info "Sending to WebSocket: $url"
log_info "Waiting for Kind 23457 response (timeout: ${TIMEOUT}s)..."
# For wss:// connections, add --insecure flag to skip certificate verification
local websocat_opts=""
if [[ "$url" == wss://* ]]; then
websocat_opts="--insecure"
log_debug "Using --insecure flag for self-signed certificate"
fi
# Use websocat to send event and receive response
local response=$(timeout $TIMEOUT websocat $websocat_opts -n1 "$url" <<< "$event" 2>&1)
local exit_code=$?
echo ""
if [[ $exit_code -eq 124 ]]; then
log_error "Timeout waiting for response after ${TIMEOUT}s"
return 1
elif [[ $exit_code -ne 0 ]]; then
log_error "WebSocket connection failed (exit code: $exit_code)"
log_error "$response"
return 1
fi
if [[ -z "$response" ]]; then
log_warning "No response received (connection may have closed)"
return 1
fi
log_success "Response received"
log_debug "Raw response:"
echo "$response"
echo ""
# Try to parse as JSON
if echo "$response" | jq . &>/dev/null; then
log_success "Valid JSON response"
# Check if it's a Kind 23457 event
local kind=$(echo "$response" | jq -r '.kind // empty' 2>/dev/null)
if [[ "$kind" == "23457" ]]; then
log_success "Received Kind 23457 response event ✓"
# Extract and display response details
local response_id=$(echo "$response" | jq -r '.id // empty')
local response_pubkey=$(echo "$response" | jq -r '.pubkey // empty')
local response_content=$(echo "$response" | jq -r '.content // empty')
local response_sig=$(echo "$response" | jq -r '.sig // empty')
echo ""
log_info "Response Event Details:"
log_info " ID: $response_id"
log_info " Pubkey: $response_pubkey"
log_info " Content: $response_content"
log_info " Sig: ${response_sig:0:32}..."
# Check if content is encrypted (NIP-44)
if [[ ${#response_content} -gt 50 ]]; then
log_info " Content appears to be NIP-44 encrypted"
log_warning " Decryption not yet implemented in test script"
else
log_info " Content (plaintext): $response_content"
fi
# Verify signature
log_info "Verifying event signature..."
if echo "$response" | nak verify 2>&1 | grep -q "signature is valid"; then
log_success "Event signature is valid ✓"
else
log_error "Event signature verification failed"
return 1
fi
else
log_warning "Response is not Kind 23457 (got kind: $kind)"
fi
# Pretty print the full response
echo ""
log_info "Full Response Event:"
echo "$response" | jq -C .
else
log_warning "Response is not valid JSON"
log_debug "Raw response: $response"
fi
echo ""
return 0
}
# Test config_query command
test_config_query() {
log_info "=== Testing config_query Command ==="
send_websocket_admin_command "config_query" "$WEBSOCKET_URL"
}
# Test with HTTP WebSocket connection
test_http_connection() {
log_info "=== Testing HTTP WebSocket Connection ==="
log_info "Connecting via HTTP (port 9001)"
send_websocket_admin_command "config_query" "$WEBSOCKET_HTTP_URL"
}
# Test with direct WebSocket connection (bypassing nginx)
test_direct_connection() {
log_info "=== Testing Direct WebSocket Connection ==="
log_info "Connecting directly to WebSocket server (port 9442)"
send_websocket_admin_command "config_query" "$WEBSOCKET_DIRECT_URL"
}
# Test invalid command
test_invalid_command() {
log_info "=== Testing Invalid Command ==="
send_websocket_admin_command "invalid_command_xyz" "$WEBSOCKET_URL" || log_warning "Expected failure for invalid command"
}
# Test connection persistence
test_connection_persistence() {
log_info "=== Testing Connection Persistence ==="
log_info "Sending multiple commands over same connection..."
# Create two events
local event1=$(create_admin_command_event "config_query")
local event2=$(create_admin_command_event "config_query")
if [[ -z "$event1" ]] || [[ -z "$event2" ]]; then
log_error "Failed to create events"
return 1
fi
# For wss:// connections, add --insecure flag
local websocat_opts=""
if [[ "$WEBSOCKET_URL" == wss://* ]]; then
websocat_opts="--insecure"
fi
# Send both events and collect responses
log_info "Sending two events sequentially..."
local responses=$(timeout $((TIMEOUT * 2)) websocat $websocat_opts -n2 "$WEBSOCKET_URL" <<EOF
$event1
$event2
EOF
)
if [[ $? -eq 0 ]]; then
log_success "Received responses for both events"
echo "$responses" | while IFS= read -r line; do
if [[ -n "$line" ]]; then
echo "$line" | jq -C . 2>/dev/null || echo "$line"
fi
done
else
log_warning "Connection persistence test inconclusive"
fi
echo ""
}
main() {
echo "=========================================="
echo " Ginxsom WebSocket Admin Test Suite"
echo " Kind 23456/23457 over WebSocket"
echo "=========================================="
echo ""
log_info "Test Configuration:"
log_info " Admin Privkey: ${ADMIN_PRIVKEY:0:16}...${ADMIN_PRIVKEY: -16}"
log_info " Admin Pubkey: $ADMIN_PUBKEY"
log_info " Server Pubkey: $SERVER_PUBKEY"
log_info " HTTPS URL: $WEBSOCKET_URL"
log_info " HTTP URL: $WEBSOCKET_HTTP_URL"
log_info " Direct URL: $WEBSOCKET_DIRECT_URL"
log_info " Timeout: ${TIMEOUT}s"
echo ""
check_dependencies
echo ""
# Test basic WebSocket connectivity
if ! test_websocket_connection "$WEBSOCKET_URL"; then
log_error "Basic WebSocket connection failed"
log_info "Trying direct connection to port 9442..."
if ! test_websocket_connection "$WEBSOCKET_DIRECT_URL"; then
log_error "Direct connection also failed"
log_error "Make sure the server is running with WebSocket admin enabled"
exit 1
fi
fi
echo ""
# Test admin commands via HTTPS
test_config_query
echo ""
# Test via HTTP
test_http_connection
echo ""
# Test direct connection (bypassing nginx)
test_direct_connection
echo ""
# Test invalid command
test_invalid_command
echo ""
# Test connection persistence
test_connection_persistence
echo ""
echo "=========================================="
log_success "WebSocket admin testing complete!"
echo "=========================================="
echo ""
log_info "Summary:"
log_info " ✓ WebSocket connection established"
log_info " ✓ Kind 23456 events sent"
log_info " ✓ Kind 23457 responses received"
log_info " ✓ Event signatures verified"
echo ""
log_warning "NOTE: NIP-44 encryption/decryption not yet implemented in test script"
log_warning "Events use plaintext command arrays for initial testing"
log_warning "Production implementation uses full NIP-44 encryption"
}
# Allow sourcing for individual function testing
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi