Compare commits

...

8 Commits

42 changed files with 9322 additions and 7121 deletions

3
.gitmodules vendored
View File

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

View File

@@ -76,6 +76,15 @@ RUN git submodule update --init --recursive
# Copy nostr_core_lib source files (cached unless nostr_core_lib changes)
COPY nostr_core_lib /build/nostr_core_lib/
# Copy c_utils_lib source files (cached unless c_utils_lib changes)
COPY c_utils_lib /build/c_utils_lib/
# Build c_utils_lib with MUSL-compatible flags (cached unless c_utils_lib changes)
RUN cd c_utils_lib && \
sed -i 's/CFLAGS = -Wall -Wextra -std=c99 -O2 -g/CFLAGS = -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -Wall -Wextra -std=c99 -O2 -g/' Makefile && \
make clean && \
make
# Build nostr_core_lib with required NIPs (cached unless nostr_core_lib changes)
# Disable fortification in build.sh to prevent __*_chk symbol issues
# NIPs: 001(Basic), 006(Keys), 013(PoW), 017(DMs), 019(Bech32), 044(Encryption), 059(Gift Wrap - required by NIP-17)
@@ -89,22 +98,23 @@ RUN cd nostr_core_lib && \
COPY src/ /build/src/
COPY Makefile /build/Makefile
# Build c-relay with full static linking (only rebuilds when src/ changes)
# Build c-relay with full static linking and debug symbols (only rebuilds when src/ changes)
# Disable fortification to avoid __*_chk symbols that don't exist in MUSL
RUN gcc -static -O2 -Wall -Wextra -std=c99 \
RUN gcc -static -g -O0 -DDEBUG -Wall -Wextra -std=c99 \
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \
-I. -Inostr_core_lib -Inostr_core_lib/nostr_core \
-I. -Ic_utils_lib/src -Inostr_core_lib -Inostr_core_lib/nostr_core \
-Inostr_core_lib/cjson -Inostr_core_lib/nostr_websocket \
src/main.c src/config.c src/dm_admin.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 \
-o /build/c_relay_static \
c_utils_lib/libc_utils.a \
nostr_core_lib/libnostr_core_x64.a \
-lwebsockets -lssl -lcrypto -lsqlite3 -lsecp256k1 \
-lcurl -lz -lpthread -lm -ldl
# Strip binary to reduce size
RUN strip /build/c_relay_static
# DO NOT strip - we need debug symbols for debugging
# RUN strip /build/c_relay_static
# Verify it's truly static
RUN echo "=== Binary Information ===" && \

View File

@@ -2,8 +2,8 @@
CC = gcc
CFLAGS = -Wall -Wextra -std=c99 -g -O2
INCLUDES = -I. -Inostr_core_lib -Inostr_core_lib/nostr_core -Inostr_core_lib/cjson -Inostr_core_lib/nostr_websocket
LIBS = -lsqlite3 -lwebsockets -lz -ldl -lpthread -lm -L/usr/local/lib -lsecp256k1 -lssl -lcrypto -L/usr/local/lib -lcurl
INCLUDES = -I. -Ic_utils_lib/src -Inostr_core_lib -Inostr_core_lib/nostr_core -Inostr_core_lib/cjson -Inostr_core_lib/nostr_websocket
LIBS = -lsqlite3 -lwebsockets -lz -ldl -lpthread -lm -L/usr/local/lib -lsecp256k1 -lssl -lcrypto -L/usr/local/lib -lcurl -Lc_utils_lib -lc_utils
# Build directory
BUILD_DIR = build
@@ -11,6 +11,7 @@ BUILD_DIR = build
# Source files
MAIN_SRC = src/main.c src/config.c src/dm_admin.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
C_UTILS_LIB = c_utils_lib/libc_utils.a
# Architecture detection
ARCH = $(shell uname -m)
@@ -38,6 +39,11 @@ $(NOSTR_CORE_LIB):
@echo "Building nostr_core_lib with required NIPs (including NIP-44 for encryption)..."
cd nostr_core_lib && ./build.sh --nips=1,6,13,17,19,44,59
# Check if c_utils_lib is built
$(C_UTILS_LIB):
@echo "Building c_utils_lib..."
cd c_utils_lib && ./build.sh lib
# Update main.h version information (requires main.h to exist)
src/main.h:
@if [ ! -f src/main.h ]; then \
@@ -75,18 +81,18 @@ force-version:
@$(MAKE) src/main.h
# Build the relay
$(TARGET): $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB)
$(TARGET): $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB) $(C_UTILS_LIB)
@echo "Compiling C-Relay for architecture: $(ARCH)"
$(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(TARGET) $(NOSTR_CORE_LIB) $(LIBS)
$(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(TARGET) $(NOSTR_CORE_LIB) $(C_UTILS_LIB) $(LIBS)
@echo "Build complete: $(TARGET)"
# Build for specific architectures
x86: $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB)
x86: $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB) $(C_UTILS_LIB)
@echo "Building C-Relay for x86_64..."
$(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(BUILD_DIR)/c_relay_x86 $(NOSTR_CORE_LIB) $(LIBS)
$(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(BUILD_DIR)/c_relay_x86 $(NOSTR_CORE_LIB) $(C_UTILS_LIB) $(LIBS)
@echo "Build complete: $(BUILD_DIR)/c_relay_x86"
arm64: $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB)
arm64: $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB) $(C_UTILS_LIB)
@echo "Cross-compiling C-Relay for ARM64..."
@if ! command -v aarch64-linux-gnu-gcc >/dev/null 2>&1; then \
echo "ERROR: ARM64 cross-compiler not found."; \
@@ -110,7 +116,7 @@ arm64: $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB)
fi
@echo "Using aarch64-linux-gnu-gcc with ARM64 libraries..."
PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig:/usr/share/pkgconfig \
aarch64-linux-gnu-gcc $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(BUILD_DIR)/c_relay_arm64 $(NOSTR_CORE_LIB) \
aarch64-linux-gnu-gcc $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(BUILD_DIR)/c_relay_arm64 $(NOSTR_CORE_LIB) $(C_UTILS_LIB) \
-L/usr/lib/aarch64-linux-gnu $(LIBS)
@echo "Build complete: $(BUILD_DIR)/c_relay_arm64"
@@ -161,9 +167,10 @@ clean:
rm -rf $(BUILD_DIR)
@echo "Clean complete"
# Clean everything including nostr_core_lib
# Clean everything including nostr_core_lib and c_utils_lib
clean-all: clean
cd nostr_core_lib && make clean 2>/dev/null || true
cd c_utils_lib && make clean 2>/dev/null || true
# Install dependencies (Ubuntu/Debian)
install-deps:

58
api/embedded.html Normal file
View File

@@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Embedded NOSTR_LOGIN_LITE</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 40px;
background: white;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
max-width: 400px;
width: 100%;
}
#login-container {
/* No styling - let embedded modal blend seamlessly */
}
</style>
</head>
<body>
<div class="container">
<div id="login-container"></div>
</div>
<script src="../lite/nostr.bundle.js"></script>
<script src="../lite/nostr-lite.js"></script>
<script>
document.addEventListener('DOMContentLoaded', async () => {
await window.NOSTR_LOGIN_LITE.init({
theme:'default',
methods: {
extension: true,
local: true,
seedphrase: true,
readonly: true,
connect: true,
remote: true,
otp: true
}
});
window.NOSTR_LOGIN_LITE.embed('#login-container', {
seamless: true
});
});
</script>
</body>
</html>

View File

@@ -33,11 +33,142 @@ body {
background-color: var(--secondary-color);
color: var(--primary-color);
/* line-height: 1.4; */
padding: 20px;
padding: 0;
max-width: none;
margin: 0;
}
/* Header Styles */
.main-header {
background-color: var(--secondary-color);
border-bottom: var(--border-width) solid var(--border-color);
padding: 15px 20px;
position: sticky;
top: 0;
z-index: 100;
max-width: 1200px;
margin: 0 auto;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
}
.header-title {
margin: 0;
font-size: 24px;
font-weight: normal;
color: var(--primary-color);
border: none;
padding: 0;
text-align: left;
}
.header-user-name {
display: block;
font-weight: 500;
color: var(--primary-color);
font-size: 10px;
text-align: center;
margin-top: 4px;
}
.profile-area {
display: flex;
align-items: center;
position: relative;
cursor: pointer;
padding: 8px 12px;
border-radius: var(--border-radius);
transition: background-color 0.2s ease;
margin-left: auto;
}
.profile-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.profile-area:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.profile-info {
display: flex;
align-items: center;
gap: 10px;
}
.header-user-image {
width: 48px; /* 50% larger than 32px */
height: 48px; /* 50% larger than 32px */
border-radius: var(--border-radius); /* Curved corners like other elements */
object-fit: cover;
border: 2px solid transparent; /* Invisible border */
background-color: var(--secondary-color);
}
.logout-dropdown {
position: absolute;
top: 100%;
right: 0;
background-color: var(--secondary-color);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
min-width: 120px;
z-index: 200;
margin-top: 4px;
}
.logout-btn {
width: 100%;
padding: 10px 15px;
background: none;
border: none;
color: var(--primary-color);
text-align: left;
cursor: pointer;
font-size: 14px;
font-family: var(--font-family);
border-radius: var(--border-radius);
transition: background-color 0.2s ease;
}
.logout-btn:hover {
background-color: rgba(0, 0, 0, 0.1);
}
/* Login Modal Styles */
.login-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.login-modal-content {
background-color: var(--secondary-color);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius);
padding: 30px;
max-width: 400px;
width: 90%;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
h1 {
border-bottom: var(--border-width) solid var(--border-color);
padding-bottom: 10px;
@@ -130,6 +261,40 @@ button:disabled {
border-color: #ccc;
}
/* Flash animation for refresh button */
@keyframes flash-red {
0% { border-color: var(--border-color); }
50% { border-color: var(--accent-color); }
100% { border-color: var(--border-color); }
}
.flash-red {
animation: flash-red 0.5s ease-in-out;
}
/* Flash animation for updated statistics values */
@keyframes flash-value {
0% { color: var(--primary-color); }
50% { color: var(--accent-color); }
100% { color: var(--primary-color); }
}
.flash-value {
animation: flash-value 0.5s ease-in-out;
}
/* Npub links styling */
.npub-link {
color: var(--primary-color);
text-decoration: none;
font-weight: normal;
transition: color 0.2s ease;
}
.npub-link:hover {
color: var(--accent-color);
}
.status {
padding: 10px;
margin: 10px 0;
@@ -218,9 +383,13 @@ button:disabled {
.config-actions-cell {
cursor: pointer;
transition: all 0.2s ease;
text-align: center;
text-align: center !important;
font-weight: bold;
vertical-align: middle;
width: 60px;
min-width: 60px;
max-width: 60px;
padding: 8px 4px;
}
.config-actions-cell:hover {
@@ -282,12 +451,21 @@ button:disabled {
.user-info-container {
display: flex;
align-items: flex-start;
gap: 20px;
flex-direction: column;
gap: 15px;
}
.user-details {
flex: 1;
order: -1; /* Show user details first when logged in */
}
.login-section {
text-align: center;
}
.logout-section {
display: flex;
justify-content: flex-end;
}
.login-logout-btn {
@@ -334,6 +512,31 @@ button:disabled {
margin: 5px 0;
}
/* User profile header with image */
.user-profile-header {
display: flex;
align-items: flex-start;
gap: 15px;
}
.user-image-container {
flex-shrink: 0;
}
.user-profile-image {
width: 60px;
height: 60px;
border-radius: var(--border-radius);
object-fit: cover;
border: 2px solid var(--border-color);
background-color: var(--bg-color);
}
.user-text-info {
flex: 1;
min-width: 0; /* Allow text to wrap */
}
.hidden {
display: none;
}
@@ -347,6 +550,40 @@ button:disabled {
padding-bottom: 10px;
}
.countdown-btn {
width: auto;
min-width: 40px;
padding: 8px 12px;
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: 10px;
/* font-weight: bold; */
cursor: pointer;
transition: all 0.2s ease;
margin-left: auto;
position: relative;
}
.countdown-btn:hover::after {
content: "countdown";
position: absolute;
top: -30px;
left: 50%;
transform: translateX(-50%);
background: var(--primary-color);
color: var(--secondary-color);
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: normal;
white-space: nowrap;
z-index: 1000;
border: 1px solid var(--border-color);
}
.auth-rules-controls {
margin-bottom: 15px;
}
@@ -460,10 +697,12 @@ button:disabled {
/* Main Sections Wrapper */
.main-sections-wrapper {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
display: flex;
flex-wrap: wrap;
gap: var(--border-width);
margin-bottom: 20px;
}
.flex-section {

View File

@@ -4,38 +4,37 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>C-Relay Admin API</title>
<title>C-Relay Admin</title>
<link rel="stylesheet" href="/api/index.css">
</head>
<body>
<h1>C-RELAY ADMIN API</h1>
<!-- Main Sections Wrapper -->
<div class="main-sections-wrapper">
<!-- Persistent Authentication Header - Always Visible -->
<div id="persistent-auth-container" class="section flex-section">
<div class="user-info-container">
<button type="button" id="login-logout-btn" class="login-logout-btn">LOGIN</button>
<div class="user-details" id="persistent-user-details" style="display: none;">
<div><strong>Name:</strong> <span id="persistent-user-name">Loading...</span></div>
<div><strong>Public Key:</strong>
<div class="user-pubkey" id="persistent-user-pubkey">Loading...</div>
</div>
<div><strong>About:</strong> <span id="persistent-user-about">Loading...</span></div>
<!-- Header with title and profile display -->
<header class="main-header">
<div class="header-content">
<div class="header-title">RELAY</div>
<div class="profile-area" id="profile-area" style="display: none;">
<div class="profile-container">
<img id="header-user-image" class="header-user-image" alt="Profile" style="display: none;">
<span id="header-user-name" class="header-user-name">Loading...</span>
</div>
<!-- Logout dropdown -->
<div class="logout-dropdown" id="logout-dropdown" style="display: none;">
<button type="button" id="logout-btn" class="logout-btn">LOGOUT</button>
</div>
</div>
</div>
</header>
<!-- Login Section -->
<div id="login-section" class="flex-section">
<div class="section">
<h2>NOSTR AUTHENTICATION</h2>
<p id="login-instructions">Please login with your Nostr identity to access the admin interface.</p>
<!-- nostr-lite login UI will be injected here -->
</div>
<!-- Login Modal Overlay -->
<div id="login-modal" class="login-modal-overlay" style="display: none;">
<div class="login-modal-content">
<div id="login-modal-container"></div>
</div>
</div>
<!-- Main Sections Wrapper -->
<div class="main-sections-wrapper">
<!-- Relay Connection Section -->
<div id="relay-connection-section" class="flex-section">
@@ -44,7 +43,7 @@
<div class="input-group">
<label for="relay-connection-url">Relay URL:</label>
<input type="text" id="relay-connection-url" value="ws://localhost:8888"
<input type="text" id="relay-connection-url" value=""
placeholder="ws://localhost:8888 or wss://relay.example.com">
</div>
@@ -86,97 +85,11 @@
</div> <!-- End Main Sections Wrapper -->
<!-- Testing Section -->
<div id="div_config" class="section flex-section" style="display: none;">
<h2>RELAY CONFIGURATION</h2>
<div id="config-display" class="hidden">
<div class="config-table-container">
<table class="config-table" id="config-table">
<thead>
<tr>
<th>Parameter</th>
<th>Value</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="config-table-body">
</tbody>
</table>
</div>
<div class="inline-buttons">
<button type="button" id="fetch-config-btn">REFRESH</button>
</div>
</div>
</div>
<!-- Auth Rules Management - Moved after configuration -->
<div class="section flex-section" id="authRulesSection" style="display: none;">
<div class="section-header">
<h2>AUTH RULES MANAGEMENT</h2>
</div>
<!-- Auth Rules Table -->
<div id="authRulesTableContainer" style="display: none;">
<table class="config-table" id="authRulesTable">
<thead>
<tr>
<th>Rule Type</th>
<th>Pattern Type</th>
<th>Pattern Value</th>
<th>Action</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="authRulesTableBody">
</tbody>
</table>
</div>
<!-- Simplified Auth Rule Input Section -->
<div id="authRuleInputSections" style="display: block;">
<!-- Combined Pubkey Auth Rule Section -->
<div class="input-group">
<label for="authRulePubkey">Pubkey (nsec or hex):</label>
<input type="text" id="authRulePubkey" placeholder="nsec1... or 64-character hex pubkey">
</div>
<div id="whitelistWarning" class="warning-box" style="display: none;">
<strong>⚠️ WARNING:</strong> Adding whitelist rules changes relay behavior to whitelist-only
mode.
Only whitelisted users will be able to interact with the relay.
</div>
<div class="inline-buttons">
<button type="button" id="addWhitelistBtn" onclick="addWhitelistRule()">ADD TO
WHITELIST</button>
<button type="button" id="addBlacklistBtn" onclick="addBlacklistRule()">ADD TO
BLACKLIST</button>
<button type="button" id="refreshAuthRulesBtn">REFRESH</button>
</div>
</div>
</div>
<!-- DATABASE STATISTICS Section -->
<div class="section">
<div class="section flex-section" id="databaseStatisticsSection" style="display: none;">
<div class="section-header">
<h2>DATABASE STATISTICS</h2>
<button type="button" id="refresh-stats-btn" class="countdown-btn"></button>
</div>
@@ -294,12 +207,92 @@
</div>
</div>
<!-- Refresh Button -->
<div class="input-group">
<button type="button" id="refresh-stats-btn">REFRESH STATISTICS</button>
</div>
<!-- Testing Section -->
<div id="div_config" class="section flex-section" style="display: none;">
<h2>RELAY CONFIGURATION</h2>
<div id="config-display" class="hidden">
<div class="config-table-container">
<table class="config-table" id="config-table">
<thead>
<tr>
<th>Parameter</th>
<th>Value</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="config-table-body">
</tbody>
</table>
</div>
<div class="inline-buttons">
<button type="button" id="fetch-config-btn">REFRESH</button>
</div>
</div>
</div>
<!-- Auth Rules Management - Moved after configuration -->
<div class="section flex-section" id="authRulesSection" style="display: none;">
<div class="section-header">
<h2>AUTH RULES MANAGEMENT</h2>
</div>
<!-- Auth Rules Table -->
<div id="authRulesTableContainer" style="display: none;">
<table class="config-table" id="authRulesTable">
<thead>
<tr>
<th>Rule Type</th>
<th>Pattern Type</th>
<th>Pattern Value</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="authRulesTableBody">
</tbody>
</table>
</div>
<!-- Simplified Auth Rule Input Section -->
<div id="authRuleInputSections" style="display: block;">
<!-- Combined Pubkey Auth Rule Section -->
<div class="input-group">
<label for="authRulePubkey">Pubkey (nsec or hex):</label>
<input type="text" id="authRulePubkey" placeholder="nsec1... or 64-character hex pubkey">
</div>
<div id="whitelistWarning" class="warning-box" style="display: none;">
<strong>⚠️ WARNING:</strong> Adding whitelist rules changes relay behavior to whitelist-only
mode.
Only whitelisted users will be able to interact with the relay.
</div>
<div class="inline-buttons">
<button type="button" id="addWhitelistBtn" onclick="addWhitelistRule()">ADD TO
WHITELIST</button>
<button type="button" id="addBlacklistBtn" onclick="addBlacklistRule()">ADD TO
BLACKLIST</button>
<button type="button" id="refreshAuthRulesBtn">REFRESH</button>
</div>
</div>
</div>
<!-- NIP-17 DIRECT MESSAGES Section -->
<div class="section" id="nip17DMSection" style="display: none;">
<div class="section-header">

File diff suppressed because it is too large Load Diff

1
c_utils_lib Submodule

Submodule c_utils_lib added at 442facd7e3

View File

@@ -1,7 +1,7 @@
#!/bin/bash
# C-Relay Static Binary Deployment Script
# Deploys build/c_relay_static_x86_64 to server via sshlt
# Deploys build/c_relay_static_x86_64 to server via ssh
set -e
@@ -21,7 +21,8 @@ ssh ubuntu@laantungir.com "sudo mv '/tmp/c_relay.tmp' '$REMOTE_BINARY_PATH'"
ssh ubuntu@laantungir.com "sudo chown c-relay:c-relay '$REMOTE_BINARY_PATH'"
ssh ubuntu@laantungir.com "sudo chmod +x '$REMOTE_BINARY_PATH'"
# Restart service
# Reload systemd and restart service
ssh ubuntu@laantungir.com "sudo systemctl daemon-reload"
ssh ubuntu@laantungir.com "sudo systemctl restart '$SERVICE_NAME'"
echo "Deployment complete!"

View File

@@ -0,0 +1,457 @@
# c_utils_lib Architecture Plan
## Overview
`c_utils_lib` is a standalone C utility library designed to provide reusable, general-purpose functions for C projects. It serves as a learning repository and a practical toolkit for common C programming tasks.
## Design Philosophy
1. **Zero External Dependencies**: Only standard C library dependencies
2. **Modular Design**: Each utility is independent and can be used separately
3. **Learning-Oriented**: Well-documented code suitable for learning C
4. **Production-Ready**: Battle-tested utilities from real projects
5. **Cross-Platform**: Works on Linux, macOS, and other POSIX systems
## Repository Structure
```
c_utils_lib/
├── README.md # Main documentation
├── LICENSE # MIT License
├── VERSION # Current version (e.g., v0.1.0)
├── build.sh # Build script
├── Makefile # Build system
├── .gitignore # Git ignore rules
├── include/ # Public headers
│ ├── c_utils.h # Main header (includes all utilities)
│ ├── debug.h # Debug/logging system
│ ├── version.h # Version utilities
│ ├── string_utils.h # String utilities (future)
│ └── memory_utils.h # Memory utilities (future)
├── src/ # Implementation files
│ ├── debug.c # Debug system implementation
│ ├── version.c # Version utilities implementation
│ ├── string_utils.c # String utilities (future)
│ └── memory_utils.c # Memory utilities (future)
├── examples/ # Usage examples
│ ├── debug_example.c # Debug system example
│ ├── version_example.c # Version utilities example
│ └── Makefile # Examples build system
├── tests/ # Unit tests
│ ├── test_debug.c # Debug system tests
│ ├── test_version.c # Version utilities tests
│ ├── run_tests.sh # Test runner
│ └── Makefile # Tests build system
└── docs/ # Additional documentation
├── API.md # Complete API reference
├── INTEGRATION.md # How to integrate into projects
├── VERSIONING.md # Versioning system guide
└── CONTRIBUTING.md # Contribution guidelines
```
## Initial Utilities (v0.1.0)
### 1. Debug System (`debug.h`, `debug.c`)
**Purpose**: Unified logging and debugging system with configurable verbosity levels.
**Features**:
- 5 debug levels: NONE, ERROR, WARN, INFO, DEBUG, TRACE
- Timestamp formatting
- File/line information at TRACE level
- Macro-based API for zero-cost when disabled
- Thread-safe (future enhancement)
**API**:
```c
// Initialization
void debug_init(int level);
// Logging macros
DEBUG_ERROR(format, ...);
DEBUG_WARN(format, ...);
DEBUG_INFO(format, ...);
DEBUG_LOG(format, ...);
DEBUG_TRACE(format, ...);
// Global debug level
extern debug_level_t g_debug_level;
```
**Usage Example**:
```c
#include <c_utils/debug.h>
int main() {
debug_init(DEBUG_LEVEL_INFO);
DEBUG_INFO("Application started");
DEBUG_ERROR("Critical error: %s", error_msg);
return 0;
}
```
### 2. Version Utilities (`version.h`, `version.c`)
**Purpose**: Reusable versioning system for C projects using git tags.
**Features**:
- Automatic version extraction from git tags
- Semantic versioning support (MAJOR.MINOR.PATCH)
- Version comparison functions
- Header file generation for embedding version info
- Build number tracking
**API**:
```c
// Version structure
typedef struct {
int major;
int minor;
int patch;
char* git_hash;
char* build_date;
} version_info_t;
// Get version from git
int version_get_from_git(version_info_t* version);
// Generate version header file
int version_generate_header(const char* output_path, const char* prefix);
// Compare versions
int version_compare(version_info_t* v1, version_info_t* v2);
// Format version string
char* version_to_string(version_info_t* version);
```
**Usage Example**:
```c
#include <c_utils/version.h>
// In your build system:
version_generate_header("src/version.h", "MY_APP");
// In your code:
#include "version.h"
printf("Version: %s\n", MY_APP_VERSION);
```
**Integration with Projects**:
```bash
# In project Makefile
version.h:
c_utils_lib/bin/generate_version src/version.h MY_PROJECT
```
## Build System
### Static Library Output
```
libc_utils.a # Static library for linking
```
### Build Targets
```bash
make # Build static library
make examples # Build examples
make test # Run tests
make install # Install to system (optional)
make clean # Clean build artifacts
```
### Build Script (`build.sh`)
```bash
#!/bin/bash
# Simplified build script similar to nostr_core_lib
case "$1" in
lib|"")
make
;;
examples)
make examples
;;
test)
make test
;;
clean)
make clean
;;
install)
make install
;;
*)
echo "Usage: ./build.sh [lib|examples|test|clean|install]"
exit 1
;;
esac
```
## Versioning System Design
### How It Works
1. **Git Tags as Source of Truth**
- Version tags: `v0.1.0`, `v0.2.0`, etc.
- Follows semantic versioning
2. **Automatic Header Generation**
- Script reads git tags
- Generates header with version macros
- Includes build date and git hash
3. **Reusable Across Projects**
- Each project calls `version_generate_header()`
- Customizable prefix (e.g., `C_RELAY_VERSION`, `NOSTR_CORE_VERSION`)
- No hardcoded version numbers in source
### Example Generated Header
```c
// Auto-generated by c_utils_lib version system
#ifndef MY_PROJECT_VERSION_H
#define MY_PROJECT_VERSION_H
#define MY_PROJECT_VERSION "v0.1.0"
#define MY_PROJECT_VERSION_MAJOR 0
#define MY_PROJECT_VERSION_MINOR 1
#define MY_PROJECT_VERSION_PATCH 0
#define MY_PROJECT_GIT_HASH "a1b2c3d"
#define MY_PROJECT_BUILD_DATE "2025-10-15"
#endif
```
### Integration Pattern
```makefile
# In consuming project's Makefile
VERSION_SCRIPT = c_utils_lib/bin/generate_version
src/version.h: .git/refs/tags/*
$(VERSION_SCRIPT) src/version.h MY_PROJECT
my_app: src/version.h src/main.c
$(CC) src/main.c -o my_app -Ic_utils_lib/include -Lc_utils_lib -lc_utils
```
## Future Utilities (Roadmap)
### String Utilities (`string_utils.h`)
- Safe string operations (bounds checking)
- String trimming, splitting, joining
- Case conversion
- Pattern matching helpers
### Memory Utilities (`memory_utils.h`)
- Safe allocation wrappers
- Memory pool management
- Leak detection helpers (debug builds)
- Arena allocators
### Configuration Utilities (`config_utils.h`)
- INI file parsing
- JSON configuration (using cJSON)
- Environment variable helpers
- Command-line argument parsing
### File Utilities (`file_utils.h`)
- Safe file operations
- Directory traversal
- Path manipulation
- File watching (inotify wrapper)
### Time Utilities (`time_utils.h`)
- Timestamp formatting
- Duration calculations
- Timer utilities
- Rate limiting helpers
## Integration Guide
### As Git Submodule
```bash
# In your project
git submodule add https://github.com/yourusername/c_utils_lib.git
git submodule update --init --recursive
# Build the library
cd c_utils_lib && ./build.sh lib && cd ..
# Update your Makefile
INCLUDES += -Ic_utils_lib/include
LIBS += -Lc_utils_lib -lc_utils
```
### In Your Makefile
```makefile
# Check if c_utils_lib is built
c_utils_lib/libc_utils.a:
cd c_utils_lib && ./build.sh lib
# Link against it
my_app: c_utils_lib/libc_utils.a src/main.c
$(CC) src/main.c -o my_app \
-Ic_utils_lib/include \
-Lc_utils_lib -lc_utils
```
### In Your Code
```c
// Option 1: Include everything
#include <c_utils/c_utils.h>
// Option 2: Include specific utilities
#include <c_utils/debug.h>
#include <c_utils/version.h>
int main() {
debug_init(DEBUG_LEVEL_INFO);
DEBUG_INFO("Starting application version %s", MY_APP_VERSION);
return 0;
}
```
## Migration Plan for c-relay
### Phase 1: Extract Debug System
1. Create `c_utils_lib` repository
2. Move [`debug.c`](../src/debug.c) and [`debug.h`](../src/debug.h)
3. Create build system
4. Add basic tests
### Phase 2: Add Versioning System
1. Extract version generation logic from c-relay
2. Create reusable version utilities
3. Update c-relay to use new system
4. Update nostr_core_lib to use new system
### Phase 3: Add as Submodule
1. Add `c_utils_lib` as submodule to c-relay
2. Update c-relay Makefile
3. Update includes in c-relay source files
4. Remove old debug files from c-relay
### Phase 4: Documentation & Examples
1. Create comprehensive README
2. Add usage examples
3. Write integration guide
4. Document API
## Benefits
### For c-relay
- Cleaner separation of concerns
- Reusable utilities across projects
- Easier to maintain and test
- Consistent logging across codebase
### For Learning C
- Real-world utility implementations
- Best practices examples
- Modular design patterns
- Build system examples
### For Future Projects
- Drop-in utility library
- Proven, tested code
- Consistent patterns
- Time savings
## Testing Strategy
### Unit Tests
- Test each utility independently
- Mock external dependencies
- Edge case coverage
- Memory leak detection (valgrind)
### Integration Tests
- Test with real projects (c-relay, nostr_core_lib)
- Cross-platform testing
- Performance benchmarks
### Continuous Integration
- GitHub Actions for automated testing
- Multiple compiler versions (gcc, clang)
- Multiple platforms (Linux, macOS)
- Static analysis (cppcheck, clang-tidy)
## Documentation Standards
### Code Documentation
- Doxygen-style comments
- Function purpose and parameters
- Return value descriptions
- Usage examples in comments
### API Documentation
- Complete API reference in `docs/API.md`
- Usage examples for each function
- Common patterns and best practices
- Migration guides
### Learning Resources
- Detailed explanations of implementations
- Links to relevant C standards
- Common pitfalls and how to avoid them
- Performance considerations
## License
MIT License - permissive and suitable for learning and commercial use.
## Version History
- **v0.1.0** (Planned)
- Initial release
- Debug system
- Version utilities
- Basic documentation
- **v0.2.0** (Future)
- String utilities
- Memory utilities
- Enhanced documentation
- **v0.3.0** (Future)
- Configuration utilities
- File utilities
- Time utilities
## Success Criteria
1. ✅ Successfully integrated into c-relay
2. ✅ Successfully integrated into nostr_core_lib
3. ✅ All tests passing
4. ✅ Documentation complete
5. ✅ Examples working
6. ✅ Zero external dependencies (except standard library)
7. ✅ Cross-platform compatibility verified
## Next Steps
1. Create repository structure
2. Implement debug system
3. Implement version utilities
4. Create build system
5. Write tests
6. Create documentation
7. Integrate into c-relay
8. Publish to GitHub
---
**Note**: This is a living document. Update as the library evolves and new utilities are added.

View File

@@ -0,0 +1,621 @@
# c_utils_lib Implementation Plan
## Overview
This document provides a step-by-step implementation plan for creating the `c_utils_lib` library and integrating it into the c-relay project.
## Phase 1: Repository Setup & Structure
### Step 1.1: Create Repository Structure
**Location**: Create outside c-relay project (sibling directory)
```bash
# Create directory structure
mkdir -p c_utils_lib/{include,src,examples,tests,docs,bin}
cd c_utils_lib
# Create subdirectories
mkdir -p include/c_utils
mkdir -p tests/results
```
### Step 1.2: Initialize Git Repository
```bash
cd c_utils_lib
git init
git branch -M main
```
### Step 1.3: Create Core Files
**Files to create**:
1. `README.md` - Main documentation
2. `LICENSE` - MIT License
3. `VERSION` - Version file (v0.1.0)
4. `.gitignore` - Git ignore rules
5. `Makefile` - Build system
6. `build.sh` - Build script
## Phase 2: Debug System Implementation
### Step 2.1: Move Debug Files
**Source files** (from c-relay):
- `src/debug.c``c_utils_lib/src/debug.c`
- `src/debug.h``c_utils_lib/include/c_utils/debug.h`
**Modifications needed**:
1. Update header guard in `debug.h`:
```c
#ifndef C_UTILS_DEBUG_H
#define C_UTILS_DEBUG_H
```
2. No namespace changes needed (keep simple API)
3. Add header documentation:
```c
/**
* @file debug.h
* @brief Debug and logging system with configurable verbosity levels
*
* Provides a simple, efficient logging system with 5 levels:
* - ERROR: Critical errors
* - WARN: Warnings
* - INFO: Informational messages
* - DEBUG: Debug messages
* - TRACE: Detailed trace with file:line info
*/
```
### Step 2.2: Create Main Header
**File**: `include/c_utils/c_utils.h`
```c
#ifndef C_UTILS_H
#define C_UTILS_H
/**
* @file c_utils.h
* @brief Main header for c_utils_lib - includes all utilities
*
* Include this header to access all c_utils_lib functionality.
* Alternatively, include specific headers for modular usage.
*/
// Version information
#define C_UTILS_VERSION "v0.1.0"
#define C_UTILS_VERSION_MAJOR 0
#define C_UTILS_VERSION_MINOR 1
#define C_UTILS_VERSION_PATCH 0
// Include all utilities
#include "debug.h"
#include "version.h"
#endif /* C_UTILS_H */
```
## Phase 3: Version Utilities Implementation
### Step 3.1: Design Version API
**File**: `include/c_utils/version.h`
```c
#ifndef C_UTILS_VERSION_H
#define C_UTILS_VERSION_H
#include <time.h>
/**
* @brief Version information structure
*/
typedef struct {
int major;
int minor;
int patch;
char git_hash[41]; // SHA-1 hash (40 chars + null)
char build_date[32]; // ISO 8601 format
char version_string[64]; // "vX.Y.Z" format
} version_info_t;
/**
* @brief Extract version from git tags
* @param version Output version structure
* @return 0 on success, -1 on error
*/
int version_get_from_git(version_info_t* version);
/**
* @brief Generate version header file for a project
* @param output_path Path to output header file
* @param prefix Prefix for macros (e.g., "MY_APP")
* @return 0 on success, -1 on error
*/
int version_generate_header(const char* output_path, const char* prefix);
/**
* @brief Compare two versions
* @return -1 if v1 < v2, 0 if equal, 1 if v1 > v2
*/
int version_compare(const version_info_t* v1, const version_info_t* v2);
/**
* @brief Format version as string
* @param version Version structure
* @param buffer Output buffer
* @param buffer_size Size of output buffer
* @return Number of characters written
*/
int version_to_string(const version_info_t* version, char* buffer, size_t buffer_size);
#endif /* C_UTILS_VERSION_H */
```
### Step 3.2: Implement Version Utilities
**File**: `src/version.c`
Key functions to implement:
1. `version_get_from_git()` - Execute `git describe --tags` and parse
2. `version_generate_header()` - Generate header file with macros
3. `version_compare()` - Semantic version comparison
4. `version_to_string()` - Format version string
### Step 3.3: Create Version Generation Script
**File**: `bin/generate_version`
```bash
#!/bin/bash
# Generate version header for a project
OUTPUT_FILE="$1"
PREFIX="$2"
if [ -z "$OUTPUT_FILE" ] || [ -z "$PREFIX" ]; then
echo "Usage: $0 <output_file> <prefix>"
exit 1
fi
# Get version from git
if [ -d .git ]; then
VERSION=$(git describe --tags --always 2>/dev/null || echo "v0.0.0")
GIT_HASH=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
else
VERSION="v0.0.0"
GIT_HASH="unknown"
fi
# Parse version
CLEAN_VERSION=$(echo "$VERSION" | sed 's/^v//' | cut -d- -f1)
MAJOR=$(echo "$CLEAN_VERSION" | cut -d. -f1)
MINOR=$(echo "$CLEAN_VERSION" | cut -d. -f2)
PATCH=$(echo "$CLEAN_VERSION" | cut -d. -f3)
BUILD_DATE=$(date -u +"%Y-%m-%d %H:%M:%S UTC")
# Generate header
cat > "$OUTPUT_FILE" << EOF
/* Auto-generated by c_utils_lib version system */
/* DO NOT EDIT - This file is automatically generated */
#ifndef ${PREFIX}_VERSION_H
#define ${PREFIX}_VERSION_H
#define ${PREFIX}_VERSION "v${CLEAN_VERSION}"
#define ${PREFIX}_VERSION_MAJOR ${MAJOR}
#define ${PREFIX}_VERSION_MINOR ${MINOR}
#define ${PREFIX}_VERSION_PATCH ${PATCH}
#define ${PREFIX}_GIT_HASH "${GIT_HASH}"
#define ${PREFIX}_BUILD_DATE "${BUILD_DATE}"
#endif /* ${PREFIX}_VERSION_H */
EOF
echo "Generated $OUTPUT_FILE with version v${CLEAN_VERSION}"
```
## Phase 4: Build System
### Step 4.1: Create Makefile
**File**: `Makefile`
```makefile
# c_utils_lib Makefile
CC = gcc
AR = ar
CFLAGS = -Wall -Wextra -std=c99 -O2 -g
INCLUDES = -Iinclude
# Directories
SRC_DIR = src
INCLUDE_DIR = include
BUILD_DIR = build
EXAMPLES_DIR = examples
TESTS_DIR = tests
# Source files
SOURCES = $(wildcard $(SRC_DIR)/*.c)
OBJECTS = $(SOURCES:$(SRC_DIR)/%.c=$(BUILD_DIR)/%.o)
# Output library
LIBRARY = libc_utils.a
# Default target
all: $(LIBRARY)
# Create build directory
$(BUILD_DIR):
mkdir -p $(BUILD_DIR)
# Compile source files
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c | $(BUILD_DIR)
$(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@
# Create static library
$(LIBRARY): $(OBJECTS)
$(AR) rcs $@ $^
@echo "Built $(LIBRARY)"
# Build examples
examples: $(LIBRARY)
$(MAKE) -C $(EXAMPLES_DIR)
# Run tests
test: $(LIBRARY)
$(MAKE) -C $(TESTS_DIR)
$(TESTS_DIR)/run_tests.sh
# Install to system (optional)
install: $(LIBRARY)
install -d /usr/local/lib
install -m 644 $(LIBRARY) /usr/local/lib/
install -d /usr/local/include/c_utils
install -m 644 $(INCLUDE_DIR)/c_utils/*.h /usr/local/include/c_utils/
@echo "Installed to /usr/local"
# Uninstall from system
uninstall:
rm -f /usr/local/lib/$(LIBRARY)
rm -rf /usr/local/include/c_utils
@echo "Uninstalled from /usr/local"
# Clean build artifacts
clean:
rm -rf $(BUILD_DIR) $(LIBRARY)
$(MAKE) -C $(EXAMPLES_DIR) clean 2>/dev/null || true
$(MAKE) -C $(TESTS_DIR) clean 2>/dev/null || true
# Help
help:
@echo "c_utils_lib Build System"
@echo ""
@echo "Targets:"
@echo " all Build static library (default)"
@echo " examples Build examples"
@echo " test Run tests"
@echo " install Install to /usr/local"
@echo " uninstall Remove from /usr/local"
@echo " clean Clean build artifacts"
@echo " help Show this help"
.PHONY: all examples test install uninstall clean help
```
### Step 4.2: Create Build Script
**File**: `build.sh`
```bash
#!/bin/bash
# c_utils_lib build script
set -e
case "$1" in
lib|"")
echo "Building c_utils_lib..."
make
;;
examples)
echo "Building examples..."
make examples
;;
test)
echo "Running tests..."
make test
;;
clean)
echo "Cleaning..."
make clean
;;
install)
echo "Installing..."
make install
;;
*)
echo "Usage: ./build.sh [lib|examples|test|clean|install]"
exit 1
;;
esac
echo "Done!"
```
## Phase 5: Examples & Tests
### Step 5.1: Create Debug Example
**File**: `examples/debug_example.c`
```c
#include <c_utils/debug.h>
int main() {
// Initialize with INFO level
debug_init(DEBUG_LEVEL_INFO);
DEBUG_INFO("Application started");
DEBUG_WARN("This is a warning");
DEBUG_ERROR("This is an error");
// This won't print (level too high)
DEBUG_LOG("This debug message won't show");
// Change level to DEBUG
g_debug_level = DEBUG_LEVEL_DEBUG;
DEBUG_LOG("Now debug messages show");
// Change to TRACE to see file:line info
g_debug_level = DEBUG_LEVEL_TRACE;
DEBUG_TRACE("Trace with file:line information");
return 0;
}
```
### Step 5.2: Create Version Example
**File**: `examples/version_example.c`
```c
#include <c_utils/version.h>
#include <stdio.h>
int main() {
version_info_t version;
// Get version from git
if (version_get_from_git(&version) == 0) {
char version_str[64];
version_to_string(&version, version_str, sizeof(version_str));
printf("Version: %s\n", version_str);
printf("Git Hash: %s\n", version.git_hash);
printf("Build Date: %s\n", version.build_date);
}
return 0;
}
```
### Step 5.3: Create Test Suite
**File**: `tests/test_debug.c`
```c
#include <c_utils/debug.h>
#include <stdio.h>
#include <string.h>
int test_debug_init() {
debug_init(DEBUG_LEVEL_INFO);
return (g_debug_level == DEBUG_LEVEL_INFO) ? 0 : -1;
}
int test_debug_levels() {
// Test that higher levels don't print at lower settings
debug_init(DEBUG_LEVEL_ERROR);
// Would need to capture stdout to verify
return 0;
}
int main() {
int failed = 0;
printf("Running debug tests...\n");
if (test_debug_init() != 0) {
printf("FAIL: test_debug_init\n");
failed++;
} else {
printf("PASS: test_debug_init\n");
}
if (test_debug_levels() != 0) {
printf("FAIL: test_debug_levels\n");
failed++;
} else {
printf("PASS: test_debug_levels\n");
}
return failed;
}
```
## Phase 6: Documentation
### Step 6.1: Create README.md
Key sections:
1. Overview and purpose
2. Quick start guide
3. Installation instructions
4. Usage examples
5. API reference (brief)
6. Integration guide
7. Contributing guidelines
8. License
### Step 6.2: Create API Documentation
**File**: `docs/API.md`
Complete API reference with:
- Function signatures
- Parameter descriptions
- Return values
- Usage examples
- Common patterns
### Step 6.3: Create Integration Guide
**File**: `docs/INTEGRATION.md`
How to integrate into projects:
1. As git submodule
2. Makefile integration
3. Code examples
4. Migration from standalone utilities
## Phase 7: Integration with c-relay
### Step 7.1: Add as Submodule
```bash
cd /path/to/c-relay
git submodule add <repo-url> c_utils_lib
git submodule update --init --recursive
```
### Step 7.2: Update c-relay Makefile
```makefile
# Add to c-relay Makefile
C_UTILS_LIB = c_utils_lib/libc_utils.a
# Update includes
INCLUDES += -Ic_utils_lib/include
# Update libs
LIBS += -Lc_utils_lib -lc_utils
# Add dependency
$(C_UTILS_LIB):
cd c_utils_lib && ./build.sh lib
# Update main target
$(TARGET): $(C_UTILS_LIB) ...
```
### Step 7.3: Update c-relay Source Files
**Changes needed**:
1. Update includes:
```c
// Old
#include "debug.h"
// New
#include <c_utils/debug.h>
```
2. Remove old debug files:
```bash
git rm src/debug.c src/debug.h
```
3. Update all files that use debug system:
- `src/main.c`
- `src/config.c`
- `src/dm_admin.c`
- `src/websockets.c`
- `src/subscriptions.c`
- Any other files using DEBUG_* macros
### Step 7.4: Test Integration
```bash
cd c-relay
make clean
make
./make_and_restart_relay.sh
```
Verify:
- Compilation succeeds
- Debug output works correctly
- No functionality regressions
## Phase 8: Version System Integration
### Step 8.1: Update c-relay Makefile for Versioning
```makefile
# Add version generation
src/version.h: .git/refs/tags/*
c_utils_lib/bin/generate_version src/version.h C_RELAY
# Add dependency
$(TARGET): src/version.h ...
```
### Step 8.2: Update c-relay to Use Generated Version
Replace hardcoded version in `src/main.h` with:
```c
#include "version.h"
// Use C_RELAY_VERSION instead of hardcoded VERSION
```
## Timeline Estimate
- **Phase 1**: Repository Setup - 1 hour
- **Phase 2**: Debug System - 2 hours
- **Phase 3**: Version Utilities - 4 hours
- **Phase 4**: Build System - 2 hours
- **Phase 5**: Examples & Tests - 3 hours
- **Phase 6**: Documentation - 3 hours
- **Phase 7**: c-relay Integration - 2 hours
- **Phase 8**: Version Integration - 2 hours
**Total**: ~19 hours
## Success Criteria
- [ ] c_utils_lib builds successfully
- [ ] All tests pass
- [ ] Examples compile and run
- [ ] c-relay integrates successfully
- [ ] Debug output works in c-relay
- [ ] Version generation works
- [ ] Documentation complete
- [ ] No regressions in c-relay functionality
## Next Steps
1. Review this plan with stakeholders
2. Create repository structure
3. Implement debug system
4. Implement version utilities
5. Create build system
6. Write tests and examples
7. Create documentation
8. Integrate into c-relay
9. Test thoroughly
10. Publish to GitHub
## Notes
- Keep the API simple and intuitive
- Focus on zero external dependencies
- Prioritize learning value in code comments
- Make integration as easy as possible
- Document everything thoroughly

562
docs/debug_system.md Normal file
View File

@@ -0,0 +1,562 @@
# Simple Debug System Proposal
## Overview
A minimal debug system with 6 levels (0-5) controlled by a single `--debug-level` flag. TRACE level (5) automatically includes file:line information for ALL messages. Uses compile-time macros to ensure **zero performance impact and zero size increase** in production builds.
## Debug Levels
```c
typedef enum {
DEBUG_LEVEL_NONE = 0, // Production: no debug output
DEBUG_LEVEL_ERROR = 1, // Errors only
DEBUG_LEVEL_WARN = 2, // Errors + Warnings
DEBUG_LEVEL_INFO = 3, // Errors + Warnings + Info
DEBUG_LEVEL_DEBUG = 4, // All above + Debug messages
DEBUG_LEVEL_TRACE = 5 // All above + Trace (very verbose)
} debug_level_t;
```
## Usage
```bash
# Production (default - no debug output)
./c_relay_x86
# Show errors only
./c_relay_x86 --debug-level=1
# Show errors and warnings
./c_relay_x86 --debug-level=2
# Show errors, warnings, and info (recommended for development)
./c_relay_x86 --debug-level=3
# Show all debug messages
./c_relay_x86 --debug-level=4
# Show everything including trace with file:line (very verbose)
./c_relay_x86 --debug-level=5
```
## Implementation
### 1. Header File (`src/debug.h`)
```c
#ifndef DEBUG_H
#define DEBUG_H
#include <stdio.h>
#include <time.h>
// Debug levels
typedef enum {
DEBUG_LEVEL_NONE = 0,
DEBUG_LEVEL_ERROR = 1,
DEBUG_LEVEL_WARN = 2,
DEBUG_LEVEL_INFO = 3,
DEBUG_LEVEL_DEBUG = 4,
DEBUG_LEVEL_TRACE = 5
} debug_level_t;
// Global debug level (set at runtime via CLI)
extern debug_level_t g_debug_level;
// Initialize debug system
void debug_init(int level);
// Core logging function
void debug_log(debug_level_t level, const char* file, int line, const char* format, ...);
// Convenience macros that check level before calling
// Note: TRACE level (5) and above include file:line information for ALL messages
#define DEBUG_ERROR(...) \
do { if (g_debug_level >= DEBUG_LEVEL_ERROR) debug_log(DEBUG_LEVEL_ERROR, __FILE__, __LINE__, __VA_ARGS__); } while(0)
#define DEBUG_WARN(...) \
do { if (g_debug_level >= DEBUG_LEVEL_WARN) debug_log(DEBUG_LEVEL_WARN, __FILE__, __LINE__, __VA_ARGS__); } while(0)
#define DEBUG_INFO(...) \
do { if (g_debug_level >= DEBUG_LEVEL_INFO) debug_log(DEBUG_LEVEL_INFO, __FILE__, __LINE__, __VA_ARGS__); } while(0)
#define DEBUG_LOG(...) \
do { if (g_debug_level >= DEBUG_LEVEL_DEBUG) debug_log(DEBUG_LEVEL_DEBUG, __FILE__, __LINE__, __VA_ARGS__); } while(0)
#define DEBUG_TRACE(...) \
do { if (g_debug_level >= DEBUG_LEVEL_TRACE) debug_log(DEBUG_LEVEL_TRACE, __FILE__, __LINE__, __VA_ARGS__); } while(0)
#endif /* DEBUG_H */
```
### 2. Implementation File (`src/debug.c`)
```c
#include "debug.h"
#include <stdarg.h>
#include <string.h>
// Global debug level (default: no debug output)
debug_level_t g_debug_level = DEBUG_LEVEL_NONE;
void debug_init(int level) {
if (level < 0) level = 0;
if (level > 5) level = 5;
g_debug_level = (debug_level_t)level;
}
void debug_log(debug_level_t level, const char* file, int line, const char* format, ...) {
// Get timestamp
time_t now = time(NULL);
struct tm* tm_info = localtime(&now);
char timestamp[32];
strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", tm_info);
// Get level string
const char* level_str = "UNKNOWN";
switch (level) {
case DEBUG_LEVEL_ERROR: level_str = "ERROR"; break;
case DEBUG_LEVEL_WARN: level_str = "WARN "; break;
case DEBUG_LEVEL_INFO: level_str = "INFO "; break;
case DEBUG_LEVEL_DEBUG: level_str = "DEBUG"; break;
case DEBUG_LEVEL_TRACE: level_str = "TRACE"; break;
default: break;
}
// Print prefix with timestamp and level
printf("[%s] [%s] ", timestamp, level_str);
// Print source location when debug level is TRACE (5) or higher
if (file && g_debug_level >= DEBUG_LEVEL_TRACE) {
// Extract just the filename (not full path)
const char* filename = strrchr(file, '/');
filename = filename ? filename + 1 : file;
printf("[%s:%d] ", filename, line);
}
// Print message
va_list args;
va_start(args, format);
vprintf(format, args);
va_end(args);
printf("\n");
fflush(stdout);
}
```
### 3. CLI Argument Parsing (add to `src/main.c`)
```c
// In main() function, add to argument parsing:
int debug_level = 0; // Default: no debug output
for (int i = 1; i < argc; i++) {
if (strncmp(argv[i], "--debug-level=", 14) == 0) {
debug_level = atoi(argv[i] + 14);
if (debug_level < 0) debug_level = 0;
if (debug_level > 5) debug_level = 5;
}
// ... other arguments ...
}
// Initialize debug system
debug_init(debug_level);
```
### 4. Update Makefile
```makefile
# Add debug.c to source files
MAIN_SRC = src/main.c src/config.c src/debug.c src/dm_admin.c src/request_validator.c ...
```
## Migration Strategy
### Keep Existing Functions
The existing `log_*` functions can remain as wrappers:
```c
// src/main.c - Update existing functions
// Note: These don't include file:line since they're wrappers
void log_info(const char* message) {
if (g_debug_level >= DEBUG_LEVEL_INFO) {
debug_log(DEBUG_LEVEL_INFO, NULL, 0, "%s", message);
}
}
void log_error(const char* message) {
if (g_debug_level >= DEBUG_LEVEL_ERROR) {
debug_log(DEBUG_LEVEL_ERROR, NULL, 0, "%s", message);
}
}
void log_warning(const char* message) {
if (g_debug_level >= DEBUG_LEVEL_WARN) {
debug_log(DEBUG_LEVEL_WARN, NULL, 0, "%s", message);
}
}
void log_success(const char* message) {
if (g_debug_level >= DEBUG_LEVEL_INFO) {
debug_log(DEBUG_LEVEL_INFO, NULL, 0, "✓ %s", message);
}
}
```
### Gradual Migration
Gradually replace log calls with debug macros:
```c
// Before:
log_info("Starting WebSocket relay server");
// After:
DEBUG_INFO("Starting WebSocket relay server");
// Before:
log_error("Failed to initialize database");
// After:
DEBUG_ERROR("Failed to initialize database");
```
### Add New Debug Levels
Add debug and trace messages where needed:
```c
// Detailed debugging
DEBUG_LOG("Processing subscription: %s", sub_id);
DEBUG_LOG("Filter count: %d", filter_count);
// Very verbose tracing
DEBUG_TRACE("Entering handle_req_message()");
DEBUG_TRACE("Subscription ID validated: %s", sub_id);
DEBUG_TRACE("Exiting handle_req_message()");
```
## Manual Guards for Expensive Operations
### The Problem
Debug macros use **runtime checks**, which means function arguments are always evaluated:
```c
// ❌ BAD: Database query executes even when debug level is 0
DEBUG_LOG("Count: %d", expensive_database_query());
```
The `expensive_database_query()` will **always execute** because function arguments are evaluated before the `if` check inside the macro.
### The Solution: Manual Guards
For expensive operations (database queries, file I/O, complex calculations), use manual guards:
```c
// ✅ GOOD: Query only executes when debugging is enabled
if (g_debug_level >= DEBUG_LEVEL_DEBUG) {
int count = expensive_database_query();
DEBUG_LOG("Count: %d", count);
}
```
### Standardized Comment Format
To make temporary debug guards easy to find and remove, use this standardized format:
```c
// DEBUG_GUARD_START
if (g_debug_level >= DEBUG_LEVEL_DEBUG) {
// Expensive operation here
sqlite3_stmt* stmt;
const char* sql = "SELECT COUNT(*) FROM events";
int count = 0;
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
count = sqlite3_column_int(stmt, 0);
}
sqlite3_finalize(stmt);
}
DEBUG_LOG("Event count: %d", count);
}
// DEBUG_GUARD_END
```
### Easy Removal
When you're done debugging, find and remove all temporary guards:
```bash
# Find all debug guards
grep -n "DEBUG_GUARD_START" src/*.c
# Remove guards with sed (between START and END markers)
sed -i '/DEBUG_GUARD_START/,/DEBUG_GUARD_END/d' src/config.c
```
Or use a simple script:
```bash
#!/bin/bash
# remove_debug_guards.sh
for file in src/*.c; do
sed -i '/DEBUG_GUARD_START/,/DEBUG_GUARD_END/d' "$file"
echo "Removed debug guards from $file"
done
```
### When to Use Manual Guards
Use manual guards for:
- ✅ Database queries
- ✅ File I/O operations
- ✅ Network requests
- ✅ Complex calculations
- ✅ Memory allocations for debug data
- ✅ String formatting with multiple operations
Don't need guards for:
- ❌ Simple variable access
- ❌ Basic arithmetic
- ❌ String literals
- ❌ Function calls that are already cheap
### Example: Database Query Guard
```c
// DEBUG_GUARD_START
if (g_debug_level >= DEBUG_LEVEL_DEBUG) {
sqlite3_stmt* count_stmt;
const char* count_sql = "SELECT COUNT(*) FROM config";
int config_count = 0;
if (sqlite3_prepare_v2(g_db, count_sql, -1, &count_stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(count_stmt) == SQLITE_ROW) {
config_count = sqlite3_column_int(count_stmt, 0);
}
sqlite3_finalize(count_stmt);
}
DEBUG_LOG("Config table has %d rows", config_count);
}
// DEBUG_GUARD_END
```
### Example: Complex String Formatting Guard
```c
// DEBUG_GUARD_START
if (g_debug_level >= DEBUG_LEVEL_TRACE) {
char filter_str[1024] = {0};
int offset = 0;
for (int i = 0; i < filter_count && offset < sizeof(filter_str) - 1; i++) {
offset += snprintf(filter_str + offset, sizeof(filter_str) - offset,
"Filter %d: kind=%d, author=%s; ",
i, filters[i].kind, filters[i].author);
}
DEBUG_TRACE("Processing filters: %s", filter_str);
}
// DEBUG_GUARD_END
```
### Alternative: Compile-Time Guards
For permanent debug code that should be completely removed in production builds, use compile-time guards:
```c
#ifdef ENABLE_DEBUG_CODE
// This code is completely removed when ENABLE_DEBUG_CODE is not defined
int count = expensive_database_query();
DEBUG_LOG("Count: %d", count);
#endif
```
Build with debug code:
```bash
make CFLAGS="-DENABLE_DEBUG_CODE"
```
Build without debug code (production):
```bash
make # No debug code compiled in
```
### Best Practices
1. **Always use standardized markers** (`DEBUG_GUARD_START`/`DEBUG_GUARD_END`) for temporary guards
2. **Add a comment** explaining what you're debugging
3. **Remove guards** when debugging is complete
4. **Use compile-time guards** for permanent debug infrastructure
5. **Keep guards simple** - one guard per logical debug operation
## Performance Impact
### Runtime Check
The macros include a runtime check:
```c
#define DEBUG_INFO(...) \
do { if (g_debug_level >= DEBUG_LEVEL_INFO) debug_log(DEBUG_LEVEL_INFO, NULL, 0, __VA_ARGS__); } while(0)
```
**Cost**: One integer comparison per debug statement (~1 CPU cycle)
**Impact**: Negligible - the comparison is faster than a function call
**Note**: Only `DEBUG_TRACE` includes `__FILE__` and `__LINE__`, which are compile-time constants with no runtime overhead.
### When Debug Level is 0 (Production)
```c
// With g_debug_level = 0:
DEBUG_INFO("Starting server");
// Becomes:
if (0 >= 3) debug_log(...); // Never executes
// Compiler optimizes to:
// (nothing - branch is eliminated)
```
**Result**: Modern compilers (gcc -O2 or higher) will completely eliminate the dead code branch.
### Size Impact
**Test Case**: 100 debug statements in code
**Without optimization** (`-O0`):
- Binary size increase: ~2KB (branch instructions)
- Runtime cost: 100 comparisons per execution
**With optimization** (`-O2` or `-O3`):
- Binary size increase: **0 bytes** (dead code eliminated when g_debug_level = 0)
- Runtime cost: **0 cycles** (branches removed by compiler)
### Verification
You can verify the optimization with:
```bash
# Compile with optimization
gcc -O2 -c debug_test.c -o debug_test.o
# Disassemble and check
objdump -d debug_test.o | grep -A 10 "debug_log"
```
When `g_debug_level = 0` (constant), you'll see the compiler has removed all debug calls.
## Example Output
### Level 0 (Production)
```
(no output)
```
### Level 1 (Errors Only)
```
[2025-01-12 14:30:15] [ERROR] Failed to open database: permission denied
[2025-01-12 14:30:20] [ERROR] WebSocket connection failed: port in use
```
### Level 2 (Errors + Warnings)
```
[2025-01-12 14:30:15] [ERROR] Failed to open database: permission denied
[2025-01-12 14:30:16] [WARN ] Port 8888 unavailable, trying 8889
[2025-01-12 14:30:17] [WARN ] Configuration key 'relay_name' not found, using default
```
### Level 3 (Errors + Warnings + Info)
```
[2025-01-12 14:30:15] [INFO ] Initializing C-Relay v0.4.6
[2025-01-12 14:30:15] [INFO ] Loading configuration from database
[2025-01-12 14:30:15] [ERROR] Failed to open database: permission denied
[2025-01-12 14:30:16] [WARN ] Port 8888 unavailable, trying 8889
[2025-01-12 14:30:17] [INFO ] WebSocket relay started on ws://127.0.0.1:8889
```
### Level 4 (All Debug Messages)
```
[2025-01-12 14:30:15] [INFO ] Initializing C-Relay v0.4.6
[2025-01-12 14:30:15] [DEBUG] Opening database: build/abc123...def.db
[2025-01-12 14:30:15] [DEBUG] Executing schema initialization
[2025-01-12 14:30:15] [INFO ] SQLite WAL mode enabled
[2025-01-12 14:30:16] [DEBUG] Attempting to bind to port 8888
[2025-01-12 14:30:16] [WARN ] Port 8888 unavailable, trying 8889
[2025-01-12 14:30:17] [DEBUG] Successfully bound to port 8889
[2025-01-12 14:30:17] [INFO ] WebSocket relay started on ws://127.0.0.1:8889
```
### Level 5 (Everything Including file:line for ALL messages)
```
[2025-01-12 14:30:15] [INFO ] [main.c:1607] Initializing C-Relay v0.4.6
[2025-01-12 14:30:15] [DEBUG] [main.c:348] Opening database: build/abc123...def.db
[2025-01-12 14:30:15] [TRACE] [main.c:330] Entering init_database()
[2025-01-12 14:30:15] [ERROR] [config.c:125] Database locked
```
## Implementation Steps
### Step 1: Create Files (5 minutes)
1. Create `src/debug.h` with the header code above
2. Create `src/debug.c` with the implementation code above
3. Update `Makefile` to include `src/debug.c` in `MAIN_SRC`
### Step 2: Add CLI Parsing (5 minutes)
Add `--debug-level` argument parsing to `main()` in `src/main.c`
### Step 3: Update Existing Functions (5 minutes)
Update the existing `log_*` functions to use the new debug macros
### Step 4: Test (5 minutes)
```bash
# Build
make clean && make
# Test different levels
./build/c_relay_x86 # No output
./build/c_relay_x86 --debug-level=1 # Errors only
./build/c_relay_x86 --debug-level=3 # Info + warnings + errors
./build/c_relay_x86 --debug-level=4 # All debug messages
./build/c_relay_x86 --debug-level=5 # Everything with file:line on TRACE
```
### Step 5: Gradual Migration (Ongoing)
As you work on different parts of the code, replace `log_*` calls with `DEBUG_*` macros and add new debug/trace statements where helpful.
## Benefits
**Simple**: Single flag, 6 levels, easy to understand
**Zero Overhead**: Compiler optimizes away unused debug code
**Zero Size Impact**: No binary size increase in production
**Backward Compatible**: Existing `log_*` functions still work
**Easy Migration**: Gradual replacement of log calls
**Flexible**: Can add detailed debugging without affecting production
## Total Implementation Time
**~20 minutes** for basic implementation
**Ongoing** for gradual migration of existing log calls
## Recommendation
This is the simplest possible debug system that provides:
- Multiple debug levels for different verbosity
- Zero performance impact in production
- Zero binary size increase
- Easy to use and understand
- Backward compatible with existing code
Start with the basic implementation, test it, then gradually migrate existing log calls and add new debug statements as needed.

View File

@@ -1,358 +0,0 @@
# Event-Based Configuration System Implementation Plan
## Overview
This document provides a detailed implementation plan for transitioning the C Nostr Relay from command line arguments and file-based configuration to a pure event-based configuration system using kind 33334 Nostr events stored directly in the database.
## Implementation Phases
### Phase 0: File Structure Preparation ✅ COMPLETED
#### 0.1 Backup and Prepare Files ✅ COMPLETED
**Actions:**
1. ✅ Rename `src/config.c` to `src/config.c.old` - DONE
2. ✅ Rename `src/config.h` to `src/config.h.old` - DONE
3. ✅ Create new empty `src/config.c` and `src/config.h` - DONE
4. ✅ Create new `src/default_config_event.h` - DONE
### Phase 1: Database Schema and Core Infrastructure ✅ COMPLETED
#### 1.1 Update Database Naming System ✅ COMPLETED
**File:** `src/main.c`, new `src/config.c`, new `src/config.h`
```c
// New functions implemented: ✅
char* get_database_name_from_relay_pubkey(const char* relay_pubkey);
int create_database_with_relay_pubkey(const char* relay_pubkey);
```
**Changes Completed:**
- ✅ Create completely new `src/config.c` and `src/config.h` files
- ✅ Rename old files to `src/config.c.old` and `src/config.h.old`
- ✅ Modify `init_database()` to use relay pubkey for database naming
- ✅ Use `nostr_core_lib` functions for all keypair generation
- ✅ Database path: `./<relay_pubkey>.nrdb`
- ✅ Remove all database path command line argument handling
#### 1.2 Configuration Event Storage ✅ COMPLETED
**File:** new `src/config.c`, new `src/default_config_event.h`
```c
// Configuration functions implemented: ✅
int store_config_event_in_database(const cJSON* event);
cJSON* load_config_event_from_database(const char* relay_pubkey);
```
**Changes Completed:**
- ✅ Create new `src/default_config_event.h` for default configuration values
- ✅ Add functions to store/retrieve kind 33334 events from events table
- ✅ Use `nostr_core_lib` functions for all event validation
- ✅ Clean separation: default config values isolated in header file
- ✅ Remove existing config table dependencies
### Phase 2: Event Processing Integration ✅ COMPLETED
#### 2.1 Real-time Configuration Processing ✅ COMPLETED
**File:** `src/main.c` (event processing functions)
**Integration Points:** ✅ IMPLEMENTED
```c
// In existing event processing loop: ✅ IMPLEMENTED
// Added kind 33334 event detection in main event loop
if (kind_num == 33334) {
if (handle_configuration_event(event, error_message, sizeof(error_message)) == 0) {
// Configuration event processed successfully
}
}
// Configuration event processing implemented: ✅
int process_configuration_event(const cJSON* event);
int handle_configuration_event(cJSON* event, char* error_message, size_t error_size);
```
#### 2.2 Configuration Application System ⚠️ PARTIALLY COMPLETED
**File:** `src/config.c`
**Status:** Configuration access functions implemented, field handlers need completion
```c
// Configuration access implemented: ✅
const char* get_config_value(const char* key);
int get_config_int(const char* key, int default_value);
int get_config_bool(const char* key, int default_value);
// Field handlers need implementation: ⏳ IN PROGRESS
// Need to implement specific apply functions for runtime changes
```
### Phase 3: First-Time Startup System ✅ COMPLETED
#### 3.1 Key Generation and Initial Setup ✅ COMPLETED
**File:** new `src/config.c`, `src/default_config_event.h`
**Status:** ✅ FULLY IMPLEMENTED with secure /dev/urandom + nostr_core_lib validation
```c
int first_time_startup_sequence() {
// 1. Generate admin keypair using nostr_core_lib
unsigned char admin_privkey_bytes[32];
char admin_privkey[65], admin_pubkey[65];
if (nostr_generate_private_key(admin_privkey_bytes) != 0) {
return -1;
}
nostr_bytes_to_hex(admin_privkey_bytes, 32, admin_privkey);
unsigned char admin_pubkey_bytes[32];
if (nostr_ec_public_key_from_private_key(admin_privkey_bytes, admin_pubkey_bytes) != 0) {
return -1;
}
nostr_bytes_to_hex(admin_pubkey_bytes, 32, admin_pubkey);
// 2. Generate relay keypair using nostr_core_lib
unsigned char relay_privkey_bytes[32];
char relay_privkey[65], relay_pubkey[65];
if (nostr_generate_private_key(relay_privkey_bytes) != 0) {
return -1;
}
nostr_bytes_to_hex(relay_privkey_bytes, 32, relay_privkey);
unsigned char relay_pubkey_bytes[32];
if (nostr_ec_public_key_from_private_key(relay_privkey_bytes, relay_pubkey_bytes) != 0) {
return -1;
}
nostr_bytes_to_hex(relay_pubkey_bytes, 32, relay_pubkey);
// 3. Create database with relay pubkey name
if (create_database_with_relay_pubkey(relay_pubkey) != 0) {
return -1;
}
// 4. Create initial configuration event using defaults from header
cJSON* config_event = create_default_config_event(admin_privkey_bytes, relay_privkey, relay_pubkey);
// 5. Store configuration event in database
store_config_event_in_database(config_event);
// 6. Print admin private key for user to save
printf("=== SAVE THIS ADMIN PRIVATE KEY ===\n");
printf("Admin Private Key: %s\n", admin_privkey);
printf("===================================\n");
return 0;
}
```
#### 3.2 Database Detection Logic ✅ COMPLETED
**File:** `src/main.c`
**Status:** ✅ FULLY IMPLEMENTED
```c
// Implemented functions: ✅
char** find_existing_nrdb_files(void);
char* extract_pubkey_from_filename(const char* filename);
int is_first_time_startup(void);
int first_time_startup_sequence(void);
int startup_existing_relay(const char* relay_pubkey);
```
### Phase 4: Legacy System Removal ✅ PARTIALLY COMPLETED
#### 4.1 Remove Command Line Arguments ✅ COMPLETED
**File:** `src/main.c`
**Status:** ✅ COMPLETED
- ✅ All argument parsing logic removed except --help and --version
-`--port`, `--config-dir`, `--config-file`, `--database-path` handling removed
- ✅ Environment variable override systems removed
- ✅ Clean help and version functions implemented
#### 4.2 Remove Configuration File System ✅ COMPLETED
**File:** `src/config.c`
**Status:** ✅ COMPLETED - New file created from scratch
- ✅ All legacy file-based configuration functions removed
- ✅ XDG configuration directory logic removed
- ✅ Pure event-based system implemented
#### 4.3 Remove Legacy Database Tables ⏳ PENDING
**File:** `src/sql_schema.h`
**Status:** ⏳ NEEDS COMPLETION
```sql
-- Still need to remove these tables:
DROP TABLE IF EXISTS config;
DROP TABLE IF EXISTS config_history;
DROP TABLE IF EXISTS config_file_cache;
DROP VIEW IF EXISTS active_config;
```
### Phase 5: Configuration Management
#### 5.1 Configuration Field Mapping
**File:** `src/config.c`
```c
// Map configuration tags to current system
static const config_field_handler_t config_handlers[] = {
{"auth_enabled", 0, apply_auth_enabled},
{"relay_port", 1, apply_relay_port}, // requires restart
{"max_connections", 0, apply_max_connections},
{"relay_description", 0, apply_relay_description},
{"relay_contact", 0, apply_relay_contact},
{"relay_pubkey", 1, apply_relay_pubkey}, // requires restart
{"relay_privkey", 1, apply_relay_privkey}, // requires restart
{"pow_min_difficulty", 0, apply_pow_difficulty},
{"nip40_expiration_enabled", 0, apply_expiration_enabled},
{"max_subscriptions_per_client", 0, apply_max_subscriptions},
{"max_event_tags", 0, apply_max_event_tags},
{"max_content_length", 0, apply_max_content_length},
{"default_limit", 0, apply_default_limit},
{"max_limit", 0, apply_max_limit},
// ... etc
};
```
#### 5.2 Startup Configuration Loading
**File:** `src/main.c`
```c
int startup_existing_relay(const char* relay_pubkey) {
// 1. Open database
if (init_database_with_pubkey(relay_pubkey) != 0) {
return -1;
}
// 2. Load configuration event from database
cJSON* config_event = load_config_event_from_database(relay_pubkey);
if (!config_event) {
log_error("No configuration event found in database");
return -1;
}
// 3. Apply all configuration from event
if (apply_configuration_from_event(config_event) != 0) {
return -1;
}
// 4. Continue with normal startup
return start_relay_services();
}
```
## Implementation Order - PROGRESS STATUS
### Step 1: Core Infrastructure ✅ COMPLETED
1. ✅ Implement database naming with relay pubkey
2. ✅ Add key generation functions using `nostr_core_lib`
3. ✅ Create configuration event storage/retrieval functions
4. ✅ Test basic event creation and storage
### Step 2: Event Processing Integration ✅ MOSTLY COMPLETED
1. ✅ Add kind 33334 event detection to event processing loop
2. ✅ Implement configuration event validation
3. ⚠️ Create configuration application handlers (basic access implemented, runtime handlers pending)
4. ⏳ Test real-time configuration updates (infrastructure ready)
### Step 3: First-Time Startup ✅ COMPLETED
1. ✅ Implement first-time startup detection
2. ✅ Add automatic key generation and database creation
3. ✅ Create default configuration event generation
4. ✅ Test complete first-time startup flow
### Step 4: Legacy Removal ⚠️ MOSTLY COMPLETED
1. ✅ Remove command line argument parsing
2. ✅ Remove configuration file system
3. ⏳ Remove legacy database tables (pending)
4. ✅ Update all references to use event-based config
### Step 5: Testing and Validation ⚠️ PARTIALLY COMPLETED
1. ✅ Test complete startup flow (first time and existing)
2. ⏳ Test configuration updates via events (infrastructure ready)
3. ⚠️ Test error handling and recovery (basic error handling implemented)
4. ⏳ Performance testing and optimization (pending)
## Migration Strategy
### For Existing Installations
Since the new system uses a completely different approach:
1. **No Automatic Migration**: The new system starts fresh
2. **Manual Migration**: Users can manually copy configuration values
3. **Documentation**: Provide clear migration instructions
4. **Coexistence**: Old and new systems use different database names
### Migration Steps for Users
1. Stop existing relay
2. Note current configuration values
3. Start new relay (generates keys and new database)
4. Create kind 33334 event with desired configuration using admin private key
5. Send event to relay to update configuration
## Testing Requirements
### Unit Tests
- Key generation functions
- Configuration event creation and validation
- Database naming logic
- Configuration application handlers
### Integration Tests
- Complete first-time startup flow
- Configuration update via events
- Error handling scenarios
- Database operations
### Performance Tests
- Startup time comparison
- Configuration update response time
- Memory usage analysis
## Security Considerations
1. **Admin Private Key**: Never stored, only printed once
2. **Event Validation**: All configuration events must be signed by admin
3. **Database Security**: Relay database contains relay private key
4. **Key Generation**: Use `nostr_core_lib` for cryptographically secure generation
## Files to Modify
### Major Changes
- `src/main.c` - Startup logic, event processing, argument removal
- `src/config.c` - Complete rewrite for event-based configuration
- `src/config.h` - Update function signatures and structures
- `src/sql_schema.h` - Remove config tables
### Minor Changes
- `Makefile` - Remove any config file generation
- `systemd/` - Update service files if needed
- Documentation updates
## Backwards Compatibility
**Breaking Changes:**
- Command line arguments removed (except --help, --version)
- Configuration files no longer used
- Database naming scheme changed
- Configuration table removed
**Migration Required:** This is a breaking change that requires manual migration for existing installations.
## Success Criteria - CURRENT STATUS
1.**Zero Command Line Arguments**: Relay starts with just `./c-relay`
2.**Automatic First-Time Setup**: Generates keys and database automatically
3. ⚠️ **Real-Time Configuration**: Infrastructure ready, handlers need completion
4.**Single Database File**: All configuration and data in one `.nrdb` file
5. ⚠️ **Admin Control**: Event processing implemented, signature validation ready
6. ⚠️ **Clean Codebase**: Most legacy code removed, database tables cleanup pending
## Risk Mitigation
1. **Backup Strategy**: Document manual backup procedures for relay database
2. **Key Loss Recovery**: Document recovery procedures if admin key is lost
3. **Testing Coverage**: Comprehensive test suite before deployment
4. **Rollback Plan**: Keep old version available during transition period
5. **Documentation**: Comprehensive user and developer documentation
This implementation plan provides a clear path from the current system to the new event-based configuration architecture while maintaining security and reliability.

View File

@@ -1,128 +0,0 @@
# Startup Configuration Design Analysis
## Review of startup_config_design.md
### Key Design Principles Identified
1. **Zero Command Line Arguments**: Complete elimination of CLI arguments for true "quick start"
2. **Event-Based Configuration**: Configuration stored as Nostr event (kind 33334) in events table
3. **Self-Contained Database**: Database named after relay pubkey (`<pubkey>.nrdb`)
4. **First-Time Setup**: Automatic key generation and initial configuration creation
5. **Configuration Consistency**: Always read from event, never from hardcoded defaults
### Implementation Gaps and Specifications Needed
#### 1. Key Generation Process
**Specification:**
```
First Startup Key Generation:
1. Generate all keys on first startup (admin private/public, relay private/public)
2. Use nostr_core_lib for key generation entropy
3. Keys are encoded in hex format
4. Print admin private key to stdout for user to save (never stored)
5. Store admin public key, relay private key, and relay public key in configuration event
6. Admin can later change the 33334 event to alter stored keys
```
#### 2. Database Naming and Location
**Specification:**
```
Database Naming:
1. Database is named using relay pubkey: ./<relay_pubkey>.nrdb
2. Database path structure: ./<relay_pubkey>.nrdb
3. If database creation fails, program quits (can't run without database)
4. c_nostr_relay.db should never exist in new system
```
#### 3. Configuration Event Structure (Kind 33334)
**Specification:**
```
Event Structure:
- Kind: 33334 (parameterized replaceable event)
- Event validation: Use nostr_core_lib to validate event
- Event content field: "C Nostr Relay Configuration" (descriptive text)
- Configuration update mechanism: TBD
- Complete tag structure provided in configuration section below
```
#### 4. Configuration Change Monitoring
**Configuration Monitoring System:**
```
Every event that is received is checked to see if it is a kind 33334 event from the admin pubkey.
If so, it is processed as a configuration update.
```
#### 5. Error Handling and Recovery
**Specification:**
```
Error Recovery Priority:
1. Try to load latest valid config event
2. Generate new default configuration event if none exists
3. Exit with error if all recovery attempts fail
Note: There is only ever one configuration event (parameterized replaceable event),
so no fallback to previous versions.
```
### Design Clarifications
**Key Management:**
- Admin private key is never stored, only printed once at first startup
- Single admin system (no multi-admin support)
- No key rotation support
**Configuration Management:**
- No configuration versioning/timestamping
- No automatic backup of configuration events
- Configuration events are not broadcastable to other relays
- Future: Auth system to restrict admin access to configuration events
---
## Complete Current Configuration Structure
Based on analysis of [`src/config.c`](src/config.c:753-795), here is the complete current configuration structure that will be converted to event tags:
### Complete Event Structure Example
```json
{
"kind": 33334,
"created_at": 1725661483,
"tags": [
["d", "<relay_pubkey>"],
["auth_enabled", "false"],
["relay_port", "8888"],
["max_connections", "100"],
["relay_description", "High-performance C Nostr relay with SQLite storage"],
["relay_contact", ""],
["relay_pubkey", "<relay_public_key>"],
["relay_privkey", "<relay_private_key>"],
["relay_software", "https://git.laantungir.net/laantungir/c-relay.git"],
["relay_version", "v1.0.0"],
["pow_min_difficulty", "0"],
["pow_mode", "basic"],
["nip40_expiration_enabled", "true"],
["nip40_expiration_strict", "true"],
["nip40_expiration_filter", "true"],
["nip40_expiration_grace_period", "300"],
["max_subscriptions_per_client", "25"],
["max_total_subscriptions", "5000"],
["max_filters_per_subscription", "10"],
["max_event_tags", "100"],
["max_content_length", "8196"],
["max_message_length", "16384"],
["default_limit", "500"],
["max_limit", "5000"]
],
"content": "C Nostr Relay Configuration",
"pubkey": "<admin_public_key>",
"id": "<computed_event_id>",
"sig": "<event_signature>"
}
```
**Note:** The `admin_pubkey` tag is omitted as it's redundant with the event's `pubkey` field.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,427 @@
# Unified Startup Sequence Design
## Overview
This document describes the new unified startup sequence where all config values are created first, then CLI overrides are applied as a separate atomic operation. This eliminates the current 3-step incremental building process.
## Current Problems
1. **Incremental Config Building**: Config is built in 3 steps:
- Step 1: `populate_default_config_values()` - adds defaults
- Step 2: CLI overrides applied via `update_config_in_table()`
- Step 3: `add_pubkeys_to_config_table()` - adds generated keys
2. **Race Conditions**: Cache can be refreshed between steps, causing incomplete config reads
3. **Complexity**: Multiple code paths for first-time vs restart scenarios
## New Design Principles
1. **Atomic Config Creation**: All config values created in single transaction
2. **Separate Override Phase**: CLI overrides applied after complete config exists
3. **Unified Code Path**: Same logic for first-time and restart scenarios
4. **Cache Safety**: Cache only loaded after config is complete
---
## Scenario 1: First-Time Startup (No Database)
### Sequence
```
1. Key Generation Phase
├─ generate_random_private_key_bytes() → admin_privkey_bytes
├─ nostr_bytes_to_hex() → admin_privkey (hex)
├─ nostr_ec_public_key_from_private_key() → admin_pubkey_bytes
├─ nostr_bytes_to_hex() → admin_pubkey (hex)
├─ generate_random_private_key_bytes() → relay_privkey_bytes
├─ nostr_bytes_to_hex() → relay_privkey (hex)
├─ nostr_ec_public_key_from_private_key() → relay_pubkey_bytes
└─ nostr_bytes_to_hex() → relay_pubkey (hex)
2. Database Creation Phase
├─ create_database_with_relay_pubkey(relay_pubkey)
│ └─ Sets g_database_path = "<relay_pubkey>.db"
└─ init_database(g_database_path)
└─ Creates database with embedded schema (includes config table)
3. Complete Config Population Phase (ATOMIC)
├─ BEGIN TRANSACTION
├─ populate_all_config_values_atomic()
│ ├─ Insert ALL default config values from DEFAULT_CONFIG_VALUES[]
│ ├─ Insert admin_pubkey
│ └─ Insert relay_pubkey
└─ COMMIT TRANSACTION
4. CLI Override Phase (ATOMIC)
├─ BEGIN TRANSACTION
├─ apply_cli_overrides()
│ ├─ IF cli_options.port_override > 0:
│ │ └─ UPDATE config SET value = ? WHERE key = 'relay_port'
│ ├─ IF cli_options.admin_pubkey_override[0]:
│ │ └─ UPDATE config SET value = ? WHERE key = 'admin_pubkey'
│ └─ IF cli_options.relay_privkey_override[0]:
│ └─ UPDATE config SET value = ? WHERE key = 'relay_privkey'
└─ COMMIT TRANSACTION
5. Secure Key Storage Phase
└─ store_relay_private_key(relay_privkey)
└─ INSERT INTO relay_seckey (private_key_hex) VALUES (?)
6. Cache Initialization Phase
└─ refresh_unified_cache_from_table()
└─ Loads complete config into g_unified_cache
```
### Function Call Sequence
```c
// In main.c - first_time_startup branch
if (is_first_time_startup()) {
// 1. Key Generation
first_time_startup_sequence(&cli_options);
// → Generates keys, stores in g_unified_cache
// → Sets g_database_path
// → Does NOT populate config yet
// 2. Database Creation
init_database(g_database_path);
// → Creates database with schema
// 3. Complete Config Population (NEW FUNCTION)
populate_all_config_values_atomic(&cli_options);
// → Inserts ALL defaults + pubkeys in single transaction
// → Does NOT apply CLI overrides yet
// 4. CLI Override Phase (NEW FUNCTION)
apply_cli_overrides_atomic(&cli_options);
// → Updates config table with CLI overrides
// → Separate transaction after complete config exists
// 5. Secure Key Storage
store_relay_private_key(relay_privkey);
// 6. Cache Initialization
refresh_unified_cache_from_table();
}
```
### New Functions Needed
```c
// In config.c
int populate_all_config_values_atomic(const cli_options_t* cli_options) {
// BEGIN TRANSACTION
// Insert ALL defaults from DEFAULT_CONFIG_VALUES[]
// Insert admin_pubkey from g_unified_cache
// Insert relay_pubkey from g_unified_cache
// COMMIT TRANSACTION
return 0;
}
int apply_cli_overrides_atomic(const cli_options_t* cli_options) {
// BEGIN TRANSACTION
// IF port_override: UPDATE config SET value = ? WHERE key = 'relay_port'
// IF admin_pubkey_override: UPDATE config SET value = ? WHERE key = 'admin_pubkey'
// IF relay_privkey_override: UPDATE config SET value = ? WHERE key = 'relay_privkey'
// COMMIT TRANSACTION
// invalidate_config_cache()
return 0;
}
```
---
## Scenario 2: Restart with Existing Database + CLI Options
### Sequence
```
1. Database Discovery Phase
├─ find_existing_db_files() → ["<relay_pubkey>.db"]
├─ extract_pubkey_from_filename() → relay_pubkey
└─ Sets g_database_path = "<relay_pubkey>.db"
2. Database Initialization Phase
└─ init_database(g_database_path)
└─ Opens existing database
3. Config Validation Phase
└─ validate_config_table_completeness()
├─ Check if all required keys exist
└─ IF missing keys: populate_missing_config_values()
4. CLI Override Phase (ATOMIC)
├─ BEGIN TRANSACTION
├─ apply_cli_overrides()
│ └─ UPDATE config SET value = ? WHERE key = ?
└─ COMMIT TRANSACTION
5. Cache Initialization Phase
└─ refresh_unified_cache_from_table()
└─ Loads complete config into g_unified_cache
```
### Function Call Sequence
```c
// In main.c - existing relay branch
else {
// 1. Database Discovery
char** existing_files = find_existing_db_files();
char* relay_pubkey = extract_pubkey_from_filename(existing_files[0]);
startup_existing_relay(relay_pubkey);
// → Sets g_database_path
// 2. Database Initialization
init_database(g_database_path);
// 3. Config Validation (NEW FUNCTION)
validate_config_table_completeness();
// → Checks for missing keys
// → Populates any missing defaults
// 4. CLI Override Phase (REUSE FUNCTION)
if (has_cli_overrides(&cli_options)) {
apply_cli_overrides_atomic(&cli_options);
}
// 5. Cache Initialization
refresh_unified_cache_from_table();
}
```
### New Functions Needed
```c
// In config.c
int validate_config_table_completeness(void) {
// Check if all DEFAULT_CONFIG_VALUES keys exist
// IF missing: populate_missing_config_values()
return 0;
}
int populate_missing_config_values(void) {
// BEGIN TRANSACTION
// For each key in DEFAULT_CONFIG_VALUES:
// IF NOT EXISTS: INSERT INTO config
// COMMIT TRANSACTION
return 0;
}
int has_cli_overrides(const cli_options_t* cli_options) {
return (cli_options->port_override > 0 ||
cli_options->admin_pubkey_override[0] != '\0' ||
cli_options->relay_privkey_override[0] != '\0');
}
```
---
## Scenario 3: Restart with Existing Database + No CLI Options
### Sequence
```
1. Database Discovery Phase
├─ find_existing_db_files() → ["<relay_pubkey>.db"]
├─ extract_pubkey_from_filename() → relay_pubkey
└─ Sets g_database_path = "<relay_pubkey>.db"
2. Database Initialization Phase
└─ init_database(g_database_path)
└─ Opens existing database
3. Config Validation Phase
└─ validate_config_table_completeness()
├─ Check if all required keys exist
└─ IF missing keys: populate_missing_config_values()
4. Cache Initialization Phase (IMMEDIATE)
└─ refresh_unified_cache_from_table()
└─ Loads complete config into g_unified_cache
```
### Function Call Sequence
```c
// In main.c - existing relay branch (no CLI overrides)
else {
// 1. Database Discovery
char** existing_files = find_existing_db_files();
char* relay_pubkey = extract_pubkey_from_filename(existing_files[0]);
startup_existing_relay(relay_pubkey);
// 2. Database Initialization
init_database(g_database_path);
// 3. Config Validation
validate_config_table_completeness();
// 4. Cache Initialization (IMMEDIATE - no overrides to apply)
refresh_unified_cache_from_table();
}
```
---
## Key Improvements
### 1. Atomic Config Creation
**Before:**
```c
populate_default_config_values(); // Step 1
update_config_in_table("relay_port", port_str); // Step 2
add_pubkeys_to_config_table(); // Step 3
```
**After:**
```c
populate_all_config_values_atomic(&cli_options); // Single transaction
apply_cli_overrides_atomic(&cli_options); // Separate transaction
```
### 2. Elimination of Race Conditions
**Before:**
- Cache could refresh between steps 1-3
- Incomplete config could be read
**After:**
- Config created atomically
- Cache only refreshed after complete config exists
### 3. Unified Code Path
**Before:**
- Different logic for first-time vs restart
- `populate_default_config_values()` vs `add_pubkeys_to_config_table()`
**After:**
- Same validation logic for both scenarios
- `validate_config_table_completeness()` handles both cases
### 4. Clear Separation of Concerns
**Before:**
- CLI overrides mixed with default population
- Unclear when overrides are applied
**After:**
- Phase 1: Complete config creation
- Phase 2: CLI overrides (if any)
- Phase 3: Cache initialization
---
## Implementation Changes Required
### 1. New Functions in config.c
```c
// Atomic config population for first-time startup
int populate_all_config_values_atomic(const cli_options_t* cli_options);
// Atomic CLI override application
int apply_cli_overrides_atomic(const cli_options_t* cli_options);
// Config validation for existing databases
int validate_config_table_completeness(void);
int populate_missing_config_values(void);
// Helper function
int has_cli_overrides(const cli_options_t* cli_options);
```
### 2. Modified Functions in config.c
```c
// Simplify to only generate keys and set database path
int first_time_startup_sequence(const cli_options_t* cli_options);
// Remove config population logic
int add_pubkeys_to_config_table(void); // DEPRECATED - logic moved to populate_all_config_values_atomic()
```
### 3. Modified Startup Flow in main.c
```c
// First-time startup
if (is_first_time_startup()) {
first_time_startup_sequence(&cli_options);
init_database(g_database_path);
populate_all_config_values_atomic(&cli_options); // NEW
apply_cli_overrides_atomic(&cli_options); // NEW
store_relay_private_key(relay_privkey);
refresh_unified_cache_from_table();
}
// Existing relay
else {
startup_existing_relay(relay_pubkey);
init_database(g_database_path);
validate_config_table_completeness(); // NEW
if (has_cli_overrides(&cli_options)) {
apply_cli_overrides_atomic(&cli_options); // NEW
}
refresh_unified_cache_from_table();
}
```
---
## Benefits
1. **Atomicity**: Config creation is atomic - no partial states
2. **Simplicity**: Clear phases with single responsibility
3. **Safety**: Cache only loaded after complete config exists
4. **Consistency**: Same validation logic for all scenarios
5. **Maintainability**: Easier to understand and modify
6. **Testability**: Each phase can be tested independently
---
## Migration Path
1. Implement new functions in config.c
2. Update main.c startup flow
3. Test first-time startup scenario
4. Test restart with CLI overrides
5. Test restart without CLI overrides
6. Remove deprecated functions
7. Update documentation
---
## Testing Strategy
### Test Cases
1. **First-time startup with defaults**
- Verify all config values created atomically
- Verify cache loads complete config
2. **First-time startup with port override**
- Verify defaults created first
- Verify port override applied second
- Verify cache reflects override
3. **Restart with complete config**
- Verify no config changes
- Verify cache loads immediately
4. **Restart with missing config keys**
- Verify missing keys populated
- Verify existing keys unchanged
5. **Restart with CLI overrides**
- Verify overrides applied atomically
- Verify cache invalidated and refreshed
### Validation Points
- Config table row count after each phase
- Cache validity state after each phase
- Transaction boundaries (BEGIN/COMMIT)
- Error handling for failed transactions

View File

@@ -0,0 +1,746 @@
# Unified Startup Implementation Plan
## Overview
This document provides a detailed implementation plan for refactoring the startup sequence to use atomic config creation followed by CLI overrides. This plan breaks down the work into discrete, testable steps.
---
## Phase 1: Create New Functions in config.c
### Step 1.1: Implement `populate_all_config_values_atomic()`
**Location**: `src/config.c`
**Purpose**: Create complete config table in single transaction for first-time startup
**Function Signature**:
```c
int populate_all_config_values_atomic(const cli_options_t* cli_options);
```
**Implementation Details**:
```c
int populate_all_config_values_atomic(const cli_options_t* cli_options) {
if (!g_database) {
DEBUG_ERROR("Database not initialized");
return -1;
}
// Begin transaction
char* err_msg = NULL;
int rc = sqlite3_exec(g_database, "BEGIN TRANSACTION;", NULL, NULL, &err_msg);
if (rc != SQLITE_OK) {
DEBUG_ERROR("Failed to begin transaction: %s", err_msg);
sqlite3_free(err_msg);
return -1;
}
// Prepare INSERT statement
sqlite3_stmt* stmt = NULL;
const char* sql = "INSERT INTO config (key, value) VALUES (?, ?)";
rc = sqlite3_prepare_v2(g_database, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
DEBUG_ERROR("Failed to prepare statement: %s", sqlite3_errmsg(g_database));
sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL);
return -1;
}
// Insert all default config values
for (size_t i = 0; i < sizeof(DEFAULT_CONFIG_VALUES) / sizeof(DEFAULT_CONFIG_VALUES[0]); i++) {
sqlite3_reset(stmt);
sqlite3_bind_text(stmt, 1, DEFAULT_CONFIG_VALUES[i].key, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, DEFAULT_CONFIG_VALUES[i].value, -1, SQLITE_STATIC);
rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE) {
DEBUG_ERROR("Failed to insert config key '%s': %s",
DEFAULT_CONFIG_VALUES[i].key, sqlite3_errmsg(g_database));
sqlite3_finalize(stmt);
sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL);
return -1;
}
}
// Insert admin_pubkey from cache
sqlite3_reset(stmt);
sqlite3_bind_text(stmt, 1, "admin_pubkey", -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, g_unified_cache.admin_pubkey, -1, SQLITE_STATIC);
rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE) {
DEBUG_ERROR("Failed to insert admin_pubkey: %s", sqlite3_errmsg(g_database));
sqlite3_finalize(stmt);
sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL);
return -1;
}
// Insert relay_pubkey from cache
sqlite3_reset(stmt);
sqlite3_bind_text(stmt, 1, "relay_pubkey", -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, g_unified_cache.relay_pubkey, -1, SQLITE_STATIC);
rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE) {
DEBUG_ERROR("Failed to insert relay_pubkey: %s", sqlite3_errmsg(g_database));
sqlite3_finalize(stmt);
sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL);
return -1;
}
sqlite3_finalize(stmt);
// Commit transaction
rc = sqlite3_exec(g_database, "COMMIT;", NULL, NULL, &err_msg);
if (rc != SQLITE_OK) {
DEBUG_ERROR("Failed to commit transaction: %s", err_msg);
sqlite3_free(err_msg);
sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL);
return -1;
}
DEBUG_INFO("Successfully populated all config values atomically");
return 0;
}
```
**Testing**:
- Verify transaction atomicity (all or nothing)
- Verify all DEFAULT_CONFIG_VALUES inserted
- Verify admin_pubkey and relay_pubkey inserted
- Verify error handling on failure
---
### Step 1.2: Implement `apply_cli_overrides_atomic()`
**Location**: `src/config.c`
**Purpose**: Apply CLI overrides to existing config table in single transaction
**Function Signature**:
```c
int apply_cli_overrides_atomic(const cli_options_t* cli_options);
```
**Implementation Details**:
```c
int apply_cli_overrides_atomic(const cli_options_t* cli_options) {
if (!g_database) {
DEBUG_ERROR("Database not initialized");
return -1;
}
if (!cli_options) {
DEBUG_ERROR("CLI options is NULL");
return -1;
}
// Check if any overrides exist
bool has_overrides = false;
if (cli_options->port_override > 0) has_overrides = true;
if (cli_options->admin_pubkey_override[0] != '\0') has_overrides = true;
if (cli_options->relay_privkey_override[0] != '\0') has_overrides = true;
if (!has_overrides) {
DEBUG_INFO("No CLI overrides to apply");
return 0;
}
// Begin transaction
char* err_msg = NULL;
int rc = sqlite3_exec(g_database, "BEGIN TRANSACTION;", NULL, NULL, &err_msg);
if (rc != SQLITE_OK) {
DEBUG_ERROR("Failed to begin transaction: %s", err_msg);
sqlite3_free(err_msg);
return -1;
}
// Prepare UPDATE statement
sqlite3_stmt* stmt = NULL;
const char* sql = "UPDATE config SET value = ? WHERE key = ?";
rc = sqlite3_prepare_v2(g_database, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
DEBUG_ERROR("Failed to prepare statement: %s", sqlite3_errmsg(g_database));
sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL);
return -1;
}
// Apply port override
if (cli_options->port_override > 0) {
char port_str[16];
snprintf(port_str, sizeof(port_str), "%d", cli_options->port_override);
sqlite3_reset(stmt);
sqlite3_bind_text(stmt, 1, port_str, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, "relay_port", -1, SQLITE_STATIC);
rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE) {
DEBUG_ERROR("Failed to update relay_port: %s", sqlite3_errmsg(g_database));
sqlite3_finalize(stmt);
sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL);
return -1;
}
DEBUG_INFO("Applied CLI override: relay_port = %s", port_str);
}
// Apply admin_pubkey override
if (cli_options->admin_pubkey_override[0] != '\0') {
sqlite3_reset(stmt);
sqlite3_bind_text(stmt, 1, cli_options->admin_pubkey_override, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, "admin_pubkey", -1, SQLITE_STATIC);
rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE) {
DEBUG_ERROR("Failed to update admin_pubkey: %s", sqlite3_errmsg(g_database));
sqlite3_finalize(stmt);
sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL);
return -1;
}
DEBUG_INFO("Applied CLI override: admin_pubkey");
}
// Apply relay_privkey override
if (cli_options->relay_privkey_override[0] != '\0') {
sqlite3_reset(stmt);
sqlite3_bind_text(stmt, 1, cli_options->relay_privkey_override, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, "relay_privkey", -1, SQLITE_STATIC);
rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE) {
DEBUG_ERROR("Failed to update relay_privkey: %s", sqlite3_errmsg(g_database));
sqlite3_finalize(stmt);
sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL);
return -1;
}
DEBUG_INFO("Applied CLI override: relay_privkey");
}
sqlite3_finalize(stmt);
// Commit transaction
rc = sqlite3_exec(g_database, "COMMIT;", NULL, NULL, &err_msg);
if (rc != SQLITE_OK) {
DEBUG_ERROR("Failed to commit transaction: %s", err_msg);
sqlite3_free(err_msg);
sqlite3_exec(g_database, "ROLLBACK;", NULL, NULL, NULL);
return -1;
}
// Invalidate cache to force refresh
invalidate_config_cache();
DEBUG_INFO("Successfully applied CLI overrides atomically");
return 0;
}
```
**Testing**:
- Verify transaction atomicity
- Verify each override type (port, admin_pubkey, relay_privkey)
- Verify cache invalidation after overrides
- Verify no-op when no overrides present
---
### Step 1.3: Implement `validate_config_table_completeness()`
**Location**: `src/config.c`
**Purpose**: Validate config table has all required keys, populate missing ones
**Function Signature**:
```c
int validate_config_table_completeness(void);
```
**Implementation Details**:
```c
int validate_config_table_completeness(void) {
if (!g_database) {
DEBUG_ERROR("Database not initialized");
return -1;
}
DEBUG_INFO("Validating config table completeness");
// Check each default config key
for (size_t i = 0; i < sizeof(DEFAULT_CONFIG_VALUES) / sizeof(DEFAULT_CONFIG_VALUES[0]); i++) {
const char* key = DEFAULT_CONFIG_VALUES[i].key;
// Check if key exists
sqlite3_stmt* stmt = NULL;
const char* sql = "SELECT COUNT(*) FROM config WHERE key = ?";
int rc = sqlite3_prepare_v2(g_database, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
DEBUG_ERROR("Failed to prepare statement: %s", sqlite3_errmsg(g_database));
return -1;
}
sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC);
rc = sqlite3_step(stmt);
int count = 0;
if (rc == SQLITE_ROW) {
count = sqlite3_column_int(stmt, 0);
}
sqlite3_finalize(stmt);
// If key missing, populate it
if (count == 0) {
DEBUG_WARN("Config key '%s' missing, populating with default", key);
rc = populate_missing_config_key(key, DEFAULT_CONFIG_VALUES[i].value);
if (rc != 0) {
DEBUG_ERROR("Failed to populate missing key '%s'", key);
return -1;
}
}
}
DEBUG_INFO("Config table validation complete");
return 0;
}
```
**Helper Function**:
```c
static int populate_missing_config_key(const char* key, const char* value) {
sqlite3_stmt* stmt = NULL;
const char* sql = "INSERT INTO config (key, value) VALUES (?, ?)";
int rc = sqlite3_prepare_v2(g_database, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
DEBUG_ERROR("Failed to prepare statement: %s", sqlite3_errmsg(g_database));
return -1;
}
sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, value, -1, SQLITE_STATIC);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) {
DEBUG_ERROR("Failed to insert config key '%s': %s", key, sqlite3_errmsg(g_database));
return -1;
}
return 0;
}
```
**Testing**:
- Verify detection of missing keys
- Verify population of missing keys with defaults
- Verify no changes when all keys present
- Verify error handling
---
### Step 1.4: Implement `has_cli_overrides()`
**Location**: `src/config.c`
**Purpose**: Check if any CLI overrides are present
**Function Signature**:
```c
bool has_cli_overrides(const cli_options_t* cli_options);
```
**Implementation Details**:
```c
bool has_cli_overrides(const cli_options_t* cli_options) {
if (!cli_options) {
return false;
}
return (cli_options->port_override > 0 ||
cli_options->admin_pubkey_override[0] != '\0' ||
cli_options->relay_privkey_override[0] != '\0');
}
```
**Testing**:
- Verify returns true when any override present
- Verify returns false when no overrides
- Verify NULL safety
---
## Phase 2: Update Function Declarations in config.h
### Step 2.1: Add New Function Declarations
**Location**: `src/config.h`
**Changes**:
```c
// Add after existing function declarations
// Atomic config population for first-time startup
int populate_all_config_values_atomic(const cli_options_t* cli_options);
// Atomic CLI override application
int apply_cli_overrides_atomic(const cli_options_t* cli_options);
// Config validation for existing databases
int validate_config_table_completeness(void);
// Helper function to check for CLI overrides
bool has_cli_overrides(const cli_options_t* cli_options);
```
---
## Phase 3: Refactor Startup Flow in main.c
### Step 3.1: Update First-Time Startup Branch
**Location**: `src/main.c` (around lines 1624-1740)
**Current Code**:
```c
if (is_first_time_startup()) {
first_time_startup_sequence(&cli_options);
init_database(g_database_path);
// Current incremental approach
populate_default_config_values();
if (cli_options.port_override > 0) {
char port_str[16];
snprintf(port_str, sizeof(port_str), "%d", cli_options.port_override);
update_config_in_table("relay_port", port_str);
}
add_pubkeys_to_config_table();
store_relay_private_key(relay_privkey);
refresh_unified_cache_from_table();
}
```
**New Code**:
```c
if (is_first_time_startup()) {
// 1. Generate keys and set database path
first_time_startup_sequence(&cli_options);
// 2. Create database with schema
init_database(g_database_path);
// 3. Populate ALL config values atomically (defaults + pubkeys)
if (populate_all_config_values_atomic(&cli_options) != 0) {
DEBUG_ERROR("Failed to populate config values");
return EXIT_FAILURE;
}
// 4. Apply CLI overrides atomically (separate transaction)
if (apply_cli_overrides_atomic(&cli_options) != 0) {
DEBUG_ERROR("Failed to apply CLI overrides");
return EXIT_FAILURE;
}
// 5. Store relay private key securely
store_relay_private_key(relay_privkey);
// 6. Load complete config into cache
refresh_unified_cache_from_table();
}
```
**Testing**:
- Verify first-time startup creates complete config
- Verify CLI overrides applied correctly
- Verify cache loads complete config
- Verify error handling at each step
---
### Step 3.2: Update Existing Relay Startup Branch
**Location**: `src/main.c` (around lines 1741-1928)
**Current Code**:
```c
else {
char** existing_files = find_existing_db_files();
char* relay_pubkey = extract_pubkey_from_filename(existing_files[0]);
startup_existing_relay(relay_pubkey);
init_database(g_database_path);
// Current approach - unclear when overrides applied
populate_default_config_values();
if (cli_options.port_override > 0) {
// ... override logic ...
}
refresh_unified_cache_from_table();
}
```
**New Code**:
```c
else {
// 1. Discover existing database
char** existing_files = find_existing_db_files();
if (!existing_files || !existing_files[0]) {
DEBUG_ERROR("No existing database files found");
return EXIT_FAILURE;
}
char* relay_pubkey = extract_pubkey_from_filename(existing_files[0]);
startup_existing_relay(relay_pubkey);
// 2. Open existing database
init_database(g_database_path);
// 3. Validate config table completeness (populate missing keys)
if (validate_config_table_completeness() != 0) {
DEBUG_ERROR("Failed to validate config table");
return EXIT_FAILURE;
}
// 4. Apply CLI overrides if present (separate transaction)
if (has_cli_overrides(&cli_options)) {
if (apply_cli_overrides_atomic(&cli_options) != 0) {
DEBUG_ERROR("Failed to apply CLI overrides");
return EXIT_FAILURE;
}
}
// 5. Load complete config into cache
refresh_unified_cache_from_table();
}
```
**Testing**:
- Verify existing relay startup with complete config
- Verify missing keys populated
- Verify CLI overrides applied when present
- Verify no changes when no overrides
- Verify cache loads correctly
---
## Phase 4: Deprecate Old Functions
### Step 4.1: Mark Functions as Deprecated
**Location**: `src/config.c`
**Functions to Deprecate**:
1. `populate_default_config_values()` - replaced by `populate_all_config_values_atomic()`
2. `add_pubkeys_to_config_table()` - logic moved to `populate_all_config_values_atomic()`
**Changes**:
```c
// Mark as deprecated in comments
// DEPRECATED: Use populate_all_config_values_atomic() instead
// This function will be removed in a future version
int populate_default_config_values(void) {
// ... existing implementation ...
}
// DEPRECATED: Use populate_all_config_values_atomic() instead
// This function will be removed in a future version
int add_pubkeys_to_config_table(void) {
// ... existing implementation ...
}
```
---
## Phase 5: Testing Strategy
### Unit Tests
1. **Test `populate_all_config_values_atomic()`**
- Test with valid cli_options
- Test transaction rollback on error
- Test all config keys inserted
- Test pubkeys inserted correctly
2. **Test `apply_cli_overrides_atomic()`**
- Test port override
- Test admin_pubkey override
- Test relay_privkey override
- Test multiple overrides
- Test no overrides
- Test transaction rollback on error
3. **Test `validate_config_table_completeness()`**
- Test with complete config
- Test with missing keys
- Test population of missing keys
4. **Test `has_cli_overrides()`**
- Test with each override type
- Test with no overrides
- Test with NULL cli_options
### Integration Tests
1. **First-Time Startup**
```bash
# Clean environment
rm -f *.db
# Start relay with defaults
./build/c_relay_x86
# Verify config table complete
sqlite3 <relay_pubkey>.db "SELECT COUNT(*) FROM config;"
# Expected: 20+ rows (all defaults + pubkeys)
# Verify cache loaded
# Check relay.log for cache refresh message
```
2. **First-Time Startup with CLI Overrides**
```bash
# Clean environment
rm -f *.db
# Start relay with port override
./build/c_relay_x86 --port 9999
# Verify port override applied
sqlite3 <relay_pubkey>.db "SELECT value FROM config WHERE key='relay_port';"
# Expected: 9999
```
3. **Restart with Existing Database**
```bash
# Start relay (creates database)
./build/c_relay_x86
# Stop relay
pkill -f c_relay_
# Restart relay
./build/c_relay_x86
# Verify config unchanged
# Check relay.log for validation message
```
4. **Restart with CLI Overrides**
```bash
# Start relay (creates database)
./build/c_relay_x86
# Stop relay
pkill -f c_relay_
# Restart with port override
./build/c_relay_x86 --port 9999
# Verify port override applied
sqlite3 <relay_pubkey>.db "SELECT value FROM config WHERE key='relay_port';"
# Expected: 9999
```
### Regression Tests
Run existing test suite to ensure no breakage:
```bash
./tests/run_all_tests.sh
```
---
## Phase 6: Documentation Updates
### Files to Update
1. **docs/configuration_guide.md**
- Update startup sequence description
- Document new atomic config creation
- Document CLI override behavior
2. **docs/startup_flows_complete.md**
- Update with new flow diagrams
- Document new function calls
3. **README.md**
- Update CLI options documentation
- Document override behavior
---
## Implementation Timeline
### Week 1: Core Functions
- Day 1-2: Implement `populate_all_config_values_atomic()`
- Day 3-4: Implement `apply_cli_overrides_atomic()`
- Day 5: Implement `validate_config_table_completeness()` and `has_cli_overrides()`
### Week 2: Integration
- Day 1-2: Update main.c startup flow
- Day 3-4: Testing and bug fixes
- Day 5: Documentation updates
### Week 3: Cleanup
- Day 1-2: Deprecate old functions
- Day 3-4: Final testing and validation
- Day 5: Code review and merge
---
## Risk Mitigation
### Potential Issues
1. **Database Lock Contention**
- Risk: Multiple transactions could cause locks
- Mitigation: Use BEGIN IMMEDIATE for write transactions
2. **Cache Invalidation Timing**
- Risk: Cache could be read before overrides applied
- Mitigation: Invalidate cache immediately after overrides
3. **Backward Compatibility**
- Risk: Existing databases might have incomplete config
- Mitigation: `validate_config_table_completeness()` handles this
4. **Transaction Rollback**
- Risk: Partial config on error
- Mitigation: All operations in transactions with proper rollback
---
## Success Criteria
1. ✅ All config values created atomically in first-time startup
2. ✅ CLI overrides applied in separate atomic transaction
3. ✅ Existing databases validated and missing keys populated
4. ✅ Cache only loaded after complete config exists
5. ✅ All existing tests pass
6. ✅ No race conditions in config creation
7. ✅ Clear separation between config creation and override phases
---
## Rollback Plan
If issues arise during implementation:
1. **Revert main.c changes** - restore original startup flow
2. **Keep new functions** - they can coexist with old code
3. **Add feature flag** - allow toggling between old and new behavior
4. **Gradual migration** - enable new behavior per scenario
```c
// Feature flag approach
#define USE_ATOMIC_CONFIG_CREATION 1
#if USE_ATOMIC_CONFIG_CREATION
// New atomic approach
populate_all_config_values_atomic(&cli_options);
apply_cli_overrides_atomic(&cli_options);
#else
// Old incremental approach
populate_default_config_values();
// ... existing code ...
#endif
```

View File

@@ -1,140 +0,0 @@
# Why MUSL Compilation Fails: Technical Explanation
## The Core Problem
**You cannot mix glibc headers/libraries with MUSL's C library.** They are fundamentally incompatible at the ABI (Application Binary Interface) level.
## What Happens When We Try
```bash
musl-gcc -I/usr/include src/main.c -lwebsockets
```
### Step-by-Step Breakdown:
1. **musl-gcc includes `<libwebsockets.h>`** from `/usr/include/libwebsockets.h`
2. **libwebsockets.h includes standard C headers:**
```c
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
```
3. **The system provides glibc's version of these headers** (from `/usr/include/`)
4. **glibc's `<string.h>` includes glibc-specific internal headers:**
```c
#include <bits/libc-header-start.h>
#include <bits/types.h>
```
5. **MUSL doesn't have these `bits/` headers** - it has a completely different structure:
- MUSL uses `/usr/include/x86_64-linux-musl/` for its headers
- MUSL's headers are simpler and don't use the `bits/` subdirectory structure
6. **Compilation fails** with:
```
fatal error: bits/libc-header-start.h: No such file or directory
```
## Why This Is Fundamental
### Different C Library Implementations
**glibc (GNU C Library):**
- Complex, feature-rich implementation
- Uses `bits/` subdirectories for platform-specific code
- Larger binary size
- More system-specific optimizations
**MUSL:**
- Minimal, clean implementation
- Simpler header structure
- Smaller binary size
- Designed for static linking and portability
### ABI Incompatibility
Even if headers compiled, the **Application Binary Interface (ABI)** is different:
- Function calling conventions may differ
- Structure layouts may differ
- System call wrappers are implemented differently
- Thread-local storage mechanisms differ
## The Solution: Build Everything with MUSL
To create a true MUSL static binary, you must:
### 1. Build libwebsockets with musl-gcc
```bash
git clone https://github.com/warmcat/libwebsockets.git
cd libwebsockets
mkdir build && cd build
cmake .. \
-DCMAKE_C_COMPILER=musl-gcc \
-DCMAKE_BUILD_TYPE=Release \
-DLWS_WITH_STATIC=ON \
-DLWS_WITH_SHARED=OFF \
-DLWS_WITHOUT_TESTAPPS=ON
make
```
### 2. Build OpenSSL with MUSL
```bash
wget https://www.openssl.org/source/openssl-3.0.0.tar.gz
tar xzf openssl-3.0.0.tar.gz
cd openssl-3.0.0
CC=musl-gcc ./config no-shared --prefix=/opt/musl-openssl
make && make install
```
### 3. Build all other dependencies
- zlib with musl-gcc
- libsecp256k1 with musl-gcc
- libcurl with musl-gcc (which itself needs OpenSSL built with MUSL)
### 4. Build c-relay with all MUSL libraries
```bash
musl-gcc -static \
-I/opt/musl-libwebsockets/include \
-I/opt/musl-openssl/include \
src/*.c \
-L/opt/musl-libwebsockets/lib -lwebsockets \
-L/opt/musl-openssl/lib -lssl -lcrypto \
...
```
## Why We Use glibc Static Instead
Building the entire dependency chain with MUSL is:
- **Time-consuming**: Hours to build all dependencies
- **Complex**: Each library has its own build quirks
- **Maintenance burden**: Must rebuild when dependencies update
- **Unnecessary for most use cases**: glibc static binaries work fine
### glibc Static Binary Advantages:
**Still fully static** - no runtime dependencies
**Works on virtually all Linux distributions**
**Much faster to build** - uses system libraries
**Easier to maintain** - no custom dependency builds
**Same practical portability** for modern Linux systems
### glibc Static Binary Limitations:
⚠️ **Slightly larger** than MUSL (glibc is bigger)
⚠️ **May not work on very old systems** (ancient glibc versions)
⚠️ **Not as universally portable** as MUSL (but close enough)
## Conclusion
**MUSL compilation fails because system libraries are compiled with glibc, and you cannot mix glibc and MUSL.**
The current approach (glibc static binary) is the pragmatic solution that provides excellent portability without the complexity of building an entire MUSL toolchain.
If true MUSL binaries are needed in the future, the solution is to use Alpine Linux (which uses MUSL natively) in a Docker container, where all system libraries are already MUSL-compiled.

View File

@@ -12,6 +12,7 @@ USE_TEST_KEYS=false
ADMIN_KEY=""
RELAY_KEY=""
PORT_OVERRIDE=""
DEBUG_LEVEL="5"
# Key validation function
validate_hex_key() {
@@ -71,6 +72,34 @@ while [[ $# -gt 0 ]]; do
USE_TEST_KEYS=true
shift
;;
--debug-level=*)
DEBUG_LEVEL="${1#*=}"
shift
;;
-d=*)
DEBUG_LEVEL="${1#*=}"
shift
;;
--debug-level)
if [ -z "$2" ]; then
echo "ERROR: Debug level option requires a value"
HELP=true
shift
else
DEBUG_LEVEL="$2"
shift 2
fi
;;
-d)
if [ -z "$2" ]; then
echo "ERROR: Debug level option requires a value"
HELP=true
shift
else
DEBUG_LEVEL="$2"
shift 2
fi
;;
--help|-h)
HELP=true
shift
@@ -104,6 +133,14 @@ if [ -n "$PORT_OVERRIDE" ]; then
fi
fi
# Validate debug level if provided
if [ -n "$DEBUG_LEVEL" ]; then
if ! [[ "$DEBUG_LEVEL" =~ ^[0-5]$ ]]; then
echo "ERROR: Debug level must be 0-5, got: $DEBUG_LEVEL"
exit 1
fi
fi
# Show help
if [ "$HELP" = true ]; then
echo "Usage: $0 [OPTIONS]"
@@ -112,6 +149,7 @@ if [ "$HELP" = true ]; then
echo " -a, --admin-key <hex> 64-character hex admin private key"
echo " -r, --relay-key <hex> 64-character hex relay private key"
echo " -p, --port <port> Custom port override (default: 8888)"
echo " -d, --debug-level <0-5> Set debug level: 0=none, 1=errors, 2=warnings, 3=info, 4=debug, 5=trace"
echo " --preserve-database Keep existing database files (don't delete for fresh start)"
echo " --test-keys, -t Use deterministic test keys for development (admin: all 'a's, relay: all '1's)"
echo " --help, -h Show this help message"
@@ -125,6 +163,8 @@ if [ "$HELP" = true ]; then
echo " $0 # Fresh start with random keys"
echo " $0 -a <admin-hex> -r <relay-hex> # Use custom keys"
echo " $0 -a <admin-hex> -p 9000 # Custom admin key on port 9000"
echo " $0 --debug-level=3 # Start with debug level 3 (info)"
echo " $0 -d=5 # Start with debug level 5 (trace)"
echo " $0 --preserve-database # Preserve existing database and keys"
echo " $0 --test-keys # Use test keys for consistent development"
echo " $0 -t --preserve-database # Use test keys and preserve database"
@@ -280,19 +320,24 @@ if [ -n "$PORT_OVERRIDE" ]; then
echo "Using custom port: $PORT_OVERRIDE"
fi
if [ -n "$DEBUG_LEVEL" ]; then
RELAY_ARGS="$RELAY_ARGS --debug-level=$DEBUG_LEVEL"
echo "Using debug level: $DEBUG_LEVEL"
fi
# Change to build directory before starting relay so database files are created there
cd build
# Start relay in background and capture its PID
if [ "$USE_TEST_KEYS" = true ]; then
echo "Using deterministic test keys for development..."
./$(basename $BINARY_PATH) -a 6a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3 -r 1111111111111111111111111111111111111111111111111111111111111111 --strict-port > ../relay.log 2>&1 &
./$(basename $BINARY_PATH) -a 6a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3 -r 1111111111111111111111111111111111111111111111111111111111111111 --debug-level=$DEBUG_LEVEL --strict-port > ../relay.log 2>&1 &
elif [ -n "$RELAY_ARGS" ]; then
echo "Starting relay with custom configuration..."
./$(basename $BINARY_PATH) $RELAY_ARGS --strict-port > ../relay.log 2>&1 &
./$(basename $BINARY_PATH) $RELAY_ARGS --debug-level=$DEBUG_LEVEL --strict-port > ../relay.log 2>&1 &
else
# No command line arguments needed for random key generation
echo "Starting relay with random key generation..."
./$(basename $BINARY_PATH) --strict-port > ../relay.log 2>&1 &
./$(basename $BINARY_PATH) --debug-level=$DEBUG_LEVEL --strict-port > ../relay.log 2>&1 &
fi
RELAY_PID=$!
# Change back to original directory

44
notes.txt Normal file
View File

@@ -0,0 +1,44 @@
Task Completed
Perfect! The core dump limit is now set correctly. You can see both:
LimitCORE=infinity
LimitCORESoft=infinity
What happens next:
The relay is now configured to generate core dumps when it crashes
Wait for the next crash to occur (based on your logs, it seems to happen intermittently)
When it crashes, the core dump will be automatically captured by systemd
After the next crash, analyze it:
# List all core dumps (most recent first)
sudo coredumpctl list
# View info about the most recent c-relay crash
sudo coredumpctl info c-relay
# Load the core dump in gdb for detailed analysis
sudo coredumpctl gdb c-relay
Inside gdb, run these commands:
(gdb) bt full # Full backtrace with all variables
(gdb) info threads # Show all threads
(gdb) thread apply all bt # Backtrace for all threads
(gdb) frame 0 # Go to crash frame
(gdb) info locals # Show local variables
(gdb) list # Show source code around crash
The backtrace will show you exactly which line of code caused the segmentation fault and the state of all variables at that moment, which should pinpoint whether it's the subscription management issue, buffer overflow, or something else.
You're all set up now - just wait for the next crash and then run the coredumpctl commands to analyze it.
--------------------------
Even simpler: Use this one-liner
# Start relay and immediately attach gdb
cd /usr/local/bin/c_relay
sudo -u c-relay ./c_relay --debug-level=5 & sleep 2 && sudo gdb -p $(pgrep c_relay)
Once gdb attaches, type continue and wait for the crash. This way the relay starts normally and gdb just monitors it.
Which approach would you like to try?

View File

@@ -1 +1 @@
1478877
3928044

View File

@@ -10,13 +10,7 @@
#include "api.h"
#include "embedded_web_content.h"
#include "config.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);
#include "debug.h"
// Forward declarations for database functions
int store_event(cJSON* event);
@@ -35,7 +29,7 @@ int handle_embedded_file_request(struct lws* wsi, const char* requested_uri) {
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");
DEBUG_WARN("Embedded file request without /api prefix");
lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL);
return -1;
}
@@ -43,7 +37,7 @@ int handle_embedded_file_request(struct lws* wsi, const char* requested_uri) {
// Get embedded file
embedded_file_t* file = get_embedded_file(file_path);
if (!file) {
log_warning("Embedded file not found");
DEBUG_WARN("Embedded file not found");
lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL);
return -1;
}
@@ -51,7 +45,7 @@ int handle_embedded_file_request(struct lws* wsi, const char* requested_uri) {
// 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");
DEBUG_ERROR("Failed to allocate embedded file session data");
return -1;
}
@@ -135,7 +129,7 @@ int handle_embedded_file_writeable(struct lws* wsi) {
// 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");
DEBUG_ERROR("Failed to allocate buffer for embedded file transmission");
free(session_data);
lws_set_wsi_user(wsi, NULL);
return -1;
@@ -151,7 +145,7 @@ int handle_embedded_file_writeable(struct lws* wsi) {
free(buf);
if (write_result < 0) {
log_error("Failed to write embedded file data");
DEBUG_ERROR("Failed to write embedded file data");
free(session_data);
lws_set_wsi_user(wsi, NULL);
return -1;

File diff suppressed because it is too large Load Diff

View File

@@ -27,89 +27,15 @@ struct lws;
// Database path for event-based config
extern char g_database_path[512];
// Unified configuration cache structure (consolidates all caching systems)
typedef struct {
// Critical keys (frequently accessed)
char admin_pubkey[65];
char relay_pubkey[65];
// Auth config (from request_validator)
int auth_required;
long max_file_size;
int admin_enabled;
int nip42_mode;
int nip42_challenge_timeout;
int nip42_time_tolerance;
int nip70_protected_events_enabled;
// Static buffer for config values (replaces static buffers in get_config_value functions)
char temp_buffer[CONFIG_VALUE_MAX_LENGTH];
// NIP-11 relay information (migrated from g_relay_info in main.c)
struct {
char name[RELAY_NAME_MAX_LENGTH];
char description[RELAY_DESCRIPTION_MAX_LENGTH];
char banner[RELAY_URL_MAX_LENGTH];
char icon[RELAY_URL_MAX_LENGTH];
char pubkey[RELAY_PUBKEY_MAX_LENGTH];
char contact[RELAY_CONTACT_MAX_LENGTH];
char software[RELAY_URL_MAX_LENGTH];
char version[64];
char privacy_policy[RELAY_URL_MAX_LENGTH];
char terms_of_service[RELAY_URL_MAX_LENGTH];
// Raw string values for parsing into cJSON arrays
char supported_nips_str[CONFIG_VALUE_MAX_LENGTH];
char language_tags_str[CONFIG_VALUE_MAX_LENGTH];
char relay_countries_str[CONFIG_VALUE_MAX_LENGTH];
// Parsed cJSON arrays
cJSON* supported_nips;
cJSON* limitation;
cJSON* retention;
cJSON* relay_countries;
cJSON* language_tags;
cJSON* tags;
char posting_policy[RELAY_URL_MAX_LENGTH];
cJSON* fees;
char payments_url[RELAY_URL_MAX_LENGTH];
} relay_info;
// NIP-13 PoW configuration (migrated from g_pow_config in main.c)
struct {
int enabled;
int min_pow_difficulty;
int validation_flags;
int require_nonce_tag;
int reject_lower_targets;
int strict_format;
int anti_spam_mode;
} pow_config;
// NIP-40 Expiration configuration (migrated from g_expiration_config in main.c)
struct {
int enabled;
int strict_mode;
int filter_responses;
int delete_expired;
long grace_period;
} expiration_config;
// Cache management
time_t cache_expires;
int cache_valid;
pthread_mutex_t cache_lock;
} unified_config_cache_t;
// Command line options structure for first-time startup
typedef struct {
int port_override; // -1 = not set, >0 = port value
char admin_pubkey_override[65]; // Empty string = not set, 64-char hex = override
char relay_privkey_override[65]; // Empty string = not set, 64-char hex = override
int strict_port; // 0 = allow port increment, 1 = fail if exact port unavailable
int debug_level; // 0-5, default 0 (no debug output)
} cli_options_t;
// Global unified configuration cache
extern unified_config_cache_t g_unified_cache;
// Core configuration functions (temporary compatibility)
int init_configuration_system(const char* config_dir_override, const char* config_file_override);
void cleanup_configuration_system(void);
@@ -137,8 +63,8 @@ int get_config_bool(const char* key, int default_value);
// First-time startup functions
int is_first_time_startup(void);
int first_time_startup_sequence(const cli_options_t* cli_options);
int startup_existing_relay(const char* relay_pubkey);
int first_time_startup_sequence(const cli_options_t* cli_options, char* admin_pubkey_out, char* relay_pubkey_out, char* relay_privkey_out);
int startup_existing_relay(const char* relay_pubkey, const cli_options_t* cli_options);
// Configuration application functions
int apply_configuration_from_event(const cJSON* event);
@@ -168,6 +94,7 @@ int set_config_value_in_table(const char* key, const char* value, const char* da
const char* description, const char* category, int requires_restart);
int update_config_in_table(const char* key, const char* value);
int populate_default_config_values(void);
int populate_all_config_values_atomic(const char* admin_pubkey, const char* relay_pubkey);
int add_pubkeys_to_config_table(void);
// Admin event processing functions (updated with WebSocket support)
@@ -187,7 +114,7 @@ cJSON* build_query_response(const char* query_type, cJSON* results_array, int to
// Auth rules management functions
int add_auth_rule_from_config(const char* rule_type, const char* pattern_type,
const char* pattern_value, const char* action);
const char* pattern_value);
int remove_auth_rule_from_config(const char* rule_type, const char* pattern_type,
const char* pattern_value);
@@ -211,6 +138,9 @@ int populate_config_table_from_event(const cJSON* event);
int process_startup_config_event(const cJSON* event);
int process_startup_config_event_with_fallback(const cJSON* event);
// Atomic CLI override application
int apply_cli_overrides_atomic(const cli_options_t* cli_options);
// Dynamic event generation functions for WebSocket configuration fetching
cJSON* generate_config_event_from_table(void);
int req_filter_requests_config_events(const cJSON* filter);

View File

@@ -28,6 +28,8 @@ static const struct {
{"nip42_auth_required_subscriptions", "false"},
{"nip42_auth_required_kinds", "4,14"}, // Default: DM kinds require auth
{"nip42_challenge_expiration", "600"}, // 10 minutes
{"nip42_challenge_timeout", "600"}, // Challenge timeout (seconds)
{"nip42_time_tolerance", "300"}, // Time tolerance (seconds)
// NIP-70 Protected Events
{"nip70_protected_events_enabled", "false"},

View File

@@ -1,5 +1,6 @@
#define _GNU_SOURCE
#include "config.h"
#include "debug.h"
#include "../nostr_core_lib/nostr_core/nostr_core.h"
#include "../nostr_core_lib/nostr_core/nip017.h"
#include "../nostr_core_lib/nostr_core/nip044.h"
@@ -15,12 +16,6 @@
// External database connection (from main.c)
extern sqlite3* g_db;
// Logging functions (defined in main.c)
extern void log_info(const char* message);
extern void log_success(const char* message);
extern void log_warning(const char* message);
extern void log_error(const char* message);
// Forward declarations for unified handlers
extern int handle_auth_query_unified(cJSON* event, const char* query_type, char* error_message, size_t error_size, struct lws* wsi);
extern int handle_config_query_unified(cJSON* event, const char* query_type, char* error_message, size_t error_size, struct lws* wsi);
@@ -35,11 +30,9 @@ extern const char* get_first_tag_name(cJSON* event);
extern const char* get_tag_value(cJSON* event, const char* tag_name, int value_index);
// Forward declarations for config functions
extern const char* get_relay_pubkey_cached(void);
extern char* get_relay_private_key(void);
extern const char* get_config_value(const char* key);
extern int get_config_bool(const char* key, int default_value);
extern const char* get_admin_pubkey_cached(void);
// Forward declarations for database functions
extern int store_event(cJSON* event);
@@ -137,14 +130,14 @@ cJSON* process_nip17_admin_message(cJSON* gift_wrap_event, char* error_message,
// This handles commands sent as direct JSON arrays, not wrapped in inner events
int process_dm_admin_command(cJSON* command_array, cJSON* event, char* error_message, size_t error_size, struct lws* wsi) {
if (!command_array || !cJSON_IsArray(command_array) || !event) {
log_error("DM Admin: Invalid command array or event");
DEBUG_ERROR("DM Admin: Invalid command array or event");
snprintf(error_message, error_size, "invalid: null command array or event");
return -1;
}
int array_size = cJSON_GetArraySize(command_array);
if (array_size < 1) {
log_error("DM Admin: Empty command array");
DEBUG_ERROR("DM Admin: Empty command array");
snprintf(error_message, error_size, "invalid: empty command array");
return -1;
}
@@ -152,7 +145,7 @@ int process_dm_admin_command(cJSON* command_array, cJSON* event, char* error_mes
// Get the command type from the first element
cJSON* command_item = cJSON_GetArrayItem(command_array, 0);
if (!command_item || !cJSON_IsString(command_item)) {
log_error("DM Admin: First element is not a string command");
DEBUG_ERROR("DM Admin: First element is not a string command");
snprintf(error_message, error_size, "invalid: command must be a string");
return -1;
}
@@ -209,7 +202,7 @@ int process_dm_admin_command(cJSON* command_array, cJSON* event, char* error_mes
if (strcmp(command_type, "auth_query") == 0) {
const char* query_type = get_tag_value(event, "auth_query", 1);
if (!query_type) {
log_error("DM Admin: Missing auth_query type parameter");
DEBUG_ERROR("DM Admin: Missing auth_query type parameter");
snprintf(error_message, error_size, "invalid: missing auth_query type");
} else {
result = handle_auth_query_unified(event, query_type, error_message, error_size, wsi);
@@ -218,7 +211,7 @@ int process_dm_admin_command(cJSON* command_array, cJSON* event, char* error_mes
else if (strcmp(command_type, "config_query") == 0) {
const char* query_type = get_tag_value(event, "config_query", 1);
if (!query_type) {
log_error("DM Admin: Missing config_query type parameter");
DEBUG_ERROR("DM Admin: Missing config_query type parameter");
snprintf(error_message, error_size, "invalid: missing config_query type");
} else {
result = handle_config_query_unified(event, query_type, error_message, error_size, wsi);
@@ -228,7 +221,7 @@ int process_dm_admin_command(cJSON* command_array, cJSON* event, char* error_mes
const char* config_key = get_tag_value(event, "config_set", 1);
const char* config_value = get_tag_value(event, "config_set", 2);
if (!config_key || !config_value) {
log_error("DM Admin: Missing config_set parameters");
DEBUG_ERROR("DM Admin: Missing config_set parameters");
snprintf(error_message, error_size, "invalid: missing config_set key or value");
} else {
result = handle_config_set_unified(event, config_key, config_value, error_message, error_size, wsi);
@@ -240,7 +233,7 @@ int process_dm_admin_command(cJSON* command_array, cJSON* event, char* error_mes
else if (strcmp(command_type, "system_command") == 0) {
const char* command = get_tag_value(event, "system_command", 1);
if (!command) {
log_error("DM Admin: Missing system_command type parameter");
DEBUG_ERROR("DM Admin: Missing system_command type parameter");
snprintf(error_message, error_size, "invalid: missing system_command type");
} else {
result = handle_system_command_unified(event, command, error_message, error_size, wsi);
@@ -253,13 +246,13 @@ int process_dm_admin_command(cJSON* command_array, cJSON* event, char* error_mes
result = handle_auth_rule_modification_unified(event, error_message, error_size, wsi);
}
else {
log_error("DM Admin: Unknown command type");
DEBUG_ERROR("DM Admin: Unknown command type");
printf(" Unknown command: %s\n", command_type);
snprintf(error_message, error_size, "invalid: unknown DM command type '%s'", command_type);
}
if (result != 0) {
log_error("DM Admin: Command processing failed");
DEBUG_ERROR("DM Admin: Command processing failed");
}
return result;
@@ -592,7 +585,7 @@ int apply_config_change(const char* key, const char* value) {
extern sqlite3* g_db;
if (!g_db) {
log_error("Database not available for config change");
DEBUG_ERROR("Database not available for config change");
return -1;
}
@@ -628,9 +621,9 @@ int apply_config_change(const char* key, const char* value) {
const char* sql = "INSERT OR REPLACE INTO config (key, value, data_type) VALUES (?, ?, ?)";
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
log_error("Failed to prepare config update statement");
DEBUG_ERROR("Failed to prepare config update statement");
const char* err_msg = sqlite3_errmsg(g_db);
log_error(err_msg);
DEBUG_ERROR(err_msg);
return -1;
}
@@ -640,9 +633,9 @@ int apply_config_change(const char* key, const char* value) {
int result = sqlite3_step(stmt);
if (result != SQLITE_DONE) {
log_error("Failed to update configuration in database");
DEBUG_ERROR("Failed to update configuration in database");
const char* err_msg = sqlite3_errmsg(g_db);
log_error(err_msg);
DEBUG_ERROR(err_msg);
sqlite3_finalize(stmt);
return -1;
}
@@ -766,7 +759,7 @@ int handle_config_confirmation(const char* admin_pubkey, const char* response) {
char error_msg[256];
int send_result = send_nip17_response(admin_pubkey, success_msg, error_msg, sizeof(error_msg));
if (send_result != 0) {
log_error(error_msg);
DEBUG_ERROR(error_msg);
}
// Remove the pending change
@@ -788,7 +781,7 @@ int handle_config_confirmation(const char* admin_pubkey, const char* response) {
char send_error_msg[256];
int send_result = send_nip17_response(admin_pubkey, error_msg, send_error_msg, sizeof(send_error_msg));
if (send_result != 0) {
log_error(send_error_msg);
DEBUG_ERROR(send_error_msg);
}
// Remove the pending change
@@ -890,7 +883,7 @@ int process_config_change_request(const char* admin_pubkey, const char* message)
char error_msg[256];
int send_result = send_nip17_response(admin_pubkey, confirmation, error_msg, sizeof(error_msg));
if (send_result != 0) {
log_error(error_msg);
DEBUG_ERROR(error_msg);
}
free(confirmation);
}
@@ -903,7 +896,7 @@ int process_config_change_request(const char* admin_pubkey, const char* message)
char* generate_stats_json(void) {
extern sqlite3* g_db;
if (!g_db) {
log_error("Database not available for stats generation");
DEBUG_ERROR("Database not available for stats generation");
return NULL;
}
@@ -1007,7 +1000,7 @@ char* generate_stats_json(void) {
cJSON_Delete(response);
if (!json_string) {
log_error("Failed to generate stats JSON");
DEBUG_ERROR("Failed to generate stats JSON");
}
return json_string;
@@ -1024,7 +1017,7 @@ int send_nip17_response(const char* sender_pubkey, const char* response_content,
}
// Get relay keys for signing
const char* relay_pubkey = get_relay_pubkey_cached();
const char* relay_pubkey = get_config_value("relay_pubkey");
char* relay_privkey_hex = get_relay_private_key();
if (!relay_pubkey || !relay_privkey_hex) {
if (relay_privkey_hex) free(relay_privkey_hex);
@@ -1093,19 +1086,30 @@ int send_nip17_response(const char* sender_pubkey, const char* response_content,
}
}
// Store the gift wrap in database
// Broadcast FIRST before storing (broadcasting needs the event intact)
// Make a copy for broadcasting to avoid use-after-free issues
cJSON* gift_wrap_copy = cJSON_Duplicate(gift_wraps[0], 1);
if (!gift_wrap_copy) {
cJSON_Delete(gift_wraps[0]);
strncpy(error_message, "NIP-17: Failed to duplicate gift wrap for broadcast", error_size - 1);
return -1;
}
// Broadcast the copy to active subscriptions
broadcast_event_to_subscriptions(gift_wrap_copy);
// Store the original in database
int store_result = store_event(gift_wraps[0]);
// Clean up both copies
cJSON_Delete(gift_wrap_copy);
cJSON_Delete(gift_wraps[0]);
if (store_result != 0) {
cJSON_Delete(gift_wraps[0]);
strncpy(error_message, "NIP-17: Failed to store response gift wrap", error_size - 1);
return -1;
}
// Broadcast the response event to active subscriptions
broadcast_event_to_subscriptions(gift_wraps[0]);
cJSON_Delete(gift_wraps[0]);
return 0;
}
@@ -1113,14 +1117,14 @@ int send_nip17_response(const char* sender_pubkey, const char* response_content,
char* generate_config_text(void) {
extern sqlite3* g_db;
if (!g_db) {
log_error("NIP-17: Database not available for config query");
DEBUG_ERROR("NIP-17: Database not available for config query");
return NULL;
}
// Build comprehensive config text from database
char* config_text = malloc(8192);
if (!config_text) {
log_error("NIP-17: Failed to allocate memory for config text");
DEBUG_ERROR("NIP-17: Failed to allocate memory for config text");
return NULL;
}
@@ -1146,7 +1150,7 @@ char* generate_config_text(void) {
sqlite3_finalize(stmt);
} else {
free(config_text);
log_error("NIP-17: Failed to query config from database");
DEBUG_ERROR("NIP-17: Failed to query config from database");
return NULL;
}
@@ -1161,7 +1165,7 @@ char* generate_config_text(void) {
char* generate_stats_text(void) {
char* stats_json = generate_stats_json();
if (!stats_json) {
log_error("NIP-17: Failed to generate stats for plain text command");
DEBUG_ERROR("NIP-17: Failed to generate stats for plain text command");
return NULL;
}
@@ -1345,7 +1349,7 @@ cJSON* process_nip17_admin_message(cJSON* gift_wrap_event, char* error_message,
// Convert hex private key to bytes
unsigned char relay_privkey[32];
if (nostr_hex_to_bytes(relay_privkey_hex, relay_privkey, sizeof(relay_privkey)) != 0) {
log_error("NIP-17: Failed to convert relay private key from hex");
DEBUG_ERROR("NIP-17: Failed to convert relay private key from hex");
free(relay_privkey_hex);
strncpy(error_message, "NIP-17: Failed to convert relay private key", error_size - 1);
return NULL;
@@ -1355,13 +1359,13 @@ cJSON* process_nip17_admin_message(cJSON* gift_wrap_event, char* error_message,
// Step 3: Decrypt and parse inner event using library function
cJSON* inner_dm = nostr_nip17_receive_dm(gift_wrap_event, relay_privkey);
if (!inner_dm) {
log_error("NIP-17: nostr_nip17_receive_dm returned NULL");
DEBUG_ERROR("NIP-17: nostr_nip17_receive_dm returned NULL");
// Debug: Print the gift wrap event
char* gift_wrap_debug = cJSON_Print(gift_wrap_event);
if (gift_wrap_debug) {
char debug_msg[1024];
snprintf(debug_msg, sizeof(debug_msg), "NIP-17: Gift wrap event: %.500s", gift_wrap_debug);
log_error(debug_msg);
DEBUG_ERROR(debug_msg);
free(gift_wrap_debug);
}
// Debug: Check if private key is valid
@@ -1435,7 +1439,7 @@ cJSON* process_nip17_admin_message(cJSON* gift_wrap_event, char* error_message,
"[\"command_processed\", \"success\", \"%s\"]", "NIP-17 admin command executed");
// Get relay pubkey for creating DM event
const char* relay_pubkey = get_relay_pubkey_cached();
const char* relay_pubkey = get_config_value("relay_pubkey");
if (relay_pubkey) {
cJSON* success_dm = nostr_nip17_create_chat_event(
response_content, // message content
@@ -1523,9 +1527,9 @@ int is_nip17_gift_wrap_for_relay(cJSON* event) {
return 0;
}
const char* relay_pubkey = get_relay_pubkey_cached();
const char* relay_pubkey = get_config_value("relay_pubkey");
if (!relay_pubkey) {
log_error("NIP-17: Could not get relay pubkey for validation");
DEBUG_ERROR("NIP-17: Could not get relay pubkey for validation");
return 0;
}
@@ -1571,7 +1575,7 @@ int process_nip17_admin_command(cJSON* dm_event, char* error_message, size_t err
const char* sender_pubkey = cJSON_GetStringValue(sender_pubkey_obj);
// Check if sender is admin
const char* admin_pubkey = get_admin_pubkey_cached();
const char* admin_pubkey = get_config_value("admin_pubkey");
int is_admin = admin_pubkey && strlen(admin_pubkey) > 0 && strcmp(sender_pubkey, admin_pubkey) == 0;
// Parse DM content as JSON array of commands
@@ -1605,7 +1609,7 @@ int process_nip17_admin_command(cJSON* dm_event, char* error_message, size_t err
free(stats_text);
if (result != 0) {
log_error(error_msg);
DEBUG_ERROR(error_msg);
return -1;
}
@@ -1623,7 +1627,7 @@ int process_nip17_admin_command(cJSON* dm_event, char* error_message, size_t err
free(config_text);
if (result != 0) {
log_error(error_msg);
DEBUG_ERROR(error_msg);
return -1;
}
@@ -1657,7 +1661,7 @@ int process_nip17_admin_command(cJSON* dm_event, char* error_message, size_t err
if (config_result > 0) {
return 1; // Return positive value to indicate response was handled
} else {
log_error("NIP-17: Configuration change request failed");
DEBUG_ERROR("NIP-17: Configuration change request failed");
return -1; // Return error to prevent generic success response
}
}
@@ -1697,7 +1701,7 @@ int process_nip17_admin_command(cJSON* dm_event, char* error_message, size_t err
cJSON_Delete(command_array);
if (result != 0) {
log_error(error_msg);
DEBUG_ERROR(error_msg);
strncpy(error_message, error_msg, error_size - 1);
return -1;
}

File diff suppressed because one or more lines are too long

View File

@@ -24,6 +24,7 @@
#include "sql_schema.h" // Embedded database schema
#include "websockets.h" // WebSocket protocol implementation
#include "subscriptions.h" // Subscription management system
#include "debug.h" // Debug system
// Forward declarations for unified request validator
int nostr_validate_unified_request(const char* json_string, size_t json_length);
@@ -73,10 +74,6 @@ struct relay_info {
char payments_url[RELAY_URL_MAX_LENGTH];
};
// Global relay information instance moved to unified cache
// static struct relay_info g_relay_info = {0}; // REMOVED - now in g_unified_cache.relay_info
// NIP-40 Expiration configuration (now in nip040.c)
extern struct expiration_config g_expiration_config;
@@ -91,16 +88,6 @@ extern subscription_manager_t g_subscription_manager;
/////////////////////////////////////////////////////////////////////////////////////////
// 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);
// Forward declaration for subscription manager configuration
void update_subscription_manager_config(void);
@@ -189,41 +176,7 @@ int validate_event_expiration(cJSON* event, char* error_message, size_t error_si
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
// Helper function to get current timestamp string
static void get_timestamp_string(char* buffer, size_t buffer_size) {
time_t now = time(NULL);
struct tm* local_time = localtime(&now);
strftime(buffer, buffer_size, "%Y-%m-%d %H:%M:%S", local_time);
}
// Logging functions
void log_info(const char* message) {
char timestamp[32];
get_timestamp_string(timestamp, sizeof(timestamp));
printf("[%s] [INFO] %s\n", timestamp, message);
fflush(stdout);
}
void log_success(const char* message) {
char timestamp[32];
get_timestamp_string(timestamp, sizeof(timestamp));
printf("[%s] [SUCCESS] %s\n", timestamp, message);
fflush(stdout);
}
void log_error(const char* message) {
char timestamp[32];
get_timestamp_string(timestamp, sizeof(timestamp));
printf("[%s] [ERROR] %s\n", timestamp, message);
fflush(stdout);
}
void log_warning(const char* message) {
char timestamp[32];
get_timestamp_string(timestamp, sizeof(timestamp));
printf("[%s] [WARNING] %s\n", timestamp, message);
fflush(stdout);
}
// Logging functions - REMOVED (replaced by debug system in debug.c)
// Update subscription manager configuration from config system
void update_subscription_manager_config(void) {
@@ -300,27 +253,27 @@ static void cleanup_stale_wal_files(const char* db_path) {
int has_shm = (access(shm_path, F_OK) == 0);
if (has_wal || has_shm) {
log_warning("Detected stale SQLite WAL files from previous unclean shutdown");
DEBUG_WARN("Detected stale SQLite WAL files from previous unclean shutdown");
// Try to remove WAL file
if (has_wal) {
if (unlink(wal_path) == 0) {
log_info("Removed stale WAL file");
DEBUG_INFO("Removed stale WAL file");
} else {
char error_msg[256];
snprintf(error_msg, sizeof(error_msg), "Failed to remove WAL file: %s", strerror(errno));
log_warning(error_msg);
DEBUG_WARN(error_msg);
}
}
// Try to remove SHM file
if (has_shm) {
if (unlink(shm_path) == 0) {
log_info("Removed stale SHM file");
DEBUG_INFO("Removed stale SHM file");
} else {
char error_msg[256];
snprintf(error_msg, sizeof(error_msg), "Failed to remove SHM file: %s", strerror(errno));
log_warning(error_msg);
DEBUG_WARN(error_msg);
}
}
}
@@ -328,6 +281,8 @@ static void cleanup_stale_wal_files(const char* db_path) {
// Initialize database connection and schema
int init_database(const char* database_path_override) {
DEBUG_TRACE("Entering init_database()");
// Priority 1: Command line database path override
const char* db_path = database_path_override;
@@ -341,15 +296,34 @@ int init_database(const char* database_path_override) {
db_path = DEFAULT_DATABASE_PATH;
}
DEBUG_LOG("Initializing database: %s", db_path);
// Clean up stale WAL files before opening database
cleanup_stale_wal_files(db_path);
int rc = sqlite3_open(db_path, &g_db);
if (rc != SQLITE_OK) {
log_error("Cannot open database");
DEBUG_ERROR("Cannot open database");
DEBUG_TRACE("Exiting init_database() - failed to open database");
return -1;
}
// DEBUG_GUARD_START
if (g_debug_level >= DEBUG_LEVEL_DEBUG) {
// Check config table row count immediately after database open
sqlite3_stmt* stmt;
if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM config", -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
int row_count = sqlite3_column_int(stmt, 0);
DEBUG_LOG("Config table row count immediately after sqlite3_open(): %d", row_count);
}
sqlite3_finalize(stmt);
} else {
DEBUG_LOG("Config table does not exist yet (first-time startup)");
}
}
// DEBUG_GUARD_END
// Check if database is already initialized by looking for the events table
const char* check_sql = "SELECT name FROM sqlite_master WHERE type='table' AND name='events'";
sqlite3_stmt* check_stmt;
@@ -379,7 +353,7 @@ int init_database(const char* database_path_override) {
} else {
char warning_msg[256];
snprintf(warning_msg, sizeof(warning_msg), "Unknown database schema version: %s", db_version);
log_warning(warning_msg);
DEBUG_WARN(warning_msg);
}
} else {
needs_migration = 1;
@@ -422,7 +396,7 @@ int init_database(const char* database_path_override) {
char error_log[512];
snprintf(error_log, sizeof(error_log), "Failed to create auth_rules table: %s",
error_msg ? error_msg : "unknown error");
log_error(error_log);
DEBUG_ERROR(error_log);
if (error_msg) sqlite3_free(error_msg);
return -1;
}
@@ -439,7 +413,7 @@ int init_database(const char* database_path_override) {
char index_error_log[512];
snprintf(index_error_log, sizeof(index_error_log), "Failed to create auth_rules indexes: %s",
index_error_msg ? index_error_msg : "unknown error");
log_error(index_error_log);
DEBUG_ERROR(index_error_log);
if (index_error_msg) sqlite3_free(index_error_msg);
return -1;
}
@@ -458,7 +432,7 @@ int init_database(const char* database_path_override) {
char error_log[512];
snprintf(error_log, sizeof(error_log), "Failed to update schema version: %s",
error_msg ? error_msg : "unknown error");
log_error(error_log);
DEBUG_ERROR(error_log);
if (error_msg) sqlite3_free(error_msg);
return -1;
}
@@ -474,7 +448,7 @@ int init_database(const char* database_path_override) {
char error_log[512];
snprintf(error_log, sizeof(error_log), "Failed to initialize database schema: %s",
error_msg ? error_msg : "unknown error");
log_error(error_log);
DEBUG_ERROR(error_log);
if (error_msg) {
sqlite3_free(error_msg);
}
@@ -483,7 +457,7 @@ int init_database(const char* database_path_override) {
}
} else {
log_error("Failed to check existing database schema");
DEBUG_ERROR("Failed to check existing database schema");
return -1;
}
@@ -493,36 +467,41 @@ int init_database(const char* database_path_override) {
if (rc != SQLITE_OK) {
char error_msg[256];
snprintf(error_msg, sizeof(error_msg), "Failed to enable WAL mode: %s",
wal_error ? wal_error : "unknown error");
log_warning(error_msg);
wal_error ? wal_error : "unknown error");
DEBUG_WARN(error_msg);
if (wal_error) sqlite3_free(wal_error);
// Continue anyway - WAL mode is optional
} else {
log_info("SQLite WAL mode enabled");
DEBUG_LOG("SQLite WAL mode enabled");
}
DEBUG_TRACE("Exiting init_database() - success");
return 0;
}
// Close database connection with proper WAL checkpoint
void close_database() {
DEBUG_TRACE("Entering close_database()");
if (g_db) {
// Perform WAL checkpoint to minimize stale files on next startup
log_info("Performing WAL checkpoint before database close");
DEBUG_LOG("Performing WAL checkpoint before database close");
char* checkpoint_error = NULL;
int rc = sqlite3_exec(g_db, "PRAGMA wal_checkpoint(TRUNCATE);", NULL, NULL, &checkpoint_error);
if (rc != SQLITE_OK) {
char error_msg[256];
snprintf(error_msg, sizeof(error_msg), "WAL checkpoint warning: %s",
checkpoint_error ? checkpoint_error : "unknown error");
log_warning(error_msg);
checkpoint_error ? checkpoint_error : "unknown error");
DEBUG_WARN(error_msg);
if (checkpoint_error) sqlite3_free(checkpoint_error);
}
sqlite3_close(g_db);
g_db = NULL;
log_info("Database connection closed");
DEBUG_LOG("Database connection closed");
}
DEBUG_TRACE("Exiting close_database()");
}
// Event type classification
@@ -691,7 +670,7 @@ int store_event(cJSON* event) {
cJSON* tags = cJSON_GetObjectItem(event, "tags");
if (!id || !pubkey || !created_at || !kind || !content || !sig) {
log_error("Invalid event - missing required fields");
DEBUG_ERROR("Invalid event - missing required fields");
return -1;
}
@@ -707,7 +686,7 @@ int store_event(cJSON* event) {
}
if (!tags_json) {
log_error("Failed to serialize tags to JSON");
DEBUG_ERROR("Failed to serialize tags to JSON");
return -1;
}
@@ -719,7 +698,7 @@ int store_event(cJSON* event) {
sqlite3_stmt* stmt;
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
log_error("Failed to prepare event insert statement");
DEBUG_ERROR("Failed to prepare event insert statement");
free(tags_json);
return -1;
}
@@ -740,13 +719,13 @@ int store_event(cJSON* event) {
if (rc != SQLITE_DONE) {
if (rc == SQLITE_CONSTRAINT) {
log_warning("Event already exists in database");
DEBUG_WARN("Event already exists in database");
free(tags_json);
return 0; // Not an error, just duplicate
}
char error_msg[256];
snprintf(error_msg, sizeof(error_msg), "Failed to insert event: %s", sqlite3_errmsg(g_db));
log_error(error_msg);
DEBUG_ERROR(error_msg);
free(tags_json);
return -1;
}
@@ -815,7 +794,7 @@ cJSON* retrieve_event(const char* event_id) {
int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, struct per_session_data *pss) {
if (!cJSON_IsArray(filters)) {
log_error("REQ filters is not an array");
DEBUG_ERROR("REQ filters is not an array");
return 0;
}
@@ -859,7 +838,7 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
// Check session subscription limits
if (pss->subscription_count >= g_subscription_manager.max_subscriptions_per_client) {
log_error("Maximum subscriptions per client exceeded");
DEBUG_ERROR("Maximum subscriptions per client exceeded");
// Update rate limiting counters for failed attempt
pss->failed_subscription_attempts++;
@@ -953,13 +932,13 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
// Create persistent subscription
subscription_t* subscription = create_subscription(sub_id, wsi, filters, pss ? pss->client_ip : "unknown");
if (!subscription) {
log_error("Failed to create subscription");
DEBUG_ERROR("Failed to create subscription");
return has_config_request ? config_events_sent : 0;
}
// Add to global manager
if (add_subscription_to_manager(subscription) != 0) {
log_error("Failed to add subscription to global manager");
DEBUG_ERROR("Failed to add subscription to global manager");
free_subscription(subscription);
// Send CLOSED notice
@@ -1007,7 +986,7 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
for (int i = 0; i < cJSON_GetArraySize(filters); i++) {
cJSON* filter = cJSON_GetArrayItem(filters, i);
if (!filter || !cJSON_IsObject(filter)) {
log_warning("Invalid filter object");
DEBUG_WARN("Invalid filter object");
continue;
}
@@ -1250,7 +1229,7 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
if (rc != SQLITE_OK) {
char error_msg[256];
snprintf(error_msg, sizeof(error_msg), "Failed to prepare subscription query: %s", sqlite3_errmsg(g_db));
log_error(error_msg);
DEBUG_ERROR(error_msg);
continue;
}
@@ -1284,10 +1263,8 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
cJSON_AddItemToObject(event, "tags", tags);
// Check expiration filtering (NIP-40) at application level
pthread_mutex_lock(&g_unified_cache.cache_lock);
int expiration_enabled = g_unified_cache.expiration_config.enabled;
int filter_responses = g_unified_cache.expiration_config.filter_responses;
pthread_mutex_unlock(&g_unified_cache.cache_lock);
int expiration_enabled = get_config_bool("expiration_enabled", 1);
int filter_responses = get_config_bool("expiration_filter", 1);
if (expiration_enabled && filter_responses) {
time_t current_time = time(NULL);
@@ -1395,7 +1372,7 @@ int is_authorized_admin_event(cJSON* event, char* error_buffer, size_t error_buf
// Step 3: Verify admin signature authorization
cJSON *pubkey_json = cJSON_GetObjectItem(event, "pubkey");
if (!pubkey_json || !cJSON_IsString(pubkey_json)) {
log_warning("Unauthorized admin event attempt: missing or invalid pubkey");
DEBUG_WARN("Unauthorized admin event attempt: missing or invalid pubkey");
snprintf(error_buffer, error_buffer_size, "Unauthorized admin event attempt: missing pubkey");
return -1;
}
@@ -1403,19 +1380,19 @@ int is_authorized_admin_event(cJSON* event, char* error_buffer, size_t error_buf
// Get admin pubkey from configuration
const char* admin_pubkey = get_config_value("admin_pubkey");
if (!admin_pubkey || strlen(admin_pubkey) == 0) {
log_warning("Unauthorized admin event attempt: no admin pubkey configured");
DEBUG_WARN("Unauthorized admin event attempt: no admin pubkey configured");
snprintf(error_buffer, error_buffer_size, "Unauthorized admin event attempt: no admin configured");
return -1;
}
// Compare pubkeys
if (strcmp(pubkey_json->valuestring, admin_pubkey) != 0) {
log_warning("Unauthorized admin event attempt: pubkey mismatch");
DEBUG_WARN("Unauthorized admin event attempt: pubkey mismatch");
char warning_msg[256];
snprintf(warning_msg, sizeof(warning_msg),
"Unauthorized admin event attempt from pubkey: %.32s...", pubkey_json->valuestring);
log_warning(warning_msg);
log_info("DEBUG: Pubkey comparison failed - event pubkey != admin pubkey");
DEBUG_WARN(warning_msg);
DEBUG_INFO("DEBUG: Pubkey comparison failed - event pubkey != admin pubkey");
snprintf(error_buffer, error_buffer_size, "Unauthorized admin event attempt: invalid admin pubkey");
return -1;
}
@@ -1423,7 +1400,7 @@ int is_authorized_admin_event(cJSON* event, char* error_buffer, size_t error_buf
// Step 4: Verify event signature
if (nostr_verify_event_signature(event) != 0) {
log_warning("Unauthorized admin event attempt: invalid signature");
DEBUG_WARN("Unauthorized admin event attempt: invalid signature");
snprintf(error_buffer, error_buffer_size, "Unauthorized admin event attempt: signature verification failed");
return -1;
}
@@ -1455,6 +1432,8 @@ void print_usage(const char* program_name) {
printf(" --strict-port Fail if exact port is unavailable (no port increment)\n");
printf(" -a, --admin-pubkey KEY Override admin public key (64-char hex or npub)\n");
printf(" -r, --relay-privkey KEY Override relay private key (64-char hex or nsec)\n");
printf(" --debug-level=N Set debug output level (0-5, default: 0)\n");
printf(" 0=none, 1=errors, 2=warnings, 3=info, 4=debug, 5=trace\n");
printf("\n");
printf("Configuration:\n");
printf(" This relay uses event-based configuration stored in the database.\n");
@@ -1492,7 +1471,8 @@ int main(int argc, char* argv[]) {
.port_override = -1, // -1 = not set
.admin_pubkey_override = {0}, // Empty string = not set
.relay_privkey_override = {0}, // Empty string = not set
.strict_port = 0 // 0 = allow port increment (default)
.strict_port = 0, // 0 = allow port increment (default)
.debug_level = 0 // 0 = no debug output (default)
};
// Parse command line arguments
@@ -1506,7 +1486,7 @@ int main(int argc, char* argv[]) {
} else if (strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "--port") == 0) {
// Port override option
if (i + 1 >= argc) {
log_error("Port option requires a value. Use --help for usage information.");
DEBUG_ERROR("Port option requires a value. Use --help for usage information.");
print_usage(argv[0]);
return 1;
}
@@ -1516,7 +1496,7 @@ int main(int argc, char* argv[]) {
long port = strtol(argv[i + 1], &endptr, 10);
if (endptr == argv[i + 1] || *endptr != '\0' || port < 1 || port > 65535) {
log_error("Invalid port number. Port must be between 1 and 65535.");
DEBUG_ERROR("Invalid port number. Port must be between 1 and 65535.");
print_usage(argv[0]);
return 1;
}
@@ -1529,7 +1509,7 @@ int main(int argc, char* argv[]) {
} else if (strcmp(argv[i], "-a") == 0 || strcmp(argv[i], "--admin-pubkey") == 0) {
// Admin public key override option
if (i + 1 >= argc) {
log_error("Admin pubkey option requires a value. Use --help for usage information.");
DEBUG_ERROR("Admin pubkey option requires a value. Use --help for usage information.");
print_usage(argv[0]);
return 1;
}
@@ -1547,7 +1527,7 @@ int main(int argc, char* argv[]) {
hex_ptr += 2;
}
} else {
log_error("Invalid admin public key format. Must be 64 hex characters or valid npub format.");
DEBUG_ERROR("Invalid admin public key format. Must be 64 hex characters or valid npub format.");
print_usage(argv[0]);
return 1;
}
@@ -1559,7 +1539,7 @@ int main(int argc, char* argv[]) {
} else if (strcmp(argv[i], "-r") == 0 || strcmp(argv[i], "--relay-privkey") == 0) {
// Relay private key override option
if (i + 1 >= argc) {
log_error("Relay privkey option requires a value. Use --help for usage information.");
DEBUG_ERROR("Relay privkey option requires a value. Use --help for usage information.");
print_usage(argv[0]);
return 1;
}
@@ -1577,7 +1557,7 @@ int main(int argc, char* argv[]) {
hex_ptr += 2;
}
} else {
log_error("Invalid relay private key format. Must be 64 hex characters or valid nsec format.");
DEBUG_ERROR("Invalid relay private key format. Must be 64 hex characters or valid nsec format.");
print_usage(argv[0]);
return 1;
}
@@ -1589,13 +1569,28 @@ int main(int argc, char* argv[]) {
} else if (strcmp(argv[i], "--strict-port") == 0) {
// Strict port mode option
cli_options.strict_port = 1;
} else if (strncmp(argv[i], "--debug-level=", 14) == 0) {
// Debug level option
char* endptr;
int debug_level = (int)strtol(argv[i] + 14, &endptr, 10);
if (endptr == argv[i] + 14 || *endptr != '\0' || debug_level < 0 || debug_level > 5) {
DEBUG_ERROR("Invalid debug level. Debug level must be between 0 and 5.");
print_usage(argv[0]);
return 1;
}
cli_options.debug_level = debug_level;
} else {
log_error("Unknown argument. Use --help for usage information.");
DEBUG_ERROR("Unknown argument. Use --help for usage information.");
print_usage(argv[0]);
return 1;
}
}
// Initialize debug system
debug_init(cli_options.debug_level);
// Set up signal handlers
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
@@ -1603,72 +1598,135 @@ int main(int argc, char* argv[]) {
printf(BLUE BOLD "=== C Nostr Relay Server ===" RESET "\n");
DEBUG_TRACE("Starting main initialization sequence");
// Initialize nostr library FIRST (required for key generation and event creation)
if (nostr_init() != 0) {
log_error("Failed to initialize nostr library");
DEBUG_ERROR("Failed to initialize nostr library");
return 1;
}
DEBUG_LOG("Nostr library initialized");
// Check if this is first-time startup or existing relay
if (is_first_time_startup()) {
DEBUG_LOG("First-time startup detected");
// Initialize event-based configuration system
if (init_configuration_system(NULL, NULL) != 0) {
log_error("Failed to initialize event-based configuration system");
DEBUG_ERROR("Failed to initialize event-based configuration system");
nostr_cleanup();
return 1;
}
// Run first-time startup sequence (generates keys, sets up database path, but doesn't store private key yet)
if (first_time_startup_sequence(&cli_options) != 0) {
log_error("Failed to complete first-time startup sequence");
char admin_pubkey[65] = {0};
char relay_pubkey[65] = {0};
char relay_privkey[65] = {0};
if (first_time_startup_sequence(&cli_options, admin_pubkey, relay_pubkey, relay_privkey) != 0) {
DEBUG_ERROR("Failed to complete first-time startup sequence");
cleanup_configuration_system();
nostr_cleanup();
return 1;
}
// Initialize database with the generated relay pubkey
DEBUG_TRACE("Initializing database for first-time startup");
if (init_database(g_database_path) != 0) {
log_error("Failed to initialize database after first-time setup");
DEBUG_ERROR("Failed to initialize database after first-time setup");
cleanup_configuration_system();
nostr_cleanup();
return 1;
}
DEBUG_LOG("Database initialized for first-time startup");
// Now that database is available, store the relay private key securely
const char* relay_privkey = get_temp_relay_private_key();
if (relay_privkey) {
if (store_relay_private_key(relay_privkey) != 0) {
log_error("Failed to store relay private key securely after database initialization");
cleanup_configuration_system();
nostr_cleanup();
return 1;
// DEBUG_GUARD_START
if (g_debug_level >= DEBUG_LEVEL_DEBUG) {
sqlite3_stmt* stmt;
if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM config", -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
int row_count = sqlite3_column_int(stmt, 0);
DEBUG_LOG("Config table row count after init_database() (first-time): %d", row_count);
}
sqlite3_finalize(stmt);
}
} else {
log_error("Relay private key not available from first-time startup");
cleanup_configuration_system();
nostr_cleanup();
return 1;
}
// DEBUG_GUARD_END
// Handle configuration setup after database is initialized
// Always populate defaults directly in config table (abandoning legacy event signing)
// Now that database is available, populate the complete config table atomically
// BUG FIX: Use the pubkeys returned from first_time_startup_sequence instead of trying to read from empty database
DEBUG_LOG("Using pubkeys from first-time startup sequence for config population");
DEBUG_LOG("admin_pubkey from startup: %s", admin_pubkey);
DEBUG_LOG("relay_pubkey from startup: %s", relay_pubkey);
// Populate default config values in table
if (populate_default_config_values() != 0) {
log_error("Failed to populate default config values");
if (populate_all_config_values_atomic(admin_pubkey, relay_pubkey) != 0) {
DEBUG_ERROR("Failed to populate complete config table");
cleanup_configuration_system();
nostr_cleanup();
close_database();
return 1;
}
// Apply CLI overrides atomically (after complete config table exists)
if (apply_cli_overrides_atomic(&cli_options) != 0) {
DEBUG_ERROR("Failed to apply CLI overrides");
cleanup_configuration_system();
nostr_cleanup();
close_database();
return 1;
}
// Now that database is available, store the relay private key securely
if (relay_privkey[0] != '\0') {
if (store_relay_private_key(relay_privkey) != 0) {
DEBUG_ERROR("Failed to store relay private key securely after database initialization");
cleanup_configuration_system();
nostr_cleanup();
close_database();
return 1;
}
} else {
DEBUG_ERROR("Relay private key not available from first-time startup");
cleanup_configuration_system();
nostr_cleanup();
close_database();
return 1;
}
// COMMENTED OUT: Old incremental config building code replaced by unified startup sequence
// The new first_time_startup_sequence() function handles all config creation atomically
/*
// Handle configuration setup after database is initialized
// Always populate defaults directly in config table (abandoning legacy event signing)
// Populate default config values in table
if (populate_default_config_values() != 0) {
DEBUG_ERROR("Failed to populate default config values");
cleanup_configuration_system();
nostr_cleanup();
close_database();
return 1;
}
// DEBUG_GUARD_START
if (g_debug_level >= DEBUG_LEVEL_DEBUG) {
sqlite3_stmt* stmt;
if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM config", -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
int row_count = sqlite3_column_int(stmt, 0);
DEBUG_LOG("Config table row count after populate_default_config_values(): %d", row_count);
}
sqlite3_finalize(stmt);
}
}
// DEBUG_GUARD_END
// Apply CLI overrides now that database is available
if (cli_options.port_override > 0) {
char port_str[16];
snprintf(port_str, sizeof(port_str), "%d", cli_options.port_override);
if (update_config_in_table("relay_port", port_str) != 0) {
log_error("Failed to update relay port override in config table");
DEBUG_ERROR("Failed to update relay port override in config table");
cleanup_configuration_system();
nostr_cleanup();
close_database();
@@ -1679,17 +1737,31 @@ int main(int argc, char* argv[]) {
// Add pubkeys to config table (single authoritative call)
if (add_pubkeys_to_config_table() != 0) {
log_error("Failed to add pubkeys to config table");
DEBUG_ERROR("Failed to add pubkeys to config table");
cleanup_configuration_system();
nostr_cleanup();
close_database();
return 1;
}
// DEBUG_GUARD_START
if (g_debug_level >= DEBUG_LEVEL_DEBUG) {
sqlite3_stmt* stmt;
if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM config", -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
int row_count = sqlite3_column_int(stmt, 0);
DEBUG_LOG("Config table row count after add_pubkeys_to_config_table() (first-time): %d", row_count);
}
sqlite3_finalize(stmt);
}
}
// DEBUG_GUARD_END
*/
} else {
// Find existing database file
char** existing_files = find_existing_db_files();
if (!existing_files || !existing_files[0]) {
log_error("No existing relay database found");
DEBUG_ERROR("No existing relay database found");
nostr_cleanup();
return 1;
}
@@ -1697,7 +1769,7 @@ int main(int argc, char* argv[]) {
// Extract relay pubkey from filename
char* relay_pubkey = extract_pubkey_from_filename(existing_files[0]);
if (!relay_pubkey) {
log_error("Failed to extract relay pubkey from database filename");
DEBUG_ERROR("Failed to extract relay pubkey from database filename");
// Free the files array
for (int i = 0; existing_files[i]; i++) {
free(existing_files[i]);
@@ -1709,7 +1781,7 @@ int main(int argc, char* argv[]) {
// Initialize event-based configuration system
if (init_configuration_system(NULL, NULL) != 0) {
log_error("Failed to initialize event-based configuration system");
DEBUG_ERROR("Failed to initialize event-based configuration system");
free(relay_pubkey);
for (int i = 0; existing_files[i]; i++) {
free(existing_files[i]);
@@ -1720,8 +1792,8 @@ int main(int argc, char* argv[]) {
}
// Setup existing relay (sets database path and loads config)
if (startup_existing_relay(relay_pubkey) != 0) {
log_error("Failed to setup existing relay");
if (startup_existing_relay(relay_pubkey, &cli_options) != 0) {
DEBUG_ERROR("Failed to setup existing relay");
cleanup_configuration_system();
free(relay_pubkey);
for (int i = 0; existing_files[i]; i++) {
@@ -1739,7 +1811,8 @@ int main(int argc, char* argv[]) {
sqlite3_stmt* stmt;
if (sqlite3_prepare_v2(temp_db, "SELECT COUNT(*) FROM config", -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
// Row count check completed
int row_count = sqlite3_column_int(stmt, 0);
printf(" Config table row count before database initialization: %d\n", row_count);
}
sqlite3_finalize(stmt);
}
@@ -1748,8 +1821,9 @@ int main(int argc, char* argv[]) {
}
// Initialize database with existing database path
DEBUG_TRACE("Initializing existing database");
if (init_database(g_database_path) != 0) {
log_error("Failed to initialize existing database");
DEBUG_ERROR("Failed to initialize existing database");
cleanup_configuration_system();
free(relay_pubkey);
for (int i = 0; existing_files[i]; i++) {
@@ -1759,30 +1833,36 @@ int main(int argc, char* argv[]) {
nostr_cleanup();
return 1;
}
DEBUG_LOG("Existing database initialized");
// Check config table row count after database initialization
{
// DEBUG_GUARD_START
if (g_debug_level >= DEBUG_LEVEL_DEBUG) {
sqlite3_stmt* stmt;
if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM config", -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
// Row count check completed
int row_count = sqlite3_column_int(stmt, 0);
DEBUG_LOG("Config table row count after init_database(): %d", row_count);
}
sqlite3_finalize(stmt);
}
}
// DEBUG_GUARD_END
// COMMENTED OUT: Old incremental config building code replaced by unified startup sequence
// The new startup_existing_relay() function handles all config loading atomically
/*
// Ensure default configuration values are populated (for any missing keys)
// This must be done AFTER database initialization
// COMMENTED OUT: Don't modify existing database config on restart
// if (populate_default_config_values() != 0) {
// log_warning("Failed to populate default config values for existing relay - continuing");
// DEBUG_WARN("Failed to populate default config values for existing relay - continuing");
// }
// Load configuration from database
cJSON* config_event = load_config_event_from_database(relay_pubkey);
if (config_event) {
if (apply_configuration_from_event(config_event) != 0) {
log_warning("Failed to apply configuration from database");
DEBUG_WARN("Failed to apply configuration from database");
}
cJSON_Delete(config_event);
} else {
@@ -1790,6 +1870,19 @@ int main(int argc, char* argv[]) {
// No longer a warning - just informational
}
// DEBUG_GUARD_START
if (g_debug_level >= DEBUG_LEVEL_DEBUG) {
sqlite3_stmt* stmt;
if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM config", -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
int row_count = sqlite3_column_int(stmt, 0);
DEBUG_LOG("Config table row count before checking pubkeys: %d", row_count);
}
sqlite3_finalize(stmt);
}
}
// DEBUG_GUARD_END
// Ensure pubkeys are in config table for existing relay
// This handles migration from old event-based config to table-based config
const char* admin_pubkey_from_table = get_config_value_from_table("admin_pubkey");
@@ -1799,14 +1892,14 @@ int main(int argc, char* argv[]) {
// Check if admin_pubkey is missing or invalid
if (!admin_pubkey_from_table || strlen(admin_pubkey_from_table) != 64) {
log_warning("Admin pubkey missing or invalid in config table - will regenerate from cache");
DEBUG_WARN("Admin pubkey missing or invalid in config table - will regenerate from cache");
need_to_add_pubkeys = 1;
}
if (admin_pubkey_from_table) free((char*)admin_pubkey_from_table);
// Check if relay_pubkey is missing or invalid
if (!relay_pubkey_from_table || strlen(relay_pubkey_from_table) != 64) {
log_warning("Relay pubkey missing or invalid in config table - will regenerate from cache");
DEBUG_WARN("Relay pubkey missing or invalid in config table - will regenerate from cache");
need_to_add_pubkeys = 1;
}
if (relay_pubkey_from_table) free((char*)relay_pubkey_from_table);
@@ -1814,12 +1907,25 @@ int main(int argc, char* argv[]) {
// If either pubkey is missing, call add_pubkeys_to_config_table to populate both
if (need_to_add_pubkeys) {
if (add_pubkeys_to_config_table() != 0) {
log_error("Failed to add pubkeys to config table for existing relay");
DEBUG_ERROR("Failed to add pubkeys to config table for existing relay");
cleanup_configuration_system();
nostr_cleanup();
close_database();
return 1;
}
// DEBUG_GUARD_START
if (g_debug_level >= DEBUG_LEVEL_DEBUG) {
sqlite3_stmt* stmt;
if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM config", -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
int row_count = sqlite3_column_int(stmt, 0);
DEBUG_LOG("Config table row count after add_pubkeys_to_config_table(): %d", row_count);
}
sqlite3_finalize(stmt);
}
}
// DEBUG_GUARD_END
}
// Apply CLI overrides for existing relay (port override should work even for existing relays)
@@ -1827,7 +1933,7 @@ int main(int argc, char* argv[]) {
char port_str[16];
snprintf(port_str, sizeof(port_str), "%d", cli_options.port_override);
if (update_config_in_table("relay_port", port_str) != 0) {
log_error("Failed to update relay port override in config table for existing relay");
DEBUG_ERROR("Failed to update relay port override in config table for existing relay");
cleanup_configuration_system();
nostr_cleanup();
close_database();
@@ -1835,6 +1941,7 @@ int main(int argc, char* argv[]) {
}
printf(" Port: %d (overriding configured port)\n", cli_options.port_override);
}
*/
// Free memory
free(relay_pubkey);
@@ -1846,7 +1953,7 @@ int main(int argc, char* argv[]) {
// Verify database is now available
if (!g_db) {
log_error("Database not available after initialization");
DEBUG_ERROR("Database not available after initialization");
cleanup_configuration_system();
nostr_cleanup();
return 1;
@@ -1857,7 +1964,7 @@ int main(int argc, char* argv[]) {
// Initialize unified request validator system
if (ginxsom_request_validator_init(g_database_path, "c-relay") != 0) {
log_error("Failed to initialize unified request validator");
DEBUG_ERROR("Failed to initialize unified request validator");
cleanup_configuration_system();
nostr_cleanup();
close_database();
@@ -1877,7 +1984,7 @@ int main(int argc, char* argv[]) {
// Initialize subscription manager mutexes
if (pthread_mutex_init(&g_subscription_manager.subscriptions_lock, NULL) != 0) {
log_error("Failed to initialize subscription manager subscriptions lock");
DEBUG_ERROR("Failed to initialize subscription manager subscriptions lock");
cleanup_configuration_system();
nostr_cleanup();
close_database();
@@ -1885,7 +1992,7 @@ int main(int argc, char* argv[]) {
}
if (pthread_mutex_init(&g_subscription_manager.ip_tracking_lock, NULL) != 0) {
log_error("Failed to initialize subscription manager IP tracking lock");
DEBUG_ERROR("Failed to initialize subscription manager IP tracking lock");
pthread_mutex_destroy(&g_subscription_manager.subscriptions_lock);
cleanup_configuration_system();
nostr_cleanup();
@@ -1912,7 +2019,7 @@ int main(int argc, char* argv[]) {
if (result == 0) {
} else {
log_error("Server shutdown with errors");
DEBUG_ERROR("Server shutdown with errors");
}
return result;

View File

@@ -6,15 +6,13 @@
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
#include <cjson/cJSON.h>
#include "debug.h"
#include <sqlite3.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include <stdio.h>
// Forward declarations for logging functions
void log_warning(const char* message);
void log_info(const char* message);
// Forward declaration for database functions
int store_event(cJSON* event);
@@ -139,7 +137,7 @@ int handle_deletion_request(cJSON* event, char* error_message, size_t error_size
// Store the deletion request itself (it should be kept according to NIP-09)
if (store_event(event) != 0) {
log_warning("Failed to store deletion request event");
DEBUG_WARN("Failed to store deletion request event");
}
error_message[0] = '\0'; // Success - empty error message
@@ -198,7 +196,7 @@ int delete_events_by_id(const char* requester_pubkey, cJSON* event_ids) {
sqlite3_finalize(check_stmt);
char warning_msg[128];
snprintf(warning_msg, sizeof(warning_msg), "Unauthorized deletion attempt for event: %.16s...", id);
log_warning(warning_msg);
DEBUG_WARN(warning_msg);
}
} else {
sqlite3_finalize(check_stmt);
@@ -244,7 +242,7 @@ int delete_events_by_address(const char* requester_pubkey, cJSON* addresses, lon
free(addr_copy);
char warning_msg[128];
snprintf(warning_msg, sizeof(warning_msg), "Unauthorized deletion attempt for address: %.32s...", addr);
log_warning(warning_msg);
DEBUG_WARN(warning_msg);
continue;
}

View File

@@ -1,6 +1,7 @@
// NIP-11 Relay Information Document module
#define _GNU_SOURCE
#include <stdio.h>
#include "debug.h"
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
@@ -8,19 +9,13 @@
#include "../nostr_core_lib/cjson/cJSON.h"
#include "config.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);
// Forward declarations for configuration functions
const char* get_config_value(const char* key);
int get_config_int(const char* key, int default_value);
int get_config_bool(const char* key, int default_value);
// Forward declarations for global cache access
extern unified_config_cache_t g_unified_cache;
// NIP-11 relay information is now managed directly from config table
// Forward declarations for constants (defined in config.h and other headers)
#define HTTP_STATUS_OK 200
@@ -79,18 +74,39 @@ cJSON* parse_comma_separated_array(const char* csv_string) {
// Initialize relay information using configuration system
void init_relay_info() {
// Get all config values first (without holding mutex to avoid deadlock)
// Note: These may be dynamically allocated strings that need to be freed
// NIP-11 relay information is now generated dynamically from config table
// No initialization needed - data is fetched directly from database when requested
}
// Clean up relay information JSON objects
void cleanup_relay_info() {
// NIP-11 relay information is now generated dynamically from config table
// No cleanup needed - data is fetched directly from database when requested
}
// Generate NIP-11 compliant JSON document
cJSON* generate_relay_info_json() {
cJSON* info = cJSON_CreateObject();
if (!info) {
DEBUG_ERROR("Failed to create relay info JSON object");
return NULL;
}
// Get all config values directly from database
const char* relay_name = get_config_value("relay_name");
const char* relay_description = get_config_value("relay_description");
const char* relay_banner = get_config_value("relay_banner");
const char* relay_icon = get_config_value("relay_icon");
const char* relay_pubkey = get_config_value("relay_pubkey");
const char* relay_contact = get_config_value("relay_contact");
const char* supported_nips_csv = get_config_value("supported_nips");
const char* relay_software = get_config_value("relay_software");
const char* relay_version = get_config_value("relay_version");
const char* relay_contact = get_config_value("relay_contact");
const char* relay_pubkey = get_config_value("relay_pubkey");
const char* supported_nips_csv = get_config_value("supported_nips");
const char* privacy_policy = get_config_value("privacy_policy");
const char* terms_of_service = get_config_value("terms_of_service");
const char* posting_policy = get_config_value("posting_policy");
const char* language_tags_csv = get_config_value("language_tags");
const char* relay_countries_csv = get_config_value("relay_countries");
const char* posting_policy = get_config_value("posting_policy");
const char* payments_url = get_config_value("payments_url");
// Get config values for limitations
@@ -100,415 +116,169 @@ void init_relay_info() {
int max_event_tags = get_config_int("max_event_tags", 100);
int max_content_length = get_config_int("max_content_length", 8196);
int default_limit = get_config_int("default_limit", 500);
int min_pow_difficulty = get_config_int("pow_min_difficulty", 0);
int admin_enabled = get_config_bool("admin_enabled", 0);
pthread_mutex_lock(&g_unified_cache.cache_lock);
// Update relay information fields
if (relay_name) {
strncpy(g_unified_cache.relay_info.name, relay_name, sizeof(g_unified_cache.relay_info.name) - 1);
free((char*)relay_name); // Free dynamically allocated string
} else {
strncpy(g_unified_cache.relay_info.name, "C Nostr Relay", sizeof(g_unified_cache.relay_info.name) - 1);
}
if (relay_description) {
strncpy(g_unified_cache.relay_info.description, relay_description, sizeof(g_unified_cache.relay_info.description) - 1);
free((char*)relay_description); // Free dynamically allocated string
} 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);
}
if (relay_software) {
strncpy(g_unified_cache.relay_info.software, relay_software, sizeof(g_unified_cache.relay_info.software) - 1);
free((char*)relay_software); // Free dynamically allocated string
} else {
strncpy(g_unified_cache.relay_info.software, "https://git.laantungir.net/laantungir/c-relay.git", sizeof(g_unified_cache.relay_info.software) - 1);
}
if (relay_version) {
strncpy(g_unified_cache.relay_info.version, relay_version, sizeof(g_unified_cache.relay_info.version) - 1);
free((char*)relay_version); // Free dynamically allocated string
} else {
strncpy(g_unified_cache.relay_info.version, "0.2.0", sizeof(g_unified_cache.relay_info.version) - 1);
}
if (relay_contact) {
strncpy(g_unified_cache.relay_info.contact, relay_contact, sizeof(g_unified_cache.relay_info.contact) - 1);
free((char*)relay_contact); // Free dynamically allocated string
}
if (relay_pubkey) {
strncpy(g_unified_cache.relay_info.pubkey, relay_pubkey, sizeof(g_unified_cache.relay_info.pubkey) - 1);
free((char*)relay_pubkey); // Free dynamically allocated string
}
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); // Free dynamically allocated string
}
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); // Free dynamically allocated string
}
// Initialize supported NIPs array from config
if (supported_nips_csv) {
g_unified_cache.relay_info.supported_nips = parse_comma_separated_array(supported_nips_csv);
free((char*)supported_nips_csv); // Free dynamically allocated string
} else {
// Fallback to default supported NIPs
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)); // NIP-01: Basic protocol
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(9)); // NIP-09: Event deletion
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(11)); // NIP-11: Relay information
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(13)); // NIP-13: Proof of Work
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(15)); // NIP-15: EOSE
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(20)); // NIP-20: Command results
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(40)); // NIP-40: Expiration Timestamp
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(42)); // NIP-42: Authentication
}
}
// Initialize server limitations using configuration
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);
}
// Initialize empty retention policies (can be configured later)
g_unified_cache.relay_info.retention = cJSON_CreateArray();
// Initialize language tags from config
if (language_tags_csv) {
g_unified_cache.relay_info.language_tags = parse_comma_separated_array(language_tags_csv);
free((char*)language_tags_csv); // Free dynamically allocated string
} else {
// Fallback to global
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("*"));
}
}
// Initialize relay countries from config
if (relay_countries_csv) {
g_unified_cache.relay_info.relay_countries = parse_comma_separated_array(relay_countries_csv);
free((char*)relay_countries_csv); // Free dynamically allocated string
} else {
// Fallback to global
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("*"));
}
}
// Initialize content tags as empty array
g_unified_cache.relay_info.tags = cJSON_CreateArray();
// Initialize fees as empty object (no payment required by default)
g_unified_cache.relay_info.fees = cJSON_CreateObject();
pthread_mutex_unlock(&g_unified_cache.cache_lock);
}
// Clean up relay information JSON objects
void cleanup_relay_info() {
pthread_mutex_lock(&g_unified_cache.cache_lock);
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.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.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.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;
}
pthread_mutex_unlock(&g_unified_cache.cache_lock);
}
// Generate NIP-11 compliant JSON document
cJSON* generate_relay_info_json() {
cJSON* info = cJSON_CreateObject();
if (!info) {
log_error("Failed to create relay info JSON object");
return NULL;
}
pthread_mutex_lock(&g_unified_cache.cache_lock);
// Defensive reinit: if relay_info appears empty (cache refresh wiped it), rebuild it directly from table
if (strlen(g_unified_cache.relay_info.name) == 0 &&
strlen(g_unified_cache.relay_info.description) == 0 &&
strlen(g_unified_cache.relay_info.software) == 0) {
log_warning("NIP-11 relay_info appears empty, rebuilding directly from config table");
// Rebuild relay_info directly from config table to avoid circular cache dependency
// Get values directly from table (similar to init_relay_info but without cache calls)
const char* relay_name = get_config_value_from_table("relay_name");
if (relay_name) {
strncpy(g_unified_cache.relay_info.name, relay_name, sizeof(g_unified_cache.relay_info.name) - 1);
free((char*)relay_name);
} else {
strncpy(g_unified_cache.relay_info.name, "C Nostr Relay", sizeof(g_unified_cache.relay_info.name) - 1);
}
const char* relay_description = get_config_value_from_table("relay_description");
if (relay_description) {
strncpy(g_unified_cache.relay_info.description, relay_description, sizeof(g_unified_cache.relay_info.description) - 1);
free((char*)relay_description);
} else {
strncpy(g_unified_cache.relay_info.description, "A high-performance Nostr relay implemented in C with SQLite storage", sizeof(g_unified_cache.relay_info.description) - 1);
}
const char* relay_software = get_config_value_from_table("relay_software");
if (relay_software) {
strncpy(g_unified_cache.relay_info.software, relay_software, sizeof(g_unified_cache.relay_info.software) - 1);
free((char*)relay_software);
} else {
strncpy(g_unified_cache.relay_info.software, "https://git.laantungir.net/laantungir/c-relay.git", sizeof(g_unified_cache.relay_info.software) - 1);
}
const char* relay_version = get_config_value_from_table("relay_version");
if (relay_version) {
strncpy(g_unified_cache.relay_info.version, relay_version, sizeof(g_unified_cache.relay_info.version) - 1);
free((char*)relay_version);
} else {
strncpy(g_unified_cache.relay_info.version, "0.2.0", sizeof(g_unified_cache.relay_info.version) - 1);
}
const char* relay_contact = get_config_value_from_table("relay_contact");
if (relay_contact) {
strncpy(g_unified_cache.relay_info.contact, relay_contact, sizeof(g_unified_cache.relay_info.contact) - 1);
free((char*)relay_contact);
}
const char* relay_pubkey = get_config_value_from_table("relay_pubkey");
if (relay_pubkey) {
strncpy(g_unified_cache.relay_info.pubkey, relay_pubkey, sizeof(g_unified_cache.relay_info.pubkey) - 1);
free((char*)relay_pubkey);
}
const char* posting_policy = get_config_value_from_table("posting_policy");
if (posting_policy) {
strncpy(g_unified_cache.relay_info.posting_policy, posting_policy, sizeof(g_unified_cache.relay_info.posting_policy) - 1);
free((char*)posting_policy);
}
const char* payments_url = get_config_value_from_table("payments_url");
if (payments_url) {
strncpy(g_unified_cache.relay_info.payments_url, payments_url, sizeof(g_unified_cache.relay_info.payments_url) - 1);
free((char*)payments_url);
}
// Rebuild supported_nips array
const char* supported_nips_csv = get_config_value_from_table("supported_nips");
if (supported_nips_csv) {
g_unified_cache.relay_info.supported_nips = parse_comma_separated_array(supported_nips_csv);
free((char*)supported_nips_csv);
} else {
g_unified_cache.relay_info.supported_nips = cJSON_CreateArray();
if (g_unified_cache.relay_info.supported_nips) {
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(1));
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(9));
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(11));
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(13));
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(15));
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(20));
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(40));
cJSON_AddItemToArray(g_unified_cache.relay_info.supported_nips, cJSON_CreateNumber(42));
}
}
// Rebuild limitation object
int max_message_length = 16384;
const char* max_msg_str = get_config_value_from_table("max_message_length");
if (max_msg_str) {
max_message_length = atoi(max_msg_str);
free((char*)max_msg_str);
}
int max_subscriptions_per_client = 20;
const char* max_subs_str = get_config_value_from_table("max_subscriptions_per_client");
if (max_subs_str) {
max_subscriptions_per_client = atoi(max_subs_str);
free((char*)max_subs_str);
}
int max_limit = 5000;
const char* max_limit_str = get_config_value_from_table("max_limit");
if (max_limit_str) {
max_limit = atoi(max_limit_str);
free((char*)max_limit_str);
}
int max_event_tags = 100;
const char* max_tags_str = get_config_value_from_table("max_event_tags");
if (max_tags_str) {
max_event_tags = atoi(max_tags_str);
free((char*)max_tags_str);
}
int max_content_length = 8196;
const char* max_content_str = get_config_value_from_table("max_content_length");
if (max_content_str) {
max_content_length = atoi(max_content_str);
free((char*)max_content_str);
}
int default_limit = 500;
const char* default_limit_str = get_config_value_from_table("default_limit");
if (default_limit_str) {
default_limit = atoi(default_limit_str);
free((char*)default_limit_str);
}
int admin_enabled = 0;
const char* admin_enabled_str = get_config_value_from_table("admin_enabled");
if (admin_enabled_str) {
admin_enabled = (strcmp(admin_enabled_str, "true") == 0) ? 1 : 0;
free((char*)admin_enabled_str);
}
g_unified_cache.relay_info.limitation = cJSON_CreateObject();
if (g_unified_cache.relay_info.limitation) {
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_message_length", max_message_length);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_subscriptions", max_subscriptions_per_client);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_limit", max_limit);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_subid_length", SUBSCRIPTION_ID_MAX_LENGTH);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_event_tags", max_event_tags);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "max_content_length", max_content_length);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "min_pow_difficulty", g_unified_cache.pow_config.min_pow_difficulty);
cJSON_AddBoolToObject(g_unified_cache.relay_info.limitation, "auth_required", admin_enabled ? cJSON_True : cJSON_False);
cJSON_AddBoolToObject(g_unified_cache.relay_info.limitation, "payment_required", cJSON_False);
cJSON_AddBoolToObject(g_unified_cache.relay_info.limitation, "restricted_writes", cJSON_False);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "created_at_lower_limit", 0);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "created_at_upper_limit", 2147483647);
cJSON_AddNumberToObject(g_unified_cache.relay_info.limitation, "default_limit", default_limit);
}
// Rebuild other arrays (empty for now)
g_unified_cache.relay_info.retention = cJSON_CreateArray();
g_unified_cache.relay_info.language_tags = cJSON_CreateArray();
if (g_unified_cache.relay_info.language_tags) {
cJSON_AddItemToArray(g_unified_cache.relay_info.language_tags, cJSON_CreateString("*"));
}
g_unified_cache.relay_info.relay_countries = cJSON_CreateArray();
if (g_unified_cache.relay_info.relay_countries) {
cJSON_AddItemToArray(g_unified_cache.relay_info.relay_countries, cJSON_CreateString("*"));
}
g_unified_cache.relay_info.tags = cJSON_CreateArray();
g_unified_cache.relay_info.fees = cJSON_CreateObject();
}
// Add basic relay information
if (strlen(g_unified_cache.relay_info.name) > 0) {
cJSON_AddStringToObject(info, "name", g_unified_cache.relay_info.name);
if (relay_name && strlen(relay_name) > 0) {
cJSON_AddStringToObject(info, "name", relay_name);
free((char*)relay_name);
} else {
cJSON_AddStringToObject(info, "name", "C Nostr Relay");
}
if (strlen(g_unified_cache.relay_info.description) > 0) {
cJSON_AddStringToObject(info, "description", g_unified_cache.relay_info.description);
if (relay_description && strlen(relay_description) > 0) {
cJSON_AddStringToObject(info, "description", relay_description);
free((char*)relay_description);
} else {
cJSON_AddStringToObject(info, "description", "A high-performance Nostr relay implemented in C with SQLite storage");
}
if (strlen(g_unified_cache.relay_info.banner) > 0) {
cJSON_AddStringToObject(info, "banner", g_unified_cache.relay_info.banner);
if (relay_banner && strlen(relay_banner) > 0) {
cJSON_AddStringToObject(info, "banner", relay_banner);
free((char*)relay_banner);
}
if (strlen(g_unified_cache.relay_info.icon) > 0) {
cJSON_AddStringToObject(info, "icon", g_unified_cache.relay_info.icon);
if (relay_icon && strlen(relay_icon) > 0) {
cJSON_AddStringToObject(info, "icon", relay_icon);
free((char*)relay_icon);
}
if (strlen(g_unified_cache.relay_info.pubkey) > 0) {
cJSON_AddStringToObject(info, "pubkey", g_unified_cache.relay_info.pubkey);
if (relay_pubkey && strlen(relay_pubkey) > 0) {
cJSON_AddStringToObject(info, "pubkey", relay_pubkey);
free((char*)relay_pubkey);
}
if (strlen(g_unified_cache.relay_info.contact) > 0) {
cJSON_AddStringToObject(info, "contact", g_unified_cache.relay_info.contact);
if (relay_contact && strlen(relay_contact) > 0) {
cJSON_AddStringToObject(info, "contact", relay_contact);
free((char*)relay_contact);
}
// Add supported NIPs
if (g_unified_cache.relay_info.supported_nips) {
cJSON_AddItemToObject(info, "supported_nips", cJSON_Duplicate(g_unified_cache.relay_info.supported_nips, 1));
if (supported_nips_csv && strlen(supported_nips_csv) > 0) {
cJSON* supported_nips = parse_comma_separated_array(supported_nips_csv);
if (supported_nips) {
cJSON_AddItemToObject(info, "supported_nips", supported_nips);
}
free((char*)supported_nips_csv);
} else {
// Default supported NIPs
cJSON* supported_nips = cJSON_CreateArray();
if (supported_nips) {
cJSON_AddItemToArray(supported_nips, cJSON_CreateNumber(1)); // NIP-01: Basic protocol
cJSON_AddItemToArray(supported_nips, cJSON_CreateNumber(9)); // NIP-09: Event deletion
cJSON_AddItemToArray(supported_nips, cJSON_CreateNumber(11)); // NIP-11: Relay information
cJSON_AddItemToArray(supported_nips, cJSON_CreateNumber(13)); // NIP-13: Proof of Work
cJSON_AddItemToArray(supported_nips, cJSON_CreateNumber(15)); // NIP-15: EOSE
cJSON_AddItemToArray(supported_nips, cJSON_CreateNumber(20)); // NIP-20: Command results
cJSON_AddItemToArray(supported_nips, cJSON_CreateNumber(40)); // NIP-40: Expiration Timestamp
cJSON_AddItemToArray(supported_nips, cJSON_CreateNumber(42)); // NIP-42: Authentication
cJSON_AddItemToObject(info, "supported_nips", supported_nips);
}
}
// Add software information
if (strlen(g_unified_cache.relay_info.software) > 0) {
cJSON_AddStringToObject(info, "software", g_unified_cache.relay_info.software);
if (relay_software && strlen(relay_software) > 0) {
cJSON_AddStringToObject(info, "software", relay_software);
free((char*)relay_software);
} else {
cJSON_AddStringToObject(info, "software", "https://git.laantungir.net/laantungir/c-relay.git");
}
if (strlen(g_unified_cache.relay_info.version) > 0) {
cJSON_AddStringToObject(info, "version", g_unified_cache.relay_info.version);
if (relay_version && strlen(relay_version) > 0) {
cJSON_AddStringToObject(info, "version", relay_version);
free((char*)relay_version);
} else {
cJSON_AddStringToObject(info, "version", "0.2.0");
}
// Add policies
if (strlen(g_unified_cache.relay_info.privacy_policy) > 0) {
cJSON_AddStringToObject(info, "privacy_policy", g_unified_cache.relay_info.privacy_policy);
if (privacy_policy && strlen(privacy_policy) > 0) {
cJSON_AddStringToObject(info, "privacy_policy", privacy_policy);
free((char*)privacy_policy);
}
if (strlen(g_unified_cache.relay_info.terms_of_service) > 0) {
cJSON_AddStringToObject(info, "terms_of_service", g_unified_cache.relay_info.terms_of_service);
if (terms_of_service && strlen(terms_of_service) > 0) {
cJSON_AddStringToObject(info, "terms_of_service", terms_of_service);
free((char*)terms_of_service);
}
if (strlen(g_unified_cache.relay_info.posting_policy) > 0) {
cJSON_AddStringToObject(info, "posting_policy", g_unified_cache.relay_info.posting_policy);
if (posting_policy && strlen(posting_policy) > 0) {
cJSON_AddStringToObject(info, "posting_policy", posting_policy);
free((char*)posting_policy);
}
// Add server limitations
if (g_unified_cache.relay_info.limitation) {
cJSON_AddItemToObject(info, "limitation", cJSON_Duplicate(g_unified_cache.relay_info.limitation, 1));
cJSON* limitation = cJSON_CreateObject();
if (limitation) {
cJSON_AddNumberToObject(limitation, "max_message_length", max_message_length);
cJSON_AddNumberToObject(limitation, "max_subscriptions", max_subscriptions_per_client);
cJSON_AddNumberToObject(limitation, "max_limit", max_limit);
cJSON_AddNumberToObject(limitation, "max_subid_length", SUBSCRIPTION_ID_MAX_LENGTH);
cJSON_AddNumberToObject(limitation, "max_event_tags", max_event_tags);
cJSON_AddNumberToObject(limitation, "max_content_length", max_content_length);
cJSON_AddNumberToObject(limitation, "min_pow_difficulty", min_pow_difficulty);
cJSON_AddBoolToObject(limitation, "auth_required", admin_enabled ? cJSON_True : cJSON_False);
cJSON_AddBoolToObject(limitation, "payment_required", cJSON_False);
cJSON_AddBoolToObject(limitation, "restricted_writes", cJSON_False);
cJSON_AddNumberToObject(limitation, "created_at_lower_limit", 0);
cJSON_AddNumberToObject(limitation, "created_at_upper_limit", 2147483647);
cJSON_AddNumberToObject(limitation, "default_limit", default_limit);
cJSON_AddItemToObject(info, "limitation", limitation);
}
// Add retention policies if configured
if (g_unified_cache.relay_info.retention && cJSON_GetArraySize(g_unified_cache.relay_info.retention) > 0) {
cJSON_AddItemToObject(info, "retention", cJSON_Duplicate(g_unified_cache.relay_info.retention, 1));
// Add retention policies (empty array for now)
cJSON* retention = cJSON_CreateArray();
if (retention) {
cJSON_AddItemToObject(info, "retention", retention);
}
// Add geographical and language information
if (g_unified_cache.relay_info.relay_countries) {
cJSON_AddItemToObject(info, "relay_countries", cJSON_Duplicate(g_unified_cache.relay_info.relay_countries, 1));
if (relay_countries_csv && strlen(relay_countries_csv) > 0) {
cJSON* relay_countries = parse_comma_separated_array(relay_countries_csv);
if (relay_countries) {
cJSON_AddItemToObject(info, "relay_countries", relay_countries);
}
free((char*)relay_countries_csv);
} else {
cJSON* relay_countries = cJSON_CreateArray();
if (relay_countries) {
cJSON_AddItemToArray(relay_countries, cJSON_CreateString("*"));
cJSON_AddItemToObject(info, "relay_countries", relay_countries);
}
}
if (g_unified_cache.relay_info.language_tags) {
cJSON_AddItemToObject(info, "language_tags", cJSON_Duplicate(g_unified_cache.relay_info.language_tags, 1));
if (language_tags_csv && strlen(language_tags_csv) > 0) {
cJSON* language_tags = parse_comma_separated_array(language_tags_csv);
if (language_tags) {
cJSON_AddItemToObject(info, "language_tags", language_tags);
}
free((char*)language_tags_csv);
} else {
cJSON* language_tags = cJSON_CreateArray();
if (language_tags) {
cJSON_AddItemToArray(language_tags, cJSON_CreateString("*"));
cJSON_AddItemToObject(info, "language_tags", language_tags);
}
}
if (g_unified_cache.relay_info.tags && cJSON_GetArraySize(g_unified_cache.relay_info.tags) > 0) {
cJSON_AddItemToObject(info, "tags", cJSON_Duplicate(g_unified_cache.relay_info.tags, 1));
// Add content tags (empty array)
cJSON* tags = cJSON_CreateArray();
if (tags) {
cJSON_AddItemToObject(info, "tags", tags);
}
// Add payment information if configured
if (strlen(g_unified_cache.relay_info.payments_url) > 0) {
cJSON_AddStringToObject(info, "payments_url", g_unified_cache.relay_info.payments_url);
}
if (g_unified_cache.relay_info.fees && cJSON_GetObjectItem(g_unified_cache.relay_info.fees, "admission")) {
cJSON_AddItemToObject(info, "fees", cJSON_Duplicate(g_unified_cache.relay_info.fees, 1));
if (payments_url && strlen(payments_url) > 0) {
cJSON_AddStringToObject(info, "payments_url", payments_url);
free((char*)payments_url);
}
pthread_mutex_unlock(&g_unified_cache.cache_lock);
// Add fees (empty object - no payment required by default)
cJSON* fees = cJSON_CreateObject();
if (fees) {
cJSON_AddItemToObject(info, "fees", fees);
}
return info;
}
@@ -534,7 +304,7 @@ int handle_nip11_http_request(struct lws* wsi, const char* accept_header) {
}
if (!accepts_nostr_json) {
log_warning("HTTP request without proper Accept header for NIP-11");
DEBUG_WARN("HTTP request without proper Accept header for NIP-11");
// Return 406 Not Acceptable
unsigned char buf[LWS_PRE + 256];
unsigned char *p = &buf[LWS_PRE];
@@ -560,7 +330,7 @@ int handle_nip11_http_request(struct lws* wsi, const char* accept_header) {
// Generate relay information JSON
cJSON* info_json = generate_relay_info_json();
if (!info_json) {
log_error("Failed to generate relay info JSON");
DEBUG_ERROR("Failed to generate relay info JSON");
unsigned char buf[LWS_PRE + 256];
unsigned char *p = &buf[LWS_PRE];
unsigned char *start = p;
@@ -586,7 +356,7 @@ int handle_nip11_http_request(struct lws* wsi, const char* accept_header) {
cJSON_Delete(info_json);
if (!json_string) {
log_error("Failed to serialize relay info JSON");
DEBUG_ERROR("Failed to serialize relay info JSON");
unsigned char buf[LWS_PRE + 256];
unsigned char *p = &buf[LWS_PRE];
unsigned char *start = p;
@@ -613,7 +383,7 @@ int handle_nip11_http_request(struct lws* wsi, const char* accept_header) {
// Allocate session data to manage buffer lifetime across callbacks
struct nip11_session_data* session_data = malloc(sizeof(struct nip11_session_data));
if (!session_data) {
log_error("Failed to allocate NIP-11 session data");
DEBUG_ERROR("Failed to allocate NIP-11 session data");
free(json_string);
return -1;
}

View File

@@ -1,5 +1,6 @@
// NIP-13 Proof of Work validation module
#include <stdio.h>
#include "debug.h"
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
@@ -8,69 +9,39 @@
#include "../nostr_core_lib/nostr_core/nip013.h"
#include "config.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);
// NIP-13 PoW configuration structure
struct pow_config {
int enabled; // 0 = disabled, 1 = enabled
int min_pow_difficulty; // Minimum required difficulty (0 = no requirement)
int validation_flags; // Bitflags for validation options
int require_nonce_tag; // 1 = require nonce tag presence
int reject_lower_targets; // 1 = reject if committed < actual difficulty
int strict_format; // 1 = enforce strict nonce tag format
int anti_spam_mode; // 1 = full anti-spam validation
};
// Configuration functions from config.c
extern int get_config_bool(const char* key, int default_value);
extern int get_config_int(const char* key, int default_value);
extern const char* get_config_value(const char* key);
// Initialize PoW configuration using configuration system
void init_pow_config() {
// Get all config values first (without holding mutex to avoid deadlock)
int pow_enabled = get_config_bool("pow_enabled", 1);
int pow_min_difficulty = get_config_int("pow_min_difficulty", 0);
const char* pow_mode = get_config_value("pow_mode");
pthread_mutex_lock(&g_unified_cache.cache_lock);
// Load PoW settings from configuration system
g_unified_cache.pow_config.enabled = pow_enabled;
g_unified_cache.pow_config.min_pow_difficulty = pow_min_difficulty;
// Configure PoW mode
if (pow_mode) {
if (strcmp(pow_mode, "strict") == 0) {
g_unified_cache.pow_config.validation_flags = NOSTR_POW_VALIDATE_ANTI_SPAM | NOSTR_POW_STRICT_FORMAT;
g_unified_cache.pow_config.require_nonce_tag = 1;
g_unified_cache.pow_config.reject_lower_targets = 1;
g_unified_cache.pow_config.strict_format = 1;
g_unified_cache.pow_config.anti_spam_mode = 1;
} else if (strcmp(pow_mode, "full") == 0) {
g_unified_cache.pow_config.validation_flags = NOSTR_POW_VALIDATE_FULL;
g_unified_cache.pow_config.require_nonce_tag = 1;
} else if (strcmp(pow_mode, "basic") == 0) {
g_unified_cache.pow_config.validation_flags = NOSTR_POW_VALIDATE_BASIC;
} else if (strcmp(pow_mode, "disabled") == 0) {
g_unified_cache.pow_config.enabled = 0;
}
free((char*)pow_mode); // Free dynamically allocated string
} else {
// Default to basic mode
g_unified_cache.pow_config.validation_flags = NOSTR_POW_VALIDATE_BASIC;
}
pthread_mutex_unlock(&g_unified_cache.cache_lock);
// Configuration is now handled directly through database queries
// No cache initialization needed
}
// Validate event Proof of Work according to NIP-13
int validate_event_pow(cJSON* event, char* error_message, size_t error_size) {
pthread_mutex_lock(&g_unified_cache.cache_lock);
int enabled = g_unified_cache.pow_config.enabled;
int min_pow_difficulty = g_unified_cache.pow_config.min_pow_difficulty;
int validation_flags = g_unified_cache.pow_config.validation_flags;
pthread_mutex_unlock(&g_unified_cache.cache_lock);
// Get PoW configuration directly from database
int enabled = get_config_bool("pow_enabled", 1);
int min_pow_difficulty = get_config_int("pow_min_difficulty", 0);
const char* pow_mode = get_config_value("pow_mode");
// Determine validation flags based on mode
int validation_flags = NOSTR_POW_VALIDATE_BASIC; // Default
if (pow_mode) {
if (strcmp(pow_mode, "strict") == 0) {
validation_flags = NOSTR_POW_VALIDATE_ANTI_SPAM | NOSTR_POW_STRICT_FORMAT;
} else if (strcmp(pow_mode, "full") == 0) {
validation_flags = NOSTR_POW_VALIDATE_FULL;
} else if (strcmp(pow_mode, "basic") == 0) {
validation_flags = NOSTR_POW_VALIDATE_BASIC;
} else if (strcmp(pow_mode, "disabled") == 0) {
enabled = 0;
}
free((char*)pow_mode);
}
if (!enabled) {
return 0; // PoW validation disabled
@@ -121,39 +92,39 @@ int validate_event_pow(cJSON* event, char* error_message, size_t error_size) {
snprintf(error_message, error_size,
"pow: insufficient difficulty: %d < %d",
pow_result.actual_difficulty, min_pow_difficulty);
log_warning("Event rejected: insufficient PoW difficulty");
DEBUG_WARN("Event rejected: insufficient PoW difficulty");
break;
case NOSTR_ERROR_NIP13_NO_NONCE_TAG:
// This should not happen with min_difficulty=0 after our check above
if (min_pow_difficulty > 0) {
snprintf(error_message, error_size, "pow: missing required nonce tag");
log_warning("Event rejected: missing nonce tag");
DEBUG_WARN("Event rejected: missing nonce tag");
} else {
return 0; // Allow when min_difficulty=0
}
break;
case NOSTR_ERROR_NIP13_INVALID_NONCE_TAG:
snprintf(error_message, error_size, "pow: invalid nonce tag format");
log_warning("Event rejected: invalid nonce tag format");
DEBUG_WARN("Event rejected: invalid nonce tag format");
break;
case NOSTR_ERROR_NIP13_TARGET_MISMATCH:
snprintf(error_message, error_size,
"pow: committed target (%d) lower than minimum (%d)",
pow_result.committed_target, min_pow_difficulty);
log_warning("Event rejected: committed target too low (anti-spam protection)");
DEBUG_WARN("Event rejected: committed target too low (anti-spam protection)");
break;
case NOSTR_ERROR_NIP13_CALCULATION:
snprintf(error_message, error_size, "pow: difficulty calculation failed");
log_error("PoW difficulty calculation error");
DEBUG_ERROR("PoW difficulty calculation error");
break;
case NOSTR_ERROR_EVENT_INVALID_ID:
snprintf(error_message, error_size, "pow: invalid event ID format");
log_warning("Event rejected: invalid event ID for PoW calculation");
DEBUG_WARN("Event rejected: invalid event ID for PoW calculation");
break;
default:
snprintf(error_message, error_size, "pow: validation failed - %s",
strlen(pow_result.error_detail) > 0 ? pow_result.error_detail : "unknown error");
log_warning("Event rejected: PoW validation failed");
DEBUG_WARN("Event rejected: PoW validation failed");
}
return validation_result;
}

View File

@@ -1,5 +1,6 @@
#define _GNU_SOURCE
#include <stdio.h>
#include "debug.h"
#include <stdlib.h>
#include <string.h>
#include <time.h>
@@ -28,9 +29,6 @@ struct expiration_config g_expiration_config = {
.grace_period = 1 // 1 second grace period for testing (was 300)
};
// Forward declarations for logging functions
void log_info(const char* message);
void log_warning(const char* message);
// Initialize expiration configuration using configuration system
void init_expiration_config() {
@@ -51,7 +49,7 @@ void init_expiration_config() {
// Validate grace period bounds
if (g_expiration_config.grace_period < 0 || g_expiration_config.grace_period > 86400) {
log_warning("Invalid grace period, using default of 300 seconds");
DEBUG_WARN("Invalid grace period, using default of 300 seconds");
g_expiration_config.grace_period = 300;
}
@@ -94,7 +92,7 @@ long extract_expiration_timestamp(cJSON* tags) {
char debug_msg[256];
snprintf(debug_msg, sizeof(debug_msg),
"Ignoring malformed expiration tag value: '%.32s'", value);
log_warning(debug_msg);
DEBUG_WARN(debug_msg);
continue; // Ignore malformed expiration tag
}
@@ -148,7 +146,7 @@ int validate_event_expiration(cJSON* event, char* error_message, size_t error_si
snprintf(error_message, error_size,
"invalid: event expired (expiration=%ld, current=%ld, grace=%ld)",
expiration_ts, (long)current_time, g_expiration_config.grace_period);
log_warning("Event rejected: expired timestamp");
DEBUG_WARN("Event rejected: expired timestamp");
return -1;
} else {
// In non-strict mode, allow expired events

View File

@@ -6,17 +6,13 @@
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
#include <pthread.h>
#include "debug.h"
#include <cjson/cJSON.h>
#include <libwebsockets.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
// Forward declarations for logging functions
void log_error(const char* message);
void log_info(const char* message);
void log_warning(const char* message);
void log_success(const char* message);
// Forward declaration for notice message function
void send_notice_message(struct lws* wsi, const char* message);
@@ -52,7 +48,7 @@ void send_nip42_auth_challenge(struct lws* wsi, struct per_session_data* pss) {
// Generate challenge using existing request_validator function
char challenge[65];
if (nostr_nip42_generate_challenge(challenge, sizeof(challenge)) != 0) {
log_error("Failed to generate NIP-42 challenge");
DEBUG_ERROR("Failed to generate NIP-42 challenge");
send_notice_message(wsi, "Authentication temporarily unavailable");
return;
}
@@ -108,7 +104,7 @@ void handle_nip42_auth_signed_event(struct lws* wsi, struct per_session_data* ps
if (current_time > challenge_expires) {
free(event_json);
send_notice_message(wsi, "Authentication challenge expired, please retry");
log_warning("NIP-42 authentication failed: challenge expired");
DEBUG_WARN("NIP-42 authentication failed: challenge expired");
return;
}
@@ -154,7 +150,7 @@ void handle_nip42_auth_signed_event(struct lws* wsi, struct per_session_data* ps
char error_msg[256];
snprintf(error_msg, sizeof(error_msg),
"NIP-42 authentication failed (error code: %d)", result);
log_warning(error_msg);
DEBUG_WARN(error_msg);
send_notice_message(wsi, "NIP-42 authentication failed - invalid signature or challenge");
}
@@ -166,6 +162,6 @@ void handle_nip42_auth_challenge_response(struct lws* wsi, struct per_session_da
// NIP-42 doesn't typically use challenge responses from client to server
// This is reserved for potential future use or protocol extensions
log_warning("Received unexpected challenge response from client (not part of standard NIP-42 flow)");
DEBUG_WARN("Received unexpected challenge response from client (not part of standard NIP-42 flow)");
send_notice_message(wsi, "Challenge responses are not supported - please send signed authentication event");
}

View File

@@ -15,6 +15,7 @@
#include "../nostr_core_lib/nostr_core/nip013.h" // NIP-13: Proof of Work
#include "../nostr_core_lib/nostr_core/nostr_common.h"
#include "../nostr_core_lib/nostr_core/utils.h"
#include "debug.h" // C-relay debug system
#include "config.h" // C-relay configuration system
#include <sqlite3.h>
#include <stdio.h>
@@ -53,7 +54,6 @@ extern struct expiration_config {
// Configuration functions from C-relay
extern int get_config_bool(const char* key, int default_value);
extern int get_config_int(const char* key, int default_value);
extern const char* get_admin_pubkey_cached(void);
// NIP-42 constants (from nostr_core_lib)
#define NOSTR_NIP42_AUTH_EVENT_KIND 22242
@@ -301,7 +301,7 @@ int nostr_validate_unified_request(const char* json_string, size_t json_length)
// 8. Check if this is a kind 23456 admin event from authorized admin
// This must happen AFTER signature validation but BEFORE auth rules
if (event_kind == 23456) {
const char* admin_pubkey = get_admin_pubkey_cached();
const char* admin_pubkey = get_config_value("admin_pubkey");
if (admin_pubkey && strcmp(event_pubkey, admin_pubkey) == 0) {
// Valid admin event - bypass remaining validation
cJSON_Delete(event);
@@ -361,11 +361,9 @@ int nostr_validate_unified_request(const char* json_string, size_t json_length)
/////////////////////////////////////////////////////////////////////
// 12. NIP-13 Proof of Work validation
pthread_mutex_lock(&g_unified_cache.cache_lock);
int pow_enabled = g_unified_cache.pow_config.enabled;
int pow_min_difficulty = g_unified_cache.pow_config.min_pow_difficulty;
int pow_validation_flags = g_unified_cache.pow_config.validation_flags;
pthread_mutex_unlock(&g_unified_cache.cache_lock);
int pow_enabled = get_config_bool("pow_enabled", 0);
int pow_min_difficulty = get_config_int("pow_min_difficulty", 0);
int pow_validation_flags = get_config_int("pow_validation_flags", 1);
if (pow_enabled && pow_min_difficulty > 0) {
nostr_pow_result_t pow_result;
@@ -506,11 +504,10 @@ void nostr_request_result_free_file_data(nostr_request_result_t *result) {
/**
* Force cache refresh - use unified cache system
* Force cache refresh - cache no longer exists, function kept for compatibility
*/
void nostr_request_validator_force_cache_refresh(void) {
// Use unified cache refresh from config.c
force_config_cache_refresh();
// Cache no longer exists - direct database queries are used
}
/**
@@ -535,6 +532,8 @@ int check_database_auth_rules(const char *pubkey, const char *operation __attrib
sqlite3_stmt *stmt = NULL;
int rc;
DEBUG_TRACE("Checking auth rules for pubkey: %s", pubkey);
if (!pubkey) {
return NOSTR_ERROR_INVALID_INPUT;
}
@@ -551,19 +550,21 @@ int check_database_auth_rules(const char *pubkey, const char *operation __attrib
// Step 1: Check pubkey blacklist (highest priority)
const char *blacklist_sql =
"SELECT rule_type, action FROM auth_rules WHERE rule_type = "
"'blacklist' AND pattern_type = 'pubkey' AND pattern_value = ? LIMIT 1";
"SELECT rule_type FROM auth_rules WHERE rule_type = "
"'blacklist' AND pattern_type = 'pubkey' AND pattern_value = ? AND active = 1 LIMIT 1";
DEBUG_TRACE("Blacklist SQL: %s", blacklist_sql);
rc = sqlite3_prepare_v2(db, blacklist_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, pubkey, -1, SQLITE_STATIC);
if (sqlite3_step(stmt) == SQLITE_ROW) {
const char *action = (const char *)sqlite3_column_text(stmt, 1);
int step_result = sqlite3_step(stmt);
DEBUG_TRACE("Blacklist query result: %s", step_result == SQLITE_ROW ? "FOUND" : "NOT_FOUND");
if (step_result == SQLITE_ROW) {
DEBUG_TRACE("BLACKLIST HIT: Denying access for pubkey: %s", pubkey);
// Set specific violation details for status code mapping
strcpy(g_last_rule_violation.violation_type, "pubkey_blacklist");
sprintf(g_last_rule_violation.reason, "Public key blacklisted: %s",
action ? action : "PUBKEY_BLACKLIST");
sprintf(g_last_rule_violation.reason, "Public key blacklisted");
sqlite3_finalize(stmt);
sqlite3_close(db);
@@ -575,19 +576,16 @@ int check_database_auth_rules(const char *pubkey, const char *operation __attrib
// Step 2: Check hash blacklist
if (resource_hash) {
const char *hash_blacklist_sql =
"SELECT rule_type, action FROM auth_rules WHERE rule_type = "
"'blacklist' AND pattern_type = 'hash' AND pattern_value = ? LIMIT 1";
"SELECT rule_type FROM auth_rules WHERE rule_type = "
"'blacklist' AND pattern_type = 'hash' AND pattern_value = ? AND active = 1 LIMIT 1";
rc = sqlite3_prepare_v2(db, hash_blacklist_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, resource_hash, -1, SQLITE_STATIC);
if (sqlite3_step(stmt) == SQLITE_ROW) {
const char *action = (const char *)sqlite3_column_text(stmt, 1);
// Set specific violation details for status code mapping
strcpy(g_last_rule_violation.violation_type, "hash_blacklist");
sprintf(g_last_rule_violation.reason, "File hash blacklisted: %s",
action ? action : "HASH_BLACKLIST");
sprintf(g_last_rule_violation.reason, "File hash blacklisted");
sqlite3_finalize(stmt);
sqlite3_close(db);
@@ -599,8 +597,8 @@ int check_database_auth_rules(const char *pubkey, const char *operation __attrib
// Step 3: Check pubkey whitelist
const char *whitelist_sql =
"SELECT rule_type, action FROM auth_rules WHERE rule_type = "
"'whitelist' AND pattern_type = 'pubkey' AND pattern_value = ? LIMIT 1";
"SELECT rule_type FROM auth_rules WHERE rule_type = "
"'whitelist' AND pattern_type = 'pubkey' AND pattern_value = ? AND active = 1 LIMIT 1";
rc = sqlite3_prepare_v2(db, whitelist_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, pubkey, -1, SQLITE_STATIC);
@@ -616,7 +614,7 @@ int check_database_auth_rules(const char *pubkey, const char *operation __attrib
// Step 4: Check if any whitelist rules exist - if yes, deny by default
const char *whitelist_exists_sql =
"SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'whitelist' "
"AND pattern_type = 'pubkey' LIMIT 1";
"AND pattern_type = 'pubkey' AND active = 1 LIMIT 1";
rc = sqlite3_prepare_v2(db, whitelist_exists_sql, -1, &stmt, NULL);
if (rc == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {

View File

@@ -142,8 +142,6 @@ CREATE TABLE auth_rules (\n\
rule_type TEXT NOT NULL CHECK (rule_type IN ('whitelist', 'blacklist', 'rate_limit', 'auth_required')),\n\
pattern_type TEXT NOT NULL CHECK (pattern_type IN ('pubkey', 'kind', 'ip', 'global')),\n\
pattern_value TEXT,\n\
action TEXT NOT NULL CHECK (action IN ('allow', 'deny', 'require_auth', 'rate_limit')),\n\
parameters TEXT, -- JSON parameters for rate limiting, etc.\n\
active INTEGER NOT NULL DEFAULT 1,\n\
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n\
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))\n\
@@ -180,34 +178,6 @@ BEGIN\n\
UPDATE config SET updated_at = strftime('%s', 'now') WHERE key = NEW.key;\n\
END;\n\
\n\
-- Insert default configuration values\n\
INSERT INTO config (key, value, data_type, description, category, requires_restart) VALUES\n\
('relay_description', 'A C Nostr Relay', 'string', 'Relay description', 'general', 0),\n\
('relay_contact', '', 'string', 'Relay contact information', 'general', 0),\n\
('relay_software', 'https://github.com/laanwj/c-relay', 'string', 'Relay software URL', 'general', 0),\n\
('relay_version', '1.0.0', 'string', 'Relay version', 'general', 0),\n\
('relay_port', '8888', 'integer', 'Relay port number', 'network', 1),\n\
('max_connections', '1000', 'integer', 'Maximum concurrent connections', 'network', 1),\n\
('auth_enabled', 'false', 'boolean', 'Enable NIP-42 authentication', 'auth', 0),\n\
('nip42_auth_required_events', 'false', 'boolean', 'Require auth for event publishing', 'auth', 0),\n\
('nip42_auth_required_subscriptions', 'false', 'boolean', 'Require auth for subscriptions', 'auth', 0),\n\
('nip42_auth_required_kinds', '[]', 'json', 'Event kinds requiring authentication', 'auth', 0),\n\
('nip42_challenge_expiration', '600', 'integer', 'Auth challenge expiration seconds', 'auth', 0),\n\
('pow_min_difficulty', '0', 'integer', 'Minimum proof-of-work difficulty', 'validation', 0),\n\
('pow_mode', 'optional', 'string', 'Proof-of-work mode', 'validation', 0),\n\
('nip40_expiration_enabled', 'true', 'boolean', 'Enable event expiration', 'validation', 0),\n\
('nip40_expiration_strict', 'false', 'boolean', 'Strict expiration mode', 'validation', 0),\n\
('nip40_expiration_filter', 'true', 'boolean', 'Filter expired events in queries', 'validation', 0),\n\
('nip40_expiration_grace_period', '60', 'integer', 'Expiration grace period seconds', 'validation', 0),\n\
('max_subscriptions_per_client', '25', 'integer', 'Maximum subscriptions per client', 'limits', 0),\n\
('max_total_subscriptions', '1000', 'integer', 'Maximum total subscriptions', 'limits', 0),\n\
('max_filters_per_subscription', '10', 'integer', 'Maximum filters per subscription', 'limits', 0),\n\
('max_event_tags', '2000', 'integer', 'Maximum tags per event', 'limits', 0),\n\
('max_content_length', '100000', 'integer', 'Maximum event content length', 'limits', 0),\n\
('max_message_length', '131072', 'integer', 'Maximum WebSocket message length', 'limits', 0),\n\
('default_limit', '100', 'integer', 'Default query limit', 'limits', 0),\n\
('max_limit', '5000', 'integer', 'Maximum query limit', 'limits', 0);\n\
\n\
-- Persistent Subscriptions Logging Tables (Phase 2)\n\
-- Optional database logging for subscription analytics and debugging\n\
\n\

View File

@@ -1,5 +1,6 @@
#define _GNU_SOURCE
#include <cjson/cJSON.h>
#include "debug.h"
#include <sqlite3.h>
#include <string.h>
#include <stdlib.h>
@@ -10,9 +11,6 @@
#include "subscriptions.h"
// Forward declarations for logging functions
void log_info(const char* message);
void log_error(const char* message);
void log_warning(const char* message);
// Forward declarations for configuration functions
const char* get_config_value(const char* key);
@@ -30,8 +28,8 @@ int validate_search_term(const char* search_term, char* error_message, size_t er
// Global database variable
extern sqlite3* g_db;
// Global unified cache
extern unified_config_cache_t g_unified_cache;
// Configuration functions from config.c
extern int get_config_bool(const char* key, int default_value);
// Global subscription manager
extern subscription_manager_t g_subscription_manager;
@@ -52,7 +50,7 @@ subscription_filter_t* create_subscription_filter(cJSON* filter_json) {
// Validate filter values before creating the filter
char error_message[512] = {0};
if (!validate_filter_values(filter_json, error_message, sizeof(error_message))) {
log_warning(error_message);
DEBUG_WARN(error_message);
return NULL;
}
@@ -150,19 +148,19 @@ static int validate_subscription_id(const char* sub_id) {
// Create a new subscription
subscription_t* create_subscription(const char* sub_id, struct lws* wsi, cJSON* filters_array, const char* client_ip) {
if (!sub_id || !wsi || !filters_array) {
log_error("create_subscription: NULL parameter(s)");
DEBUG_ERROR("create_subscription: NULL parameter(s)");
return NULL;
}
// Validate subscription ID
if (!validate_subscription_id(sub_id)) {
log_error("create_subscription: invalid subscription ID format or length");
DEBUG_ERROR("create_subscription: invalid subscription ID format or length");
return NULL;
}
subscription_t* sub = calloc(1, sizeof(subscription_t));
if (!sub) {
log_error("create_subscription: failed to allocate subscription");
DEBUG_ERROR("create_subscription: failed to allocate subscription");
return NULL;
}
@@ -199,7 +197,7 @@ subscription_t* create_subscription(const char* sub_id, struct lws* wsi, cJSON*
cJSON* filter_json = NULL;
cJSON_ArrayForEach(filter_json, filters_array) {
if (filter_count >= MAX_FILTERS_PER_SUBSCRIPTION) {
log_warning("Maximum filters per subscription exceeded, ignoring excess filters");
DEBUG_WARN("Maximum filters per subscription exceeded, ignoring excess filters");
break;
}
@@ -218,7 +216,7 @@ subscription_t* create_subscription(const char* sub_id, struct lws* wsi, cJSON*
}
if (filter_count == 0) {
log_error("No valid filters found for subscription");
DEBUG_ERROR("No valid filters found for subscription");
free(sub);
return NULL;
}
@@ -246,7 +244,7 @@ int add_subscription_to_manager(subscription_t* sub) {
// Check global limits
if (g_subscription_manager.total_subscriptions >= g_subscription_manager.max_total_subscriptions) {
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
log_error("Maximum total subscriptions reached");
DEBUG_ERROR("Maximum total subscriptions reached");
return -1;
}
@@ -267,13 +265,13 @@ int add_subscription_to_manager(subscription_t* sub) {
// Remove subscription from global manager (thread-safe)
int remove_subscription_from_manager(const char* sub_id, struct lws* wsi) {
if (!sub_id) {
log_error("remove_subscription_from_manager: NULL subscription ID");
DEBUG_ERROR("remove_subscription_from_manager: NULL subscription ID");
return -1;
}
// Validate subscription ID format
if (!validate_subscription_id(sub_id)) {
log_error("remove_subscription_from_manager: invalid subscription ID format");
DEBUG_ERROR("remove_subscription_from_manager: invalid subscription ID format");
return -1;
}
@@ -319,14 +317,17 @@ int remove_subscription_from_manager(const char* sub_id, struct lws* wsi) {
char debug_msg[256];
snprintf(debug_msg, sizeof(debug_msg), "Subscription '%s' not found for removal", sub_id);
log_warning(debug_msg);
DEBUG_WARN(debug_msg);
return -1;
}
// Check if an event matches a subscription filter
int event_matches_filter(cJSON* event, subscription_filter_t* filter) {
DEBUG_TRACE("Checking event against subscription filter");
if (!event || !filter) {
DEBUG_TRACE("Exiting event_matches_filter - null parameters");
return 0;
}
@@ -502,6 +503,7 @@ int event_matches_filter(cJSON* event, subscription_filter_t* filter) {
}
}
DEBUG_TRACE("Exiting event_matches_filter - match found");
return 1; // All filters passed
}
@@ -524,15 +526,16 @@ int event_matches_subscription(cJSON* event, subscription_t* subscription) {
// Broadcast event to all matching subscriptions (thread-safe)
int broadcast_event_to_subscriptions(cJSON* event) {
DEBUG_TRACE("Broadcasting event to subscriptions");
if (!event) {
DEBUG_TRACE("Exiting broadcast_event_to_subscriptions - null event");
return 0;
}
// Check if event is expired and should not be broadcast (NIP-40)
pthread_mutex_lock(&g_unified_cache.cache_lock);
int expiration_enabled = g_unified_cache.expiration_config.enabled;
int filter_responses = g_unified_cache.expiration_config.filter_responses;
pthread_mutex_unlock(&g_unified_cache.cache_lock);
int expiration_enabled = get_config_bool("expiration_enabled", 1);
int filter_responses = get_config_bool("expiration_filter", 1);
if (expiration_enabled && filter_responses) {
time_t current_time = time(NULL);
@@ -584,7 +587,7 @@ int broadcast_event_to_subscriptions(cJSON* event) {
matching_subs = temp;
matching_count++;
} else {
log_error("broadcast_event_to_subscriptions: failed to allocate temp subscription");
DEBUG_ERROR("broadcast_event_to_subscriptions: failed to allocate temp subscription");
}
}
sub = sub->next;
@@ -619,7 +622,8 @@ int broadcast_event_to_subscriptions(cJSON* event) {
subscription_t* update_sub = g_subscription_manager.active_subscriptions;
while (update_sub) {
if (update_sub->wsi == current_temp->wsi &&
strcmp(update_sub->id, current_temp->id) == 0) {
strcmp(update_sub->id, current_temp->id) == 0 &&
update_sub->active) { // Add active check to prevent use-after-free
update_sub->events_sent++;
break;
}
@@ -655,6 +659,8 @@ int broadcast_event_to_subscriptions(cJSON* event) {
g_subscription_manager.total_events_broadcast += broadcasts;
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
DEBUG_LOG("Event broadcast complete: %d subscriptions matched", broadcasts);
DEBUG_TRACE("Exiting broadcast_event_to_subscriptions");
return broadcasts;
}

View File

@@ -3,6 +3,7 @@
// Includes
#include <stdio.h>
#include "debug.h"
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
@@ -31,10 +32,6 @@
#include "dm_admin.h" // DM admin functions including NIP-17
// 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);
// Forward declarations for configuration functions
const char* get_config_value(const char* key);
@@ -96,8 +93,8 @@ int validate_filter_array(cJSON* filters, char* error_message, size_t error_size
// Forward declarations for NOTICE message support
void send_notice_message(struct lws* wsi, const char* message);
// Forward declarations for unified cache access
extern unified_config_cache_t g_unified_cache;
// Configuration functions from config.c
extern int get_config_bool(const char* key, int default_value);
// Forward declarations for global state
extern sqlite3* g_db;
@@ -201,7 +198,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
// 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");
DEBUG_ERROR("Failed to allocate buffer for NIP-11 body transmission");
// Clean up session data
free(session_data->json_buffer);
free(session_data);
@@ -219,7 +216,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
free(json_buf);
if (write_result < 0) {
log_error("Failed to write NIP-11 JSON body");
DEBUG_ERROR("Failed to write NIP-11 JSON body");
// Clean up session data
free(session_data->json_buffer);
free(session_data);
@@ -244,6 +241,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
break;
case LWS_CALLBACK_ESTABLISHED:
DEBUG_TRACE("WebSocket connection established");
memset(pss, 0, sizeof(*pss));
pthread_mutex_init(&pss->session_lock, NULL);
@@ -258,6 +256,8 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
memcpy(pss->client_ip, client_ip, copy_len);
pss->client_ip[copy_len] = '\0';
DEBUG_LOG("WebSocket connection established from %s", pss->client_ip);
// Initialize NIP-42 authentication state
pss->authenticated = 0;
pss->nip42_auth_required_events = get_config_bool("nip42_auth_required_events", 0);
@@ -267,6 +267,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
memset(pss->active_challenge, 0, sizeof(pss->active_challenge));
pss->challenge_created = 0;
pss->challenge_expires = 0;
DEBUG_TRACE("WebSocket connection initialization complete");
break;
case LWS_CALLBACK_RECEIVE:
@@ -310,7 +311,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
if (event_kind == 14 && event_obj && cJSON_IsObject(event_obj)) {
cJSON* tags = cJSON_GetObjectItem(event_obj, "tags");
if (tags && cJSON_IsArray(tags)) {
const char* relay_pubkey = get_relay_pubkey_cached();
const char* relay_pubkey = get_config_value("relay_pubkey");
if (relay_pubkey) {
cJSON* tag = NULL;
cJSON_ArrayForEach(tag, tags) {
@@ -332,11 +333,11 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
// Special case: allow kind 23456 admin events from authorized admin to bypass auth
if (event_kind == 23456 && event_pubkey) {
const char* admin_pubkey = get_admin_pubkey_cached();
const char* admin_pubkey = get_config_value("admin_pubkey");
if (admin_pubkey && strcmp(event_pubkey, admin_pubkey) == 0) {
bypass_auth = 1;
} else {
log_info("DEBUG: Kind 23456 event but pubkey mismatch or no admin pubkey");
DEBUG_INFO("DEBUG: Kind 23456 event but pubkey mismatch or no admin pubkey");
}
}
@@ -353,7 +354,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
"NIP-42 authentication required for event kind %d", event_kind);
}
send_notice_message(wsi, auth_msg);
log_warning("Event rejected: NIP-42 authentication required for kind");
DEBUG_WARN("Event rejected: NIP-42 authentication required for kind");
}
cJSON_Delete(json);
free(message);
@@ -368,7 +369,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
// Extract event JSON string for unified validator
char *event_json_str = cJSON_Print(event);
if (!event_json_str) {
log_error("Failed to serialize event JSON for validation");
DEBUG_ERROR("Failed to serialize event JSON for validation");
cJSON* error_response = cJSON_CreateArray();
cJSON_AddItemToArray(error_response, cJSON_CreateString("OK"));
cJSON_AddItemToArray(error_response, cJSON_CreateString("unknown"));
@@ -452,15 +453,15 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
}
if (is_protected_event) {
// Check if protected events are enabled using unified cache
int protected_events_enabled = g_unified_cache.nip70_protected_events_enabled;
// Check if protected events are enabled using config
int protected_events_enabled = get_config_bool("nip70_protected_events_enabled", 0);
if (!protected_events_enabled) {
// Protected events not supported
result = -1;
strncpy(error_message, "blocked: protected events not supported", sizeof(error_message) - 1);
error_message[sizeof(error_message) - 1] = '\0';
log_warning("Protected event rejected: protected events not enabled");
DEBUG_WARN("Protected event rejected: protected events not enabled");
} else {
// Protected events enabled - check authentication
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
@@ -472,7 +473,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
result = -1;
strncpy(error_message, "auth-required: protected event requires authentication", sizeof(error_message) - 1);
error_message[sizeof(error_message) - 1] = '\0';
log_warning("Protected event rejected: authentication required");
DEBUG_WARN("Protected event rejected: authentication required");
}
}
}
@@ -484,8 +485,11 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
if (kind_obj && cJSON_IsNumber(kind_obj)) {
int event_kind = (int)cJSON_GetNumberValue(kind_obj);
DEBUG_TRACE("Processing event kind %d", event_kind);
// Log reception of Kind 23456 events
if (event_kind == 23456) {
DEBUG_LOG("Admin event (kind 23456) received");
}
if (event_kind == 23456) {
@@ -496,7 +500,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
if (auth_result != 0) {
// Authorization failed - log and reject
log_warning("Admin event authorization failed");
DEBUG_WARN("Admin event authorization failed");
result = -1;
size_t error_len = strlen(auth_error);
size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1;
@@ -512,14 +516,39 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
if (event_kind == 23456) {
if (admin_result != 0) {
char error_result_msg[512];
snprintf(error_result_msg, sizeof(error_result_msg),
"ERROR: Kind %d event processing failed: %s", event_kind, admin_error);
log_error(error_result_msg);
if (strlen(admin_error) > 0) {
// Safely truncate admin_error if too long
size_t max_error_len = sizeof(error_result_msg) - 50; // Leave room for prefix
size_t error_len = strlen(admin_error);
if (error_len > max_error_len) {
error_len = max_error_len;
}
char truncated_error[512];
memcpy(truncated_error, admin_error, error_len);
truncated_error[error_len] = '\0';
// Use a safer approach to avoid truncation warning
size_t prefix_len = snprintf(error_result_msg, sizeof(error_result_msg),
"ERROR: Kind %d event processing failed: ", event_kind);
if (prefix_len < sizeof(error_result_msg)) {
size_t remaining = sizeof(error_result_msg) - prefix_len;
size_t copy_len = strlen(truncated_error);
if (copy_len >= remaining) {
copy_len = remaining - 1;
}
memcpy(error_result_msg + prefix_len, truncated_error, copy_len);
error_result_msg[prefix_len + copy_len] = '\0';
}
} else {
snprintf(error_result_msg, sizeof(error_result_msg),
"ERROR: Kind %d event processing failed", event_kind);
}
DEBUG_ERROR(error_result_msg);
}
}
if (admin_result != 0) {
log_error("Failed to process admin event");
DEBUG_ERROR("Failed to process admin event");
result = -1;
size_t error_len = strlen(admin_error);
size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1;
@@ -539,7 +568,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
// Check if this is an error or if the command was already handled
if (strlen(nip17_error) > 0) {
// There was an actual error
log_error("NIP-17 admin message processing failed");
DEBUG_ERROR("NIP-17 admin message processing failed");
result = -1;
size_t error_len = strlen(nip17_error);
size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1;
@@ -550,7 +579,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
// No error message means the command was already handled (plain text commands)
// Store the original gift wrap event in database
if (store_event(event) != 0) {
log_error("Failed to store gift wrap event in database");
DEBUG_ERROR("Failed to store gift wrap event in database");
result = -1;
strncpy(error_message, "error: failed to store gift wrap event", sizeof(error_message) - 1);
}
@@ -558,7 +587,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
} else {
// Store the original gift wrap event in database (unlike kind 23456)
if (store_event(event) != 0) {
log_error("Failed to store gift wrap event in database");
DEBUG_ERROR("Failed to store gift wrap event in database");
result = -1;
strncpy(error_message, "error: failed to store gift wrap event", sizeof(error_message) - 1);
cJSON_Delete(response_event);
@@ -577,7 +606,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
int dm_result = process_dm_stats_command(event, dm_error, sizeof(dm_error), wsi);
if (dm_result != 0) {
log_error("DM stats command processing failed");
DEBUG_ERROR("DM stats command processing failed");
result = -1;
size_t error_len = strlen(dm_error);
size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1;
@@ -587,7 +616,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
} else {
// Store the DM event in database
if (store_event(event) != 0) {
log_error("Failed to store DM event in database");
DEBUG_ERROR("Failed to store DM event in database");
result = -1;
strncpy(error_message, "error: failed to store DM event", sizeof(error_message) - 1);
} else {
@@ -596,21 +625,23 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
}
}
} else {
DEBUG_TRACE("Storing regular event in database");
// Regular event - store in database and broadcast
if (store_event(event) != 0) {
log_error("Failed to store event in database");
DEBUG_ERROR("Failed to store event in database");
result = -1;
strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1);
} else {
DEBUG_LOG("Event stored and broadcast (kind %d)", event_kind);
// Broadcast event to matching persistent subscriptions
broadcast_event_to_subscriptions(event);
}
}
} else {
// Event without valid kind - try normal storage
log_warning("Event without valid kind - trying normal storage");
DEBUG_WARN("Event without valid kind - trying normal storage");
if (store_event(event) != 0) {
log_error("Failed to store event without kind in database");
DEBUG_ERROR("Failed to store event without kind in database");
result = -1;
strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1);
} else {
@@ -651,7 +682,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
send_nip42_auth_challenge(wsi, pss);
} else {
send_notice_message(wsi, "NIP-42 authentication required for subscriptions");
log_warning("REQ rejected: NIP-42 authentication required");
DEBUG_WARN("REQ rejected: NIP-42 authentication required");
}
cJSON_Delete(json);
free(message);
@@ -659,26 +690,28 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
}
// Handle REQ message
cJSON* sub_id = cJSON_GetArrayItem(json, 1);
cJSON* sub_id = cJSON_GetArrayItem(json, 1);
if (sub_id && cJSON_IsString(sub_id)) {
const char* subscription_id = cJSON_GetStringValue(sub_id);
if (sub_id && cJSON_IsString(sub_id)) {
const char* subscription_id = cJSON_GetStringValue(sub_id);
// Validate subscription ID before processing
if (!subscription_id) {
send_notice_message(wsi, "error: invalid subscription ID");
log_warning("REQ rejected: NULL subscription ID");
record_malformed_request(pss);
cJSON_Delete(json);
free(message);
return 0;
}
DEBUG_TRACE("Processing REQ message for subscription %s", subscription_id);
// Validate subscription ID before processing
if (!subscription_id) {
send_notice_message(wsi, "error: invalid subscription ID");
DEBUG_WARN("REQ rejected: NULL subscription ID");
record_malformed_request(pss);
cJSON_Delete(json);
free(message);
return 0;
}
// Check subscription ID format and length
size_t id_len = strlen(subscription_id);
if (id_len == 0 || id_len >= SUBSCRIPTION_ID_MAX_LENGTH) {
send_notice_message(wsi, "error: subscription ID too long or empty");
log_warning("REQ rejected: invalid subscription ID length");
DEBUG_WARN("REQ rejected: invalid subscription ID length");
cJSON_Delete(json);
free(message);
return 0;
@@ -704,7 +737,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
snprintf(debug_msg, sizeof(debug_msg),
"REQ rejected: invalid character '%c' (0x%02X) at position %zu in subscription ID: '%s'",
invalid_char, (unsigned char)invalid_char, invalid_pos, subscription_id);
log_warning(debug_msg);
DEBUG_WARN(debug_msg);
send_notice_message(wsi, "error: invalid characters in subscription ID");
cJSON_Delete(json);
free(message);
@@ -715,7 +748,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
cJSON* filters = cJSON_CreateArray();
if (!filters) {
send_notice_message(wsi, "error: failed to process filters");
log_error("REQ failed: could not create filters array");
DEBUG_ERROR("REQ failed: could not create filters array");
cJSON_Delete(json);
free(message);
return 0;
@@ -733,7 +766,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
char filter_error[512] = {0};
if (!validate_filter_array(filters, filter_error, sizeof(filter_error))) {
send_notice_message(wsi, filter_error);
log_warning("REQ rejected: invalid filters");
DEBUG_WARN("REQ rejected: invalid filters");
record_malformed_request(pss);
cJSON_Delete(filters);
cJSON_Delete(json);
@@ -746,6 +779,8 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
// Clean up the filters array we created
cJSON_Delete(filters);
DEBUG_LOG("REQ subscription %s processed, sending EOSE", subscription_id);
// Send EOSE (End of Stored Events)
cJSON* eose_response = cJSON_CreateArray();
if (eose_response) {
@@ -767,7 +802,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
}
} else {
send_notice_message(wsi, "error: missing or invalid subscription ID in REQ");
log_warning("REQ rejected: missing or invalid subscription ID");
DEBUG_WARN("REQ rejected: missing or invalid subscription ID");
}
} else if (strcmp(msg_type, "COUNT") == 0) {
// Check NIP-42 authentication for COUNT requests if required
@@ -776,7 +811,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
send_nip42_auth_challenge(wsi, pss);
} else {
send_notice_message(wsi, "NIP-42 authentication required for count requests");
log_warning("COUNT rejected: NIP-42 authentication required");
DEBUG_WARN("COUNT rejected: NIP-42 authentication required");
}
cJSON_Delete(json);
free(message);
@@ -803,7 +838,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
char filter_error[512] = {0};
if (!validate_filter_array(filters, filter_error, sizeof(filter_error))) {
send_notice_message(wsi, filter_error);
log_warning("COUNT rejected: invalid filters");
DEBUG_WARN("COUNT rejected: invalid filters");
record_malformed_request(pss);
cJSON_Delete(filters);
cJSON_Delete(json);
@@ -825,7 +860,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
// Validate subscription ID before processing
if (!subscription_id) {
send_notice_message(wsi, "error: invalid subscription ID in CLOSE");
log_warning("CLOSE rejected: NULL subscription ID");
DEBUG_WARN("CLOSE rejected: NULL subscription ID");
cJSON_Delete(json);
free(message);
return 0;
@@ -835,7 +870,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
size_t id_len = strlen(subscription_id);
if (id_len == 0 || id_len >= SUBSCRIPTION_ID_MAX_LENGTH) {
send_notice_message(wsi, "error: subscription ID too long or empty in CLOSE");
log_warning("CLOSE rejected: invalid subscription ID length");
DEBUG_WARN("CLOSE rejected: invalid subscription ID length");
cJSON_Delete(json);
free(message);
return 0;
@@ -854,16 +889,15 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
if (!valid_id) {
send_notice_message(wsi, "error: invalid characters in subscription ID for CLOSE");
log_warning("CLOSE rejected: invalid characters in subscription ID");
DEBUG_WARN("CLOSE rejected: invalid characters in subscription ID");
cJSON_Delete(json);
free(message);
return 0;
}
// Remove from global manager
remove_subscription_from_manager(subscription_id, wsi);
// Remove from session list if present
// CRITICAL FIX: Remove from session list FIRST (while holding lock)
// to prevent race condition where global manager frees the subscription
// while we're still iterating through the session list
if (pss) {
pthread_mutex_lock(&pss->session_lock);
@@ -881,10 +915,14 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
pthread_mutex_unlock(&pss->session_lock);
}
// Remove from global manager AFTER removing from session list
// This prevents use-after-free when iterating session subscriptions
remove_subscription_from_manager(subscription_id, wsi);
// Subscription closed
} else {
send_notice_message(wsi, "error: missing or invalid subscription ID in CLOSE");
log_warning("CLOSE rejected: missing or invalid subscription ID");
DEBUG_WARN("CLOSE rejected: missing or invalid subscription ID");
}
} else if (strcmp(msg_type, "AUTH") == 0) {
// Handle NIP-42 AUTH message
@@ -899,17 +937,17 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
handle_nip42_auth_signed_event(wsi, pss, auth_payload);
} else {
send_notice_message(wsi, "Invalid AUTH message format");
log_warning("Received AUTH message with invalid payload type");
DEBUG_WARN("Received AUTH message with invalid payload type");
}
} else {
send_notice_message(wsi, "AUTH message requires payload");
log_warning("Received AUTH message without payload");
DEBUG_WARN("Received AUTH message without payload");
}
} else {
// Unknown message type
char unknown_msg[128];
snprintf(unknown_msg, sizeof(unknown_msg), "Unknown message type: %.32s", msg_type);
log_warning(unknown_msg);
DEBUG_WARN(unknown_msg);
send_notice_message(wsi, "Unknown message type");
}
}
@@ -922,6 +960,8 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
break;
case LWS_CALLBACK_CLOSED:
DEBUG_TRACE("WebSocket connection closed");
DEBUG_LOG("WebSocket connection closed from %s", pss ? pss->client_ip : "unknown");
// Clean up session subscriptions
if (pss) {
@@ -940,6 +980,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
pthread_mutex_unlock(&pss->session_lock);
pthread_mutex_destroy(&pss->session_lock);
}
DEBUG_TRACE("WebSocket connection cleanup complete");
break;
default:
@@ -1041,13 +1082,13 @@ int start_websocket_relay(int port_override, int strict_port) {
char error_msg[256];
snprintf(error_msg, sizeof(error_msg),
"Strict port mode: port %d is not available", actual_port);
log_error(error_msg);
DEBUG_ERROR(error_msg);
return -1;
} else if (port_attempts < max_port_attempts) {
char retry_msg[256];
snprintf(retry_msg, sizeof(retry_msg), "Port %d is in use, trying port %d (attempt %d/%d)",
actual_port, actual_port + 1, port_attempts + 1, max_port_attempts);
log_warning(retry_msg);
DEBUG_WARN(retry_msg);
actual_port++;
continue;
} else {
@@ -1055,7 +1096,7 @@ int start_websocket_relay(int port_override, int strict_port) {
snprintf(error_msg, sizeof(error_msg),
"Failed to find available port after %d attempts (tried ports %d-%d)",
max_port_attempts, configured_port, actual_port);
log_error(error_msg);
DEBUG_ERROR(error_msg);
return -1;
}
}
@@ -1077,14 +1118,14 @@ int start_websocket_relay(int port_override, int strict_port) {
char lws_error_msg[256];
snprintf(lws_error_msg, sizeof(lws_error_msg),
"libwebsockets failed to bind to port %d (errno: %d)", actual_port, errno_saved);
log_warning(lws_error_msg);
DEBUG_WARN(lws_error_msg);
port_attempts++;
if (strict_port) {
char error_msg[256];
snprintf(error_msg, sizeof(error_msg),
"Strict port mode: failed to bind to port %d", actual_port);
log_error(error_msg);
DEBUG_ERROR(error_msg);
break;
} else if (port_attempts < max_port_attempts) {
actual_port++;
@@ -1100,7 +1141,7 @@ int start_websocket_relay(int port_override, int strict_port) {
snprintf(error_msg, sizeof(error_msg),
"Failed to create libwebsockets context after %d attempts. Last attempted port: %d",
port_attempts, actual_port);
log_error(error_msg);
DEBUG_ERROR(error_msg);
perror("libwebsockets creation error");
return -1;
}
@@ -1110,7 +1151,7 @@ int start_websocket_relay(int port_override, int strict_port) {
snprintf(startup_msg, sizeof(startup_msg),
"WebSocket relay started on ws://127.0.0.1:%d (configured port %d was unavailable)",
actual_port, configured_port);
log_warning(startup_msg);
DEBUG_WARN(startup_msg);
} else {
snprintf(startup_msg, sizeof(startup_msg), "WebSocket relay started on ws://127.0.0.1:%d", actual_port);
}
@@ -1120,7 +1161,7 @@ int start_websocket_relay(int port_override, int strict_port) {
int result = lws_service(ws_context, 1000);
if (result < 0) {
log_error("libwebsockets service error");
DEBUG_ERROR("libwebsockets service error");
break;
}
}
@@ -1146,7 +1187,7 @@ int process_dm_stats_command(cJSON* dm_event, char* error_message, size_t error_
return -1;
}
const char* relay_pubkey = get_relay_pubkey_cached();
const char* relay_pubkey = get_config_value("relay_pubkey");
if (!relay_pubkey) {
strncpy(error_message, "Could not get relay pubkey", error_size - 1);
return -1;
@@ -1184,7 +1225,7 @@ int process_dm_stats_command(cJSON* dm_event, char* error_message, size_t error_
const char* sender_pubkey = cJSON_GetStringValue(pubkey_obj);
// Check if sender is admin
const char* admin_pubkey = get_admin_pubkey_cached();
const char* admin_pubkey = get_config_value("admin_pubkey");
if (!admin_pubkey || strlen(admin_pubkey) == 0 ||
strcmp(sender_pubkey, admin_pubkey) != 0) {
strncpy(error_message, "Unauthorized: not admin", error_size - 1);
@@ -1304,7 +1345,7 @@ int handle_count_message(const char* sub_id, cJSON* filters, struct lws *wsi, st
(void)pss; // Suppress unused parameter warning
if (!cJSON_IsArray(filters)) {
log_error("COUNT filters is not an array");
DEBUG_ERROR("COUNT filters is not an array");
return 0;
}
@@ -1319,7 +1360,7 @@ int handle_count_message(const char* sub_id, cJSON* filters, struct lws *wsi, st
for (int i = 0; i < cJSON_GetArraySize(filters); i++) {
cJSON* filter = cJSON_GetArrayItem(filters, i);
if (!filter || !cJSON_IsObject(filter)) {
log_warning("Invalid filter object in COUNT");
DEBUG_WARN("Invalid filter object in COUNT");
continue;
}
@@ -1566,7 +1607,7 @@ int handle_count_message(const char* sub_id, cJSON* filters, struct lws *wsi, st
if (rc != SQLITE_OK) {
char error_msg[256];
snprintf(error_msg, sizeof(error_msg), "Failed to prepare COUNT query: %s", sqlite3_errmsg(g_db));
log_error(error_msg);
DEBUG_ERROR(error_msg);
continue;
}
@@ -1661,7 +1702,7 @@ int is_client_rate_limited_for_malformed_requests(struct per_session_data *pss)
if (pss->malformed_request_count >= MAX_MALFORMED_REQUESTS_PER_HOUR) {
// Block for the specified duration
pss->malformed_request_blocked_until = now + MALFORMED_REQUEST_BLOCK_DURATION;
log_warning("Client rate limited for malformed requests");
DEBUG_WARN("Client rate limited for malformed requests");
return 1;
}

View File

@@ -9,7 +9,8 @@ Type=simple
User=c-relay
Group=c-relay
WorkingDirectory=/opt/c-relay
ExecStart=/opt/c-relay/c_relay_x86
Environment=DEBUG_LEVEL=0
ExecStart=/opt/c-relay/c_relay_x86 --debug-level=$DEBUG_LEVEL
Restart=always
RestartSec=5
StandardOutput=journal

40
tests/post_events.sh Executable file
View File

@@ -0,0 +1,40 @@
#!/bin/bash
# Test script to post kind 1 events to the relay every second
# Cycles through three different secret keys
# Content includes current timestamp
# Array of secret keys to cycle through
SECRET_KEYS=(
"3fdd8227a920c2385559400b2b14e464f22e80df312a73cc7a86e1d7e91d608f"
"a156011cd65b71f84b4a488ac81687f2aed57e490b31c28f58195d787030db60"
"1618aaa21f5bd45c5ffede0d9a60556db67d4a046900e5f66b0bae5c01c801fb"
)
RELAY_URL="ws://localhost:8888"
KEY_INDEX=0
echo "Starting event posting test to $RELAY_URL"
echo "Press Ctrl+C to stop"
while true; do
# Get current timestamp
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S UTC")
# Get current secret key
CURRENT_KEY=${SECRET_KEYS[$KEY_INDEX]}
# Create content with timestamp
CONTENT="Test event at $TIMESTAMP"
echo "[$TIMESTAMP] Posting event with key ${KEY_INDEX}: ${CURRENT_KEY:0:16}..."
# Post event using nak
nak event -c "$CONTENT" --sec "$CURRENT_KEY" "$RELAY_URL"
# Cycle to next key
KEY_INDEX=$(( (KEY_INDEX + 1) % ${#SECRET_KEYS[@]} ))
# Wait 1 second
sleep 1
done

View File

@@ -1,18 +0,0 @@
2025-10-11 13:46:17 - ==========================================
2025-10-11 13:46:17 - C-Relay Comprehensive Test Suite Runner
2025-10-11 13:46:17 - ==========================================
2025-10-11 13:46:17 - Relay URL: ws://127.0.0.1:8888
2025-10-11 13:46:17 - Log file: test_results_20251011_134617.log
2025-10-11 13:46:17 - Report file: test_report_20251011_134617.html
2025-10-11 13:46:17 -
2025-10-11 13:46:17 - Checking relay status at ws://127.0.0.1:8888...
2025-10-11 13:46:17 - \033[0;32m✓ Relay HTTP endpoint is accessible\033[0m
2025-10-11 13:46:17 -
2025-10-11 13:46:17 - Starting comprehensive test execution...
2025-10-11 13:46:17 -
2025-10-11 13:46:17 - \033[0;34m=== SECURITY TEST SUITES ===\033[0m
2025-10-11 13:46:17 - ==========================================
2025-10-11 13:46:17 - Running Test Suite: SQL Injection Tests
2025-10-11 13:46:17 - Description: Comprehensive SQL injection vulnerability testing
2025-10-11 13:46:17 - ==========================================
2025-10-11 13:46:17 - \033[0;31mERROR: Test script tests/sql_injection_tests.sh not found\033[0m

View File

@@ -1,629 +0,0 @@
2025-10-11 13:48:07 - ==========================================
2025-10-11 13:48:07 - C-Relay Comprehensive Test Suite Runner
2025-10-11 13:48:07 - ==========================================
2025-10-11 13:48:07 - Relay URL: ws://127.0.0.1:8888
2025-10-11 13:48:07 - Log file: test_results_20251011_134807.log
2025-10-11 13:48:07 - Report file: test_report_20251011_134807.html
2025-10-11 13:48:07 -
2025-10-11 13:48:07 - Checking relay status at ws://127.0.0.1:8888...
2025-10-11 13:48:07 - \033[0;32m✓ Relay HTTP endpoint is accessible\033[0m
2025-10-11 13:48:07 -
2025-10-11 13:48:07 - Starting comprehensive test execution...
2025-10-11 13:48:07 -
2025-10-11 13:48:07 - \033[0;34m=== SECURITY TEST SUITES ===\033[0m
2025-10-11 13:48:07 - ==========================================
2025-10-11 13:48:07 - Running Test Suite: SQL Injection Tests
2025-10-11 13:48:07 - Description: Comprehensive SQL injection vulnerability testing
2025-10-11 13:48:07 - ==========================================
==========================================
C-Relay SQL Injection Test Suite
==========================================
Testing against relay at ws://127.0.0.1:8888
=== Basic Connectivity Test ===
Testing Basic connectivity... PASSED - Valid query works
=== Authors Filter SQL Injection Tests ===
Testing Authors filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: admin'--... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: /*... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: */... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: /**/... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: #... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error)
=== IDs Filter SQL Injection Tests ===
Testing IDs filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: admin'--... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: /*... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: */... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: /**/... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: #... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error)
=== Kinds Filter SQL Injection Tests ===
Testing Kinds filter with string injection... PASSED - SQL injection blocked (rejected with error)
Testing Kinds filter with negative value... PASSED - SQL injection blocked (rejected with error)
Testing Kinds filter with very large value... PASSED - SQL injection blocked (rejected with error)
=== Search Filter SQL Injection Tests ===
Testing Search filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing Search filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: admin'--... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: /*... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: */... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: /**/... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing Search filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing Search filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing Search filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error)
=== Tag Filter SQL Injection Tests ===
Testing #e tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: */... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: */... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: */... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: */... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: */... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized)
=== Timestamp Filter SQL Injection Tests ===
Testing Since parameter injection... PASSED - SQL injection blocked (rejected with error)
Testing Until parameter injection... PASSED - SQL injection blocked (rejected with error)
=== Limit Parameter SQL Injection Tests ===
Testing Limit parameter injection... PASSED - SQL injection blocked (rejected with error)
Testing Limit with UNION... PASSED - SQL injection blocked (rejected with error)
=== Complex Multi-Filter SQL Injection Tests ===
Testing Multi-filter with authors injection... PASSED - SQL injection blocked (rejected with error)
Testing Multi-filter with search injection... PASSED - SQL injection blocked (rejected with error)
Testing Multi-filter with tag injection... PASSED - SQL injection blocked (query sanitized)
=== COUNT Message SQL Injection Tests ===
Testing COUNT with authors payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' OR '1'='1... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing COUNT with authors payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: admin'--... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: admin'--... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: /*... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: /*... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: */... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: */... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: /**/... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: /**/... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: #... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: #... PASSED - SQL injection blocked (query sanitized)
Testing COUNT with authors payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing COUNT with authors payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing COUNT with authors payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error)
=== Edge Case SQL Injection Tests ===
Testing Empty string injection... PASSED - SQL injection blocked (rejected with error)
Testing Null byte injection... PASSED - SQL injection blocked (silently rejected)
Testing Unicode injection... PASSED - SQL injection blocked (rejected with error)
Testing Very long injection payload... PASSED - SQL injection blocked (rejected with error)
=== Subscription ID SQL Injection Tests ===
Testing Subscription ID injection... PASSED - SQL injection blocked (rejected with error)
Testing Subscription ID with quotes... PASSED - SQL injection blocked (silently rejected)
=== CLOSE Message SQL Injection Tests ===
Testing CLOSE with injection... PASSED - SQL injection blocked (rejected with error)
=== Test Results ===
Total tests: 318
Passed: 318
Failed: 0
✓ All SQL injection tests passed!
The relay appears to be protected against SQL injection attacks.
2025-10-11 13:48:30 - \033[0;32m✓ SQL Injection Tests PASSED\033[0m (Duration: 23s)
2025-10-11 13:48:30 - ==========================================
2025-10-11 13:48:30 - Running Test Suite: Filter Validation Tests
2025-10-11 13:48:30 - Description: Input validation for REQ and COUNT messages
2025-10-11 13:48:30 - ==========================================
=== C-Relay Filter Validation Tests ===
Testing against relay at ws://127.0.0.1:8888
Testing Valid REQ message... PASSED
Testing Valid COUNT message... PASSED
=== Testing Filter Array Validation ===
Testing Non-object filter... PASSED
Testing Too many filters... PASSED
=== Testing Authors Validation ===
Testing Invalid author type... PASSED
Testing Invalid author hex... PASSED
Testing Too many authors... PASSED
=== Testing IDs Validation ===
Testing Invalid ID type... PASSED
Testing Invalid ID hex... PASSED
Testing Too many IDs... PASSED
=== Testing Kinds Validation ===
Testing Invalid kind type... PASSED
Testing Negative kind... PASSED
Testing Too large kind... PASSED
Testing Too many kinds... PASSED
=== Testing Timestamp Validation ===
Testing Invalid since type... PASSED
Testing Negative since... PASSED
Testing Invalid until type... PASSED
Testing Negative until... PASSED
=== Testing Limit Validation ===
Testing Invalid limit type... PASSED
Testing Negative limit... PASSED
Testing Too large limit... PASSED
=== Testing Search Validation ===
Testing Invalid search type... PASSED
Testing Search too long... PASSED
Testing Search SQL injection... PASSED
=== Testing Tag Filter Validation ===
Testing Invalid tag filter type... PASSED
Testing Too many tag values... PASSED
Testing Tag value too long... PASSED
=== Testing Rate Limiting ===
Testing rate limiting with malformed requests... UNCERTAIN - Rate limiting may not have triggered (this could be normal)
=== Test Results ===
Total tests: 28
Passed: 28
Failed: 0
All tests passed!
2025-10-11 13:48:35 - \033[0;32m✓ Filter Validation Tests PASSED\033[0m (Duration: 5s)
2025-10-11 13:48:35 - ==========================================
2025-10-11 13:48:35 - Running Test Suite: Subscription Validation Tests
2025-10-11 13:48:35 - Description: Subscription ID and message validation
2025-10-11 13:48:35 - ==========================================
Testing subscription ID validation fixes...
Testing malformed subscription IDs...
Valid ID test: Success
Testing CLOSE message validation...
CLOSE valid ID test: Success
Subscription validation tests completed.
2025-10-11 13:48:36 - \033[0;32m✓ Subscription Validation Tests PASSED\033[0m (Duration: 1s)
2025-10-11 13:48:36 - ==========================================
2025-10-11 13:48:36 - Running Test Suite: Memory Corruption Tests
2025-10-11 13:48:36 - Description: Buffer overflow and memory safety testing
2025-10-11 13:48:36 - ==========================================
==========================================
C-Relay Memory Corruption Test Suite
==========================================
Testing against relay at ws://127.0.0.1:8888
Note: These tests may cause the relay to crash if vulnerabilities exist
=== Basic Connectivity Test ===
Testing Basic connectivity... PASSED - No memory corruption detected
=== Subscription ID Memory Corruption Tests ===
Testing Empty subscription ID... UNCERTAIN - Expected error but got normal response
Testing Very long subscription ID (1KB)... UNCERTAIN - Expected error but got normal response
Testing Very long subscription ID (10KB)... UNCERTAIN - Expected error but got normal response
Testing Subscription ID with null bytes... UNCERTAIN - Expected error but got normal response
Testing Subscription ID with special chars... UNCERTAIN - Expected error but got normal response
Testing Unicode subscription ID... UNCERTAIN - Expected error but got normal response
Testing Subscription ID with path traversal... UNCERTAIN - Expected error but got normal response
=== Filter Array Memory Corruption Tests ===
Testing Too many filters (50)... UNCERTAIN - Expected error but got normal response
=== Concurrent Access Memory Tests ===
Testing Concurrent subscription creation... ["EVENT", "concurrent_1760204917502714788", { "id": "b3a2a79b768c304a8ad315a97319e3c6fd9d521844fc9f1e4228c75c453dd882", "pubkey": "aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4", "created_at": 1760196143, "kind": 30001, "content": "Updated addressable event", "sig": "795671a831de31fbbdd6282585529f274f61bb6e8c974e597560d70989355f24c8ecfe70caf043e8fbc24ce65d9b0d562297c682af958cfcdd2ee137dd9bccb4", "tags": [["d", "test-article"], ["type", "addressable"], ["updated", "true"]] }]
["EVENT", "concurrent_1760204917502714788", { "id": "b3a2a79b768c304a8ad315a97319e3c6fd9d521844fc9f1e4228c75c453dd882", "pubkey": "aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4", "created_at": 1760196143, "kind": 30001, "content": "Updated addressable event", "sig": "795671a831de31fbbdd6282585529f274f61bb6e8c974e597560d70989355f24c8ecfe70caf043e8fbc24ce65d9b0d562297c682af958cfcdd2ee137dd9bccb4", "tags": [["d", "test-article"], ["type", "addressable"], ["updated", "true"]] }]
["EVENT", "concurrent_1760204917502714788", { "id": "b3a2a79b768c304a8ad315a97319e3c6fd9d521844fc9f1e4228c75c453dd882", "pubkey": "aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4", "created_at": 1760196143, "kind": 30001, "content": "Updated addressable event", "sig": "795671a831de31fbbdd6282585529f274f61bb6e8c974e597560d70989355f24c8ecfe70caf043e8fbc24ce65d9b0d562297c682af958cfcdd2ee137dd9bccb4", "tags": [["d", "test-article"], ["type", "addressable"], ["updated", "true"]] }]
["EVENT", "concurrent_1760204917502714788", { "id": "b3a2a79b768c304a8ad315a97319e3c6fd9d521844fc9f1e4228c75c453dd882", "pubkey": "aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4", "created_at": 1760196143, "kind": 30001, "content": "Updated addressable event", "sig": "795671a831de31fbbdd6282585529f274f61bb6e8c974e597560d70989355f24c8ecfe70caf043e8fbc24ce65d9b0d562297c682af958cfcdd2ee137dd9bccb4", "tags": [["d", "test-article"], ["type", "addressable"], ["updated", "true"]] }]
["EVENT", "concurrent_1760204917502714788", { "id": "b3a2a79b768c304a8ad315a97319e3c6fd9d521844fc9f1e4228c75c453dd882", "pubkey": "aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4", "created_at": 1760196143, "kind": 30001, "content": "Updated addressable event", "sig": "795671a831de31fbbdd6282585529f274f61bb6e8c974e597560d70989355f24c8ecfe70caf043e8fbc24ce65d9b0d562297c682af958cfcdd2ee137dd9bccb4", "tags": [["d", "test-article"], ["type", "addressable"], ["updated", "true"]] }]
["EVENT", "concurrent_1760204917502714788", { "id": "b3a2a79b768c304a8ad315a97319e3c6fd9d521844fc9f1e4228c75c453dd882", "pubkey": "aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4", "created_at": 1760196143, "kind": 30001, "content": "Updated addressable event", "sig": "795671a831de31fbbdd6282585529f274f61bb6e8c974e597560d70989355f24c8ecfe70caf043e8fbc24ce65d9b0d562297c682af958cfcdd2ee137dd9bccb4", "tags": [["d", "test-article"], ["type", "addressable"], ["updated", "true"]] }]
["EVENT", "concurrent_1760204917502714788", { "id": "b3a2a79b768c304a8ad315a97319e3c6fd9d521844fc9f1e4228c75c453dd882", "pubkey": "aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4", "created_at": 1760196143, "kind": 30001, "content": "Updated addressable event", "sig": "795671a831de31fbbdd6282585529f274f61bb6e8c974e597560d70989355f24c8ecfe70caf043e8fbc24ce65d9b0d562297c682af958cfcdd2ee137dd9bccb4", "tags": [["d", "test-article"], ["type", "addressable"], ["updated", "true"]] }]
["EVENT", "concurrent_1760204917502714788", { "id": "b3a2a79b768c304a8ad315a97319e3c6fd9d521844fc9f1e4228c75c453dd882", "pubkey": "aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4", "created_at": 1760196143, "kind": 30001, "content": "Updated addressable event", "sig": "795671a831de31fbbdd6282585529f274f61bb6e8c974e597560d70989355f24c8ecfe70caf043e8fbc24ce65d9b0d562297c682af958cfcdd2ee137dd9bccb4", "tags": [["d", "test-article"], ["type", "addressable"], ["updated", "true"]] }]
["EVENT", "concurrent_1760204917502714788", { "id": "b3a2a79b768c304a8ad315a97319e3c6fd9d521844fc9f1e4228c75c453dd882", "pubkey": "aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4", "created_at": 1760196143, "kind": 30001, "content": "Updated addressable event", "sig": "795671a831de31fbbdd6282585529f274f61bb6e8c974e597560d70989355f24c8ecfe70caf043e8fbc24ce65d9b0d562297c682af958cfcdd2ee137dd9bccb4", "tags": [["d", "test-article"], ["type", "addressable"], ["updated", "true"]] }]
["EVENT", "concurrent_1760204917502714788", { "id": "b3a2a79b768c304a8ad315a97319e3c6fd9d521844fc9f1e4228c75c453dd882", "pubkey": "aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4", "created_at": 1760196143, "kind": 30001, "content": "Updated addressable event", "sig": "795671a831de31fbbdd6282585529f274f61bb6e8c974e597560d70989355f24c8ecfe70caf043e8fbc24ce65d9b0d562297c682af958cfcdd2ee137dd9bccb4", "tags": [["d", "test-article"], ["type", "addressable"], ["updated", "true"]] }]
PASSED - Concurrent access handled safely
Testing Concurrent CLOSE operations...
PASSED - Concurrent access handled safely
=== Malformed JSON Memory Tests ===
Testing Unclosed JSON object... UNCERTAIN - Expected error but got normal response
Testing Mismatched brackets... UNCERTAIN - Expected error but got normal response
Testing Extra closing brackets... UNCERTAIN - Expected error but got normal response
Testing Null bytes in JSON... UNCERTAIN - Expected error but got normal response
=== Large Message Memory Tests ===
Testing Very large filter array... UNCERTAIN - Expected error but got normal response
Testing Very long search term... UNCERTAIN - Expected error but got normal response
=== Test Results ===
Total tests: 17
Passed: 17
Failed: 0
✓ All memory corruption tests passed!
The relay appears to handle memory safely.
2025-10-11 13:48:38 - \033[0;32m✓ Memory Corruption Tests PASSED\033[0m (Duration: 2s)
2025-10-11 13:48:38 - ==========================================
2025-10-11 13:48:38 - Running Test Suite: Input Validation Tests
2025-10-11 13:48:38 - Description: Comprehensive input boundary testing
2025-10-11 13:48:38 - ==========================================
==========================================
C-Relay Input Validation Test Suite
==========================================
Testing against relay at ws://127.0.0.1:8888
=== Basic Connectivity Test ===
Testing Basic connectivity... PASSED - Input accepted correctly
=== Message Type Validation ===
Testing Invalid message type - string... PASSED - Invalid input properly rejected
Testing Invalid message type - number... PASSED - Invalid input properly rejected
Testing Invalid message type - null... PASSED - Invalid input properly rejected
Testing Invalid message type - object... PASSED - Invalid input properly rejected
Testing Empty message type... PASSED - Invalid input properly rejected
Testing Very long message type... PASSED - Invalid input properly rejected
=== Message Structure Validation ===
Testing Too few arguments... PASSED - Invalid input properly rejected
Testing Too many arguments... PASSED - Invalid input properly rejected
Testing Non-array message... PASSED - Invalid input properly rejected
Testing Empty array... PASSED - Invalid input properly rejected
Testing Nested arrays incorrectly... PASSED - Invalid input properly rejected
=== Subscription ID Boundary Tests ===
Testing Valid subscription ID... PASSED - Input accepted correctly
Testing Empty subscription ID... PASSED - Invalid input properly rejected
Testing Subscription ID with spaces... PASSED - Invalid input properly rejected
Testing Subscription ID with newlines... PASSED - Invalid input properly rejected
Testing Subscription ID with tabs... PASSED - Invalid input properly rejected
Testing Subscription ID with control chars... PASSED - Invalid input properly rejected
Testing Unicode subscription ID... PASSED - Invalid input properly rejected
Testing Very long subscription ID... PASSED - Invalid input properly rejected
=== Filter Object Validation ===
Testing Valid empty filter... PASSED - Input accepted correctly
Testing Non-object filter... PASSED - Invalid input properly rejected
Testing Null filter... PASSED - Invalid input properly rejected
Testing Array filter... PASSED - Invalid input properly rejected
Testing Filter with invalid keys... PASSED - Input accepted correctly
=== Authors Field Validation ===
Testing Valid authors array... PASSED - Input accepted correctly
Testing Empty authors array... PASSED - Input accepted correctly
Testing Non-array authors... PASSED - Invalid input properly rejected
Testing Invalid hex in authors... PASSED - Invalid input properly rejected
Testing Short pubkey in authors... PASSED - Invalid input properly rejected
=== IDs Field Validation ===
Testing Valid ids array... PASSED - Input accepted correctly
Testing Empty ids array... PASSED - Input accepted correctly
Testing Non-array ids... PASSED - Invalid input properly rejected
=== Kinds Field Validation ===
Testing Valid kinds array... PASSED - Input accepted correctly
Testing Empty kinds array... PASSED - Input accepted correctly
Testing Non-array kinds... PASSED - Invalid input properly rejected
Testing String in kinds... PASSED - Invalid input properly rejected
=== Timestamp Field Validation ===
Testing Valid since timestamp... PASSED - Input accepted correctly
Testing Valid until timestamp... PASSED - Input accepted correctly
Testing String since timestamp... PASSED - Invalid input properly rejected
Testing Negative timestamp... PASSED - Invalid input properly rejected
=== Limit Field Validation ===
Testing Valid limit... PASSED - Input accepted correctly
Testing Zero limit... PASSED - Input accepted correctly
Testing String limit... PASSED - Invalid input properly rejected
Testing Negative limit... PASSED - Invalid input properly rejected
=== Multiple Filters ===
Testing Two valid filters... PASSED - Input accepted correctly
Testing Many filters... PASSED - Input accepted correctly
=== Test Results ===
Total tests: 47
Passed: 47
Failed: 0
✓ All input validation tests passed!
The relay properly validates input.
2025-10-11 13:48:42 - \033[0;32m✓ Input Validation Tests PASSED\033[0m (Duration: 4s)
2025-10-11 13:48:42 -
2025-10-11 13:48:42 - \033[0;34m=== PERFORMANCE TEST SUITES ===\033[0m
2025-10-11 13:48:42 - ==========================================
2025-10-11 13:48:42 - Running Test Suite: Subscription Limit Tests
2025-10-11 13:48:42 - Description: Subscription limit enforcement testing
2025-10-11 13:48:42 - ==========================================
=== Subscription Limit Test ===
[INFO] Testing relay at: ws://127.0.0.1:8888
[INFO] Note: This test assumes default subscription limits (max 25 per client)
=== Test 1: Basic Connectivity ===
[INFO] Testing basic WebSocket connection...
[PASS] Basic connectivity works
=== Test 2: Subscription Limit Enforcement ===
[INFO] Testing subscription limits by creating multiple subscriptions...
[INFO] Creating multiple subscriptions within a single connection...
[INFO] Hit subscription limit at subscription 26
[PASS] Subscription limit enforcement working (limit hit after 25 subscriptions)
=== Test Complete ===
2025-10-11 13:48:42 - \033[0;32m✓ Subscription Limit Tests PASSED\033[0m (Duration: 0s)
2025-10-11 13:48:42 - ==========================================
2025-10-11 13:48:42 - Running Test Suite: Load Testing
2025-10-11 13:48:42 - Description: High concurrent connection testing
2025-10-11 13:48:42 - ==========================================
==========================================
C-Relay Load Testing Suite
==========================================
Testing against relay at ws://127.0.0.1:8888
=== Basic Connectivity Test ===
✗ Cannot connect to relay. Aborting tests.
2025-10-11 13:48:47 - \033[0;31m✗ Load Testing FAILED\033[0m (Duration: 5s)

View File

@@ -1,728 +0,0 @@
2025-10-11 14:11:34 - ==========================================
2025-10-11 14:11:34 - C-Relay Comprehensive Test Suite Runner
2025-10-11 14:11:34 - ==========================================
2025-10-11 14:11:34 - Relay URL: ws://127.0.0.1:8888
2025-10-11 14:11:34 - Log file: test_results_20251011_141134.log
2025-10-11 14:11:34 - Report file: test_report_20251011_141134.html
2025-10-11 14:11:34 -
2025-10-11 14:11:34 - Checking relay status at ws://127.0.0.1:8888...
2025-10-11 14:11:34 - \033[0;32m✓ Relay HTTP endpoint is accessible\033[0m
2025-10-11 14:11:34 -
2025-10-11 14:11:34 - Starting comprehensive test execution...
2025-10-11 14:11:34 -
2025-10-11 14:11:34 - \033[0;34m=== SECURITY TEST SUITES ===\033[0m
2025-10-11 14:11:34 - ==========================================
2025-10-11 14:11:34 - Running Test Suite: SQL Injection Tests
2025-10-11 14:11:34 - Description: Comprehensive SQL injection vulnerability testing
2025-10-11 14:11:34 - ==========================================
==========================================
C-Relay SQL Injection Test Suite
==========================================
Testing against relay at ws://127.0.0.1:8888
=== Basic Connectivity Test ===
Testing Basic connectivity... PASSED - Valid query works
=== Authors Filter SQL Injection Tests ===
Testing Authors filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: admin'--... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: /*... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: */... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: /**/... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: #... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error)
Testing Authors filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error)
=== IDs Filter SQL Injection Tests ===
Testing IDs filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: admin'--... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: /*... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: */... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: /**/... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: #... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error)
Testing IDs filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error)
=== Kinds Filter SQL Injection Tests ===
Testing Kinds filter with string injection... PASSED - SQL injection blocked (rejected with error)
Testing Kinds filter with negative value... PASSED - SQL injection blocked (rejected with error)
Testing Kinds filter with very large value... PASSED - SQL injection blocked (rejected with error)
=== Search Filter SQL Injection Tests ===
Testing Search filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing Search filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: admin'--... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: /*... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: */... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: /**/... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing Search filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing Search filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing Search filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error)
Testing Search filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error)
=== Tag Filter SQL Injection Tests ===
Testing #e tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: */... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized)
Testing #e tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: */... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized)
Testing #p tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: */... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized)
Testing #t tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: */... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized)
Testing #r tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' OR 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: admin'--... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: /*... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: */... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: /**/... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: #... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' AND 1=1 --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' AND 1=2 --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (query sanitized)
Testing #d tag filter with payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (query sanitized)
=== Timestamp Filter SQL Injection Tests ===
Testing Since parameter injection... PASSED - SQL injection blocked (rejected with error)
Testing Until parameter injection... PASSED - SQL injection blocked (rejected with error)
=== Limit Parameter SQL Injection Tests ===
Testing Limit parameter injection... PASSED - SQL injection blocked (rejected with error)
Testing Limit with UNION... PASSED - SQL injection blocked (rejected with error)
=== Complex Multi-Filter SQL Injection Tests ===
Testing Multi-filter with authors injection... PASSED - SQL injection blocked (rejected with error)
Testing Multi-filter with search injection... PASSED - SQL injection blocked (rejected with error)
Testing Multi-filter with tag injection... PASSED - SQL injection blocked (query sanitized)
=== COUNT Message SQL Injection Tests ===
Testing COUNT with authors payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' OR '1'='1... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' OR '1'='1... PASSED - SQL injection blocked (query sanitized)
Testing COUNT with authors payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1; SELECT * FROM sqlite_master; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' OR 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: admin'--... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: admin'--... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' UNION SELECT password FROM users --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' AND SLEEP(5) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' AND 1=CAST((SELECT version()) AS INT) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' UNION SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' UNION SELECT 1,2,3 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' UNION ALL SELECT NULL,NULL,NULL --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; SELECT * FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; DELETE FROM events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: /*... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: /*... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: */... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: */... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: /**/... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: /**/... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: #... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: #... PASSED - SQL injection blocked (query sanitized)
Testing COUNT with authors payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (query sanitized)
Testing COUNT with authors payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (query sanitized)
Testing COUNT with authors payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '))); DROP TABLE events; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ')) UNION SELECT NULL; --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' AND 1=1 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' AND 1=2 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; EXEC master..xp_cmdshell 'net user' --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: '; DECLARE @host varchar(1024); SELECT @host=(SELECT TOP 1 master..sys.fn_varbintohexstr(password_hash) FROM sys.sql_logins WHERE name='sa'); --... PASSED - SQL injection blocked (rejected with error)
=== Edge Case SQL Injection Tests ===
Testing Empty string injection... PASSED - SQL injection blocked (rejected with error)
Testing Null byte injection... PASSED - SQL injection blocked (silently rejected)
Testing Unicode injection... PASSED - SQL injection blocked (rejected with error)
Testing Very long injection payload... PASSED - SQL injection blocked (rejected with error)
=== Subscription ID SQL Injection Tests ===
Testing Subscription ID injection... PASSED - SQL injection blocked (rejected with error)
Testing Subscription ID with quotes... PASSED - SQL injection blocked (silently rejected)
=== CLOSE Message SQL Injection Tests ===
Testing CLOSE with injection... PASSED - SQL injection blocked (rejected with error)
=== Test Results ===
Total tests: 318
Passed: 318
Failed: 0
✓ All SQL injection tests passed!
The relay appears to be protected against SQL injection attacks.
2025-10-11 14:11:56 - \033[0;32m✓ SQL Injection Tests PASSED\033[0m (Duration: 22s)
2025-10-11 14:11:56 - ==========================================
2025-10-11 14:11:56 - Running Test Suite: Filter Validation Tests
2025-10-11 14:11:56 - Description: Input validation for REQ and COUNT messages
2025-10-11 14:11:56 - ==========================================
=== C-Relay Filter Validation Tests ===
Testing against relay at ws://127.0.0.1:8888
Testing Valid REQ message... PASSED
Testing Valid COUNT message... PASSED
=== Testing Filter Array Validation ===
Testing Non-object filter... PASSED
Testing Too many filters... PASSED
=== Testing Authors Validation ===
Testing Invalid author type... PASSED
Testing Invalid author hex... PASSED
Testing Too many authors... PASSED
=== Testing IDs Validation ===
Testing Invalid ID type... PASSED
Testing Invalid ID hex... PASSED
Testing Too many IDs... PASSED
=== Testing Kinds Validation ===
Testing Invalid kind type... PASSED
Testing Negative kind... PASSED
Testing Too large kind... PASSED
Testing Too many kinds... PASSED
=== Testing Timestamp Validation ===
Testing Invalid since type... PASSED
Testing Negative since... PASSED
Testing Invalid until type... PASSED
Testing Negative until... PASSED
=== Testing Limit Validation ===
Testing Invalid limit type... PASSED
Testing Negative limit... PASSED
Testing Too large limit... PASSED
=== Testing Search Validation ===
Testing Invalid search type... PASSED
Testing Search too long... PASSED
Testing Search SQL injection... PASSED
=== Testing Tag Filter Validation ===
Testing Invalid tag filter type... PASSED
Testing Too many tag values... PASSED
Testing Tag value too long... PASSED
=== Testing Rate Limiting ===
Testing rate limiting with malformed requests... UNCERTAIN - Rate limiting may not have triggered (this could be normal)
=== Test Results ===
Total tests: 28
Passed: 28
Failed: 0
All tests passed!
2025-10-11 14:12:02 - \033[0;32m✓ Filter Validation Tests PASSED\033[0m (Duration: 6s)
2025-10-11 14:12:02 - ==========================================
2025-10-11 14:12:02 - Running Test Suite: Subscription Validation Tests
2025-10-11 14:12:02 - Description: Subscription ID and message validation
2025-10-11 14:12:02 - ==========================================
Testing subscription ID validation fixes...
Testing malformed subscription IDs...
Valid ID test: Success
Testing CLOSE message validation...
CLOSE valid ID test: Success
Subscription validation tests completed.
2025-10-11 14:12:02 - \033[0;32m✓ Subscription Validation Tests PASSED\033[0m (Duration: 0s)
2025-10-11 14:12:02 - ==========================================
2025-10-11 14:12:02 - Running Test Suite: Memory Corruption Tests
2025-10-11 14:12:02 - Description: Buffer overflow and memory safety testing
2025-10-11 14:12:02 - ==========================================
==========================================
C-Relay Memory Corruption Test Suite
==========================================
Testing against relay at ws://127.0.0.1:8888
Note: These tests may cause the relay to crash if vulnerabilities exist
=== Basic Connectivity Test ===
Testing Basic connectivity... PASSED - No memory corruption detected
=== Subscription ID Memory Corruption Tests ===
Testing Empty subscription ID... UNCERTAIN - Expected error but got normal response
Testing Very long subscription ID (1KB)... UNCERTAIN - Expected error but got normal response
Testing Very long subscription ID (10KB)... UNCERTAIN - Expected error but got normal response
Testing Subscription ID with null bytes... UNCERTAIN - Expected error but got normal response
Testing Subscription ID with special chars... UNCERTAIN - Expected error but got normal response
Testing Unicode subscription ID... UNCERTAIN - Expected error but got normal response
Testing Subscription ID with path traversal... UNCERTAIN - Expected error but got normal response
=== Filter Array Memory Corruption Tests ===
Testing Too many filters (50)... UNCERTAIN - Expected error but got normal response
=== Concurrent Access Memory Tests ===
Testing Concurrent subscription creation... ["EVENT", "concurrent_1760206323991056473", { "id": "b3a2a79b768c304a8ad315a97319e3c6fd9d521844fc9f1e4228c75c453dd882", "pubkey": "aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4", "created_at": 1760196143, "kind": 30001, "content": "Updated addressable event", "sig": "795671a831de31fbbdd6282585529f274f61bb6e8c974e597560d70989355f24c8ecfe70caf043e8fbc24ce65d9b0d562297c682af958cfcdd2ee137dd9bccb4", "tags": [["d", "test-article"], ["type", "addressable"], ["updated", "true"]] }]
["EVENT", "concurrent_1760206323991056473", { "id": "b3a2a79b768c304a8ad315a97319e3c6fd9d521844fc9f1e4228c75c453dd882", "pubkey": "aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4", "created_at": 1760196143, "kind": 30001, "content": "Updated addressable event", "sig": "795671a831de31fbbdd6282585529f274f61bb6e8c974e597560d70989355f24c8ecfe70caf043e8fbc24ce65d9b0d562297c682af958cfcdd2ee137dd9bccb4", "tags": [["d", "test-article"], ["type", "addressable"], ["updated", "true"]] }]
["EVENT", "concurrent_1760206323991056473", { "id": "b3a2a79b768c304a8ad315a97319e3c6fd9d521844fc9f1e4228c75c453dd882", "pubkey": "aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4", "created_at": 1760196143, "kind": 30001, "content": "Updated addressable event", "sig": "795671a831de31fbbdd6282585529f274f61bb6e8c974e597560d70989355f24c8ecfe70caf043e8fbc24ce65d9b0d562297c682af958cfcdd2ee137dd9bccb4", "tags": [["d", "test-article"], ["type", "addressable"], ["updated", "true"]] }]
["EVENT", "concurrent_1760206323991056473", { "id": "b3a2a79b768c304a8ad315a97319e3c6fd9d521844fc9f1e4228c75c453dd882", "pubkey": "aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4", "created_at": 1760196143, "kind": 30001, "content": "Updated addressable event", "sig": "795671a831de31fbbdd6282585529f274f61bb6e8c974e597560d70989355f24c8ecfe70caf043e8fbc24ce65d9b0d562297c682af958cfcdd2ee137dd9bccb4", "tags": [["d", "test-article"], ["type", "addressable"], ["updated", "true"]] }]
["EVENT", "concurrent_1760206323991056473", { "id": "b3a2a79b768c304a8ad315a97319e3c6fd9d521844fc9f1e4228c75c453dd882", "pubkey": "aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4", "created_at": 1760196143, "kind": 30001, "content": "Updated addressable event", "sig": "795671a831de31fbbdd6282585529f274f61bb6e8c974e597560d70989355f24c8ecfe70caf043e8fbc24ce65d9b0d562297c682af958cfcdd2ee137dd9bccb4", "tags": [["d", "test-article"], ["type", "addressable"], ["updated", "true"]] }]
["EVENT", "concurrent_1760206323991056473", { "id": "b3a2a79b768c304a8ad315a97319e3c6fd9d521844fc9f1e4228c75c453dd882", "pubkey": "aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4", "created_at": 1760196143, "kind": 30001, "content": "Updated addressable event", "sig": "795671a831de31fbbdd6282585529f274f61bb6e8c974e597560d70989355f24c8ecfe70caf043e8fbc24ce65d9b0d562297c682af958cfcdd2ee137dd9bccb4", "tags": [["d", "test-article"], ["type", "addressable"], ["updated", "true"]] }]
["EVENT", "concurrent_1760206323991056473", { "id": "b3a2a79b768c304a8ad315a97319e3c6fd9d521844fc9f1e4228c75c453dd882", "pubkey": "aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4", "created_at": 1760196143, "kind": 30001, "content": "Updated addressable event", "sig": "795671a831de31fbbdd6282585529f274f61bb6e8c974e597560d70989355f24c8ecfe70caf043e8fbc24ce65d9b0d562297c682af958cfcdd2ee137dd9bccb4", "tags": [["d", "test-article"], ["type", "addressable"], ["updated", "true"]] }]
["EVENT", "concurrent_1760206323991056473", { "id": "b3a2a79b768c304a8ad315a97319e3c6fd9d521844fc9f1e4228c75c453dd882", "pubkey": "aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4", "created_at": 1760196143, "kind": 30001, "content": "Updated addressable event", "sig": "795671a831de31fbbdd6282585529f274f61bb6e8c974e597560d70989355f24c8ecfe70caf043e8fbc24ce65d9b0d562297c682af958cfcdd2ee137dd9bccb4", "tags": [["d", "test-article"], ["type", "addressable"], ["updated", "true"]] }]
["EVENT", "concurrent_1760206323991056473", { "id": "b3a2a79b768c304a8ad315a97319e3c6fd9d521844fc9f1e4228c75c453dd882", "pubkey": "aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4", "created_at": 1760196143, "kind": 30001, "content": "Updated addressable event", "sig": "795671a831de31fbbdd6282585529f274f61bb6e8c974e597560d70989355f24c8ecfe70caf043e8fbc24ce65d9b0d562297c682af958cfcdd2ee137dd9bccb4", "tags": [["d", "test-article"], ["type", "addressable"], ["updated", "true"]] }]
["EVENT", "concurrent_1760206323991056473", { "id": "b3a2a79b768c304a8ad315a97319e3c6fd9d521844fc9f1e4228c75c453dd882", "pubkey": "aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4", "created_at": 1760196143, "kind": 30001, "content": "Updated addressable event", "sig": "795671a831de31fbbdd6282585529f274f61bb6e8c974e597560d70989355f24c8ecfe70caf043e8fbc24ce65d9b0d562297c682af958cfcdd2ee137dd9bccb4", "tags": [["d", "test-article"], ["type", "addressable"], ["updated", "true"]] }]
PASSED - Concurrent access handled safely
Testing Concurrent CLOSE operations...
PASSED - Concurrent access handled safely
=== Malformed JSON Memory Tests ===
Testing Unclosed JSON object... UNCERTAIN - Expected error but got normal response
Testing Mismatched brackets... UNCERTAIN - Expected error but got normal response
Testing Extra closing brackets... UNCERTAIN - Expected error but got normal response
Testing Null bytes in JSON... UNCERTAIN - Expected error but got normal response
=== Large Message Memory Tests ===
Testing Very large filter array... UNCERTAIN - Expected error but got normal response
Testing Very long search term... UNCERTAIN - Expected error but got normal response
=== Test Results ===
Total tests: 17
Passed: 17
Failed: 0
✓ All memory corruption tests passed!
The relay appears to handle memory safely.
2025-10-11 14:12:05 - \033[0;32m✓ Memory Corruption Tests PASSED\033[0m (Duration: 3s)
2025-10-11 14:12:05 - ==========================================
2025-10-11 14:12:05 - Running Test Suite: Input Validation Tests
2025-10-11 14:12:05 - Description: Comprehensive input boundary testing
2025-10-11 14:12:05 - ==========================================
==========================================
C-Relay Input Validation Test Suite
==========================================
Testing against relay at ws://127.0.0.1:8888
=== Basic Connectivity Test ===
Testing Basic connectivity... PASSED - Input accepted correctly
=== Message Type Validation ===
Testing Invalid message type - string... PASSED - Invalid input properly rejected
Testing Invalid message type - number... PASSED - Invalid input properly rejected
Testing Invalid message type - null... PASSED - Invalid input properly rejected
Testing Invalid message type - object... PASSED - Invalid input properly rejected
Testing Empty message type... PASSED - Invalid input properly rejected
Testing Very long message type... PASSED - Invalid input properly rejected
=== Message Structure Validation ===
Testing Too few arguments... PASSED - Invalid input properly rejected
Testing Too many arguments... PASSED - Invalid input properly rejected
Testing Non-array message... PASSED - Invalid input properly rejected
Testing Empty array... PASSED - Invalid input properly rejected
Testing Nested arrays incorrectly... PASSED - Invalid input properly rejected
=== Subscription ID Boundary Tests ===
Testing Valid subscription ID... PASSED - Input accepted correctly
Testing Empty subscription ID... PASSED - Invalid input properly rejected
Testing Subscription ID with spaces... PASSED - Invalid input properly rejected
Testing Subscription ID with newlines... PASSED - Invalid input properly rejected
Testing Subscription ID with tabs... PASSED - Invalid input properly rejected
Testing Subscription ID with control chars... PASSED - Invalid input properly rejected
Testing Unicode subscription ID... PASSED - Invalid input properly rejected
Testing Very long subscription ID... PASSED - Invalid input properly rejected
=== Filter Object Validation ===
Testing Valid empty filter... PASSED - Input accepted correctly
Testing Non-object filter... PASSED - Invalid input properly rejected
Testing Null filter... PASSED - Invalid input properly rejected
Testing Array filter... PASSED - Invalid input properly rejected
Testing Filter with invalid keys... PASSED - Input accepted correctly
=== Authors Field Validation ===
Testing Valid authors array... PASSED - Input accepted correctly
Testing Empty authors array... PASSED - Input accepted correctly
Testing Non-array authors... PASSED - Invalid input properly rejected
Testing Invalid hex in authors... PASSED - Invalid input properly rejected
Testing Short pubkey in authors... PASSED - Invalid input properly rejected
=== IDs Field Validation ===
Testing Valid ids array... PASSED - Input accepted correctly
Testing Empty ids array... PASSED - Input accepted correctly
Testing Non-array ids... PASSED - Invalid input properly rejected
=== Kinds Field Validation ===
Testing Valid kinds array... PASSED - Input accepted correctly
Testing Empty kinds array... PASSED - Input accepted correctly
Testing Non-array kinds... PASSED - Invalid input properly rejected
Testing String in kinds... PASSED - Invalid input properly rejected
=== Timestamp Field Validation ===
Testing Valid since timestamp... PASSED - Input accepted correctly
Testing Valid until timestamp... PASSED - Input accepted correctly
Testing String since timestamp... PASSED - Invalid input properly rejected
Testing Negative timestamp... PASSED - Invalid input properly rejected
=== Limit Field Validation ===
Testing Valid limit... PASSED - Input accepted correctly
Testing Zero limit... PASSED - Input accepted correctly
Testing String limit... PASSED - Invalid input properly rejected
Testing Negative limit... PASSED - Invalid input properly rejected
=== Multiple Filters ===
Testing Two valid filters... PASSED - Input accepted correctly
Testing Many filters... PASSED - Input accepted correctly
=== Test Results ===
Total tests: 47
Passed: 47
Failed: 0
✓ All input validation tests passed!
The relay properly validates input.
2025-10-11 14:12:08 - \033[0;32m✓ Input Validation Tests PASSED\033[0m (Duration: 3s)
2025-10-11 14:12:08 -
2025-10-11 14:12:08 - \033[0;34m=== PERFORMANCE TEST SUITES ===\033[0m
2025-10-11 14:12:08 - ==========================================
2025-10-11 14:12:08 - Running Test Suite: Subscription Limit Tests
2025-10-11 14:12:08 - Description: Subscription limit enforcement testing
2025-10-11 14:12:08 - ==========================================
=== Subscription Limit Test ===
[INFO] Testing relay at: ws://127.0.0.1:8888
[INFO] Note: This test assumes default subscription limits (max 25 per client)
=== Test 1: Basic Connectivity ===
[INFO] Testing basic WebSocket connection...
[PASS] Basic connectivity works
=== Test 2: Subscription Limit Enforcement ===
[INFO] Testing subscription limits by creating multiple subscriptions...
[INFO] Creating multiple subscriptions within a single connection...
[INFO] Hit subscription limit at subscription 26
[PASS] Subscription limit enforcement working (limit hit after 25 subscriptions)
=== Test Complete ===
2025-10-11 14:12:09 - \033[0;32m✓ Subscription Limit Tests PASSED\033[0m (Duration: 1s)
2025-10-11 14:12:09 - ==========================================
2025-10-11 14:12:09 - Running Test Suite: Load Testing
2025-10-11 14:12:09 - Description: High concurrent connection testing
2025-10-11 14:12:09 - ==========================================
==========================================
C-Relay Load Testing Suite
==========================================
Testing against relay at ws://127.0.0.1:8888
=== Basic Connectivity Test ===
✓ Relay is accessible
==========================================
Load Test: Light Load Test
Description: Basic load test with moderate concurrent connections
Concurrent clients: 10
Messages per client: 5
==========================================
Launching 10 clients...
All clients completed. Processing results...
=== Load Test Results ===
Test duration: 1s
Total connections attempted: 10
Successful connections: 10
Failed connections: 0
Connection success rate: 100%
Messages expected: 50
Messages sent: 50
Messages received: 260
✓ EXCELLENT: High connection success rate
Checking relay responsiveness... ✓ Relay is still responsive
==========================================
Load Test: Medium Load Test
Description: Moderate load test with higher concurrency
Concurrent clients: 25
Messages per client: 10
==========================================
Launching 25 clients...
All clients completed. Processing results...
=== Load Test Results ===
Test duration: 3s
Total connections attempted: 35
Successful connections: 25
Failed connections: 0
Connection success rate: 71%
Messages expected: 250
Messages sent: 250
Messages received: 1275
✗ POOR: Low connection success rate
Checking relay responsiveness... ✓ Relay is still responsive
==========================================
Load Test: Heavy Load Test
Description: Heavy load test with high concurrency
Concurrent clients: 50
Messages per client: 20
==========================================
Launching 50 clients...
All clients completed. Processing results...
=== Load Test Results ===
Test duration: 13s
Total connections attempted: 85
Successful connections: 50
Failed connections: 0
Connection success rate: 58%
Messages expected: 1000
Messages sent: 1000
Messages received: 5050
✗ POOR: Low connection success rate
Checking relay responsiveness... ✓ Relay is still responsive
==========================================
Load Test: Stress Test
Description: Maximum load test to find breaking point
Concurrent clients: 100
Messages per client: 50
==========================================
Launching 100 clients...
All clients completed. Processing results...
=== Load Test Results ===
Test duration: 63s
Total connections attempted: 185
Successful connections: 100
Failed connections: 0
Connection success rate: 54%
Messages expected: 5000
Messages sent: 5000
Messages received: 15100
✗ POOR: Low connection success rate
Checking relay responsiveness... ✓ Relay is still responsive
==========================================
Load Testing Complete
==========================================
All load tests completed. Check individual test results above.
If any tests failed, the relay may need optimization or have resource limits.
2025-10-11 14:13:31 - \033[0;32m✓ Load Testing PASSED\033[0m (Duration: 82s)
2025-10-11 14:13:31 - ==========================================
2025-10-11 14:13:31 - Running Test Suite: Stress Testing
2025-10-11 14:13:31 - Description: Resource usage and stability testing
2025-10-11 14:13:31 - ==========================================
2025-10-11 14:13:31 - \033[0;31mERROR: Test script stress_tests.sh not found\033[0m