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

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

View File

@@ -2,4 +2,6 @@
description: "Brief description of what this command does"
---
Run build_and_push.sh, and supply a good git commit message.
Run build_and_push.sh, and supply a good git commit message. For example:
./build_and_push.sh "Fixed the bug with nip05 implementation"

1
.rooignore Normal file
View File

@@ -0,0 +1 @@
src/embedded_web_content.c

View File

@@ -9,7 +9,7 @@ LIBS = -lsqlite3 -lwebsockets -lz -ldl -lpthread -lm -L/usr/local/lib -lsecp256k
BUILD_DIR = build
# Source files
MAIN_SRC = src/main.c src/config.c src/request_validator.c src/nip009.c src/nip011.c src/nip013.c src/nip040.c src/nip042.c src/websockets.c src/subscriptions.c
MAIN_SRC = src/main.c src/config.c src/request_validator.c src/nip009.c src/nip011.c src/nip013.c src/nip040.c src/nip042.c src/websockets.c src/subscriptions.c src/api.c src/embedded_web_content.c
NOSTR_CORE_LIB = nostr_core_lib/libnostr_core_x64.a
# Architecture detection

View File

@@ -22,6 +22,18 @@ Do NOT modify the formatting, add emojis, or change the text. Keep the simple fo
- [x] NIP-50: Keywords filter
- [x] NIP-70: Protected Events
## 🌐 Web Admin Interface
C-Relay includes a **built-in web-based administration interface** accessible at `http://localhost:8888/api/`. The interface provides:
- **Real-time Configuration Management**: View and edit all relay settings through a web UI
- **Database Statistics Dashboard**: Monitor event counts, storage usage, and performance metrics
- **Auth Rules Management**: Configure whitelist/blacklist rules for pubkeys
- **NIP-42 Authentication**: Secure access using your Nostr identity
- **Event-Based Updates**: All changes are applied as cryptographically signed Nostr events
The web interface serves embedded static files with no external dependencies and includes proper CORS headers for browser compatibility.
## 🔧 Administrator API
C-Relay uses an innovative **event-based administration system** where all configuration and management commands are sent as signed Nostr events using the admin private key generated during first startup. All admin commands use **NIP-44 encrypted command arrays** for security and compatibility.
@@ -87,6 +99,7 @@ All commands are sent as NIP-44 encrypted JSON arrays in the event content. The
| **System Commands** |
| `system_clear_auth` | `["system_command", "clear_all_auth_rules"]` | Clear all auth rules |
| `system_status` | `["system_command", "system_status"]` | Get system status |
| `stats_query` | `["stats_query"]` | Get comprehensive database statistics |
### Available Configuration Keys
@@ -229,3 +242,18 @@ All admin commands return **signed EVENT responses** via WebSocket following sta
"sig": "response_event_signature"
}]
```
**Database Statistics Query Response:**
```json
["EVENT", "temp_sub_id", {
"id": "response_event_id",
"pubkey": "relay_public_key",
"created_at": 1234567890,
"kind": 23457,
"content": "nip44 encrypted:{\"query_type\": \"stats_query\", \"timestamp\": 1234567890, \"database_size_bytes\": 1048576, \"total_events\": 15432, \"database_created_at\": 1234567800, \"latest_event_at\": 1234567890, \"event_kinds\": [{\"kind\": 1, \"count\": 12000, \"percentage\": 77.8}, {\"kind\": 0, \"count\": 2500, \"percentage\": 16.2}], \"time_stats\": {\"total\": 15432, \"last_24h\": 234, \"last_7d\": 1456, \"last_30d\": 5432}, \"top_pubkeys\": [{\"pubkey\": \"abc123...\", \"event_count\": 1234, \"percentage\": 8.0}, {\"pubkey\": \"def456...\", \"event_count\": 987, \"percentage\": 6.4}]}",
"tags": [
["p", "admin_public_key"]
],
"sig": "response_event_signature"
}]
```

4095
api/index copy.html Normal file

File diff suppressed because it is too large Load Diff

455
api/index.css Normal file
View File

@@ -0,0 +1,455 @@
:root {
/* Core Variables (7) */
--primary-color: #000000;
--secondary-color: #ffffff;
--accent-color: #ff0000;
--muted-color: #dddddd;
--border-color: var(--muted-color);
--font-family: "Courier New", Courier, monospace;
--border-radius: 15px;
--border-width: 1px;
/* Floating Tab Variables (8) */
--tab-bg-logged-out: #ffffff;
--tab-bg-logged-in: #ffffff;
--tab-bg-opacity-logged-out: 0.9;
--tab-bg-opacity-logged-in: 0.2;
--tab-color-logged-out: #000000;
--tab-color-logged-in: #ffffff;
--tab-border-logged-out: #000000;
--tab-border-logged-in: #ff0000;
--tab-border-opacity-logged-out: 1.0;
--tab-border-opacity-logged-in: 0.1;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-family);
background-color: var(--secondary-color);
color: var(--primary-color);
/* line-height: 1.4; */
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
h1 {
border-bottom: var(--border-width) solid var(--border-color);
padding-bottom: 10px;
margin-bottom: 30px;
font-weight: normal;
font-size: 24px;
font-family: var(--font-family);
color: var(--primary-color);
}
h2 {
font-weight: normal;
padding-left: 10px;
font-size: 16px;
font-family: var(--font-family);
color: var(--primary-color);
}
.section {
background: var(--secondary-color);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius);
padding: 20px;
margin-bottom: 20px;
}
.input-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
font-size: 14px;
font-family: var(--font-family);
color: var(--primary-color);
}
input,
textarea,
select {
width: 100%;
padding: 8px;
background: var(--secondary-color);
color: var(--primary-color);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius);
font-family: var(--font-family);
font-size: 14px;
box-sizing: border-box;
transition: all 0.2s ease;
}
input:focus,
textarea:focus,
select:focus {
border-color: var(--accent-color);
outline: none;
}
button {
width: 100%;
padding: 8px;
background: var(--secondary-color);
color: var(--primary-color);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius);
font-family: var(--font-family);
font-size: 14px;
cursor: pointer;
margin: 5px 0;
font-weight: bold;
transition: all 0.2s ease;
}
button:hover {
border-color: var(--accent-color);
}
button:active {
background: var(--accent-color);
color: var(--secondary-color);
}
button:disabled {
background-color: #ccc;
color: var(--muted-color);
cursor: not-allowed;
border-color: #ccc;
}
.status {
padding: 10px;
margin: 10px 0;
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius);
font-weight: bold;
font-family: var(--font-family);
transition: all 0.2s ease;
}
.status.connected {
background-color: var(--primary-color);
color: var(--secondary-color);
}
.status.disconnected {
background-color: var(--secondary-color);
color: var(--primary-color);
}
.status.authenticated {
background-color: var(--primary-color);
color: var(--secondary-color);
}
.status.error {
background-color: var(--secondary-color);
color: var(--primary-color);
border-color: var(--accent-color);
}
.config-table {
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
width: 100%;
border-collapse: separate;
border-spacing: 0;
margin: 10px 0;
overflow: hidden;
}
.config-table th,
.config-table td {
border: 0.1px solid var(--muted-color);
padding: 4px;
text-align: left;
font-family: var(--font-family);
font-size: 10px;
}
.config-table-container {
overflow-x: auto;
max-width: 100%;
}
.config-table th {
font-weight: bold;
}
.config-table tr:hover {
background-color: var(--muted-color);
}
.json-display {
background-color: var(--secondary-color);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius);
padding: 10px;
font-family: var(--font-family);
font-size: 12px;
white-space: pre-wrap;
max-height: 300px;
overflow-y: auto;
margin: 10px 0;
}
.log-panel {
height: 200px;
overflow-y: auto;
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius);
padding: 10px;
font-size: 12px;
background-color: var(--secondary-color);
font-family: var(--font-family);
}
.log-entry {
margin-bottom: 5px;
border-bottom: 1px solid var(--muted-color);
padding-bottom: 5px;
}
.log-timestamp {
font-weight: bold;
font-family: var(--font-family);
}
.inline-buttons {
display: flex;
gap: 10px;
}
.inline-buttons button {
flex: 1;
}
.user-info {
padding: 10px;
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius);
margin: 10px 0;
background-color: var(--secondary-color);
}
.user-info-container {
display: flex;
align-items: flex-start;
gap: 20px;
}
.user-details {
flex: 1;
}
.login-logout-btn {
width: auto;
min-width: 120px;
padding: 12px 16px;
background: var(--secondary-color);
color: var(--primary-color);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius);
font-family: var(--font-family);
font-size: 14px;
font-weight: bold;
cursor: pointer;
transition: all 0.2s ease;
margin: 0;
flex-shrink: 0;
}
.login-logout-btn:hover {
border-color: var(--accent-color);
}
.login-logout-btn:active {
background: var(--accent-color);
color: var(--secondary-color);
}
.login-logout-btn.logout-state {
background: var(--accent-color);
color: var(--secondary-color);
border-color: var(--accent-color);
}
.login-logout-btn.logout-state:hover {
background: var(--primary-color);
border-color: var(--border-color);
}
.user-pubkey {
font-family: var(--font-family);
font-size: 12px;
word-break: break-all;
margin: 5px 0;
}
.hidden {
display: none;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
border-bottom: var(--border-width) solid var(--border-color);
padding-bottom: 10px;
}
.auth-rules-controls {
margin-bottom: 15px;
}
.section-header .status {
margin: 0;
padding: 5px 10px;
min-width: auto;
font-size: 12px;
}
/* Auth Rule Input Sections Styling */
.auth-rule-section {
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius);
padding: 15px;
margin: 15px 0;
background-color: var(--secondary-color);
}
.auth-rule-section h3 {
margin: 0 0 10px 0;
font-size: 14px;
font-weight: bold;
border-left: 4px solid var(--border-color);
padding-left: 8px;
font-family: var(--font-family);
color: var(--primary-color);
}
.auth-rule-section p {
margin: 0 0 15px 0;
font-size: 13px;
color: var(--muted-color);
font-family: var(--font-family);
}
.rule-status {
margin-top: 10px;
padding: 8px;
border: var(--border-width) solid var(--muted-color);
border-radius: var(--border-radius);
font-size: 12px;
min-height: 20px;
background-color: var(--secondary-color);
font-family: var(--font-family);
transition: all 0.2s ease;
}
.rule-status.success {
border-color: #4CAF50;
background-color: #E8F5E8;
color: #2E7D32;
}
.rule-status.error {
border-color: var(--accent-color);
background-color: #FFEBEE;
color: #C62828;
}
.rule-status.warning {
border-color: #FF9800;
background-color: #FFF3E0;
color: #E65100;
}
.warning-box {
border: var(--border-width) solid #FF9800;
border-radius: var(--border-radius);
background-color: #FFF3E0;
padding: 10px;
margin: 10px 0;
font-size: 13px;
color: #E65100;
font-family: var(--font-family);
}
.warning-box strong {
color: #D84315;
}
#login-section {
text-align: center;
padding: 20px;
}
/* Floating tab styles */
.floating-tab {
font-family: var(--font-family);
border-radius: var(--border-radius);
border: var(--border-width) solid;
transition: all 0.2s ease;
}
.floating-tab--logged-out {
background: rgba(255, 255, 255, var(--tab-bg-opacity-logged-out));
color: var(--tab-color-logged-out);
border-color: rgba(0, 0, 0, var(--tab-border-opacity-logged-out));
}
.floating-tab--logged-in {
background: rgba(0, 0, 0, var(--tab-bg-opacity-logged-in));
color: var(--tab-color-logged-in);
border-color: rgba(255, 0, 0, var(--tab-border-opacity-logged-in));
}
.transition {
transition: all 0.2s ease;
}
/* Main Sections Wrapper */
.main-sections-wrapper {
display: flex;
flex-wrap: wrap;
gap: var(--border-width);
margin-bottom: 20px;
}
.flex-section {
flex: 1;
min-width: 300px;
}
@media (max-width: 700px) {
body {
padding: 10px;
}
.inline-buttons {
flex-direction: column;
}
h1 {
font-size: 20px;
}
h2 {
font-size: 14px;
}
}

File diff suppressed because it is too large Load Diff

3277
api/index.js Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

3
deploy_local.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
cp build/c_relay_x86 ~/Storage/c_relay/crelay

View File

@@ -6,6 +6,7 @@ Complete guide for deploying, configuring, and managing the C Nostr Relay with e
- [Quick Start](#quick-start)
- [Installation](#installation)
- [Web Admin Interface](#web-admin-interface)
- [Configuration Management](#configuration-management)
- [Administration](#administration)
- [Monitoring](#monitoring)
@@ -43,7 +44,8 @@ Admin Public Key: 68394d08ab87f936a42ff2deb15a84fbdfbe0996ee0eb20cda064aae67328
### 3. Connect Clients
Your relay is now available at:
- **WebSocket**: `ws://localhost:8888`
- **NIP-11 Info**: `http://localhost:8888`
- **NIP-11 Info**: `http://localhost:8888` (with `Accept: application/nostr+json` header)
- **Web Admin Interface**: `http://localhost:8888/api/` (serves embedded admin interface)
## Installation
@@ -211,6 +213,38 @@ Send this to your relay via WebSocket, and changes are applied immediately.
| `nip40_expiration_filter` | Filter expired events | "true" | "true", "false" |
| `nip40_expiration_grace_period` | Grace period (seconds) | "300" | 0-86400 |
## Web Admin Interface
The relay includes a built-in web-based administration interface that provides a user-friendly way to manage your relay without command-line tools.
### Accessing the Interface
1. **Open your browser** and navigate to: `http://localhost:8888/api/`
2. **Authenticate** using your Nostr identity (the admin interface uses NIP-42 authentication)
3. **Manage configuration** through the web interface
### Features
- **Real-time Configuration**: View and edit all relay settings
- **Database Statistics**: Monitor event counts, storage usage, and performance metrics
- **Auth Rules Management**: Configure whitelist/blacklist rules for pubkeys
- **Relay Connection Testing**: Verify WebSocket connectivity and NIP-11 information
- **Event-Based Updates**: All changes are applied as signed Nostr events
### Security Notes
- The web interface requires NIP-42 authentication with your admin pubkey
- All configuration changes are cryptographically signed
- The interface serves embedded static files (no external dependencies)
- CORS headers are included for proper browser operation
### Browser Compatibility
The admin interface works with modern browsers that support:
- WebSocket connections
- ES6 JavaScript features
- Modern CSS Grid and Flexbox layouts
## Administration
### Viewing Current Configuration

128
embed_web_files.sh Executable file
View File

@@ -0,0 +1,128 @@
#!/bin/bash
# Script to embed web files into C headers for the C-Relay admin interface
# Converts HTML, CSS, and JS files from api/ directory into C byte arrays
set -e
echo "Embedding web files into C headers..."
# Output directory for generated headers
OUTPUT_DIR="src"
mkdir -p "$OUTPUT_DIR"
# Function to convert a file to C byte array
file_to_c_array() {
local input_file="$1"
local array_name="$2"
local output_file="$3"
# Get file size
local file_size=$(stat -c%s "$input_file" 2>/dev/null || stat -f%z "$input_file" 2>/dev/null || echo "0")
echo "// Auto-generated from $input_file" >> "$output_file"
echo "static const unsigned char ${array_name}_data[] = {" >> "$output_file"
# Convert file to hex bytes
hexdump -v -e '1/1 "0x%02x,"' "$input_file" >> "$output_file"
echo "};" >> "$output_file"
echo "static const size_t ${array_name}_size = $file_size;" >> "$output_file"
echo "" >> "$output_file"
}
# Generate the header file
HEADER_FILE="$OUTPUT_DIR/embedded_web_content.h"
echo "// Auto-generated embedded web content header" > "$HEADER_FILE"
echo "// Do not edit manually - generated by embed_web_files.sh" >> "$HEADER_FILE"
echo "" >> "$HEADER_FILE"
echo "#ifndef EMBEDDED_WEB_CONTENT_H" >> "$HEADER_FILE"
echo "#define EMBEDDED_WEB_CONTENT_H" >> "$HEADER_FILE"
echo "" >> "$HEADER_FILE"
echo "#include <stddef.h>" >> "$HEADER_FILE"
echo "" >> "$HEADER_FILE"
# Generate the C file
SOURCE_FILE="$OUTPUT_DIR/embedded_web_content.c"
echo "// Auto-generated embedded web content" > "$SOURCE_FILE"
echo "// Do not edit manually - generated by embed_web_files.sh" >> "$SOURCE_FILE"
echo "" >> "$SOURCE_FILE"
echo "#include \"embedded_web_content.h\"" >> "$SOURCE_FILE"
echo "#include <string.h>" >> "$SOURCE_FILE"
echo "" >> "$SOURCE_FILE"
# Process each web file
declare -A file_map
# Find all web files
for file in api/*.html api/*.css api/*.js; do
if [ -f "$file" ]; then
# Get filename without path
basename=$(basename "$file")
# Create C identifier from filename
c_name=$(echo "$basename" | sed 's/[^a-zA-Z0-9_]/_/g' | sed 's/^_//')
# Determine content type
case "$file" in
*.html) content_type="text/html" ;;
*.css) content_type="text/css" ;;
*.js) content_type="application/javascript" ;;
*) content_type="text/plain" ;;
esac
echo "Processing $file -> ${c_name}"
# No extern declarations needed - data is accessed through get_embedded_file()
# Add to source
file_to_c_array "$file" "$c_name" "$SOURCE_FILE"
# Store mapping for lookup function
file_map["/$basename"]="$c_name:$content_type"
if [ "$basename" = "index.html" ]; then
file_map["/"]="$c_name:$content_type"
fi
fi
done
# Generate lookup function
echo "// Embedded file lookup function" >> "$HEADER_FILE"
echo "typedef struct {" >> "$HEADER_FILE"
echo " const char *path;" >> "$HEADER_FILE"
echo " const unsigned char *data;" >> "$HEADER_FILE"
echo " size_t size;" >> "$HEADER_FILE"
echo " const char *content_type;" >> "$HEADER_FILE"
echo "} embedded_file_t;" >> "$HEADER_FILE"
echo "" >> "$HEADER_FILE"
echo "embedded_file_t *get_embedded_file(const char *path);" >> "$HEADER_FILE"
echo "" >> "$HEADER_FILE"
echo "#endif // EMBEDDED_WEB_CONTENT_H" >> "$HEADER_FILE"
# Generate lookup function implementation
echo "// File mapping" >> "$SOURCE_FILE"
echo "static embedded_file_t embedded_files[] = {" >> "$SOURCE_FILE"
for path in "${!file_map[@]}"; do
entry="${file_map[$path]}"
c_name="${entry%:*}"
content_type="${entry#*:}"
echo " {\"$path\", ${c_name}_data, ${c_name}_size, \"$content_type\"}," >> "$SOURCE_FILE"
done
echo " {NULL, NULL, 0, NULL} // Sentinel" >> "$SOURCE_FILE"
echo "};" >> "$SOURCE_FILE"
echo "" >> "$SOURCE_FILE"
echo "embedded_file_t *get_embedded_file(const char *path) {" >> "$SOURCE_FILE"
echo " for (int i = 0; embedded_files[i].path != NULL; i++) {" >> "$SOURCE_FILE"
echo " if (strcmp(path, embedded_files[i].path) == 0) {" >> "$SOURCE_FILE"
echo " return &embedded_files[i];" >> "$SOURCE_FILE"
echo " }" >> "$SOURCE_FILE"
echo " }" >> "$SOURCE_FILE"
echo " return NULL;" >> "$SOURCE_FILE"
echo "}" >> "$SOURCE_FILE"
echo "Web file embedding complete. Generated:" >&2
echo " $HEADER_FILE" >&2
echo " $SOURCE_FILE" >&2

3
nip_11_curl.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
curl -H "Accept: application/nostr+json" http://localhost:8888/

View File

@@ -1 +1 @@
135445
716467

165
src/api.c Normal file
View File

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

20
src/api.h Normal file
View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -355,6 +355,172 @@ cJSON* generate_relay_info_json() {
pthread_mutex_lock(&g_unified_cache.cache_lock);
// Defensive reinit: if relay_info appears empty (cache refresh wiped it), rebuild it directly from table
if (strlen(g_unified_cache.relay_info.name) == 0 &&
strlen(g_unified_cache.relay_info.description) == 0 &&
strlen(g_unified_cache.relay_info.software) == 0) {
log_warning("NIP-11 relay_info appears empty, rebuilding directly from config table");
// Rebuild relay_info directly from config table to avoid circular cache dependency
// Get values directly from table (similar to init_relay_info but without cache calls)
const char* relay_name = get_config_value_from_table("relay_name");
if (relay_name) {
strncpy(g_unified_cache.relay_info.name, relay_name, sizeof(g_unified_cache.relay_info.name) - 1);
free((char*)relay_name);
} else {
strncpy(g_unified_cache.relay_info.name, "C Nostr Relay", sizeof(g_unified_cache.relay_info.name) - 1);
}
const char* relay_description = get_config_value_from_table("relay_description");
if (relay_description) {
strncpy(g_unified_cache.relay_info.description, relay_description, sizeof(g_unified_cache.relay_info.description) - 1);
free((char*)relay_description);
} else {
strncpy(g_unified_cache.relay_info.description, "A high-performance Nostr relay implemented in C with SQLite storage", sizeof(g_unified_cache.relay_info.description) - 1);
}
const char* relay_software = get_config_value_from_table("relay_software");
if (relay_software) {
strncpy(g_unified_cache.relay_info.software, relay_software, sizeof(g_unified_cache.relay_info.software) - 1);
free((char*)relay_software);
} else {
strncpy(g_unified_cache.relay_info.software, "https://git.laantungir.net/laantungir/c-relay.git", sizeof(g_unified_cache.relay_info.software) - 1);
}
const char* relay_version = get_config_value_from_table("relay_version");
if (relay_version) {
strncpy(g_unified_cache.relay_info.version, relay_version, sizeof(g_unified_cache.relay_info.version) - 1);
free((char*)relay_version);
} else {
strncpy(g_unified_cache.relay_info.version, "0.2.0", sizeof(g_unified_cache.relay_info.version) - 1);
}
const char* relay_contact = get_config_value_from_table("relay_contact");
if (relay_contact) {
strncpy(g_unified_cache.relay_info.contact, relay_contact, sizeof(g_unified_cache.relay_info.contact) - 1);
free((char*)relay_contact);
}
const char* relay_pubkey = get_config_value_from_table("relay_pubkey");
if (relay_pubkey) {
strncpy(g_unified_cache.relay_info.pubkey, relay_pubkey, sizeof(g_unified_cache.relay_info.pubkey) - 1);
free((char*)relay_pubkey);
}
const char* posting_policy = get_config_value_from_table("posting_policy");
if (posting_policy) {
strncpy(g_unified_cache.relay_info.posting_policy, posting_policy, sizeof(g_unified_cache.relay_info.posting_policy) - 1);
free((char*)posting_policy);
}
const char* payments_url = get_config_value_from_table("payments_url");
if (payments_url) {
strncpy(g_unified_cache.relay_info.payments_url, payments_url, sizeof(g_unified_cache.relay_info.payments_url) - 1);
free((char*)payments_url);
}
// Rebuild supported_nips array
const char* supported_nips_csv = get_config_value_from_table("supported_nips");
if (supported_nips_csv) {
g_unified_cache.relay_info.supported_nips = parse_comma_separated_array(supported_nips_csv);
free((char*)supported_nips_csv);
} else {
g_unified_cache.relay_info.supported_nips = cJSON_CreateArray();
if (g_unified_cache.relay_info.supported_nips) {
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(1));
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(9));
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(11));
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(13));
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(15));
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(20));
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(40));
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(42));
}
}
// Rebuild limitation object
int max_message_length = 16384;
const char* max_msg_str = get_config_value_from_table("max_message_length");
if (max_msg_str) {
max_message_length = atoi(max_msg_str);
free((char*)max_msg_str);
}
int max_subscriptions_per_client = 20;
const char* max_subs_str = get_config_value_from_table("max_subscriptions_per_client");
if (max_subs_str) {
max_subscriptions_per_client = atoi(max_subs_str);
free((char*)max_subs_str);
}
int max_limit = 5000;
const char* max_limit_str = get_config_value_from_table("max_limit");
if (max_limit_str) {
max_limit = atoi(max_limit_str);
free((char*)max_limit_str);
}
int max_event_tags = 100;
const char* max_tags_str = get_config_value_from_table("max_event_tags");
if (max_tags_str) {
max_event_tags = atoi(max_tags_str);
free((char*)max_tags_str);
}
int max_content_length = 8196;
const char* max_content_str = get_config_value_from_table("max_content_length");
if (max_content_str) {
max_content_length = atoi(max_content_str);
free((char*)max_content_str);
}
int default_limit = 500;
const char* default_limit_str = get_config_value_from_table("default_limit");
if (default_limit_str) {
default_limit = atoi(default_limit_str);
free((char*)default_limit_str);
}
int admin_enabled = 0;
const char* admin_enabled_str = get_config_value_from_table("admin_enabled");
if (admin_enabled_str) {
admin_enabled = (strcmp(admin_enabled_str, "true") == 0) ? 1 : 0;
free((char*)admin_enabled_str);
}
g_unified_cache.relay_info.limitation = cJSON_CreateObject();
if (g_unified_cache.relay_info.limitation) {
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_message_length", max_message_length);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_subscriptions", max_subscriptions_per_client);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_limit", max_limit);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_subid_length", SUBSCRIPTION_ID_MAX_LENGTH);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_event_tags", max_event_tags);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_content_length", max_content_length);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "min_pow_difficulty", g_unified_cache.pow_config.min_pow_difficulty);
cJSON_AddBoolToObject(g_unified_cache.relay_info.limitation, "auth_required", admin_enabled ? cJSON_True : cJSON_False);
cJSON_AddBoolToObject(g_unified_cache.relay_info.limitation, "payment_required", cJSON_False);
cJSON_AddBoolToObject(g_unified_cache.relay_info.limitation, "restricted_writes", cJSON_False);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "created_at_lower_limit", 0);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "created_at_upper_limit", 2147483647);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "default_limit", default_limit);
}
// Rebuild other arrays (empty for now)
g_unified_cache.relay_info.retention = cJSON_CreateArray();
g_unified_cache.relay_info.language_tags = cJSON_CreateArray();
if (g_unified_cache.relay_info.language_tags) {
cJSON_AddItemToArray(g_unified_cache.relay_info.language_tags, cJSON_CreateString("*"));
}
g_unified_cache.relay_info.relay_countries = cJSON_CreateArray();
if (g_unified_cache.relay_info.relay_countries) {
cJSON_AddItemToArray(g_unified_cache.relay_info.relay_countries, cJSON_CreateString("*"));
}
g_unified_cache.relay_info.tags = cJSON_CreateArray();
g_unified_cache.relay_info.fees = cJSON_CreateObject();
log_info("NIP-11 relay_info rebuilt directly from config table");
}
// Add basic relay information
if (strlen(g_unified_cache.relay_info.name) > 0) {
cJSON_AddStringToObject(info, "name", g_unified_cache.relay_info.name);
@@ -435,6 +601,7 @@ cJSON* generate_relay_info_json() {
// NIP-11 HTTP session data structure for managing buffer lifetime
struct nip11_session_data {
int type; // 0 for NIP-11
char* json_buffer;
size_t json_length;
int headers_sent;
@@ -529,6 +696,9 @@ int handle_nip11_http_request(struct lws* wsi, const char* accept_header) {
}
size_t json_len = strlen(json_string);
log_info("Generated NIP-11 JSON");
printf(" JSON length: %zu bytes\n", json_len);
printf(" JSON preview: %.100s%s\n", json_string, json_len > 100 ? "..." : "");
// Allocate session data to manage buffer lifetime across callbacks
struct nip11_session_data* session_data = malloc(sizeof(struct nip11_session_data));
@@ -539,6 +709,7 @@ int handle_nip11_http_request(struct lws* wsi, const char* accept_header) {
}
// Store JSON buffer in session data for asynchronous handling
session_data->type = 0; // NIP-11
session_data->json_buffer = json_string;
session_data->json_length = json_len;
session_data->headers_sent = 0;
@@ -595,6 +766,13 @@ int handle_nip11_http_request(struct lws* wsi, const char* accept_header) {
return -1;
}
// Add Connection: close to ensure connection closes after response
if (lws_add_http_header_by_name(wsi, (unsigned char*)"connection:", (unsigned char*)"close", 5, &p, end)) {
free(session_data->json_buffer);
free(session_data);
return -1;
}
// Finalize headers
if (lws_finalize_http_header(wsi, &p, end)) {
free(session_data->json_buffer);

View File

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

View File

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

View File

@@ -34,6 +34,7 @@ struct per_session_data {
// NIP-11 HTTP session data structure for managing buffer lifetime
struct nip11_session_data {
int type; // 0 for NIP-11
char* json_buffer;
size_t json_length;
int headers_sent;

View File

@@ -1,133 +0,0 @@
#!/bin/bash
# Test dynamic config updates without restart
set -e
# Configuration from relay startup
ADMIN_PRIVKEY="ddea442930976541e199a05248eb6cd92f2a65ba366a883a8f6880add9bdc9c9"
RELAY_PUBKEY="1bd4a5e2e32401737f8c16cc0dfa89b93f25f395770a2896fe78c9fb61582dfc"
RELAY_URL="ws://localhost:8888"
# Colors
GREEN='\033[0;32m'
RED='\033[0;31m'
BLUE='\033[0;34m'
NC='\033[0m'
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Check if nak is available
if ! command -v nak &> /dev/null; then
log_error "nak command not found. Please install nak first."
exit 1
fi
log_info "Testing dynamic config updates without restart..."
# Test 1: Check current NIP-11 info
log_info "Checking current NIP-11 relay info..."
CURRENT_DESC=$(curl -s -H "Accept: application/nostr+json" http://localhost:8888 | jq -r '.description')
log_info "Current description: $CURRENT_DESC"
# Test 2: Update relay description dynamically
NEW_DESC="Dynamic Config Test - Updated at $(date)"
log_info "Updating relay description to: $NEW_DESC"
COMMAND="[\"config_update\", [{\"key\": \"relay_description\", \"value\": \"$NEW_DESC\", \"data_type\": \"string\", \"category\": \"relay\"}]]"
# Encrypt the command
ENCRYPTED_COMMAND=$(nak encrypt "$COMMAND" --sec "$ADMIN_PRIVKEY" --recipient-pubkey "$RELAY_PUBKEY")
if [ -z "$ENCRYPTED_COMMAND" ]; then
log_error "Failed to encrypt config update command"
exit 1
fi
# Create admin event
ADMIN_EVENT=$(nak event \
--kind 23456 \
--content "$ENCRYPTED_COMMAND" \
--sec "$ADMIN_PRIVKEY" \
--tag "p=$RELAY_PUBKEY")
# Send the admin command
log_info "Sending config update command..."
ADMIN_RESULT=$(echo "$ADMIN_EVENT" | nak event "$RELAY_URL")
if echo "$ADMIN_RESULT" | grep -q "error\|failed\|denied"; then
log_error "Failed to send config update: $ADMIN_RESULT"
exit 1
fi
log_success "Config update command sent successfully"
# Wait for processing
sleep 3
# Test 3: Check if NIP-11 info updated without restart
log_info "Checking if NIP-11 info was updated without restart..."
UPDATED_DESC=$(curl -s -H "Accept: application/nostr+json" http://localhost:8888 | jq -r '.description')
if [ "$UPDATED_DESC" = "$NEW_DESC" ]; then
log_success "SUCCESS: Relay description updated dynamically without restart!"
log_success "Old: $CURRENT_DESC"
log_success "New: $UPDATED_DESC"
else
log_error "FAILED: Relay description was not updated"
log_error "Expected: $NEW_DESC"
log_error "Got: $UPDATED_DESC"
exit 1
fi
# Test 4: Test another dynamic config - max_subscriptions_per_client
log_info "Testing another dynamic config: max_subscriptions_per_client"
# Get current value from database
OLD_LIMIT=$(sqlite3 build/*.db "SELECT value FROM config WHERE key = 'max_subscriptions_per_client';" 2>/dev/null || echo "25")
log_info "Current max_subscriptions_per_client: $OLD_LIMIT"
NEW_LIMIT=50
COMMAND2="[\"config_update\", [{\"key\": \"max_subscriptions_per_client\", \"value\": \"$NEW_LIMIT\", \"data_type\": \"integer\", \"category\": \"limits\"}]]"
ENCRYPTED_COMMAND2=$(nak encrypt "$COMMAND2" --sec "$ADMIN_PRIVKEY" --recipient-pubkey "$RELAY_PUBKEY")
ADMIN_EVENT2=$(nak event \
--kind 23456 \
--content "$ENCRYPTED_COMMAND2" \
--sec "$ADMIN_PRIVKEY" \
--tag "p=$RELAY_PUBKEY")
log_info "Updating max_subscriptions_per_client to $NEW_LIMIT..."
ADMIN_RESULT2=$(echo "$ADMIN_EVENT2" | nak event "$RELAY_URL")
if echo "$ADMIN_RESULT2" | grep -q "error\|failed\|denied"; then
log_error "Failed to send second config update: $ADMIN_RESULT2"
exit 1
fi
sleep 3
# Check updated value from database
UPDATED_LIMIT=$(sqlite3 build/*.db "SELECT value FROM config WHERE key = 'max_subscriptions_per_client';" 2>/dev/null || echo "25")
if [ "$UPDATED_LIMIT" = "$NEW_LIMIT" ]; then
log_success "SUCCESS: max_subscriptions_per_client updated dynamically!"
log_success "Old: $OLD_LIMIT, New: $UPDATED_LIMIT"
else
log_error "FAILED: max_subscriptions_per_client was not updated"
log_error "Expected: $NEW_LIMIT, Got: $UPDATED_LIMIT"
fi
log_success "Dynamic config update testing completed successfully!"

View File

@@ -1,97 +0,0 @@
#!/bin/bash
# Test script for NIP-50 search functionality
# This script tests the new search field in filter objects
echo "=== Testing NIP-50 Search Functionality ==="
# Function to send WebSocket message and capture response
send_ws_message() {
local message="$1"
echo "Sending: $message"
echo "$message" | websocat ws://127.0.0.1:8888 --text --no-close --one-message 2>/dev/null
}
# Function to publish an event
publish_event() {
local content="$1"
local kind="${2:-1}"
local tags="${3:-[]}"
# Create event JSON
local event="[\"EVENT\", {\"id\": \"\", \"pubkey\": \"\", \"created_at\": $(date +%s), \"kind\": $kind, \"tags\": $tags, \"content\": \"$content\", \"sig\": \"\"}]"
# Send the event
send_ws_message "$event"
}
# Function to search for events
search_events() {
local search_term="$1"
local sub_id="${2:-search_test}"
# Create search filter
local filter="{\"search\": \"$search_term\"}"
local req="[\"REQ\", \"$sub_id\", $filter]"
# Send the search request
send_ws_message "$req"
# Wait a moment for response
sleep 0.5
# Send CLOSE to end subscription
local close="[\"CLOSE\", \"$sub_id\"]"
send_ws_message "$close"
}
# Function to count events with search
count_events() {
local search_term="$1"
local sub_id="${2:-count_test}"
# Create count filter with search
local filter="{\"search\": \"$search_term\"}"
local count_req="[\"COUNT\", \"$sub_id\", $filter]"
# Send the count request
send_ws_message "$count_req"
}
echo "Publishing test events with searchable content..."
# Publish some test events with different content
publish_event "This is a test message about Bitcoin"
publish_event "Another message about Lightning Network"
publish_event "Nostr protocol discussion"
publish_event "Random content without keywords"
publish_event "Bitcoin and Lightning are great technologies"
publish_event "Discussion about Nostr and Bitcoin integration"
echo "Waiting for events to be stored..."
sleep 2
echo ""
echo "Testing search functionality..."
echo "1. Searching for 'Bitcoin':"
search_events "Bitcoin"
echo ""
echo "2. Searching for 'Nostr':"
search_events "Nostr"
echo ""
echo "3. Searching for 'Lightning':"
search_events "Lightning"
echo ""
echo "4. Testing COUNT with search:"
count_events "Bitcoin"
echo ""
echo "5. Testing COUNT with search for 'Nostr':"
count_events "Nostr"
echo ""
echo "=== NIP-50 Search Test Complete ==="

129
tests/stats_query_test.sh Executable file
View File

@@ -0,0 +1,129 @@
#!/bin/bash
# Test script for database statistics query functionality
# Tests the new stats_query admin API command
set -e
# Configuration
RELAY_HOST="127.0.0.1"
RELAY_PORT="8888"
ADMIN_PRIVKEY="f2f2bee9e45bec8ce1921f4c6dd6f6633c86ff291f56e480ac2bc47362dc2771"
ADMIN_PUBKEY="7a7a78cc7bd4c9879d67e2edd980730bda0d2a5e9e99b712e9307780b6bdbc03"
RELAY_PUBKEY="790ce38fbbbc9fdfa1723abe8f1a171c4005c869ab45df3dea4e0a0f201ba340"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
print_info() {
echo -e "${YELLOW}[INFO]${NC} $1"
}
print_success() {
echo -e "${GREEN}[PASS]${NC} $1"
}
print_failure() {
echo -e "${RED}[FAIL]${NC} $1"
}
print_test() {
echo -e "${BLUE}[TEST]${NC} $1"
}
# Check if relay is running
check_relay_running() {
if pgrep -f "c_relay_" > /dev/null; then
return 0
else
return 1
fi
}
# Create a stats_query event
create_stats_query_event() {
# Create the command array
COMMAND='["stats_query"]'
# Create the event JSON
EVENT=$(cat <<EOF
{
"id": "$(openssl rand -hex 32)",
"pubkey": "$ADMIN_PUBKEY",
"created_at": $(date +%s),
"kind": 23456,
"content": "encrypted_placeholder",
"tags": [
["p", "$RELAY_PUBKEY"]
],
"sig": "signature_placeholder"
}
EOF
)
echo "$EVENT"
}
print_test "Database Statistics Query Test"
if ! check_relay_running; then
print_failure "Relay is not running. Please start the relay first."
exit 1
fi
print_info "Relay is running, proceeding with stats_query test"
# Create the stats query event
EVENT_JSON=$(create_stats_query_event)
print_info "Created stats_query event"
# For now, we'll just test that the relay accepts connections
# A full end-to-end test would require implementing NIP-44 encryption/decryption
# and WebSocket communication, which is complex for a shell script
print_info "Testing basic WebSocket connectivity..."
# Test basic WebSocket connection with a simple ping
if command -v websocat >/dev/null 2>&1; then
print_info "Using websocat to test WebSocket connection"
# Send a basic Nostr REQ message to test connectivity
TEST_MESSAGE='["REQ", "test_sub", {"kinds": [1], "limit": 1}]'
# This is a basic connectivity test - full stats_query testing would require
# implementing NIP-44 encryption and proper event signing
if echo "$TEST_MESSAGE" | timeout 5 websocat "ws://$RELAY_HOST:$RELAY_PORT" >/dev/null 2>&1; then
print_success "WebSocket connection to relay successful"
else
print_failure "WebSocket connection to relay failed"
fi
elif command -v wscat >/dev/null 2>&1; then
print_info "Using wscat to test WebSocket connection"
# Basic connectivity test
if echo "$TEST_MESSAGE" | timeout 5 wscat -c "ws://$RELAY_HOST:$RELAY_PORT" >/dev/null 2>&1; then
print_success "WebSocket connection to relay successful"
else
print_failure "WebSocket connection to relay failed"
fi
else
print_info "No WebSocket client found (websocat or wscat). Testing HTTP endpoint instead..."
# Test HTTP endpoint (NIP-11)
if curl -s -H "Accept: application/nostr+json" "http://$RELAY_HOST:$RELAY_PORT" >/dev/null 2>&1; then
print_success "HTTP endpoint accessible"
else
print_failure "HTTP endpoint not accessible"
fi
fi
print_info "Basic connectivity test completed"
print_info "Note: Full stats_query testing requires NIP-44 encryption implementation"
print_info "The backend stats_query handler has been implemented and integrated"
print_info "Manual testing via the web interface (api/index.html) is recommended"
print_success "Stats query backend implementation test completed"