v0.1.11 - Last push before changing logging system
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
ADMIN_PRIVKEY='22cc83aa57928a2800234c939240c9a6f0f44a33ea3838a860ed38930b195afd'
|
||||
ADMIN_PUBKEY='8ff74724ed641b3c28e5a86d7c5cbc49c37638ace8c6c38935860e7a5eedde0e'
|
||||
SERVER_PRIVKEY='c4e0d2ed7d36277d6698650f68a6e9199f91f3abb476a67f07303e81309c48f1'
|
||||
SERVER_PUBKEY='ebe82fbff0ff79b2973892eb285cafc767863e434f894838a548580266b70254'
|
||||
SERVER_PUBKEY='52e366edfa4e9cc6a6d4653828e51ccf828a2f5a05227d7a768f33b5a198681a'
|
||||
|
||||
4
Makefile
4
Makefile
@@ -1,8 +1,8 @@
|
||||
# Ginxsom Blossom Server Makefile
|
||||
|
||||
CC = gcc
|
||||
CFLAGS = -Wall -Wextra -std=c99 -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
|
||||
CFLAGS = -Wall -Wextra -std=gnu99 -O2 -Inostr_core_lib/nostr_core -Inostr_core_lib/cjson $(shell pkg-config --cflags libwebsockets)
|
||||
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)
|
||||
SRCDIR = src
|
||||
BUILDDIR = build
|
||||
TARGET = $(BUILDDIR)/ginxsom-fcgi
|
||||
|
||||
Binary file not shown.
Binary file not shown.
BIN
build/main.o
BIN
build/main.o
Binary file not shown.
@@ -219,6 +219,25 @@ http {
|
||||
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/*)
|
||||
location /api/ {
|
||||
if ($request_method !~ ^(GET|PUT|POST)$) {
|
||||
@@ -570,6 +589,25 @@ http {
|
||||
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/*)
|
||||
location /api/ {
|
||||
if ($request_method !~ ^(GET|PUT|POST)$) {
|
||||
|
||||
Binary file not shown.
8
ginxsom.code-workspace
Normal file
8
ginxsom.code-workspace
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
||||
@@ -246,24 +246,37 @@ else
|
||||
echo -e "${YELLOW}Starting FastCGI in production mode - will generate new keys and create database${NC}"
|
||||
fi
|
||||
|
||||
# Start FastCGI application with proper logging (daemonized but with redirected streams)
|
||||
# Start FastCGI application with proper logging
|
||||
echo "FastCGI starting at $(date)" >> logs/app/stderr.log
|
||||
spawn-fcgi -s "$SOCKET_PATH" -M 666 -u "$USER" -g "$USER" -P "$PID_FILE" -- "$FCGI_BINARY" $FCGI_ARGS 1>>logs/app/stdout.log 2>>logs/app/stderr.log
|
||||
|
||||
if [ $? -eq 0 ] && [ -f "$PID_FILE" ]; then
|
||||
PID=$(cat "$PID_FILE")
|
||||
# Use nohup with spawn-fcgi -n to keep process running with redirected output
|
||||
# The key is: nohup prevents HUP signal, -n prevents daemonization (keeps stderr connected)
|
||||
nohup spawn-fcgi -n -s "$SOCKET_PATH" -M 666 -u "$USER" -g "$USER" -- "$FCGI_BINARY" $FCGI_ARGS >>logs/app/stdout.log 2>>logs/app/stderr.log </dev/null &
|
||||
SPAWN_PID=$!
|
||||
|
||||
# Wait for spawn-fcgi to spawn the child
|
||||
sleep 1
|
||||
|
||||
# Get the actual FastCGI process PID (child of spawn-fcgi)
|
||||
FCGI_PID=$(pgrep -f "ginxsom-fcgi.*--storage-dir" | head -1)
|
||||
if [ -z "$FCGI_PID" ]; then
|
||||
echo -e "${RED}Warning: Could not find FastCGI process${NC}"
|
||||
FCGI_PID=$SPAWN_PID
|
||||
fi
|
||||
|
||||
# Save PID
|
||||
echo $FCGI_PID > "$PID_FILE"
|
||||
|
||||
# Give it a moment to start
|
||||
sleep 1
|
||||
|
||||
if check_process "$FCGI_PID"; then
|
||||
echo -e "${GREEN}FastCGI application started successfully${NC}"
|
||||
echo "PID: $PID"
|
||||
|
||||
# Verify it's actually running
|
||||
if check_process "$PID"; then
|
||||
echo -e "${GREEN}Process confirmed running${NC}"
|
||||
else
|
||||
echo -e "${RED}Warning: Process may have crashed immediately${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo "PID: $FCGI_PID"
|
||||
echo -e "${GREEN}Process confirmed running${NC}"
|
||||
else
|
||||
echo -e "${RED}Failed to start FastCGI application${NC}"
|
||||
echo -e "${RED}Process may have crashed immediately${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -335,3 +348,7 @@ echo -e "${YELLOW}To monitor logs, check: logs/nginx/error.log, logs/nginx/acces
|
||||
echo -e "\n${YELLOW}Server is available at:${NC}"
|
||||
echo -e " ${GREEN}HTTP:${NC} http://localhost:9001"
|
||||
echo -e " ${GREEN}HTTPS:${NC} https://localhost:9443"
|
||||
echo -e "\n${YELLOW}Admin WebSocket endpoint:${NC}"
|
||||
echo -e " ${GREEN}WSS:${NC} wss://localhost:9443/admin (via nginx proxy)"
|
||||
echo -e " ${GREEN}WS:${NC} ws://localhost:9001/admin (via nginx proxy)"
|
||||
echo -e " ${GREEN}Direct:${NC} ws://localhost:9442 (direct connection)"
|
||||
@@ -1,163 +1,541 @@
|
||||
/*
|
||||
* Ginxsom Admin WebSocket Module
|
||||
* Ginxsom Admin WebSocket Server
|
||||
* Handles WebSocket connections for Kind 23456/23457 admin commands
|
||||
* Based on c-relay's WebSocket implementation
|
||||
* Based on c-relay's WebSocket implementation using libwebsockets
|
||||
*/
|
||||
|
||||
#include "ginxsom.h"
|
||||
#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_auth.c
|
||||
int process_admin_command(cJSON *event, char ***command_array_out, int *command_count_out, char **admin_pubkey_out);
|
||||
void free_command_array(char **command_array, int command_count);
|
||||
int create_admin_response(const char *response_json, const char *admin_pubkey, const char *original_event_id, cJSON **response_event_out);
|
||||
// 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);
|
||||
|
||||
// Forward declarations from admin_handlers.c (to be created)
|
||||
int execute_admin_command(char **command_array, int command_count, const char *admin_pubkey, char **response_json_out);
|
||||
// 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;
|
||||
};
|
||||
|
||||
// Handle WebSocket admin command endpoint (/api/admin)
|
||||
void handle_admin_websocket_request(void) {
|
||||
// For now, this is a placeholder for WebSocket implementation
|
||||
// In a full implementation, this would:
|
||||
// 1. Upgrade HTTP connection to WebSocket
|
||||
// 2. Handle WebSocket frames
|
||||
// 3. Process Kind 23456 events
|
||||
// 4. Send Kind 23457 responses
|
||||
// Global WebSocket context
|
||||
static struct lws_context *ws_context = NULL;
|
||||
static volatile int force_exit = 0;
|
||||
|
||||
printf("Status: 501 Not Implemented\r\n");
|
||||
printf("Content-Type: application/json\r\n\r\n");
|
||||
printf("{\n");
|
||||
printf(" \"error\": \"websocket_not_implemented\",\n");
|
||||
printf(" \"message\": \"WebSocket admin endpoint not yet implemented\",\n");
|
||||
printf(" \"note\": \"Use HTTP POST to /api/admin for now\"\n");
|
||||
printf("}\n");
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Handle HTTP POST admin command endpoint (/api/admin)
|
||||
void handle_admin_command_post_request(void) {
|
||||
// Read the request body (should contain Kind 23456 event JSON)
|
||||
const char *content_length_str = getenv("CONTENT_LENGTH");
|
||||
if (!content_length_str) {
|
||||
printf("Status: 400 Bad Request\r\n");
|
||||
printf("Content-Type: application/json\r\n\r\n");
|
||||
printf("{\n");
|
||||
printf(" \"error\": \"missing_content_length\",\n");
|
||||
printf(" \"message\": \"Content-Length header required\"\n");
|
||||
printf("}\n");
|
||||
return;
|
||||
}
|
||||
|
||||
long content_length = atol(content_length_str);
|
||||
if (content_length <= 0 || content_length > 1024 * 1024) { // 1MB limit
|
||||
printf("Status: 400 Bad Request\r\n");
|
||||
printf("Content-Type: application/json\r\n\r\n");
|
||||
printf("{\n");
|
||||
printf(" \"error\": \"invalid_content_length\",\n");
|
||||
printf(" \"message\": \"Content-Length must be between 1 and 1MB\"\n");
|
||||
printf("}\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// Read the request body
|
||||
char *request_body = malloc(content_length + 1);
|
||||
if (!request_body) {
|
||||
printf("Status: 500 Internal Server Error\r\n");
|
||||
printf("Content-Type: application/json\r\n\r\n");
|
||||
printf("{\n");
|
||||
printf(" \"error\": \"memory_allocation_failed\",\n");
|
||||
printf(" \"message\": \"Failed to allocate memory for request body\"\n");
|
||||
printf("}\n");
|
||||
return;
|
||||
}
|
||||
|
||||
size_t bytes_read = fread(request_body, 1, content_length, stdin);
|
||||
if (bytes_read != (size_t)content_length) {
|
||||
free(request_body);
|
||||
printf("Status: 400 Bad Request\r\n");
|
||||
printf("Content-Type: application/json\r\n\r\n");
|
||||
printf("{\n");
|
||||
printf(" \"error\": \"incomplete_request_body\",\n");
|
||||
printf(" \"message\": \"Failed to read complete request body\"\n");
|
||||
printf("}\n");
|
||||
return;
|
||||
}
|
||||
|
||||
request_body[content_length] = '\0';
|
||||
|
||||
// Parse the JSON event
|
||||
cJSON *event = cJSON_Parse(request_body);
|
||||
free(request_body);
|
||||
/**
|
||||
* 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) {
|
||||
printf("Status: 400 Bad Request\r\n");
|
||||
printf("Content-Type: application/json\r\n\r\n");
|
||||
printf("{\n");
|
||||
printf(" \"error\": \"invalid_json\",\n");
|
||||
printf(" \"message\": \"Request body is not valid JSON\"\n");
|
||||
printf("}\n");
|
||||
return;
|
||||
fprintf(stderr, "[WebSocket] Invalid JSON\n");
|
||||
fflush(stderr);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Process the admin command
|
||||
char **command_array = NULL;
|
||||
int command_count = 0;
|
||||
char *admin_pubkey = NULL;
|
||||
// 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;
|
||||
}
|
||||
|
||||
int result = process_admin_command(event, &command_array, &command_count, &admin_pubkey);
|
||||
// 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) {
|
||||
printf("Status: 400 Bad Request\r\n");
|
||||
printf("Content-Type: application/json\r\n\r\n");
|
||||
printf("{\n");
|
||||
printf(" \"error\": \"invalid_admin_command\",\n");
|
||||
printf(" \"message\": \"Failed to process admin command\"\n");
|
||||
printf("}\n");
|
||||
return;
|
||||
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;
|
||||
}
|
||||
|
||||
// Execute the command
|
||||
char *response_json = NULL;
|
||||
int exec_result = execute_admin_command(command_array, command_count, admin_pubkey, &response_json);
|
||||
free_command_array(command_array, command_count);
|
||||
free(admin_pubkey);
|
||||
|
||||
if (exec_result != 0) {
|
||||
printf("Status: 500 Internal Server Error\r\n");
|
||||
printf("Content-Type: application/json\r\n\r\n");
|
||||
printf("{\n");
|
||||
printf(" \"error\": \"command_execution_failed\",\n");
|
||||
printf(" \"message\": \"Failed to execute admin command\"\n");
|
||||
printf("}\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the response event (Kind 23457)
|
||||
cJSON *response_event = NULL;
|
||||
int create_result = create_admin_response(response_json, admin_pubkey, NULL, &response_event);
|
||||
free(response_json);
|
||||
|
||||
if (create_result != 0) {
|
||||
printf("Status: 500 Internal Server Error\r\n");
|
||||
printf("Content-Type: application/json\r\n\r\n");
|
||||
printf("{\n");
|
||||
printf(" \"error\": \"response_creation_failed\",\n");
|
||||
printf(" \"message\": \"Failed to create admin response\"\n");
|
||||
printf("}\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// Return the response event as JSON
|
||||
char *response_json_str = cJSON_Print(response_event);
|
||||
cJSON_Delete(response_event);
|
||||
|
||||
printf("Status: 200 OK\r\n");
|
||||
printf("Content-Type: application/json\r\n\r\n");
|
||||
printf("%s\n", response_json_str);
|
||||
|
||||
free(response_json_str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@@ -10,8 +10,8 @@
|
||||
// Version information (auto-updated by build system)
|
||||
#define VERSION_MAJOR 0
|
||||
#define VERSION_MINOR 1
|
||||
#define VERSION_PATCH 10
|
||||
#define VERSION "v0.1.10"
|
||||
#define VERSION_PATCH 11
|
||||
#define VERSION "v0.1.11"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
@@ -283,6 +283,10 @@ void send_json_response(int status, const char* json_content);
|
||||
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);
|
||||
|
||||
// Admin WebSocket server functions
|
||||
int start_admin_websocket_server(int port);
|
||||
void stop_admin_websocket_server(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
43
src/main.c
43
src/main.c
@@ -1830,8 +1830,18 @@ void handle_auth_challenge_request(void) {
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
// Redirect stderr to log file BEFORE any other operations
|
||||
// This is necessary because spawn-fcgi doesn't preserve stderr redirections
|
||||
FILE *stderr_log = freopen("logs/app/stderr.log", "a", stderr);
|
||||
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");
|
||||
fflush(stderr);
|
||||
|
||||
// Parse command line arguments
|
||||
int use_test_keys = 0;
|
||||
@@ -1934,6 +1944,9 @@ int main(int argc, char *argv[]) {
|
||||
if (end && (end - start) == 64) {
|
||||
strncpy(test_server_privkey, start, 64);
|
||||
test_server_privkey[64] = '\0';
|
||||
fprintf(stderr, "TEST MODE: Parsed SERVER_PRIVKEY: %s\n", test_server_privkey);
|
||||
} else {
|
||||
fprintf(stderr, "TEST MODE: Failed to parse SERVER_PRIVKEY (length: %ld)\n", end ? (long)(end - start) : -1L);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2022,6 +2035,7 @@ int main(int argc, char *argv[]) {
|
||||
if (db_path_specified) {
|
||||
fprintf(stderr, "\n=== SCENARIO 5: DATABASE + KEYS (VALIDATION) ===\n");
|
||||
strncpy(g_db_path, specified_db_path, sizeof(g_db_path) - 1);
|
||||
g_db_path[sizeof(g_db_path) - 1] = '\0';
|
||||
|
||||
// Check if database exists
|
||||
struct stat st;
|
||||
@@ -2117,6 +2131,7 @@ int main(int argc, char *argv[]) {
|
||||
else if (db_path_specified) {
|
||||
fprintf(stderr, "\n=== SCENARIO 2: DATABASE SPECIFIED ===\n");
|
||||
strncpy(g_db_path, specified_db_path, sizeof(g_db_path) - 1);
|
||||
g_db_path[sizeof(g_db_path) - 1] = '\0';
|
||||
|
||||
// Check if database exists
|
||||
struct stat st;
|
||||
@@ -2205,6 +2220,32 @@ if (!config_loaded /* && !initialize_server_config() */) {
|
||||
"STARTUP: Request validator system initialized successfully\r\n");
|
||||
fflush(stderr);
|
||||
|
||||
// Start WebSocket admin server if enabled
|
||||
sqlite3 *db;
|
||||
sqlite3_stmt *stmt;
|
||||
int rc = sqlite3_open_v2(g_db_path, &db, SQLITE_OPEN_READONLY, NULL);
|
||||
if (rc == SQLITE_OK) {
|
||||
const char *sql = "SELECT value FROM config WHERE key = 'admin_enabled'";
|
||||
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 {
|
||||
fprintf(stderr, "WARNING: Failed to start WebSocket admin server\n");
|
||||
}
|
||||
} else {
|
||||
fprintf(stderr, "STARTUP: Admin interface disabled in config\n");
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
sqlite3_close(db);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// THIS IS WHERE THE REQUESTS ENTER THE FastCGI
|
||||
|
||||
397
tests/websocket_admin_test.sh
Executable file
397
tests/websocket_admin_test.sh
Executable file
@@ -0,0 +1,397 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user