v2.1.10 - Fix NIP-50 PG search semantics and NIP-17 test compatibility

This commit is contained in:
Laan Tungir
2026-04-02 13:00:34 -04:00
parent 5577b57149
commit 9a9365dcfa
27 changed files with 6179 additions and 1452 deletions
+18 -6
View File
@@ -2,11 +2,13 @@
# Produces truly portable binaries with zero runtime dependencies
ARG DEBUG_BUILD=false
ARG DB_BACKEND=sqlite
FROM alpine:3.19 AS builder
# Re-declare build argument in this stage
# Re-declare build arguments in this stage
ARG DEBUG_BUILD=false
ARG DB_BACKEND=sqlite
# Install build dependencies
RUN apk add --no-cache \
@@ -26,6 +28,7 @@ RUN apk add --no-cache \
curl-static \
sqlite-dev \
sqlite-static \
postgresql-dev \
linux-headers \
wget \
bash \
@@ -101,7 +104,7 @@ COPY Makefile /build/Makefile
# Build c-relay-pg with full static linking (only rebuilds when src/ changes)
# Disable fortification to avoid __*_chk symbols that don't exist in MUSL
# Use conditional compilation flags based on DEBUG_BUILD argument
# Use conditional compilation flags based on DEBUG_BUILD and DB_BACKEND build args
RUN if [ "$DEBUG_BUILD" = "true" ]; then \
CFLAGS="-g -O2 -DDEBUG"; \
STRIP_CMD="echo 'Keeping debug symbols'"; \
@@ -111,19 +114,28 @@ RUN if [ "$DEBUG_BUILD" = "true" ]; then \
STRIP_CMD="strip /build/c_relay_pg_static"; \
echo "Building optimized production binary (symbols stripped)"; \
fi && \
gcc -static $CFLAGS -Wall -Wextra -std=c99 \
if [ "$DB_BACKEND" = "postgres" ]; then \
DB_FLAGS="-DDB_BACKEND_POSTGRES -DHAVE_LIBPQ"; \
DB_LIBS="-lpq -lpgcommon -lpgport"; \
echo "Compiling with PostgreSQL backend"; \
else \
DB_FLAGS=""; \
DB_LIBS=""; \
echo "Compiling with SQLite backend"; \
fi && \
gcc -static $CFLAGS $DB_FLAGS -Wall -Wextra -std=c99 \
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \
-I. -Ic_utils_lib/src -Inostr_core_lib -Inostr_core_lib/nostr_core \
-Inostr_core_lib/cjson -Inostr_core_lib/nostr_websocket \
-Inostr_core_lib/cjson -Inostr_core_lib/nostr_websocket -I/usr/include/postgresql \
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 src/ip_ban.c \
src/db_ops.c src/thread_pool.c \
src/db_ops.c src/db_ops_sqlite.c src/db_ops_postgres.c src/thread_pool.c \
-o /build/c_relay_pg_static \
c_utils_lib/libc_utils.a \
nostr_core_lib/libnostr_core_x64.a \
-lwebsockets -lssl -lcrypto -lsqlite3 -lsecp256k1 \
-lcurl -lz -lpthread -lm -ldl && \
-lcurl -lz -lpthread -lm -ldl $DB_LIBS && \
eval "$STRIP_CMD"
# Verify it's truly static
+22 -10
View File
@@ -5,11 +5,23 @@ CFLAGS = -Wall -Wextra -std=c99 -g -O2
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
DB_BACKEND ?= sqlite
# Build directory
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 src/ip_ban.c src/db_ops.c src/thread_pool.c
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 src/ip_ban.c src/thread_pool.c
DB_OPS_SRC = src/db_ops.c
ifeq ($(DB_BACKEND),postgres)
CFLAGS += -DDB_BACKEND_POSTGRES -DHAVE_LIBPQ
DB_OPS_SRC += src/db_ops_postgres.c
LIBS += -lpq
else
DB_OPS_SRC += src/db_ops_sqlite.c
endif
NOSTR_CORE_LIB = nostr_core_lib/libnostr_core_x64.a
C_UTILS_LIB = c_utils_lib/libc_utils.a
@@ -81,19 +93,19 @@ force-version:
@$(MAKE) src/main.h
# Build the relay
$(TARGET): $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB) $(C_UTILS_LIB)
@echo "Compiling C-Relay-PG for architecture: $(ARCH)"
$(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(TARGET) $(NOSTR_CORE_LIB) $(C_UTILS_LIB) $(LIBS)
$(TARGET): $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(DB_OPS_SRC) $(NOSTR_CORE_LIB) $(C_UTILS_LIB)
@echo "Compiling C-Relay-PG for architecture: $(ARCH) (backend: $(DB_BACKEND))"
$(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) $(DB_OPS_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) $(C_UTILS_LIB)
@echo "Building C-Relay-PG for x86_64..."
$(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) -o $(BUILD_DIR)/c_relay_pg_x86 $(NOSTR_CORE_LIB) $(C_UTILS_LIB) $(LIBS)
x86: $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(DB_OPS_SRC) $(NOSTR_CORE_LIB) $(C_UTILS_LIB)
@echo "Building C-Relay-PG for x86_64 (backend: $(DB_BACKEND))..."
$(CC) $(CFLAGS) $(INCLUDES) $(MAIN_SRC) $(DB_OPS_SRC) -o $(BUILD_DIR)/c_relay_pg_x86 $(NOSTR_CORE_LIB) $(C_UTILS_LIB) $(LIBS)
@echo "Build complete: $(BUILD_DIR)/c_relay_pg_x86"
arm64: $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(NOSTR_CORE_LIB) $(C_UTILS_LIB)
@echo "Cross-compiling C-Relay-PG for ARM64..."
arm64: $(BUILD_DIR) src/main.h src/sql_schema.h $(MAIN_SRC) $(DB_OPS_SRC) $(NOSTR_CORE_LIB) $(C_UTILS_LIB)
@echo "Cross-compiling C-Relay-PG for ARM64 (backend: $(DB_BACKEND))..."
@if ! command -v aarch64-linux-gnu-gcc >/dev/null 2>&1; then \
echo "ERROR: ARM64 cross-compiler not found."; \
echo "Install with: make install-cross-tools"; \
@@ -116,7 +128,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_pg_arm64 $(NOSTR_CORE_LIB) $(C_UTILS_LIB) \
aarch64-linux-gnu-gcc $(CFLAGS) $(INCLUDES) $(MAIN_SRC) $(DB_OPS_SRC) -o $(BUILD_DIR)/c_relay_pg_arm64 $(NOSTR_CORE_LIB) $(C_UTILS_LIB) \
-L/usr/lib/aarch64-linux-gnu $(LIBS)
@echo "Build complete: $(BUILD_DIR)/c_relay_pg_arm64"
+38 -2
View File
@@ -11,8 +11,40 @@ DOCKERFILE="$SCRIPT_DIR/Dockerfile.alpine-musl"
# Parse command line arguments
DEBUG_BUILD=false
if [[ "$1" == "--debug" ]]; then
DEBUG_BUILD=true
DB_BACKEND="${DB_BACKEND:-postgres}"
while [[ $# -gt 0 ]]; do
case "$1" in
--debug)
DEBUG_BUILD=true
shift
;;
--db-backend)
if [[ -z "$2" ]]; then
echo "ERROR: --db-backend requires a value (sqlite|postgres)"
exit 1
fi
DB_BACKEND="$2"
shift 2
;;
--db-backend=*)
DB_BACKEND="${1#*=}"
shift
;;
*)
echo "ERROR: Unknown argument: $1"
echo "Usage: $0 [--debug] [--db-backend postgres|sqlite]"
exit 1
;;
esac
done
if [[ "$DB_BACKEND" != "sqlite" && "$DB_BACKEND" != "postgres" ]]; then
echo "ERROR: Invalid DB backend '$DB_BACKEND'. Use sqlite or postgres."
exit 1
fi
if [[ "$DEBUG_BUILD" == "true" ]]; then
echo "=========================================="
echo "C-Relay-PG MUSL Static Binary Builder (DEBUG MODE)"
echo "=========================================="
@@ -24,6 +56,7 @@ fi
echo "Project directory: $SCRIPT_DIR"
echo "Build directory: $BUILD_DIR"
echo "Debug build: $DEBUG_BUILD"
echo "DB backend: $DB_BACKEND"
echo ""
# Create build directory
@@ -122,6 +155,7 @@ echo ""
$DOCKER_CMD build \
--platform "$PLATFORM" \
--build-arg DEBUG_BUILD=$DEBUG_BUILD \
--build-arg DB_BACKEND=$DB_BACKEND \
-f "$DOCKERFILE" \
-t c-relay-pg-musl-builder:latest \
--progress=plain \
@@ -145,6 +179,7 @@ echo "=========================================="
$DOCKER_CMD build \
--platform "$PLATFORM" \
--build-arg DEBUG_BUILD=$DEBUG_BUILD \
--build-arg DB_BACKEND=$DB_BACKEND \
--target builder \
-f "$DOCKERFILE" \
-t c-relay-pg-static-builder-stage:latest \
@@ -224,6 +259,7 @@ if [ "$DEBUG_BUILD" = true ]; then
else
echo "Build Type: PRODUCTION (optimized, stripped)"
fi
echo "DB Backend: $DB_BACKEND"
if [ "$TRULY_STATIC" = true ]; then
echo "Linkage: Fully static binary (Alpine MUSL-based)"
echo "Portability: Works on ANY Linux distribution"
+222 -4
View File
@@ -13,6 +13,13 @@ ADMIN_KEY=""
RELAY_KEY=""
PORT_OVERRIDE=""
DEBUG_LEVEL="5"
DB_BACKEND="postgres"
DB_CONNSTRING=""
DB_HOST=""
DB_PORT=""
DB_NAME=""
DB_USER=""
DB_PASSWORD=""
# Key validation function
validate_hex_key() {
@@ -118,6 +125,84 @@ while [[ $# -gt 0 ]]; do
shift 2
fi
;;
--db-backend)
if [ -z "$2" ]; then
echo "ERROR: --db-backend requires a value (sqlite|postgres)"
HELP=true
shift
else
DB_BACKEND="$2"
shift 2
fi
;;
--db-backend=*)
DB_BACKEND="${1#*=}"
shift
;;
--db-connstring)
if [ -z "$2" ]; then
echo "ERROR: --db-connstring requires a value"
HELP=true
shift
else
DB_CONNSTRING="$2"
shift 2
fi
;;
--db-connstring=*)
DB_CONNSTRING="${1#*=}"
shift
;;
--db-host)
if [ -z "$2" ]; then
echo "ERROR: --db-host requires a value"
HELP=true
shift
else
DB_HOST="$2"
shift 2
fi
;;
--db-port)
if [ -z "$2" ]; then
echo "ERROR: --db-port requires a value"
HELP=true
shift
else
DB_PORT="$2"
shift 2
fi
;;
--db-name)
if [ -z "$2" ]; then
echo "ERROR: --db-name requires a value"
HELP=true
shift
else
DB_NAME="$2"
shift 2
fi
;;
--db-user)
if [ -z "$2" ]; then
echo "ERROR: --db-user requires a value"
HELP=true
shift
else
DB_USER="$2"
shift 2
fi
;;
--db-password)
if [ -z "$2" ]; then
echo "ERROR: --db-password requires a value"
HELP=true
shift
else
DB_PASSWORD="$2"
shift 2
fi
;;
--help|-h)
HELP=true
shift
@@ -164,6 +249,98 @@ if [ -n "$DEBUG_LEVEL" ]; then
fi
fi
# Validate DB backend
if [ "$DB_BACKEND" != "sqlite" ] && [ "$DB_BACKEND" != "postgres" ]; then
echo "ERROR: Invalid --db-backend value '$DB_BACKEND'. Use sqlite or postgres."
exit 1
fi
# Validate DB port if provided
if [ -n "$DB_PORT" ]; then
if ! [[ "$DB_PORT" =~ ^[0-9]+$ ]] || [ "$DB_PORT" -lt 1 ] || [ "$DB_PORT" -gt 65535 ]; then
echo "ERROR: --db-port must be a number between 1 and 65535"
exit 1
fi
fi
if [ "$DB_BACKEND" = "postgres" ] && [ -z "$DB_CONNSTRING" ]; then
[ -z "$DB_HOST" ] && DB_HOST="localhost"
[ -z "$DB_PORT" ] && DB_PORT="5432"
[ -z "$DB_NAME" ] && DB_NAME="crelay"
[ -z "$DB_USER" ] && DB_USER="crelay"
[ -z "$DB_PASSWORD" ] && DB_PASSWORD="crelay"
fi
ensure_postgres_database() {
if ! command -v psql >/dev/null 2>&1; then
echo "ERROR: psql not found. Install PostgreSQL client tools to continue."
return 1
fi
echo "Ensuring PostgreSQL database exists: host=$DB_HOST port=$DB_PORT dbname=$DB_NAME user=$DB_USER"
if [ -n "$DB_PASSWORD" ]; then
export PGPASSWORD="$DB_PASSWORD"
fi
# Prefer local postgres superuser for provisioning when available.
if sudo -n -u postgres psql -d postgres -tAc "SELECT 1" >/dev/null 2>&1; then
sudo -u postgres psql -d postgres -tAc "SELECT 1 FROM pg_roles WHERE rolname = '$DB_USER'" | grep -q 1 || \
sudo -u postgres psql -d postgres -v ON_ERROR_STOP=1 -c "CREATE ROLE \"$DB_USER\" LOGIN PASSWORD '$DB_PASSWORD';" || return 1
sudo -u postgres psql -d postgres -v ON_ERROR_STOP=1 -c "ALTER ROLE \"$DB_USER\" WITH LOGIN PASSWORD '$DB_PASSWORD';" >/dev/null || return 1
if [ "$PRESERVE_DATABASE" = false ]; then
echo "Resetting PostgreSQL database '$DB_NAME' for fresh start..."
# Ensure no active sessions block DROP DATABASE (common when relay is still running).
sudo -u postgres psql -d postgres -v ON_ERROR_STOP=1 -c \
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '$DB_NAME' AND pid <> pg_backend_pid();" >/dev/null || return 1
# Prefer force drop when supported; fallback to regular dropdb.
if ! sudo -u postgres dropdb --if-exists --force "$DB_NAME" >/dev/null 2>&1; then
sudo -u postgres dropdb --if-exists "$DB_NAME" >/dev/null 2>&1 || return 1
fi
sudo -u postgres createdb -O "$DB_USER" "$DB_NAME" >/dev/null 2>&1 || return 1
echo "✓ Recreated PostgreSQL database '$DB_NAME' owned by '$DB_USER'"
else
DB_EXISTS=$(sudo -u postgres psql -d postgres -tAc "SELECT 1 FROM pg_database WHERE datname = '$DB_NAME'" 2>/dev/null | tr -d '[:space:]')
if [ "$DB_EXISTS" != "1" ]; then
sudo -u postgres createdb -O "$DB_USER" "$DB_NAME" >/dev/null 2>&1 || return 1
echo "✓ Created PostgreSQL database '$DB_NAME'"
else
echo "✓ PostgreSQL database '$DB_NAME' already exists"
fi
fi
sudo -u postgres psql -d postgres -v ON_ERROR_STOP=1 -c "GRANT ALL PRIVILEGES ON DATABASE \"$DB_NAME\" TO \"$DB_USER\";" >/dev/null || return 1
sudo -u postgres psql -d "$DB_NAME" -v ON_ERROR_STOP=1 -c "ALTER SCHEMA public OWNER TO \"$DB_USER\"; GRANT ALL ON SCHEMA public TO \"$DB_USER\";" >/dev/null || return 1
return 0
fi
# Fallback: use configured DB user directly.
if ! psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres -v ON_ERROR_STOP=1 -tAc "SELECT 1" >/dev/null 2>&1; then
echo "ERROR: Cannot connect to PostgreSQL server/database 'postgres' with current settings"
echo " host=$DB_HOST port=$DB_PORT user=$DB_USER"
return 1
fi
DB_EXISTS=$(psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres -tAc "SELECT 1 FROM pg_database WHERE datname = '$DB_NAME'" 2>/dev/null | tr -d '[:space:]')
if [ "$DB_EXISTS" != "1" ]; then
echo "Database '$DB_NAME' not found. Creating..."
if ! createdb -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" "$DB_NAME" >/dev/null 2>&1; then
echo "ERROR: Failed to create PostgreSQL database '$DB_NAME'"
return 1
fi
echo "✓ Created PostgreSQL database '$DB_NAME'"
else
echo "✓ PostgreSQL database '$DB_NAME' already exists"
fi
return 0
}
# Show help
if [ "$HELP" = true ]; then
echo "Usage: $0 [OPTIONS]"
@@ -175,6 +352,13 @@ if [ "$HELP" = true ]; then
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 " --db-backend <name> Database backend: postgres (default) or sqlite"
echo " --db-connstring <str> PostgreSQL libpq connection string"
echo " --db-host <host> PostgreSQL host"
echo " --db-port <port> PostgreSQL port"
echo " --db-name <name> PostgreSQL database name"
echo " --db-user <user> PostgreSQL database user"
echo " --db-password <pass> PostgreSQL database password"
echo " --help, -h Show this help message"
echo ""
echo "Event-Based Configuration:"
@@ -194,6 +378,9 @@ if [ "$HELP" = true ]; then
echo " $0 --test-keys # Use test keys for consistent development"
echo " $0 -t --preserve-database # Use test keys and preserve database"
echo ""
echo "Default PostgreSQL connection (when no DB flags provided):"
echo " host=localhost port=5432 dbname=crelay user=crelay password=crelay"
echo ""
echo "Key Format: Keys must be exactly 64 hexadecimal characters (0-9, a-f, A-F)"
echo "Default behavior: Deletes existing database files to start fresh with new keys"
echo " for development purposes"
@@ -217,13 +404,17 @@ fi
rm -rf dev-config/ 2>/dev/null
rm -f db/c_nostr_relay.db* 2>/dev/null
if [ "$DB_BACKEND" = "postgres" ] && [ -z "$DB_CONNSTRING" ]; then
ensure_postgres_database || exit 1
fi
# Embed web files into C headers before building
echo "Embedding web files..."
./embed_web_files.sh
# Build the project - ONLY static build
echo "Building project (static binary with SQLite JSON1 extension)..."
./build_static.sh
echo "Building project (static binary, backend: $DB_BACKEND)..."
./build_static.sh --db-backend "$DB_BACKEND"
# Exit if static build fails - no fallback
if [ $? -ne 0 ]; then
@@ -350,15 +541,42 @@ if [ -n "$DEBUG_LEVEL" ]; then
echo "Using debug level: $DEBUG_LEVEL"
fi
if [ -n "$DB_CONNSTRING" ]; then
RELAY_ARGS="$RELAY_ARGS --db-connstring '$DB_CONNSTRING'"
echo "Using PostgreSQL connection string override"
fi
if [ -n "$DB_HOST" ]; then
RELAY_ARGS="$RELAY_ARGS --db-host '$DB_HOST'"
fi
if [ -n "$DB_PORT" ]; then
RELAY_ARGS="$RELAY_ARGS --db-port '$DB_PORT'"
fi
if [ -n "$DB_NAME" ]; then
RELAY_ARGS="$RELAY_ARGS --db-name '$DB_NAME'"
fi
if [ -n "$DB_USER" ]; then
RELAY_ARGS="$RELAY_ARGS --db-user '$DB_USER'"
fi
if [ -n "$DB_PASSWORD" ]; then
RELAY_ARGS="$RELAY_ARGS --db-password '$DB_PASSWORD'"
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 test keys from .test_keys file..."
./$(basename $BINARY_PATH) -a "$ADMIN_KEY" -r "$RELAY_KEY" --debug-level=$DEBUG_LEVEL --strict-port > ../relay.log 2>&1 &
# shellcheck disable=SC2086
eval ./$(basename $BINARY_PATH) -a "$ADMIN_KEY" -r "$RELAY_KEY" $RELAY_ARGS --strict-port > ../relay.log 2>&1 &
elif [ -n "$RELAY_ARGS" ]; then
echo "Starting relay with custom configuration..."
./$(basename $BINARY_PATH) $RELAY_ARGS --debug-level=$DEBUG_LEVEL --strict-port > ../relay.log 2>&1 &
# shellcheck disable=SC2086
eval ./$(basename $BINARY_PATH) $RELAY_ARGS --strict-port > ../relay.log 2>&1 &
else
# No command line arguments needed for random key generation
echo "Starting relay with random key generation..."
Submodule nostr-rs-relay deleted from 64cfcaf44a
+410
View File
@@ -0,0 +1,410 @@
# C-Relay-PG: PostgreSQL Implementation Plan
## Current State Assessment
### What We Have
- **PostgreSQL 18.3** installed and running on localhost:5432
- **libpq development headers** available via pkg-config (`/usr/include/postgresql`, `/usr/lib/x86_64-linux-gnu`)
- **`db_ops.h`** abstraction layer with 40+ functions already defined
- **`db_ops.c`** — single 1,398-line file implementing all functions against SQLite
- **`ip_ban.c`** — already uses `db_ops` API (not raw SQLite) for persistence
- **`config.c`** — uses `db_ops` API for all database access
### SQLite Coupling Points (Must Fix)
| Location | Issue |
|----------|-------|
| `src/main.c:11` | `#include <sqlite3.h>` |
| `src/main.c:51` | `sqlite3* g_db = NULL;` — global handle owned by main.c |
| `src/main.c:897` | `if (g_db)` — direct handle check |
| `src/main.c:2228` | `if (!g_db)` — direct handle check |
| `src/main.c:2358` | `g_db = NULL;` — strict mode nullification |
| `src/main.c:743` | Log message references `sqlite3_open()` |
| `src/main.c:854-885` | SQLite PRAGMAs (WAL, mmap_size, cache_size) |
| `src/main.c:795-824` | SQLite-specific DDL in migration code |
| `src/main.c:900` | `PRAGMA wal_checkpoint(TRUNCATE)` |
| `src/config.c:4727` | `sqlite_master` query |
| `src/config.c:4388` | `INSERT OR REPLACE` SQL |
| `src/db_ops.c:13` | `extern sqlite3* g_db;` |
| `src/db_ops.c:871` | `strftime('%s', 'now')` — SQLite-specific |
| `src/db_ops.c:889,909,956` | `INSERT OR REPLACE` — SQLite-specific |
| `src/db_ops.c:1341` | `sqlite_master` — SQLite-specific |
| `src/db_ops.c:1387-1395` | `sqlite3_wal_checkpoint_v2()` — SQLite-only |
| `src/ip_ban.c:122,205` | `strftime('%s', 'now')` and `INSERT OR REPLACE` in SQL strings |
| `src/sql_schema.h` | Entire embedded schema is SQLite DDL |
| `src/subscriptions.c:1187-1197` | Commented-out raw SQLite code |
### What Already Works Through `db_ops`
- `ip_ban.c` — uses `db_exec_sql()`, `db_prepare()`, `db_step_stmt()`, etc.
- `config.c` — uses `db_get_config_value_dup()`, `db_set_config_value_full()`, etc.
- `subscriptions.c` — uses `db_log_subscription_*()` functions
- `api.c` — uses `db_execute_readonly_query_json()`, stat helpers
- `nip009.c` — uses `db_delete_event_by_id()`, `db_get_event_pubkey()`
---
## Implementation Phases
### Phase 0: PostgreSQL Setup & Smoke Test
Create the PostgreSQL role, database, and verify connectivity from C.
```
Tasks:
0.1 Create PostgreSQL role "crelay" with password
0.2 Create database "crelay" owned by role "crelay"
0.3 Write a minimal C test program that connects via libpq and runs SELECT 1
0.4 Verify the test compiles and links with: gcc test_pg.c -lpq -o test_pg
```
### Phase 1: Internalize g_db — Move Database Ownership Into db_ops
**Goal**: Eliminate `extern sqlite3* g_db` from main.c. The `db_ops` module must own its connection handle internally. This is the critical prerequisite — without it, swapping backends is impossible.
```mermaid
flowchart LR
subgraph Before
MAIN[main.c owns sqlite3* g_db] --> DBOPS[db_ops.c uses extern g_db]
end
subgraph After
MAIN2[main.c calls db_init/db_close] --> DBOPS2[db_ops.c owns static g_db internally]
end
```
```
Tasks:
1.1 Move `sqlite3* g_db` from main.c into db_ops.c as a static variable
1.2 Move `char g_database_path[512]` from config.c into db_ops.c as static
1.3 Remove `#include <sqlite3.h>` from main.c
1.4 Replace all `g_db` references in main.c with db_ops API calls:
- `if (g_db)` → `if (db_is_available())`
- `g_db = NULL` → `db_close()` (or new `db_detach_main()`)
1.5 Replace SQLite PRAGMA calls in main.c init_database() with new db_ops functions:
- `db_set_journal_mode_wal()`
- `db_set_mmap_size(long size)`
- `db_set_cache_size(int kb)`
1.6 Move schema migration logic from main.c into db_ops (new `db_apply_schema()`)
1.7 Replace `sqlite_master` query in config.c:4727 with `db_table_exists()`
1.8 Replace `INSERT OR REPLACE` in config.c:4388 with `db_prepare()`/`db_step_stmt()`
1.9 Build and run existing test suite to confirm no regressions
```
### Phase 2: Rename db_ops.c → db_ops_sqlite.c, Create Dispatcher
**Goal**: Split the single `db_ops.c` into a backend-specific file and a thin dispatcher, preparing the architecture for a second backend.
```
New file layout:
src/db_ops.h — unchanged public API
src/db_ops_sqlite.c — renamed from db_ops.c (all SQLite implementation)
src/db_ops_postgres.c — new file (stub initially)
src/db_ops.c — thin dispatcher that forwards to active backend
```
```
Tasks:
2.1 Rename src/db_ops.c → src/db_ops_sqlite.c
2.2 Prefix all function names in db_ops_sqlite.c with `sqlite_` (e.g., `sqlite_db_init()`)
2.3 Create src/db_ops.c dispatcher with compile-time backend selection:
- #ifdef DB_BACKEND_POSTGRES → call postgres_* functions
- #else → call sqlite_* functions (default)
2.4 Create src/db_ops_postgres.c with stub implementations that return DB_ERROR
2.5 Update Makefile with DB_BACKEND variable:
- `DB_BACKEND ?= sqlite`
- Conditional source file inclusion and -lpq linking
2.6 Build with DB_BACKEND=sqlite — verify identical behavior
2.7 Build with DB_BACKEND=postgres — verify it compiles (stubs return errors)
```
### Phase 3: PostgreSQL Schema
**Goal**: Create the PostgreSQL schema file and apply it to the database.
```
Tasks:
3.1 Create src/pg_schema.sql with PostgreSQL DDL from the plan:
- events table with JSONB tags column
- GIN index on tags
- config, auth_rules, relay_seckey, subscriptions, ip_bans tables
- Materialized views for dashboard
3.2 Create src/pg_schema.h — embedded schema as C string (like sql_schema.h)
3.3 Add `postgres_db_apply_schema()` function in db_ops_postgres.c
3.4 Apply schema to the crelay database and verify with psql
```
### Phase 4: Implement db_ops_postgres.c — Core Functions
**Goal**: Implement the PostgreSQL backend for all `db_ops.h` functions using libpq.
This is the largest phase. Functions are grouped by dependency order.
#### 4A: Connection Management
```
Tasks:
4A.1 Implement postgres_db_init() — PQconnectdb() with connection string
4A.2 Implement postgres_db_close() — PQfinish()
4A.3 Implement postgres_db_is_available() — PQstatus() check
4A.4 Implement postgres_db_last_error() — PQerrorMessage()
4A.5 Implement postgres_db_get_database_path() — return connection string
4A.6 Implement thread-local connection pool:
- postgres_db_set_thread_connection()
- postgres_db_clear_thread_connection()
- postgres_db_open_worker_connection() — new PQconnectdb per worker
- postgres_db_close_worker_connection() — PQfinish per worker
```
#### 4B: Statement API (db_stmt_t wrapper for PGresult)
```
Tasks:
4B.1 Define postgres db_stmt_t struct wrapping PGresult + cursor state
4B.2 Implement postgres_db_prepare() — PQprepare() with auto-generated name
4B.3 Implement postgres_db_bind_text_param/int_param/int64_param
Note: libpq uses $1,$2 params, not ?. Need parameter array approach.
4B.4 Implement postgres_db_step_stmt() — PQexecPrepared() + row iteration
4B.5 Implement postgres_db_column_text_value/int_value/int64_value/double_value
4B.6 Implement postgres_db_reset_stmt() and postgres_db_finalize_stmt()
4B.7 Implement postgres_db_exec_sql() — PQexec() for DDL/DML
```
**Key design decision**: libpq doesn't have a step-based cursor like SQLite. Two approaches:
1. **PQexecPrepared** returns all rows at once — iterate with row index
2. **DECLARE CURSOR / FETCH** for streaming large result sets
For this relay's workload (most queries return <1000 rows), approach 1 is simpler and sufficient.
#### 4C: Config & Key Storage
```
Tasks:
4C.1 Implement postgres_db_get_all_config_rows()
4C.2 Implement postgres_db_get_config_value_dup()
4C.3 Implement postgres_db_set_config_value_full()
— Use INSERT ... ON CONFLICT (key) DO UPDATE instead of INSERT OR REPLACE
4C.4 Implement postgres_db_update_config_value_only()
— Use EXTRACT(EPOCH FROM NOW()) instead of strftime('%s','now')
4C.5 Implement postgres_db_upsert_config_value()
4C.6 Implement postgres_db_store_relay_private_key_hex()
4C.7 Implement postgres_db_get_relay_private_key_hex_dup()
4C.8 Implement postgres_db_store_config_event()
4C.9 Implement postgres_db_get_config_row_count()
```
#### 4D: Event Storage & Retrieval
```
Tasks:
4D.1 Implement postgres_db_insert_event_with_json()
— INSERT ... ON CONFLICT (id) DO NOTHING
— Map extended_errcode to DB_CONSTRAINT for duplicates
4D.2 Implement postgres_db_event_id_exists()
4D.3 Implement postgres_db_retrieve_event_by_id()
4D.4 Implement postgres_db_get_event_pubkey()
4D.5 Implement postgres_db_get_event_time_bounds()
4D.6 Implement postgres_db_get_latest_event_pubkey_for_kind_dup()
```
#### 4E: Event Deletion (NIP-09)
```
Tasks:
4E.1 Implement postgres_db_delete_event_by_id()
4E.2 Implement postgres_db_delete_events_by_address()
```
#### 4F: Tag Operations
```
Tasks:
4F.1 Implement postgres_db_store_event_tags_cjson()
— Note: With JSONB+GIN, this may become a no-op for PostgreSQL
— Tags are already stored in the events.tags JSONB column
4F.2 Implement postgres_db_populate_event_tags_from_existing()
— No-op for PostgreSQL (no separate event_tags table needed)
```
#### 4G: Auth Rules
```
Tasks:
4G.1 Implement postgres_db_is_pubkey_blacklisted()
4G.2 Implement postgres_db_is_hash_blacklisted()
4G.3 Implement postgres_db_is_pubkey_whitelisted()
4G.4 Implement postgres_db_count_active_whitelist_rules()
4G.5 Implement postgres_db_add_auth_rule()
4G.6 Implement postgres_db_remove_auth_rule()
4G.7 Implement postgres_db_delete_wot_whitelist_rules()
4G.8 Implement postgres_db_count_wot_whitelist_rules()
```
#### 4H: Subscription Logging
```
Tasks:
4H.1 Implement postgres_db_log_subscription_created()
— Use INSERT ... ON CONFLICT DO UPDATE
4H.2 Implement postgres_db_log_subscription_closed()
— Use EXTRACT(EPOCH FROM NOW()) for timestamps
4H.3 Implement postgres_db_log_subscription_disconnected()
4H.4 Implement postgres_db_update_subscription_events_sent()
4H.5 Implement postgres_db_cleanup_orphaned_subscriptions()
```
#### 4I: Monitoring & Stats
```
Tasks:
4I.1 Implement postgres_db_get_total_event_count_ll()
4I.2 Implement postgres_db_get_event_count_since()
4I.3 Implement postgres_db_get_event_kind_distribution_rows()
4I.4 Implement postgres_db_get_top_pubkeys_rows()
4I.5 Implement postgres_db_get_subscription_details_rows()
4I.6 Implement postgres_db_count_with_sql()
4I.7 Implement postgres_db_execute_readonly_query_json()
— Map PG column types instead of SQLITE_INTEGER/SQLITE_TEXT/etc.
```
#### 4J: Schema & DDL Helpers
```
Tasks:
4J.1 Implement postgres_db_table_exists()
— Use information_schema.tables instead of sqlite_master
4J.2 Implement postgres_db_get_schema_version_dup()
4J.3 Implement postgres_db_wal_checkpoint_passive() — no-op for PostgreSQL
4J.4 Implement postgres_db_wal_checkpoint_truncate() — no-op for PostgreSQL
```
### Phase 5: Fix SQLite-Specific SQL in Callers
**Goal**: Ensure SQL strings passed through `db_prepare()` / `db_exec_sql()` from outside `db_ops` are portable.
```
Tasks:
5.1 Audit ip_ban.c SQL strings:
— Replace `strftime('%s', 'now')` with `EXTRACT(EPOCH FROM NOW())::BIGINT`
or pass timestamp as bind parameter from C (time(NULL))
— Replace `INSERT OR REPLACE` with `INSERT ... ON CONFLICT DO UPDATE`
5.2 Audit config.c SQL strings:
— Replace `sqlite_master` query with db_table_exists()
— Replace `INSERT OR REPLACE` with ON CONFLICT syntax
5.3 Audit main.c init_database():
— Move all PRAGMA calls behind db_ops backend-specific init
— Move schema migration behind db_apply_schema()
5.4 Strategy decision: use C-side timestamps (time(NULL)) as bind params
instead of database-side timestamp functions for portability
```
### Phase 6: CLI Connection String Support
**Goal**: Accept PostgreSQL connection parameters from the command line.
```
Tasks:
6.1 Add --db-host, --db-port, --db-name, --db-user, --db-password CLI options
6.2 Add --db-connstring option for full libpq connection string
6.3 Construct connection string from individual params if --db-connstring not given
6.4 Pass connection string to db_init() (works for both backends)
6.5 Update make_and_restart_relay.sh to support PostgreSQL connection params
6.6 Update AGENTS.md with new CLI options
```
### Phase 7: Integration Testing
**Goal**: Verify the PostgreSQL backend passes all existing tests.
```
Tasks:
7.1 Build with DB_BACKEND=postgres
7.2 Start relay with PostgreSQL connection string
7.3 Run tests/run_nip_tests.sh — verify NIP compliance
7.4 Run tests/run_all_tests.sh — verify full test suite
7.5 Run tests/performance_benchmarks.sh — compare with SQLite baseline
7.6 Test first-time startup flow (key generation, schema creation)
7.7 Test config event storage and retrieval
7.8 Test auth rules (whitelist/blacklist)
7.9 Test IP ban persistence across restarts
7.10 Test thread pool worker connections with PostgreSQL
```
### Phase 8: Query Builder — JSONB Tag Queries
**Goal**: Replace the `event_tags` JOIN queries with PostgreSQL JSONB containment queries for tag filtering in REQ handling.
```
Tasks:
8.1 Identify where REQ filter → SQL translation happens (websockets.c / subscriptions.c)
8.2 Create backend-aware query builder or use #ifdef for tag filter SQL:
— SQLite: `id IN (SELECT event_id FROM event_tags WHERE tag_name=? AND tag_value IN (?))`
— PostgreSQL: `tags @> '[["p", "value"]]'::jsonb`
8.3 Test tag-filtered REQ queries against PostgreSQL
8.4 Verify GIN index is being used (EXPLAIN ANALYZE)
```
---
## SQL Dialect Mapping Reference
| SQLite | PostgreSQL |
|--------|-----------|
| `INSERT OR REPLACE INTO t ...` | `INSERT INTO t ... ON CONFLICT (pk) DO UPDATE SET ...` |
| `strftime('%s', 'now')` | `EXTRACT(EPOCH FROM NOW())::BIGINT` |
| `SELECT 1 FROM sqlite_master WHERE type='table' AND name=?` | `SELECT 1 FROM information_schema.tables WHERE table_name=?` |
| `PRAGMA journal_mode=WAL` | N/A (PostgreSQL handles WAL internally) |
| `PRAGMA mmap_size=N` | N/A |
| `PRAGMA cache_size=-N` | `SET shared_buffers` (server-level, not per-connection) |
| `sqlite3_wal_checkpoint_v2()` | N/A (no-op) |
| `sqlite3_changes()` | `PQcmdTuples(result)` |
| `sqlite3_extended_errcode()` | `PQresultErrorField(result, PG_DIAG_SQLSTATE)` |
| `?` bind parameter | `$1`, `$2`, ... positional parameters |
| `JSON` column type | `JSONB` column type |
| `json_each()` for tag queries | `@>` containment operator with GIN index |
| `INTEGER PRIMARY KEY` auto-increment | `SERIAL` or `BIGSERIAL` |
---
## Architecture After Implementation
```mermaid
flowchart TD
subgraph Application Layer
MAIN[main.c] --> DBOPS_H[db_ops.h - public API]
CONFIG[config.c] --> DBOPS_H
IPBAN[ip_ban.c] --> DBOPS_H
SUBS[subscriptions.c] --> DBOPS_H
API[api.c] --> DBOPS_H
NIP09[nip009.c] --> DBOPS_H
WS[websockets.c] --> DBOPS_H
end
subgraph Database Abstraction
DBOPS_H --> DISPATCH[db_ops.c - dispatcher]
DISPATCH -->|DB_BACKEND_SQLITE| SQLITE[db_ops_sqlite.c]
DISPATCH -->|DB_BACKEND_POSTGRES| PG[db_ops_postgres.c]
end
subgraph Backends
SQLITE --> SQLITE3[libsqlite3]
PG --> LIBPQ[libpq]
LIBPQ --> PGSERVER[PostgreSQL Server]
end
```
---
## Risk Mitigation
| Risk | Mitigation |
|------|-----------|
| Breaking SQLite backend during refactor | Phase 1-2 are pure refactors — test suite must pass after each |
| libpq statement API mismatch | Use PQexecParams with row-index iteration, not cursor-based |
| SQL dialect differences in caller code | Phase 5 audits all SQL outside db_ops; use C timestamps for portability |
| Connection failures | Implement reconnection with exponential backoff in postgres_db_init |
| Thread safety | Each worker thread gets its own PGconn via db_open_worker_connection |
| Performance regression | Phase 7 benchmarks compare SQLite vs PostgreSQL on same workload |
---
## Build Commands
```bash
# SQLite backend (default, unchanged behavior)
make
# PostgreSQL backend
make DB_BACKEND=postgres
# Run with PostgreSQL
./build/c_relay_pg_x86 --db-connstring "host=localhost dbname=crelay user=crelay password=secret"
```
+1 -1
View File
@@ -1 +1 @@
804819
1048978
+21 -26
View File
@@ -1986,7 +1986,7 @@ int process_admin_config_event(cJSON* event, char* error_message, size_t error_s
}
// Begin transaction for atomic config updates
if (db_exec_sql("BEGIN IMMEDIATE TRANSACTION") != 0) {
if (db_exec_sql("BEGIN") != 0) {
snprintf(error_message, error_size, "failed to begin config transaction");
return -1;
}
@@ -3636,7 +3636,7 @@ int handle_auth_rule_modification_unified(cJSON* event, char* error_message, siz
}
// Begin transaction for atomic auth rule updates
if (db_exec_sql("BEGIN IMMEDIATE TRANSACTION") != 0) {
if (db_exec_sql("BEGIN") != 0) {
snprintf(error_message, error_size, "failed to begin auth rule transaction");
return -1;
}
@@ -3972,7 +3972,7 @@ int handle_config_update_unified(cJSON* event, char* error_message, size_t error
}
// Begin transaction for atomic config updates
int rc = db_exec_sql("BEGIN IMMEDIATE TRANSACTION");
int rc = db_exec_sql("BEGIN");
if (rc != DB_OK) {
cJSON_Delete(config_objects_array);
snprintf(error_message, error_size, "failed to begin config update transaction");
@@ -4311,7 +4311,7 @@ int apply_cli_overrides_atomic(const cli_options_t* cli_options) {
DEBUG_INFO("Applying CLI overrides atomically");
// Begin transaction
int rc = db_exec_sql("BEGIN IMMEDIATE TRANSACTION");
int rc = db_exec_sql("BEGIN");
if (rc != 0) {
DEBUG_ERROR("Failed to begin CLI overrides transaction: %s", db_last_error());
return -1;
@@ -4383,9 +4383,16 @@ int populate_all_config_values_atomic(const char* admin_pubkey, const char* rela
return -1;
}
// Prepare INSERT OR REPLACE statement with all required fields
// Prepare UPSERT statement with all required fields (SQLite + PostgreSQL compatible)
db_stmt_t* stmt = NULL;
const char* sql = "INSERT OR REPLACE INTO config (key, value, data_type, description, category, requires_restart) VALUES (?, ?, ?, ?, ?, ?)";
const char* sql =
"INSERT INTO config (key, value, data_type, description, category, requires_restart) VALUES (?, ?, ?, ?, ?, ?) "
"ON CONFLICT(key) DO UPDATE SET "
"value = excluded.value, "
"data_type = excluded.data_type, "
"description = excluded.description, "
"category = excluded.category, "
"requires_restart = excluded.requires_restart";
rc = db_prepare(sql, &stmt);
if (rc != DB_OK) {
DEBUG_ERROR("Failed to prepare statement: %s", db_last_error());
@@ -4723,38 +4730,26 @@ const char* get_config_value_hybrid(const char* key) {
// Check if config table is ready
int is_config_table_ready(void) {
if (!db_is_available()) return 0;
const char* sql = "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='config'";
db_stmt_t* stmt;
int rc = db_prepare(sql, &stmt);
if (rc != DB_OK) {
return 0;
}
int table_exists = 0;
if (db_step_stmt(stmt) == DB_ROW) {
table_exists = db_column_int_value(stmt, 0) > 0;
}
db_finalize_stmt(stmt);
if (!table_exists) {
if (db_table_exists("config", &table_exists) != DB_OK || !table_exists) {
return 0;
}
// Check if table has configuration data
const char* count_sql = "SELECT COUNT(*) FROM config";
rc = db_prepare(count_sql, &stmt);
db_stmt_t* stmt;
int rc = db_prepare(count_sql, &stmt);
if (rc != DB_OK) {
return 0;
}
int config_count = 0;
if (db_step_stmt(stmt) == DB_ROW) {
config_count = db_column_int_value(stmt, 0);
}
db_finalize_stmt(stmt);
return config_count > 0;
}
@@ -4972,7 +4967,7 @@ int process_startup_config_event(const cJSON* event) {
}
// Begin transaction for atomic config updates
int rc = db_exec_sql("BEGIN IMMEDIATE TRANSACTION");
int rc = db_exec_sql("BEGIN");
if (rc != DB_OK) {
DEBUG_ERROR("Failed to begin startup config transaction");
return -1;
+6
View File
@@ -33,6 +33,12 @@ typedef struct {
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)
char db_connstring_override[1024];
char db_host[128];
int db_port; // 0 = not set
char db_name[128];
char db_user[128];
char db_password[128];
} cli_options_t;
// Core configuration functions (temporary compatibility)
+214 -1296
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+97
View File
@@ -0,0 +1,97 @@
#ifndef DB_OPS_POSTGRES_H
#define DB_OPS_POSTGRES_H
#include "db_ops.h"
typedef struct postgres_db_stmt postgres_db_stmt_t;
int postgres_db_init(const char* connection_string);
void postgres_db_close(void);
int postgres_db_is_available(void);
const char* postgres_db_last_error(void);
const char* postgres_db_get_database_path(void);
int postgres_db_apply_schema(void);
int postgres_db_set_thread_connection(void* connection);
void postgres_db_clear_thread_connection(void);
int postgres_db_open_worker_connection(const char* db_path, void** out_connection);
void postgres_db_close_worker_connection(void* connection);
int postgres_db_prepare(const char* sql, postgres_db_stmt_t** out_stmt);
int postgres_db_bind_text_param(postgres_db_stmt_t* stmt, int index, const char* value);
int postgres_db_bind_int_param(postgres_db_stmt_t* stmt, int index, int value);
int postgres_db_bind_int64_param(postgres_db_stmt_t* stmt, int index, long long value);
int postgres_db_step_stmt(postgres_db_stmt_t* stmt);
int postgres_db_reset_stmt(postgres_db_stmt_t* stmt);
const char* postgres_db_column_text_value(postgres_db_stmt_t* stmt, int col);
int postgres_db_column_int_value(postgres_db_stmt_t* stmt, int col);
long long postgres_db_column_int64_value(postgres_db_stmt_t* stmt, int col);
double postgres_db_column_double_value(postgres_db_stmt_t* stmt, int col);
void postgres_db_finalize_stmt(postgres_db_stmt_t* stmt);
int postgres_db_log_subscription_created(const char* sub_id, const char* wsi_ptr,
const char* client_ip, const char* filter_json);
int postgres_db_log_subscription_closed(const char* sub_id, const char* client_ip);
int postgres_db_log_subscription_disconnected(const char* client_ip);
int postgres_db_update_subscription_events_sent(const char* sub_id, int events_sent);
int postgres_db_cleanup_orphaned_subscriptions(void);
int postgres_db_get_event_pubkey(const char* event_id, char* pubkey_out, size_t pubkey_out_size);
int postgres_db_delete_event_by_id(const char* event_id, const char* requester_pubkey);
int postgres_db_delete_events_by_address(const char* pubkey, int kind,
const char* d_tag, long before_timestamp);
int postgres_db_is_pubkey_blacklisted(const char* pubkey);
int postgres_db_is_hash_blacklisted(const char* resource_hash);
int postgres_db_is_pubkey_whitelisted(const char* pubkey);
int postgres_db_count_active_whitelist_rules(void);
int postgres_db_count_with_sql(const char* sql, const char** bind_params, int bind_param_count, int* out_count);
char* postgres_db_execute_readonly_query_json(const char* query, const char* request_id,
char* error_message, size_t error_size,
int max_rows, int timeout_ms);
int postgres_db_get_total_event_count_ll(long long* out_count);
int postgres_db_get_event_count_since(time_t cutoff, long long* out_count);
cJSON* postgres_db_get_event_kind_distribution_rows(long long* out_total_events);
cJSON* postgres_db_get_top_pubkeys_rows(int limit);
cJSON* postgres_db_get_subscription_details_rows(void);
cJSON* postgres_db_get_all_config_rows(void);
char* postgres_db_get_config_value_dup(const char* key);
int postgres_db_set_config_value_full(const char* key, const char* value, const char* data_type,
const char* description, const char* category, int requires_restart);
int postgres_db_update_config_value_only(const char* key, const char* value);
int postgres_db_upsert_config_value(const char* key, const char* value, const char* data_type);
int postgres_db_store_relay_private_key_hex(const char* relay_privkey_hex);
char* postgres_db_get_relay_private_key_hex_dup(void);
int postgres_db_store_config_event(const cJSON* event);
int postgres_db_insert_event_with_json(const char* id, const char* pubkey, long long created_at,
int kind, const char* event_type, const char* content,
const char* sig, const char* tags_json, const char* event_json,
int* out_step_rc, int* out_extended_errcode);
int postgres_db_get_event_time_bounds(long long* out_min_created_at, long long* out_max_created_at);
int postgres_db_event_id_exists(const char* event_id, int* out_exists);
cJSON* postgres_db_retrieve_event_by_id(const char* event_id);
char* postgres_db_get_latest_event_pubkey_for_kind_dup(int kind);
int postgres_db_get_config_row_count(int* out_count);
int postgres_db_store_event_tags_cjson(const char* event_id, const cJSON* tags);
int postgres_db_populate_event_tags_from_existing(void);
int postgres_db_add_auth_rule(const char* rule_type, const char* pattern_type, const char* pattern_value);
int postgres_db_remove_auth_rule(const char* rule_type, const char* pattern_type, const char* pattern_value);
int postgres_db_delete_wot_whitelist_rules(void);
int postgres_db_count_wot_whitelist_rules(void);
int postgres_db_table_exists(const char* table_name, int* out_exists);
char* postgres_db_get_schema_version_dup(void);
int postgres_db_exec_sql(const char* sql);
int postgres_db_wal_checkpoint_passive(void);
int postgres_db_wal_checkpoint_truncate(void);
#endif // DB_OPS_POSTGRES_H
+1397
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+24 -7
View File
@@ -119,7 +119,7 @@ void ip_ban_load_from_db(void) {
" total_failures INTEGER NOT NULL DEFAULT 0,"
" total_successes INTEGER NOT NULL DEFAULT 0,"
" first_seen INTEGER NOT NULL DEFAULT 0,"
" updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))"
" updated_at INTEGER NOT NULL DEFAULT 0"
");";
if (db_exec_sql(create_sql) != 0) {
DEBUG_ERROR("Failed to create ip_bans table: %s", db_last_error());
@@ -197,12 +197,28 @@ void ip_ban_save_to_db(void) {
if (!db_is_available() || !g_initialized) return;
const char* upsert_sql =
"INSERT OR REPLACE INTO ip_bans"
"INSERT INTO ip_bans"
" (ip, failure_count, ban_count, banned_until, first_failure,"
" has_authed_successfully, last_success_at, total_connections,"
" total_failures, total_successes, first_seen, updated_at,"
" idle_failure_count, idle_ban_count, idle_banned_until, idle_first_failure)"
" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, strftime('%s', 'now'), ?, ?, ?, ?)";
" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
" ON CONFLICT(ip) DO UPDATE SET"
" failure_count=excluded.failure_count,"
" ban_count=excluded.ban_count,"
" banned_until=excluded.banned_until,"
" first_failure=excluded.first_failure,"
" has_authed_successfully=excluded.has_authed_successfully,"
" last_success_at=excluded.last_success_at,"
" total_connections=excluded.total_connections,"
" total_failures=excluded.total_failures,"
" total_successes=excluded.total_successes,"
" first_seen=excluded.first_seen,"
" updated_at=excluded.updated_at,"
" idle_failure_count=excluded.idle_failure_count,"
" idle_ban_count=excluded.idle_ban_count,"
" idle_banned_until=excluded.idle_banned_until,"
" idle_first_failure=excluded.idle_first_failure";
db_stmt_t* stmt;
if (db_prepare(upsert_sql, &stmt) != DB_OK) {
@@ -230,11 +246,12 @@ void ip_ban_save_to_db(void) {
db_bind_int_param(stmt, 9, entry->total_failures);
db_bind_int_param(stmt, 10, entry->total_successes);
db_bind_int64_param(stmt, 11, (long long)entry->first_seen);
db_bind_int64_param(stmt, 12, (long long)time(NULL));
// Idle tracking fields
db_bind_int_param(stmt, 12, entry->idle_failure_count);
db_bind_int_param(stmt, 13, entry->idle_ban_count);
db_bind_int64_param(stmt, 14, (long long)entry->idle_banned_until);
db_bind_int64_param(stmt, 15, (long long)entry->idle_first_failure);
db_bind_int_param(stmt, 13, entry->idle_failure_count);
db_bind_int_param(stmt, 14, entry->idle_ban_count);
db_bind_int64_param(stmt, 15, (long long)entry->idle_banned_until);
db_bind_int64_param(stmt, 16, (long long)entry->idle_first_failure);
if (db_step_stmt(stmt) != DB_DONE) {
DEBUG_WARN("Failed to save ip_ban entry for %s", entry->ip);
+207 -23
View File
@@ -8,7 +8,6 @@
#include <signal.h>
#include <time.h>
#include <pthread.h>
#include <sqlite3.h>
#include <libwebsockets.h>
#include <errno.h>
#include <sys/socket.h>
@@ -48,7 +47,6 @@ int nostr_nip42_verify_auth_event(cJSON *event, const char *challenge_id,
#define RESET "\033[0m"
// Global state
sqlite3* g_db = NULL; // Non-static so config.c can access it
int g_server_running = 1; // Non-static so websockets.c can access it
volatile sig_atomic_t g_shutdown_flag = 0; // Non-static so config.c can access it for restart functionality
int g_restart_requested = 0; // Non-static so config.c can access it for restart functionality
@@ -704,11 +702,53 @@ static void cleanup_stale_wal_files(const char* db_path) {
}
}
// Build DB connection target from CLI overrides.
// Returns 1 when out_connection_string contains an override to use, else 0.
static int build_db_connection_override(const cli_options_t* cli_options,
char* out_connection_string,
size_t out_size) {
if (!cli_options || !out_connection_string || out_size == 0) {
return 0;
}
out_connection_string[0] = '\0';
#ifdef DB_BACKEND_POSTGRES
if (cli_options->db_connstring_override[0] != '\0') {
strncpy(out_connection_string, cli_options->db_connstring_override, out_size - 1);
out_connection_string[out_size - 1] = '\0';
return 1;
}
const char* host = (cli_options->db_host[0] != '\0') ? cli_options->db_host : "localhost";
int port = (cli_options->db_port > 0) ? cli_options->db_port : 5432;
const char* db_name = (cli_options->db_name[0] != '\0') ? cli_options->db_name : "crelay";
const char* db_user = (cli_options->db_user[0] != '\0') ? cli_options->db_user : "crelay";
const char* db_password = (cli_options->db_password[0] != '\0') ? cli_options->db_password : "";
int written = snprintf(out_connection_string,
out_size,
"host=%s port=%d dbname=%s user=%s password=%s",
host,
port,
db_name,
db_user,
db_password);
if (written <= 0 || (size_t)written >= out_size) {
out_connection_string[0] = '\0';
return 0;
}
return 1;
#endif
return 0;
}
// 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
// Priority 1: Command line database path / connection string override
const char* db_path = database_path_override;
// Priority 2: Configuration system (if available)
@@ -723,8 +763,10 @@ int init_database(const char* database_path_override) {
DEBUG_LOG("Initializing database: %s", db_path);
// Clean up stale WAL files before opening database
#ifndef DB_BACKEND_POSTGRES
// Clean up stale WAL files before opening database (SQLite only)
cleanup_stale_wal_files(db_path);
#endif
int rc = db_init(db_path);
if (rc != DB_OK) {
@@ -732,6 +774,13 @@ int init_database(const char* database_path_override) {
DEBUG_TRACE("Exiting init_database() - failed to open database");
return -1;
}
#ifdef DB_BACKEND_POSTGRES
// Schema/bootstrap is handled by postgres_db_init()/postgres_db_apply_schema().
DEBUG_TRACE("PostgreSQL backend active - skipping SQLite migration/PRAGMA paths");
DEBUG_TRACE("Exiting init_database() - success");
return 0;
#endif
// DEBUG_GUARD_START
if (g_debug_level >= DEBUG_LEVEL_DEBUG) {
@@ -818,11 +867,16 @@ int init_database(const char* database_path_override) {
// auth_rules table already exists, skipping creation
}
// Update schema version to v6
const char* update_version_sql =
"INSERT OR REPLACE INTO schema_info (key, value, updated_at) "
"VALUES ('version', '6', strftime('%s', 'now'))";
// Update schema version to v6 (portable UPSERT + C-side timestamp)
char update_version_sql[256];
snprintf(update_version_sql,
sizeof(update_version_sql),
"INSERT INTO schema_info (key, value, updated_at) "
"VALUES ('version', '6', %lld) "
"ON CONFLICT(key) DO UPDATE SET value='6', updated_at=%lld",
(long long)time(NULL),
(long long)time(NULL));
if (db_exec_sql(update_version_sql) != 0) {
DEBUG_ERROR("Failed to update schema version");
if (db_version) free(db_version);
@@ -894,12 +948,14 @@ int init_database(const char* database_path_override) {
void close_database() {
DEBUG_TRACE("Entering close_database()");
if (g_db) {
// Perform WAL checkpoint to minimize stale files on next startup
if (db_is_available()) {
#ifndef DB_BACKEND_POSTGRES
// Perform WAL checkpoint to minimize stale files on next startup (SQLite only)
DEBUG_LOG("Performing WAL checkpoint before database close");
if (db_exec_sql("PRAGMA wal_checkpoint(TRUNCATE);") != 0) {
DEBUG_WARN("WAL checkpoint warning");
}
#endif
db_close();
DEBUG_LOG("Database connection closed");
@@ -1455,7 +1511,7 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
// Build SQL query based on filter - exclude ephemeral events (kinds 20000-29999) from historical queries
// Select event_json for fast retrieval (no JSON reconstruction needed)
char sql[1408] = "SELECT event_json FROM events WHERE 1=1 AND (kind < 20000 OR kind >= 30000)";
char sql[8192] = "SELECT event_json FROM events WHERE 1=1 AND (kind < 20000 OR kind >= 30000)";
char* sql_ptr = sql + strlen(sql);
int remaining = sizeof(sql) - strlen(sql);
@@ -1589,20 +1645,56 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
const char* filter_key = filter_item->string;
if (filter_key && filter_key[0] == '#' && strlen(filter_key) > 1) {
// This is a tag filter like "#e", "#p", etc.
const char* tag_name = filter_key + 1; // Get the tag name (e, p, t, type, etc.)
const char* tag_name = filter_key + 1; // e, p, t, type, ...
if (cJSON_IsArray(filter_item)) {
int tag_value_count = 0;
// Count valid tag values
for (int j = 0; j < cJSON_GetArraySize(filter_item); j++) {
cJSON* tag_value = cJSON_GetArrayItem(filter_item, j);
if (cJSON_IsString(tag_value)) {
tag_value_count++;
}
}
if (tag_value_count > 0) {
// Use indexed event_tags table instead of json_each()
snprintf(sql_ptr, remaining, " AND id IN (SELECT event_id FROM event_tags WHERE tag_name = ? AND tag_value IN (");
#ifdef DB_BACKEND_POSTGRES
// PostgreSQL path: JSONB containment against events.tags using GIN index.
// Match Nostr semantics: tag name + first value only, allowing extra trailing elements.
snprintf(sql_ptr, remaining, " AND (");
sql_ptr += strlen(sql_ptr);
remaining = sizeof(sql) - strlen(sql);
int clause_idx = 0;
for (int j = 0; j < cJSON_GetArraySize(filter_item); j++) {
cJSON* tag_value = cJSON_GetArrayItem(filter_item, j);
if (!cJSON_IsString(tag_value)) {
continue;
}
if (clause_idx > 0) {
snprintf(sql_ptr, remaining, " OR ");
sql_ptr += strlen(sql_ptr);
remaining = sizeof(sql) - strlen(sql);
}
snprintf(sql_ptr, remaining,
"tags @> jsonb_build_array(jsonb_build_array(?::text, ?::text))");
sql_ptr += strlen(sql_ptr);
remaining = sizeof(sql) - strlen(sql);
add_bind_param(&bind_params, &bind_param_count, &bind_param_capacity, tag_name);
add_bind_param(&bind_params, &bind_param_count, &bind_param_capacity,
cJSON_GetStringValue(tag_value));
clause_idx++;
}
snprintf(sql_ptr, remaining, ")");
sql_ptr += strlen(sql_ptr);
remaining = sizeof(sql) - strlen(sql);
#else
// SQLite path: keep indexed event_tags lookup.
snprintf(sql_ptr, remaining,
" AND id IN (SELECT event_id FROM event_tags WHERE tag_name = ? AND tag_value IN (");
sql_ptr += strlen(sql_ptr);
remaining = sizeof(sql) - strlen(sql);
@@ -1620,14 +1712,15 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
sql_ptr += strlen(sql_ptr);
remaining = sizeof(sql) - strlen(sql);
// Add tag name and values to bind params
add_bind_param(&bind_params, &bind_param_count, &bind_param_capacity, tag_name);
for (int j = 0; j < cJSON_GetArraySize(filter_item); j++) {
cJSON* tag_value = cJSON_GetArrayItem(filter_item, j);
if (cJSON_IsString(tag_value)) {
add_bind_param(&bind_params, &bind_param_count, &bind_param_capacity, cJSON_GetStringValue(tag_value));
add_bind_param(&bind_params, &bind_param_count, &bind_param_capacity,
cJSON_GetStringValue(tag_value));
}
}
#endif
}
}
}
@@ -1653,9 +1746,14 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
escaped_search[escaped_len] = '\0';
// Add search conditions for content and tags
#ifdef DB_BACKEND_POSTGRES
snprintf(sql_ptr, remaining, " AND (content ILIKE '%%%s%%' OR tags::text ILIKE '%%\"%s\"%%')",
escaped_search, escaped_search);
#else
// Use tags LIKE to search within the JSON string representation of tags
snprintf(sql_ptr, remaining, " AND (content LIKE '%%%s%%' OR tags LIKE '%%\"%s\"%%')",
escaped_search, escaped_search);
#endif
sql_ptr += strlen(sql_ptr);
remaining = sizeof(sql) - strlen(sql);
}
@@ -1694,6 +1792,11 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
snprintf(sql_ptr, remaining, " LIMIT 500");
}
if (remaining <= 1) {
DEBUG_ERROR("REQ SQL query exceeded internal buffer capacity; skipping filter");
continue;
}
// Submit async REQ query (results processed on lws thread via completion queue)
if (submit_req_query_async(async_state, sql, (const char**)bind_params, bind_param_count) != 0) {
DEBUG_ERROR("Failed to submit async REQ query for subscription %s", sub_id);
@@ -1850,6 +1953,12 @@ void print_usage(const char* program_name) {
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(" --db-connstring STR PostgreSQL libpq connection string\n");
printf(" --db-host HOST PostgreSQL host (default: localhost)\n");
printf(" --db-port PORT PostgreSQL port (default: 5432)\n");
printf(" --db-name NAME PostgreSQL database name (default: crelay)\n");
printf(" --db-user USER PostgreSQL user (default: crelay)\n");
printf(" --db-password PASS PostgreSQL password\n");
printf("\n");
printf("Configuration:\n");
printf(" This relay uses event-based configuration stored in the database.\n");
@@ -1983,6 +2092,69 @@ int main(int argc, char* argv[]) {
cli_options.relay_privkey_override[sizeof(cli_options.relay_privkey_override) - 1] = '\0';
i++; // Skip the key argument
} else if (strcmp(argv[i], "--db-connstring") == 0) {
if (i + 1 >= argc) {
DEBUG_ERROR("--db-connstring requires a value.");
print_usage(argv[0]);
return 1;
}
strncpy(cli_options.db_connstring_override, argv[i + 1], sizeof(cli_options.db_connstring_override) - 1);
cli_options.db_connstring_override[sizeof(cli_options.db_connstring_override) - 1] = '\0';
i++;
} else if (strncmp(argv[i], "--db-connstring=", 16) == 0) {
strncpy(cli_options.db_connstring_override, argv[i] + 16, sizeof(cli_options.db_connstring_override) - 1);
cli_options.db_connstring_override[sizeof(cli_options.db_connstring_override) - 1] = '\0';
} else if (strcmp(argv[i], "--db-host") == 0) {
if (i + 1 >= argc) {
DEBUG_ERROR("--db-host requires a value.");
print_usage(argv[0]);
return 1;
}
strncpy(cli_options.db_host, argv[i + 1], sizeof(cli_options.db_host) - 1);
cli_options.db_host[sizeof(cli_options.db_host) - 1] = '\0';
i++;
} else if (strcmp(argv[i], "--db-port") == 0) {
if (i + 1 >= argc) {
DEBUG_ERROR("--db-port requires a value.");
print_usage(argv[0]);
return 1;
}
char* endptr;
long db_port = strtol(argv[i + 1], &endptr, 10);
if (endptr == argv[i + 1] || *endptr != '\0' || db_port < 1 || db_port > 65535) {
DEBUG_ERROR("Invalid --db-port value. Must be between 1 and 65535.");
print_usage(argv[0]);
return 1;
}
cli_options.db_port = (int)db_port;
i++;
} else if (strcmp(argv[i], "--db-name") == 0) {
if (i + 1 >= argc) {
DEBUG_ERROR("--db-name requires a value.");
print_usage(argv[0]);
return 1;
}
strncpy(cli_options.db_name, argv[i + 1], sizeof(cli_options.db_name) - 1);
cli_options.db_name[sizeof(cli_options.db_name) - 1] = '\0';
i++;
} else if (strcmp(argv[i], "--db-user") == 0) {
if (i + 1 >= argc) {
DEBUG_ERROR("--db-user requires a value.");
print_usage(argv[0]);
return 1;
}
strncpy(cli_options.db_user, argv[i + 1], sizeof(cli_options.db_user) - 1);
cli_options.db_user[sizeof(cli_options.db_user) - 1] = '\0';
i++;
} else if (strcmp(argv[i], "--db-password") == 0) {
if (i + 1 >= argc) {
DEBUG_ERROR("--db-password requires a value.");
print_usage(argv[0]);
return 1;
}
strncpy(cli_options.db_password, argv[i + 1], sizeof(cli_options.db_password) - 1);
cli_options.db_password[sizeof(cli_options.db_password) - 1] = '\0';
i++;
} else if (strcmp(argv[i], "--strict-port") == 0) {
// Strict port mode option
cli_options.strict_port = 1;
@@ -2059,7 +2231,12 @@ int main(int argc, char* argv[]) {
// Initialize database with the generated relay pubkey
DEBUG_TRACE("Initializing database for first-time startup");
if (init_database(g_database_path) != 0) {
char db_connection_override[1024] = {0};
const char* init_db_target = g_database_path;
if (build_db_connection_override(&cli_options, db_connection_override, sizeof(db_connection_override))) {
init_db_target = db_connection_override;
}
if (init_database(init_db_target) != 0) {
DEBUG_ERROR("Failed to initialize database after first-time setup");
cleanup_configuration_system();
nostr_cleanup();
@@ -2166,7 +2343,12 @@ int main(int argc, char* argv[]) {
// Initialize database with the database path set by startup_existing_relay()
DEBUG_TRACE("Initializing existing database");
if (init_database(g_database_path) != 0) {
char db_connection_override[1024] = {0};
const char* init_db_target = g_database_path;
if (build_db_connection_override(&cli_options, db_connection_override, sizeof(db_connection_override))) {
init_db_target = db_connection_override;
}
if (init_database(init_db_target) != 0) {
DEBUG_ERROR("Failed to initialize existing database");
cleanup_configuration_system();
free(relay_pubkey);
@@ -2225,7 +2407,7 @@ int main(int argc, char* argv[]) {
}
// Verify database is now available
if (!g_db) {
if (!db_is_available()) {
DEBUG_ERROR("Database not available after initialization");
cleanup_configuration_system();
nostr_cleanup();
@@ -2341,7 +2523,9 @@ int main(int argc, char* argv[]) {
memset(&tp_cfg, 0, sizeof(tp_cfg));
tp_cfg.reader_threads = get_config_int("thread_pool_readers", 1);
tp_cfg.max_queue_depth = get_config_int("thread_pool_max_queue_depth", 4096);
tp_cfg.db_path = g_database_path;
// Use active DB backend path/connection target.
// For PostgreSQL this is the libpq connection string, not g_database_path (.db filename).
tp_cfg.db_path = db_get_database_path();
tp_cfg.wake_loop_cb = wake_event_loop_from_thread_pool;
tp_cfg.wake_loop_ctx = NULL;
@@ -2355,7 +2539,7 @@ int main(int argc, char* argv[]) {
// Phase 5 strict mode: remove main-thread fallback to global DB handle.
// Runtime DB access must go through thread-bound worker connections.
g_db = NULL;
// Ownership now resides in db_ops.c; no direct global handle mutation here.
// Start WebSocket Nostr relay server (port from CLI override or configuration)
int result = start_websocket_relay(cli_options.port_override, cli_options.strict_port); // Use CLI port override if specified, otherwise config
+2 -2
View File
@@ -13,8 +13,8 @@
// Using CRELAY_ prefix to avoid conflicts with nostr_core_lib VERSION macros
#define CRELAY_VERSION_MAJOR 2
#define CRELAY_VERSION_MINOR 1
#define CRELAY_VERSION_PATCH 9
#define CRELAY_VERSION "v2.1.9"
#define CRELAY_VERSION_PATCH 10
#define CRELAY_VERSION "v2.1.10"
// Relay metadata (authoritative source for NIP-11 information)
#define RELAY_NAME "C-Relay-PG"
+249
View File
@@ -0,0 +1,249 @@
#ifndef PG_SCHEMA_H
#define PG_SCHEMA_H
#define EMBEDDED_PG_SCHEMA_VERSION "2"
static const char* const EMBEDDED_PG_SCHEMA_SQL =
"-- C-Relay-PG PostgreSQL Schema\n"
"-- Initial PostgreSQL backend schema\n"
"\n"
"BEGIN;\n"
"\n"
"CREATE TABLE IF NOT EXISTS schema_info (\n"
" key TEXT PRIMARY KEY,\n"
" value TEXT NOT NULL,\n"
" updated_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT\n"
");\n"
"\n"
"CREATE TABLE IF NOT EXISTS events (\n"
" id TEXT PRIMARY KEY,\n"
" pubkey TEXT NOT NULL,\n"
" created_at BIGINT NOT NULL,\n"
" kind INTEGER NOT NULL,\n"
" event_type TEXT NOT NULL CHECK (event_type IN ('regular', 'replaceable', 'ephemeral', 'addressable')),\n"
" content TEXT NOT NULL,\n"
" sig TEXT NOT NULL,\n"
" tags JSONB NOT NULL DEFAULT '[]'::jsonb,\n"
" d_tag_value TEXT GENERATED ALWAYS AS (\n"
" COALESCE(\n"
" NULLIF(\n"
" jsonb_path_query_first(tags, '$[*] ? (@[0] == \"d\")[1]') #>> '{}',\n"
" ''\n"
" ),\n"
" ''\n"
" )\n"
" ) STORED,\n"
" expires_at BIGINT,\n"
" event_json TEXT NOT NULL,\n"
" first_seen BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT\n"
");\n"
"\n"
"ALTER TABLE events\n"
" ADD COLUMN IF NOT EXISTS expires_at BIGINT;\n"
"\n"
"DO $$\n"
"BEGIN\n"
" IF NOT EXISTS (\n"
" SELECT 1\n"
" FROM information_schema.columns\n"
" WHERE table_schema = current_schema()\n"
" AND table_name = 'events'\n"
" AND column_name = 'd_tag_value'\n"
" ) THEN\n"
" ALTER TABLE events\n"
" ADD COLUMN d_tag_value TEXT GENERATED ALWAYS AS (\n"
" COALESCE(\n"
" NULLIF(\n"
" jsonb_path_query_first(tags, '$[*] ? (@[0] == \"d\")[1]') #>> '{}',\n"
" ''\n"
" ),\n"
" ''\n"
" )\n"
" ) STORED;\n"
" END IF;\n"
"END\n"
"$$;\n"
"\n"
"CREATE INDEX IF NOT EXISTS idx_events_pubkey ON events(pubkey);\n"
"CREATE INDEX IF NOT EXISTS idx_events_kind ON events(kind);\n"
"CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at DESC);\n"
"CREATE INDEX IF NOT EXISTS idx_events_event_type ON events(event_type);\n"
"CREATE INDEX IF NOT EXISTS idx_events_kind_created_at ON events(kind, created_at DESC);\n"
"CREATE INDEX IF NOT EXISTS idx_events_pubkey_created_at ON events(pubkey, created_at DESC);\n"
"CREATE INDEX IF NOT EXISTS idx_events_pubkey_kind ON events(pubkey, kind);\n"
"CREATE INDEX IF NOT EXISTS idx_events_tags_gin ON events USING GIN(tags);\n"
"CREATE INDEX IF NOT EXISTS idx_events_expires_at ON events(expires_at DESC) WHERE expires_at IS NOT NULL;\n"
"CREATE INDEX IF NOT EXISTS idx_events_d_tag_value ON events(d_tag_value) WHERE d_tag_value <> '';\n"
"CREATE UNIQUE INDEX IF NOT EXISTS uq_events_replaceable_pubkey_kind\n"
" ON events(pubkey, kind)\n"
" WHERE event_type = 'replaceable';\n"
"CREATE UNIQUE INDEX IF NOT EXISTS uq_events_addressable_pubkey_kind_dtag\n"
" ON events(pubkey, kind, d_tag_value)\n"
" WHERE event_type = 'addressable';\n"
"\n"
"-- Keep this table for compatibility with existing query paths during migration.\n"
"CREATE TABLE IF NOT EXISTS event_tags (\n"
" event_id TEXT NOT NULL REFERENCES events(id) ON DELETE CASCADE,\n"
" tag_name TEXT NOT NULL,\n"
" tag_value TEXT NOT NULL,\n"
" tag_index INTEGER NOT NULL DEFAULT 0,\n"
" PRIMARY KEY (event_id, tag_name, tag_value, tag_index)\n"
");\n"
"\n"
"CREATE INDEX IF NOT EXISTS idx_event_tags_lookup ON event_tags(tag_name, tag_value);\n"
"CREATE INDEX IF NOT EXISTS idx_event_tags_event ON event_tags(event_id);\n"
"CREATE INDEX IF NOT EXISTS idx_event_tags_value_name ON event_tags(tag_value, tag_name);\n"
"\n"
"CREATE OR REPLACE FUNCTION set_event_derived_fields()\n"
"RETURNS TRIGGER AS $$\n"
"DECLARE\n"
" expiration_value TEXT;\n"
"BEGIN\n"
" NEW.expires_at := NULL;\n"
"\n"
" SELECT tag->>1\n"
" INTO expiration_value\n"
" FROM jsonb_array_elements(NEW.tags) AS tag\n"
" WHERE jsonb_typeof(tag) = 'array'\n"
" AND jsonb_array_length(tag) >= 2\n"
" AND tag->>0 = 'expiration'\n"
" LIMIT 1;\n"
"\n"
" IF expiration_value IS NOT NULL AND expiration_value ~ '^[0-9]+$' THEN\n"
" NEW.expires_at := expiration_value::BIGINT;\n"
" END IF;\n"
"\n"
" RETURN NEW;\n"
"END;\n"
"$$ LANGUAGE plpgsql;\n"
"\n"
"CREATE OR REPLACE FUNCTION sync_event_tags_from_events()\n"
"RETURNS TRIGGER AS $$\n"
"BEGIN\n"
" DELETE FROM event_tags WHERE event_id = NEW.id;\n"
"\n"
" INSERT INTO event_tags (event_id, tag_name, tag_value, tag_index)\n"
" SELECT NEW.id,\n"
" tag->>0 AS tag_name,\n"
" tag->>1 AS tag_value,\n"
" (ord - 1)::INTEGER AS tag_index\n"
" FROM jsonb_array_elements(NEW.tags) WITH ORDINALITY AS expanded(tag, ord)\n"
" WHERE jsonb_typeof(tag) = 'array'\n"
" AND jsonb_array_length(tag) >= 2\n"
" AND COALESCE(tag->>0, '') <> ''\n"
" AND COALESCE(tag->>1, '') <> '';\n"
"\n"
" RETURN NEW;\n"
"END;\n"
"$$ LANGUAGE plpgsql;\n"
"\n"
"DROP TRIGGER IF EXISTS trg_events_set_derived_fields ON events;\n"
"CREATE TRIGGER trg_events_set_derived_fields\n"
"BEFORE INSERT OR UPDATE OF tags ON events\n"
"FOR EACH ROW\n"
"EXECUTE FUNCTION set_event_derived_fields();\n"
"\n"
"DROP TRIGGER IF EXISTS trg_events_sync_event_tags ON events;\n"
"CREATE TRIGGER trg_events_sync_event_tags\n"
"AFTER INSERT OR UPDATE OF tags ON events\n"
"FOR EACH ROW\n"
"EXECUTE FUNCTION sync_event_tags_from_events();\n"
"\n"
"CREATE TABLE IF NOT EXISTS relay_seckey (\n"
" id SMALLINT PRIMARY KEY DEFAULT 1,\n"
" private_key_hex TEXT NOT NULL CHECK (char_length(private_key_hex) = 64),\n"
" created_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT,\n"
" CONSTRAINT relay_seckey_singleton CHECK (id = 1)\n"
");\n"
"\n"
"CREATE TABLE IF NOT EXISTS auth_rules (\n"
" id BIGSERIAL PRIMARY KEY,\n"
" rule_type TEXT NOT NULL CHECK (rule_type IN ('whitelist', 'blacklist', 'rate_limit', 'auth_required', 'wot_whitelist')),\n"
" pattern_type TEXT NOT NULL CHECK (pattern_type IN ('pubkey', 'kind', 'ip', 'global', 'event_id', 'content', 'hash')),\n"
" pattern_value TEXT,\n"
" active INTEGER NOT NULL DEFAULT 1,\n"
" created_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT,\n"
" updated_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT\n"
");\n"
"\n"
"CREATE INDEX IF NOT EXISTS idx_auth_rules_pattern ON auth_rules(pattern_type, pattern_value);\n"
"CREATE INDEX IF NOT EXISTS idx_auth_rules_type ON auth_rules(rule_type);\n"
"CREATE INDEX IF NOT EXISTS idx_auth_rules_active ON auth_rules(active);\n"
"\n"
"CREATE TABLE IF NOT EXISTS config (\n"
" key TEXT PRIMARY KEY,\n"
" value TEXT NOT NULL,\n"
" data_type TEXT NOT NULL CHECK (data_type IN ('string', 'integer', 'boolean', 'json')),\n"
" description TEXT,\n"
" category TEXT DEFAULT 'general',\n"
" requires_restart INTEGER DEFAULT 0,\n"
" created_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT,\n"
" updated_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT\n"
");\n"
"\n"
"CREATE INDEX IF NOT EXISTS idx_config_category ON config(category);\n"
"CREATE INDEX IF NOT EXISTS idx_config_restart ON config(requires_restart);\n"
"CREATE INDEX IF NOT EXISTS idx_config_updated ON config(updated_at DESC);\n"
"\n"
"CREATE TABLE IF NOT EXISTS subscriptions (\n"
" id BIGSERIAL PRIMARY KEY,\n"
" subscription_id TEXT NOT NULL,\n"
" wsi_pointer TEXT NOT NULL,\n"
" client_ip TEXT NOT NULL,\n"
" event_type TEXT NOT NULL CHECK (event_type IN ('created', 'closed', 'expired', 'disconnected')),\n"
" filter_json TEXT,\n"
" events_sent INTEGER DEFAULT 0,\n"
" created_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT,\n"
" ended_at BIGINT,\n"
" duration BIGINT,\n"
" UNIQUE(subscription_id, wsi_pointer)\n"
");\n"
"\n"
"CREATE TABLE IF NOT EXISTS subscription_metrics (\n"
" id BIGSERIAL PRIMARY KEY,\n"
" date TEXT NOT NULL UNIQUE,\n"
" total_created INTEGER DEFAULT 0,\n"
" total_closed INTEGER DEFAULT 0,\n"
" total_events_broadcast INTEGER DEFAULT 0,\n"
" avg_duration DOUBLE PRECISION DEFAULT 0,\n"
" peak_concurrent INTEGER DEFAULT 0,\n"
" updated_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT\n"
");\n"
"\n"
"CREATE INDEX IF NOT EXISTS idx_subscriptions_id ON subscriptions(subscription_id);\n"
"CREATE INDEX IF NOT EXISTS idx_subscriptions_type ON subscriptions(event_type);\n"
"CREATE INDEX IF NOT EXISTS idx_subscriptions_created ON subscriptions(created_at DESC);\n"
"CREATE INDEX IF NOT EXISTS idx_subscriptions_client ON subscriptions(client_ip);\n"
"CREATE INDEX IF NOT EXISTS idx_subscriptions_wsi ON subscriptions(wsi_pointer);\n"
"CREATE INDEX IF NOT EXISTS idx_subscriptions_active_log ON subscriptions(event_type, ended_at, created_at DESC);\n"
"CREATE INDEX IF NOT EXISTS idx_subscription_metrics_date ON subscription_metrics(date DESC);\n"
"\n"
"CREATE TABLE IF NOT EXISTS ip_bans (\n"
" ip TEXT PRIMARY KEY,\n"
" failure_count INTEGER NOT NULL DEFAULT 0,\n"
" ban_count INTEGER NOT NULL DEFAULT 0,\n"
" banned_until BIGINT NOT NULL DEFAULT 0,\n"
" first_failure BIGINT NOT NULL DEFAULT 0,\n"
" has_authed_successfully INTEGER NOT NULL DEFAULT 0,\n"
" last_success_at BIGINT NOT NULL DEFAULT 0,\n"
" total_connections INTEGER NOT NULL DEFAULT 0,\n"
" total_failures INTEGER NOT NULL DEFAULT 0,\n"
" total_successes INTEGER NOT NULL DEFAULT 0,\n"
" first_seen BIGINT NOT NULL DEFAULT 0,\n"
" updated_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT,\n"
" idle_failure_count INTEGER NOT NULL DEFAULT 0,\n"
" idle_ban_count INTEGER NOT NULL DEFAULT 0,\n"
" idle_banned_until BIGINT NOT NULL DEFAULT 0,\n"
" idle_first_failure BIGINT NOT NULL DEFAULT 0\n"
");\n"
"\n"
"INSERT INTO schema_info(key, value, updated_at)\n"
"VALUES ('version', '2', EXTRACT(EPOCH FROM NOW())::BIGINT)\n"
"ON CONFLICT (key) DO UPDATE SET\n"
" value = EXCLUDED.value,\n"
" updated_at = EXCLUDED.updated_at;\n"
"\n"
"COMMIT;\n"
;
#endif // PG_SCHEMA_H
+240
View File
@@ -0,0 +1,240 @@
-- C-Relay-PG PostgreSQL Schema
-- Initial PostgreSQL backend schema
BEGIN;
CREATE TABLE IF NOT EXISTS schema_info (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT
);
CREATE TABLE IF NOT EXISTS events (
id TEXT PRIMARY KEY,
pubkey TEXT NOT NULL,
created_at BIGINT NOT NULL,
kind INTEGER NOT NULL,
event_type TEXT NOT NULL CHECK (event_type IN ('regular', 'replaceable', 'ephemeral', 'addressable')),
content TEXT NOT NULL,
sig TEXT NOT NULL,
tags JSONB NOT NULL DEFAULT '[]'::jsonb,
d_tag_value TEXT GENERATED ALWAYS AS (
COALESCE(
NULLIF(
jsonb_path_query_first(tags, '$[*] ? (@[0] == "d")[1]') #>> '{}',
''
),
''
)
) STORED,
expires_at BIGINT,
event_json TEXT NOT NULL,
first_seen BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT
);
ALTER TABLE events
ADD COLUMN IF NOT EXISTS expires_at BIGINT;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = current_schema()
AND table_name = 'events'
AND column_name = 'd_tag_value'
) THEN
ALTER TABLE events
ADD COLUMN d_tag_value TEXT GENERATED ALWAYS AS (
COALESCE(
NULLIF(
jsonb_path_query_first(tags, '$[*] ? (@[0] == "d")[1]') #>> '{}',
''
),
''
)
) STORED;
END IF;
END
$$;
CREATE INDEX IF NOT EXISTS idx_events_pubkey ON events(pubkey);
CREATE INDEX IF NOT EXISTS idx_events_kind ON events(kind);
CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_events_event_type ON events(event_type);
CREATE INDEX IF NOT EXISTS idx_events_kind_created_at ON events(kind, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_events_pubkey_created_at ON events(pubkey, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_events_pubkey_kind ON events(pubkey, kind);
CREATE INDEX IF NOT EXISTS idx_events_tags_gin ON events USING GIN(tags);
CREATE INDEX IF NOT EXISTS idx_events_expires_at ON events(expires_at DESC) WHERE expires_at IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_events_d_tag_value ON events(d_tag_value) WHERE d_tag_value <> '';
CREATE UNIQUE INDEX IF NOT EXISTS uq_events_replaceable_pubkey_kind
ON events(pubkey, kind)
WHERE event_type = 'replaceable';
CREATE UNIQUE INDEX IF NOT EXISTS uq_events_addressable_pubkey_kind_dtag
ON events(pubkey, kind, d_tag_value)
WHERE event_type = 'addressable';
-- Keep this table for compatibility with existing query paths during migration.
CREATE TABLE IF NOT EXISTS event_tags (
event_id TEXT NOT NULL REFERENCES events(id) ON DELETE CASCADE,
tag_name TEXT NOT NULL,
tag_value TEXT NOT NULL,
tag_index INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (event_id, tag_name, tag_value, tag_index)
);
CREATE INDEX IF NOT EXISTS idx_event_tags_lookup ON event_tags(tag_name, tag_value);
CREATE INDEX IF NOT EXISTS idx_event_tags_event ON event_tags(event_id);
CREATE INDEX IF NOT EXISTS idx_event_tags_value_name ON event_tags(tag_value, tag_name);
CREATE OR REPLACE FUNCTION set_event_derived_fields()
RETURNS TRIGGER AS $$
DECLARE
expiration_value TEXT;
BEGIN
NEW.expires_at := NULL;
SELECT tag->>1
INTO expiration_value
FROM jsonb_array_elements(NEW.tags) AS tag
WHERE jsonb_typeof(tag) = 'array'
AND jsonb_array_length(tag) >= 2
AND tag->>0 = 'expiration'
LIMIT 1;
IF expiration_value IS NOT NULL AND expiration_value ~ '^[0-9]+$' THEN
NEW.expires_at := expiration_value::BIGINT;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION sync_event_tags_from_events()
RETURNS TRIGGER AS $$
BEGIN
DELETE FROM event_tags WHERE event_id = NEW.id;
INSERT INTO event_tags (event_id, tag_name, tag_value, tag_index)
SELECT NEW.id,
tag->>0 AS tag_name,
tag->>1 AS tag_value,
(ord - 1)::INTEGER AS tag_index
FROM jsonb_array_elements(NEW.tags) WITH ORDINALITY AS expanded(tag, ord)
WHERE jsonb_typeof(tag) = 'array'
AND jsonb_array_length(tag) >= 2
AND COALESCE(tag->>0, '') <> ''
AND COALESCE(tag->>1, '') <> '';
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_events_set_derived_fields ON events;
CREATE TRIGGER trg_events_set_derived_fields
BEFORE INSERT OR UPDATE OF tags ON events
FOR EACH ROW
EXECUTE FUNCTION set_event_derived_fields();
DROP TRIGGER IF EXISTS trg_events_sync_event_tags ON events;
CREATE TRIGGER trg_events_sync_event_tags
AFTER INSERT OR UPDATE OF tags ON events
FOR EACH ROW
EXECUTE FUNCTION sync_event_tags_from_events();
CREATE TABLE IF NOT EXISTS relay_seckey (
id SMALLINT PRIMARY KEY DEFAULT 1,
private_key_hex TEXT NOT NULL CHECK (char_length(private_key_hex) = 64),
created_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT,
CONSTRAINT relay_seckey_singleton CHECK (id = 1)
);
CREATE TABLE IF NOT EXISTS auth_rules (
id BIGSERIAL PRIMARY KEY,
rule_type TEXT NOT NULL CHECK (rule_type IN ('whitelist', 'blacklist', 'rate_limit', 'auth_required', 'wot_whitelist')),
pattern_type TEXT NOT NULL CHECK (pattern_type IN ('pubkey', 'kind', 'ip', 'global', 'event_id', 'content', 'hash')),
pattern_value TEXT,
active INTEGER NOT NULL DEFAULT 1,
created_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT,
updated_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT
);
CREATE INDEX IF NOT EXISTS idx_auth_rules_pattern ON auth_rules(pattern_type, pattern_value);
CREATE INDEX IF NOT EXISTS idx_auth_rules_type ON auth_rules(rule_type);
CREATE INDEX IF NOT EXISTS idx_auth_rules_active ON auth_rules(active);
CREATE TABLE IF NOT EXISTS config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
data_type TEXT NOT NULL CHECK (data_type IN ('string', 'integer', 'boolean', 'json')),
description TEXT,
category TEXT DEFAULT 'general',
requires_restart INTEGER DEFAULT 0,
created_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT,
updated_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT
);
CREATE INDEX IF NOT EXISTS idx_config_category ON config(category);
CREATE INDEX IF NOT EXISTS idx_config_restart ON config(requires_restart);
CREATE INDEX IF NOT EXISTS idx_config_updated ON config(updated_at DESC);
CREATE TABLE IF NOT EXISTS subscriptions (
id BIGSERIAL PRIMARY KEY,
subscription_id TEXT NOT NULL,
wsi_pointer TEXT NOT NULL,
client_ip TEXT NOT NULL,
event_type TEXT NOT NULL CHECK (event_type IN ('created', 'closed', 'expired', 'disconnected')),
filter_json TEXT,
events_sent INTEGER DEFAULT 0,
created_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT,
ended_at BIGINT,
duration BIGINT,
UNIQUE(subscription_id, wsi_pointer)
);
CREATE TABLE IF NOT EXISTS subscription_metrics (
id BIGSERIAL PRIMARY KEY,
date TEXT NOT NULL UNIQUE,
total_created INTEGER DEFAULT 0,
total_closed INTEGER DEFAULT 0,
total_events_broadcast INTEGER DEFAULT 0,
avg_duration DOUBLE PRECISION DEFAULT 0,
peak_concurrent INTEGER DEFAULT 0,
updated_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT
);
CREATE INDEX IF NOT EXISTS idx_subscriptions_id ON subscriptions(subscription_id);
CREATE INDEX IF NOT EXISTS idx_subscriptions_type ON subscriptions(event_type);
CREATE INDEX IF NOT EXISTS idx_subscriptions_created ON subscriptions(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_subscriptions_client ON subscriptions(client_ip);
CREATE INDEX IF NOT EXISTS idx_subscriptions_wsi ON subscriptions(wsi_pointer);
CREATE INDEX IF NOT EXISTS idx_subscriptions_active_log ON subscriptions(event_type, ended_at, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_subscription_metrics_date ON subscription_metrics(date DESC);
CREATE TABLE IF NOT EXISTS ip_bans (
ip TEXT PRIMARY KEY,
failure_count INTEGER NOT NULL DEFAULT 0,
ban_count INTEGER NOT NULL DEFAULT 0,
banned_until BIGINT NOT NULL DEFAULT 0,
first_failure BIGINT NOT NULL DEFAULT 0,
has_authed_successfully INTEGER NOT NULL DEFAULT 0,
last_success_at BIGINT NOT NULL DEFAULT 0,
total_connections INTEGER NOT NULL DEFAULT 0,
total_failures INTEGER NOT NULL DEFAULT 0,
total_successes INTEGER NOT NULL DEFAULT 0,
first_seen BIGINT NOT NULL DEFAULT 0,
updated_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())::BIGINT,
idle_failure_count INTEGER NOT NULL DEFAULT 0,
idle_ban_count INTEGER NOT NULL DEFAULT 0,
idle_banned_until BIGINT NOT NULL DEFAULT 0,
idle_first_failure BIGINT NOT NULL DEFAULT 0
);
INSERT INTO schema_info(key, value, updated_at)
VALUES ('version', '2', EXTRACT(EPOCH FROM NOW())::BIGINT)
ON CONFLICT (key) DO UPDATE SET
value = EXCLUDED.value,
updated_at = EXCLUDED.updated_at;
COMMIT;
+95
View File
@@ -0,0 +1,95 @@
#ifndef SQLITE_DB_OPS_H
#define SQLITE_DB_OPS_H
#include "db_ops.h"
typedef struct sqlite_db_stmt sqlite_db_stmt_t;
int sqlite_db_init(const char* connection_string);
void sqlite_db_close(void);
int sqlite_db_is_available(void);
const char* sqlite_db_last_error(void);
const char* sqlite_db_get_database_path(void);
int sqlite_db_set_thread_connection(void* connection);
void sqlite_db_clear_thread_connection(void);
int sqlite_db_open_worker_connection(const char* db_path, void** out_connection);
void sqlite_db_close_worker_connection(void* connection);
int sqlite_db_prepare(const char* sql, sqlite_db_stmt_t** out_stmt);
int sqlite_db_bind_text_param(sqlite_db_stmt_t* stmt, int index, const char* value);
int sqlite_db_bind_int_param(sqlite_db_stmt_t* stmt, int index, int value);
int sqlite_db_bind_int64_param(sqlite_db_stmt_t* stmt, int index, long long value);
int sqlite_db_step_stmt(sqlite_db_stmt_t* stmt);
int sqlite_db_reset_stmt(sqlite_db_stmt_t* stmt);
const char* sqlite_db_column_text_value(sqlite_db_stmt_t* stmt, int col);
int sqlite_db_column_int_value(sqlite_db_stmt_t* stmt, int col);
long long sqlite_db_column_int64_value(sqlite_db_stmt_t* stmt, int col);
double sqlite_db_column_double_value(sqlite_db_stmt_t* stmt, int col);
void sqlite_db_finalize_stmt(sqlite_db_stmt_t* stmt);
int sqlite_db_log_subscription_created(const char* sub_id, const char* wsi_ptr,
const char* client_ip, const char* filter_json);
int sqlite_db_log_subscription_closed(const char* sub_id, const char* client_ip);
int sqlite_db_log_subscription_disconnected(const char* client_ip);
int sqlite_db_update_subscription_events_sent(const char* sub_id, int events_sent);
int sqlite_db_cleanup_orphaned_subscriptions(void);
int sqlite_db_get_event_pubkey(const char* event_id, char* pubkey_out, size_t pubkey_out_size);
int sqlite_db_delete_event_by_id(const char* event_id, const char* requester_pubkey);
int sqlite_db_delete_events_by_address(const char* pubkey, int kind,
const char* d_tag, long before_timestamp);
int sqlite_db_is_pubkey_blacklisted(const char* pubkey);
int sqlite_db_is_hash_blacklisted(const char* resource_hash);
int sqlite_db_is_pubkey_whitelisted(const char* pubkey);
int sqlite_db_count_active_whitelist_rules(void);
int sqlite_db_count_with_sql(const char* sql, const char** bind_params, int bind_param_count, int* out_count);
char* sqlite_db_execute_readonly_query_json(const char* query, const char* request_id,
char* error_message, size_t error_size,
int max_rows, int timeout_ms);
int sqlite_db_get_total_event_count_ll(long long* out_count);
int sqlite_db_get_event_count_since(time_t cutoff, long long* out_count);
cJSON* sqlite_db_get_event_kind_distribution_rows(long long* out_total_events);
cJSON* sqlite_db_get_top_pubkeys_rows(int limit);
cJSON* sqlite_db_get_subscription_details_rows(void);
cJSON* sqlite_db_get_all_config_rows(void);
char* sqlite_db_get_config_value_dup(const char* key);
int sqlite_db_set_config_value_full(const char* key, const char* value, const char* data_type,
const char* description, const char* category, int requires_restart);
int sqlite_db_update_config_value_only(const char* key, const char* value);
int sqlite_db_upsert_config_value(const char* key, const char* value, const char* data_type);
int sqlite_db_store_relay_private_key_hex(const char* relay_privkey_hex);
char* sqlite_db_get_relay_private_key_hex_dup(void);
int sqlite_db_store_config_event(const cJSON* event);
int sqlite_db_insert_event_with_json(const char* id, const char* pubkey, long long created_at,
int kind, const char* event_type, const char* content,
const char* sig, const char* tags_json, const char* event_json,
int* out_step_rc, int* out_extended_errcode);
int sqlite_db_get_event_time_bounds(long long* out_min_created_at, long long* out_max_created_at);
int sqlite_db_event_id_exists(const char* event_id, int* out_exists);
cJSON* sqlite_db_retrieve_event_by_id(const char* event_id);
char* sqlite_db_get_latest_event_pubkey_for_kind_dup(int kind);
int sqlite_db_get_config_row_count(int* out_count);
int sqlite_db_store_event_tags_cjson(const char* event_id, const cJSON* tags);
int sqlite_db_populate_event_tags_from_existing(void);
int sqlite_db_add_auth_rule(const char* rule_type, const char* pattern_type, const char* pattern_value);
int sqlite_db_remove_auth_rule(const char* rule_type, const char* pattern_type, const char* pattern_value);
int sqlite_db_delete_wot_whitelist_rules(void);
int sqlite_db_count_wot_whitelist_rules(void);
int sqlite_db_table_exists(const char* table_name, int* out_exists);
char* sqlite_db_get_schema_version_dup(void);
int sqlite_db_exec_sql(const char* sql);
int sqlite_db_wal_checkpoint_passive(void);
int sqlite_db_wal_checkpoint_truncate(void);
#endif // SQLITE_DB_OPS_H
+74 -5
View File
@@ -497,11 +497,26 @@ static int event_is_async_eligible(cJSON* event, int* out_kind, char event_id_ou
int kind = (int)cJSON_GetNumberValue(kind_obj);
// Keep admin command path on main thread for existing behavior.
if (kind == 23456) {
// Keep special command/DM/protected paths on main thread for existing behavior.
if (kind == 23456 || kind == 1059 || kind == 14) {
return 0;
}
// NIP-70 protected events must stay on sync path so auth checks run.
cJSON* tags_obj = cJSON_GetObjectItemCaseSensitive(event, "tags");
if (tags_obj && cJSON_IsArray(tags_obj)) {
cJSON* tag = NULL;
cJSON_ArrayForEach(tag, tags_obj) {
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 1) {
cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
if (tag_name && cJSON_IsString(tag_name) &&
strcmp(cJSON_GetStringValue(tag_name), "-") == 0) {
return 0;
}
}
}
}
strncpy(event_id_out, event_id, 64);
event_id_out[64] = '\0';
*out_kind = kind;
@@ -3693,7 +3708,7 @@ int handle_count_message(const char* sub_id, cJSON* filters, struct lws *wsi, st
bind_param_capacity = 0;
// Build SQL COUNT query based on filter - exclude ephemeral events (kinds 20000-29999) from historical queries
char sql[1024] = "SELECT COUNT(*) FROM events WHERE 1=1 AND (kind < 20000 OR kind >= 30000)";
char sql[8192] = "SELECT COUNT(*) FROM events WHERE 1=1 AND (kind < 20000 OR kind >= 30000)";
char* sql_ptr = sql + strlen(sql);
int remaining = sizeof(sql) - strlen(sql);
@@ -3822,7 +3837,7 @@ int handle_count_message(const char* sub_id, cJSON* filters, struct lws *wsi, st
const char* filter_key = filter_item->string;
if (filter_key && filter_key[0] == '#' && strlen(filter_key) > 1) {
// This is a tag filter like "#e", "#p", etc.
const char* tag_name = filter_key + 1; // Get the tag name (e, p, t, type, etc.)
const char* tag_name = filter_key + 1; // e, p, t, type, ...
if (cJSON_IsArray(filter_item)) {
int tag_value_count = 0;
@@ -3834,7 +3849,50 @@ int handle_count_message(const char* sub_id, cJSON* filters, struct lws *wsi, st
}
}
if (tag_value_count > 0) {
// Use EXISTS with parameterized query
#ifdef DB_BACKEND_POSTGRES
// PostgreSQL path: JSONB containment against events.tags using GIN index.
// Match Nostr semantics: tag name + first value only, allowing extra trailing elements.
snprintf(sql_ptr, remaining, " AND (");
sql_ptr += strlen(sql_ptr);
remaining = sizeof(sql) - strlen(sql);
int clause_idx = 0;
for (int i = 0; i < cJSON_GetArraySize(filter_item); i++) {
cJSON* tag_value = cJSON_GetArrayItem(filter_item, i);
if (!cJSON_IsString(tag_value)) {
continue;
}
if (clause_idx > 0) {
snprintf(sql_ptr, remaining, " OR ");
sql_ptr += strlen(sql_ptr);
remaining = sizeof(sql) - strlen(sql);
}
snprintf(sql_ptr, remaining,
"tags @> jsonb_build_array(jsonb_build_array(?::text, ?::text))");
sql_ptr += strlen(sql_ptr);
remaining = sizeof(sql) - strlen(sql);
if (bind_param_count >= bind_param_capacity) {
bind_param_capacity = bind_param_capacity == 0 ? 16 : bind_param_capacity * 2;
bind_params = realloc(bind_params, bind_param_capacity * sizeof(char*));
}
bind_params[bind_param_count++] = strdup(tag_name);
if (bind_param_count >= bind_param_capacity) {
bind_param_capacity = bind_param_capacity == 0 ? 16 : bind_param_capacity * 2;
bind_params = realloc(bind_params, bind_param_capacity * sizeof(char*));
}
bind_params[bind_param_count++] = strdup(cJSON_GetStringValue(tag_value));
clause_idx++;
}
snprintf(sql_ptr, remaining, ")");
sql_ptr += strlen(sql_ptr);
remaining = sizeof(sql) - strlen(sql);
#else
// SQLite path: keep json_each/json_extract behavior.
snprintf(sql_ptr, remaining, " AND EXISTS (SELECT 1 FROM json_each(json(tags)) WHERE json_extract(value, '$[0]') = ? AND json_extract(value, '$[1]') IN (");
sql_ptr += strlen(sql_ptr);
remaining = sizeof(sql) - strlen(sql);
@@ -3869,6 +3927,7 @@ int handle_count_message(const char* sub_id, cJSON* filters, struct lws *wsi, st
bind_params[bind_param_count++] = strdup(cJSON_GetStringValue(tag_value));
}
}
#endif
}
}
}
@@ -3894,9 +3953,14 @@ int handle_count_message(const char* sub_id, cJSON* filters, struct lws *wsi, st
escaped_search[escaped_len] = '\0';
// Add search conditions for content and tags
#ifdef DB_BACKEND_POSTGRES
snprintf(sql_ptr, remaining, " AND (content ILIKE '%%%s%%' OR tags::text ILIKE '%%\"%s\"%%')",
escaped_search, escaped_search);
#else
// Use tags LIKE to search within the JSON string representation of tags
snprintf(sql_ptr, remaining, " AND (content LIKE '%%%s%%' OR tags LIKE '%%\"%s\"%%')",
escaped_search, escaped_search);
#endif
sql_ptr += strlen(sql_ptr);
remaining = sizeof(sql) - strlen(sql);
}
@@ -3918,6 +3982,11 @@ int handle_count_message(const char* sub_id, cJSON* filters, struct lws *wsi, st
remaining = sizeof(sql) - strlen(sql);
}
if (remaining <= 1) {
DEBUG_ERROR("COUNT SQL query exceeded internal buffer capacity; skipping filter");
continue;
}
// Submit COUNT query asynchronously to DB reader pool.
if (submit_count_query_async(async_state, sql, (const char**)bind_params, bind_param_count) != 0) {
DEBUG_ERROR("Failed to submit COUNT query async");
-18
View File
@@ -1,18 +0,0 @@
2026-04-01 09:57:22 - ==========================================
2026-04-01 09:57:22 - C-Relay Comprehensive Test Suite Runner
2026-04-01 09:57:22 - ==========================================
2026-04-01 09:57:22 - Relay URL: ws://127.0.0.1:8888
2026-04-01 09:57:22 - Log file: test_results_20260401_095722.log
2026-04-01 09:57:22 - Report file: test_report_20260401_095722.html
2026-04-01 09:57:22 -
2026-04-01 09:57:22 - Checking relay status at ws://127.0.0.1:8888...
2026-04-01 09:57:22 - \033[0;32m✓ Relay HTTP endpoint is accessible\033[0m
2026-04-01 09:57:22 -
2026-04-01 09:57:22 - Starting comprehensive test execution...
2026-04-01 09:57:22 -
2026-04-01 09:57:22 - \033[0;34m=== SECURITY TEST SUITES ===\033[0m
2026-04-01 09:57:22 - ==========================================
2026-04-01 09:57:22 - Running Test Suite: SQL Injection Tests
2026-04-01 09:57:22 - Description: Comprehensive SQL injection vulnerability testing
2026-04-01 09:57:22 - ==========================================
2026-04-01 09:57:22 - \033[0;31mERROR: Test script sql_injection_tests.sh not found\033[0m
-18
View File
@@ -1,18 +0,0 @@
2026-04-01 11:01:47 - ==========================================
2026-04-01 11:01:47 - C-Relay Comprehensive Test Suite Runner
2026-04-01 11:01:47 - ==========================================
2026-04-01 11:01:47 - Relay URL: ws://127.0.0.1:8888
2026-04-01 11:01:47 - Log file: test_results_20260401_110147.log
2026-04-01 11:01:47 - Report file: test_report_20260401_110147.html
2026-04-01 11:01:47 -
2026-04-01 11:01:47 - Checking relay status at ws://127.0.0.1:8888...
2026-04-01 11:01:47 - \033[0;32m✓ Relay HTTP endpoint is accessible\033[0m
2026-04-01 11:01:47 -
2026-04-01 11:01:47 - Starting comprehensive test execution...
2026-04-01 11:01:47 -
2026-04-01 11:01:47 - \033[0;34m=== SECURITY TEST SUITES ===\033[0m
2026-04-01 11:01:47 - ==========================================
2026-04-01 11:01:47 - Running Test Suite: SQL Injection Tests
2026-04-01 11:01:47 - Description: Comprehensive SQL injection vulnerability testing
2026-04-01 11:01:47 - ==========================================
2026-04-01 11:01:47 - \033[0;31mERROR: Test script sql_injection_tests.sh not found\033[0m
+18
View File
@@ -0,0 +1,18 @@
2026-04-02 11:34:06 - ==========================================
2026-04-02 11:34:06 - C-Relay-PG Comprehensive Test Suite Runner
2026-04-02 11:34:06 - ==========================================
2026-04-02 11:34:06 - Relay URL: ws://127.0.0.1:8888
2026-04-02 11:34:06 - Log file: test_results_20260402_113406.log
2026-04-02 11:34:06 - Report file: test_report_20260402_113406.html
2026-04-02 11:34:06 -
2026-04-02 11:34:06 - Checking relay status at ws://127.0.0.1:8888...
2026-04-02 11:34:07 - \033[0;32m✓ Relay HTTP endpoint is accessible\033[0m
2026-04-02 11:34:07 -
2026-04-02 11:34:07 - Starting comprehensive test execution...
2026-04-02 11:34:07 -
2026-04-02 11:34:07 - \033[0;34m=== SECURITY TEST SUITES ===\033[0m
2026-04-02 11:34:07 - ==========================================
2026-04-02 11:34:07 - Running Test Suite: SQL Injection Tests
2026-04-02 11:34:07 - Description: Comprehensive SQL injection vulnerability testing
2026-04-02 11:34:07 - ==========================================
2026-04-02 11:34:07 - \033[0;31mERROR: Test script sql_injection_tests.sh not found\033[0m
+24 -3
View File
@@ -28,7 +28,28 @@ echo "✓ nak command found"
# Check if relay is running by testing connection
echo "Testing relay connection..."
if ! timeout 5 nc -z localhost 8888 2>/dev/null; then
relay_up=0
if command -v nc &> /dev/null; then
if timeout 5 nc -z localhost 8888 2>/dev/null; then
relay_up=1
fi
fi
# Fallback for environments where netcat probing is unreliable/unsupported
if [ "$relay_up" -eq 0 ] && command -v websocat &> /dev/null; then
if timeout 5 bash -c "echo 'ping' | websocat -n1 $RELAY_URL >/dev/null 2>&1"; then
relay_up=1
fi
fi
# Additional fallback: POSIX TCP open check without external tools
if [ "$relay_up" -eq 0 ]; then
if timeout 5 bash -c "exec 3<>/dev/tcp/localhost/8888" 2>/dev/null; then
relay_up=1
fi
fi
if [ "$relay_up" -eq 0 ]; then
echo "ERROR: Relay does not appear to be running on localhost:8888"
echo "Please start the relay first with: ./make_and_restart_relay.sh"
exit 1
@@ -91,10 +112,10 @@ sleep 3
# Query for gift wrap responses from the relay (kind 1059 events authored by relay)
echo ""
echo "Querying for gift wrap responses from relay..."
echo "Command: nak req -k 1059 --authors $RELAY_PUBLIC_KEY $RELAY_URL"
echo "Command: nak req -k 1059 -a $RELAY_PUBLIC_KEY $RELAY_URL"
# Capture the output and filter for events
RESPONSE_OUTPUT=$(nak req -k 1059 --authors "$RELAY_PUBLIC_KEY" "$RELAY_URL" 2>&1)
RESPONSE_OUTPUT=$(nak req -k 1059 -a "$RELAY_PUBLIC_KEY" "$RELAY_URL" 2>&1)
REQ_EXIT_CODE=$?
echo ""
+26 -26
View File
@@ -1,5 +1,5 @@
=== NIP-42 Authentication Test Started ===
2026-04-01 10:00:44 - Starting NIP-42 authentication tests
2026-04-02 11:37:51 - Starting NIP-42 authentication tests
[INFO] === Starting NIP-42 Authentication Tests ===
[INFO] Checking dependencies...
[WARNING] wscat not found. Some manual WebSocket tests will be skipped
@@ -7,7 +7,7 @@
[SUCCESS] Dependencies check complete
[INFO] Test 1: Checking NIP-42 support in relay info
[SUCCESS] NIP-42 is advertised in supported NIPs
2026-04-01 10:00:44 - Supported NIPs: 1,2,4,9,11,12,13,15,16,20,22,33,40,42,50,70
2026-04-02 11:37:51 - Supported NIPs: 1,2,4,9,11,12,13,15,16,20,22,33,40,42,50,70
[INFO] Test 2: Testing AUTH challenge generation
[WARNING] Could not extract admin private key from relay.log - using manual test approach
[INFO] Manual test: Connect to relay and send an event without auth to trigger challenge
@@ -15,8 +15,8 @@
[INFO] Generated test keypair: test_pubkey
[INFO] Attempting to publish event without authentication...
[INFO] Publishing test event to relay...
2026-04-01 10:00:45 - Event publish result: connecting to localhost:8888... ok.
{"kind":1,"id":"28174ed72f9ef5484a6596db6c6be181d2ed10e46d8c22a9f7a267becbc9a47a","pubkey":"b5f9adc3b362c0e81569a46648a3996708d6b47754d36c617683f18e1f1daf6b","created_at":1775052045,"tags":[],"content":"NIP-42 test event - should require auth","sig":"0041347b233a21d6542a263634f52f8ca7e3b5ecf07bc37c86f390f1a7ab0bc2051306b14aa70edff020bd7311b259d03c2c35a035b285f9e19051a924546666"}
2026-04-02 11:37:51 - Event publish result: connecting to localhost:8888... ok.
{"kind":1,"id":"6ac010a9c55b7328ba3bb5a88015328531fa88df7f65c0ce7378605ea24b8bf5","pubkey":"d3615971b581cc9e1e8bf2ee0687eff0d760bf3ce8bf718287b5fce686ca0fbb","created_at":1775144271,"tags":[],"content":"NIP-42 test event - should require auth","sig":"8b1cdd42a4fda7c48dfabf7f7a362363280cc4e3d304c7bca94e9778b674a8a71d9db22beff1db5dca398c916e30678849c797b55482a0e5686fd482b203e978"}
publishing to ws://localhost:8888... success.
[SUCCESS] Relay requested authentication as expected
[INFO] Test 4: Testing WebSocket AUTH message handling
@@ -26,50 +26,50 @@ publishing to ws://localhost:8888... success.
[WARNING] Could not retrieve configuration events
[INFO] Test 6: Testing NIP-42 performance and stability
[INFO] Testing multiple authentication attempts...
2026-04-01 10:00:46 - Attempt 1: .195528002s - connecting to localhost:8888... ok.
{"kind":1,"id":"72526c57e50d721bfcdcca23856f2c5d3021c3100f0121538d9d829042e9b8b5","pubkey":"29af457d7eea750c11c836dfbeed3076fa0bf1f1b67a404ee4c25733d9b54238","created_at":1775052046,"tags":[],"content":"Performance test event 1","sig":"1a10db86a39aaf47bda6d0cca5f888691bae32bfd85df8c178dddbf24861503a491bfe4da8d66dfe3e5e0afec7bf8644e7eb69c9f051c601673ae13284f9ab31"}
2026-04-02 11:37:52 - Attempt 1: .225656996s - connecting to localhost:8888... ok.
{"kind":1,"id":"c1fba7786a94a9dc4dd4947376a12e070839a0d8c63654b711ae508a019da600","pubkey":"9949df0f774a6b0df7ca763d146d85a37844ec0a70f37aac0286371fb2c39c38","created_at":1775144272,"tags":[],"content":"Performance test event 1","sig":"3517c788b91c642cfe08c491eef3720e353c1a4acc6cb2ccf7528138be32f67bdfa4ab09d1001a2a55392b5a46051f085f9eda7aa4c748663b31223d824e0b40"}
publishing to ws://localhost:8888... success.
2026-04-01 10:00:46 - Attempt 2: .185198180s - connecting to localhost:8888... ok.
{"kind":1,"id":"84eeb907205806bb88fc353bb83f1bf0149f3c93c18695306d8480d36752878b","pubkey":"29af457d7eea750c11c836dfbeed3076fa0bf1f1b67a404ee4c25733d9b54238","created_at":1775052046,"tags":[],"content":"Performance test event 2","sig":"e568dbca3113966f61e69eff05576d296fac42b5ff2ab7341778acc02cf6e9440b59bf651a8a2995d89180a62ec20ffc1143e211cf2960c5abaa358a82992ac6"}
2026-04-02 11:37:53 - Attempt 2: .203239970s - connecting to localhost:8888... ok.
{"kind":1,"id":"e641632699daf7452c4a4d850bd5d86dbd933087ab65561a05d7d97792b0805b","pubkey":"9949df0f774a6b0df7ca763d146d85a37844ec0a70f37aac0286371fb2c39c38","created_at":1775144273,"tags":[],"content":"Performance test event 2","sig":"4ae948baab0fc42c4645ca6159fa2d6f0b3f75cdf460b6655d2b0dff68d069083342992387d76bed36ceda5ddd297ff26cf57b6ef444271a13b09851945dfa75"}
publishing to ws://localhost:8888... success.
2026-04-01 10:00:47 - Attempt 3: .197159540s - connecting to localhost:8888... ok.
{"kind":1,"id":"0874716ba0b2a845d379e703226513f90db6228ce958369f4dce5aff3df64c5a","pubkey":"29af457d7eea750c11c836dfbeed3076fa0bf1f1b67a404ee4c25733d9b54238","created_at":1775052046,"tags":[],"content":"Performance test event 3","sig":"e74e885eb464998bddf655c24c95a18d7299908c63fb82a47f8d14af65992f90e73794a9f289b550bc1dec1c0298112d2b25941be7e26e3f17dd66b4c5dc742e"}
2026-04-02 11:37:53 - Attempt 3: .213087727s - connecting to localhost:8888... ok.
{"kind":1,"id":"2ae84c0da0c6d1157bac6d090cc8cfd0cc925d671bc85ddb400e9763431c719f","pubkey":"9949df0f774a6b0df7ca763d146d85a37844ec0a70f37aac0286371fb2c39c38","created_at":1775144273,"tags":[],"content":"Performance test event 3","sig":"dc0322eb09771241bd9dafb44b58b8174ed3eec724f021371b1ec73b78919248f37aa21bb16148db123a47374dc3caa734091a6bcf91a713bf0565d73985c788"}
publishing to ws://localhost:8888... success.
2026-04-01 10:00:47 - Attempt 4: .202520319s - connecting to localhost:8888... ok.
{"kind":1,"id":"5614d1b795cd028e096720418bb4dbc64870d442c6295b9bb2931564a332099a","pubkey":"29af457d7eea750c11c836dfbeed3076fa0bf1f1b67a404ee4c25733d9b54238","created_at":1775052047,"tags":[],"content":"Performance test event 4","sig":"1c0645a791ab9df573b152b782884bcb5ec2a18b8f4e1c18d7dc2175977b7d1d572674169ea1d2b3e3fa8d228de90f6cc281d56a3ef471ba4f9510e920ed586e"}
2026-04-02 11:37:54 - Attempt 4: .204430585s - connecting to localhost:8888... ok.
{"kind":1,"id":"2460c60bbb8fc4aa556276e7f6b694d8729c142e4a6e596c5d273d4f83fc93d4","pubkey":"9949df0f774a6b0df7ca763d146d85a37844ec0a70f37aac0286371fb2c39c38","created_at":1775144274,"tags":[],"content":"Performance test event 4","sig":"79ff1fec916ef892b449e7700f23158d7cab17c0e999ec4cd3afbceedc4bba6d83d342d6fd8474b0443cf0d68e5166b8e7a578f7cb303db7547a8b69978c57df"}
publishing to ws://localhost:8888... success.
2026-04-01 10:00:47 - Attempt 5: .222530325s - connecting to localhost:8888... ok.
{"kind":1,"id":"9139f1eb39c31c43cb9f02e60a0f75a0f99f6c7d9693a69a225c8b3e530f774a","pubkey":"29af457d7eea750c11c836dfbeed3076fa0bf1f1b67a404ee4c25733d9b54238","created_at":1775052047,"tags":[],"content":"Performance test event 5","sig":"bd245a7629c33619c0ed426f631e0c2413adb4488c281bea09886ceb6407b40e247d42f82e374e2f53ee5281f0d8361661a4c613c6414c59d09e5bb23eeeb02d"}
2026-04-02 11:37:54 - Attempt 5: .209970671s - connecting to localhost:8888... ok.
{"kind":1,"id":"cbb299987558cf81865ef25fb8d0a709ec7a5ea5efabda6dc471e0c4cf69e08c","pubkey":"9949df0f774a6b0df7ca763d146d85a37844ec0a70f37aac0286371fb2c39c38","created_at":1775144274,"tags":[],"content":"Performance test event 5","sig":"76b0c6457b26091deabc29369bbe583eed608051ac1e87e9c72be075cf09e65d8d499e1ef2746d926014c1a444da36e7e7a2b630abc5956ea7d685e8af5790db"}
publishing to ws://localhost:8888... success.
[SUCCESS] Performance test completed: 5/5 successful responses
[INFO] Test 7: Testing kind-specific NIP-42 authentication requirements
[INFO] Generated test keypair for kind-specific tests: test_pubkey
[INFO] Testing kind 1 event (regular note) - should work without authentication...
2026-04-01 10:00:48 - Kind 1 event result: connecting to localhost:8888... ok.
{"kind":1,"id":"def386c66576a91f144a7321e9520a3ed46f9fecc56d9e79d32a162f249cace9","pubkey":"7c9b00749c966e50d18d4b807b44d4612bf9a8f67925fac3f00d21918b6e7f9c","created_at":1775052048,"tags":[],"content":"Regular note - should not require auth","sig":"7ee8fbf45e96d040df1a688e9bf028faaa49b81b26eeae6452b3a32cd9fd941c27b41734f14268d3deef14dbb19e514d29789211fffb27bfa9b73d20cddcc83e"}
2026-04-02 11:37:55 - Kind 1 event result: connecting to localhost:8888... ok.
{"kind":1,"id":"602b7d29986c41d636ec346b99da3b66f350e39a5d76cd1677234564b541832c","pubkey":"b7f954286120b29d57ca8fff6336dd0b8642c1a358985e19e6309361fbc05155","created_at":1775144275,"tags":[],"content":"Regular note - should not require auth","sig":"7b9e2b13dae0727ce4e759b4f98d5e6b86619a0f0c1c1a89cb37cf1ee33513e3f0d3983508843c525aecff8d623f3a60fd072772a577be4832451c2daee032a4"}
publishing to ws://localhost:8888... success.
[SUCCESS] Kind 1 event accepted without authentication (correct behavior)
[INFO] Testing kind 4 event (direct message) - should require authentication...
2026-04-01 10:00:58 - Kind 4 event result: connecting to localhost:8888... ok.
{"kind":4,"id":"a2f2d176a835d17a0c9229c0ce3b1ba61a5eb81308e4a3b3446634f468721238","pubkey":"7c9b00749c966e50d18d4b807b44d4612bf9a8f67925fac3f00d21918b6e7f9c","created_at":1775052048,"tags":[["p,test_pubkey"]],"content":"This is a direct message - should require auth","sig":"441f9caba6ebcec6b6c55d13ffd9571f3fe441a54ddd0692f2868429a5087378d791c97289b8b990560dcf40f5c66e86ba6b41f1de78220a5f389c1f119bb887"}
2026-04-02 11:38:05 - Kind 4 event result: connecting to localhost:8888... ok.
{"kind":4,"id":"d4aabea6fc355944df9b498393f2be870070727230c8a84c3136589405c303be","pubkey":"b7f954286120b29d57ca8fff6336dd0b8642c1a358985e19e6309361fbc05155","created_at":1775144275,"tags":[["p,test_pubkey"]],"content":"This is a direct message - should require auth","sig":"ce224297726a8adcd2d1473189e530ada98cde5db58df27b255b5cf389efc0710c773f2118ca4e83246ff437e54a2e504ca3dd5117c08ce3a576067360ae783e"}
publishing to ws://localhost:8888...
[SUCCESS] Kind 4 event requested authentication (correct behavior for DMs)
[INFO] Testing kind 14 event (chat message) - should require authentication...
2026-04-01 10:01:09 - Kind 14 event result: connecting to localhost:8888... ok.
{"kind":14,"id":"e234a6faa63f95cd24f8596fbfac573730533bb0abcde47b4d341ff0acace657","pubkey":"7c9b00749c966e50d18d4b807b44d4612bf9a8f67925fac3f00d21918b6e7f9c","created_at":1775052059,"tags":[["p,test_pubkey"]],"content":"Chat message - should require auth","sig":"fd6ba0ddaa7e2379aa49c5cda6599d0a38264799b228a8e907b948bef645080603f7f910c587f6cfb1ef1bac946c47dc681fdab6abd248376cd552bcd535ef9e"}
2026-04-02 11:38:15 - Kind 14 event result: connecting to localhost:8888... ok.
{"kind":14,"id":"833fe0aaf161797e93928b5d177981189abaa64ad989d2fb0a80a3bb6d9eec41","pubkey":"b7f954286120b29d57ca8fff6336dd0b8642c1a358985e19e6309361fbc05155","created_at":1775144285,"tags":[["p,test_pubkey"]],"content":"Chat message - should require auth","sig":"53c9bb966444db3ef7ce586240be01b8a89db8e171a7fe63680864d1d666d5d64c81eecf048f3c708b38cfa1a0cc4a17f64d1c5c188edf7edd3e256dcd035ca3"}
publishing to ws://localhost:8888...
[SUCCESS] Kind 14 event requested authentication (correct behavior for DMs)
[INFO] Testing other event kinds - should work without authentication...
2026-04-01 10:01:09 - Kind 0 event result: connecting to localhost:8888... ok.
{"kind":0,"id":"d87b69e8a2c8f6d98229b55cde3f0fe1b70c6cafd080e4462209849338eafc76","pubkey":"7c9b00749c966e50d18d4b807b44d4612bf9a8f67925fac3f00d21918b6e7f9c","created_at":1775052069,"tags":[],"content":"Test event kind 0 - should not require auth","sig":"e3222bd4882c881d039e6790a6a8d9f8e20a71d0f9d5089e98604acaa4feff7c1b1b2d9b4171d640da3bba955408a9391a0fd52334cc4a4165fa932e5163f9dd"}
2026-04-02 11:38:16 - Kind 0 event result: connecting to localhost:8888... ok.
{"kind":0,"id":"85ee336d00674d9eeeea48e5e00f5e8299e42317055672553d680c6619b8eb2d","pubkey":"b7f954286120b29d57ca8fff6336dd0b8642c1a358985e19e6309361fbc05155","created_at":1775144296,"tags":[],"content":"Test event kind 0 - should not require auth","sig":"9393835c49a75417c2b840b6ff818323707ffd9ea266ac34e32c144ecdf524ee0ba7a5947a5b7dd1cd09f0ef1f1bb3c024d92de12e207730e163d2632b3be79b"}
publishing to ws://localhost:8888... success.
[SUCCESS] Kind 0 event accepted without authentication (correct)
2026-04-01 10:01:09 - Kind 3 event result: connecting to localhost:8888... ok.
{"kind":3,"id":"ac0d45d7db2818bb5e674d4760711349928cf3d5dc8f1fc3717d9e7274e26fb0","pubkey":"7c9b00749c966e50d18d4b807b44d4612bf9a8f67925fac3f00d21918b6e7f9c","created_at":1775052069,"tags":[],"content":"Test event kind 3 - should not require auth","sig":"b65b317c81bc733ee2ff5921430b34d722e8d177147c5d1cddf14036573223582a3b7232810fc13169eb7a61ab3b99cd37ce13991673fce142d1ea10c702305e"}
2026-04-02 11:38:16 - Kind 3 event result: connecting to localhost:8888... ok.
{"kind":3,"id":"bebb07003d416013c7b32dbcd9c98f53e103e92e1edb13ab9759a64d649bda2b","pubkey":"b7f954286120b29d57ca8fff6336dd0b8642c1a358985e19e6309361fbc05155","created_at":1775144296,"tags":[],"content":"Test event kind 3 - should not require auth","sig":"c56cb6cc38fa7db7b847d41ca9c79172cf044710912a84696c157ad416e5e7dc43b7162a8bbff493c5efee7d8babc3434d9e26143c13f9a373437a0f6c1a2f53"}
publishing to ws://localhost:8888... success.
[SUCCESS] Kind 3 event accepted without authentication (correct)
2026-04-01 10:01:10 - Kind 7 event result: connecting to localhost:8888... ok.
{"kind":7,"id":"ad7e77948a4742abfca2d63c5bb748f3db3940c85841f521b3b7d634bf09955a","pubkey":"7c9b00749c966e50d18d4b807b44d4612bf9a8f67925fac3f00d21918b6e7f9c","created_at":1775052070,"tags":[],"content":"Test event kind 7 - should not require auth","sig":"8a5e5a03956cec04d56934314a0876fccbd221546432aaccdbc249b5f9cedc221219abf6e2856b98675de205afaa0e91ea642933ec6724576a40db43d17aa758"}
2026-04-02 11:38:17 - Kind 7 event result: connecting to localhost:8888... ok.
{"kind":7,"id":"091fd348650f84c252e5476b8a8166b2703fbb483c45fe21f83c9e7ec7d4b281","pubkey":"b7f954286120b29d57ca8fff6336dd0b8642c1a358985e19e6309361fbc05155","created_at":1775144296,"tags":[],"content":"Test event kind 7 - should not require auth","sig":"af57b5e8793e46077d518d8692881884372793588d5fe2cc9d825d27602e87112d575ebf4b41024538d4a46cc43dc39fcfe532db9c2cd4262941141b85b95206"}
publishing to ws://localhost:8888... success.
[SUCCESS] Kind 7 event accepted without authentication (correct)
[INFO] Kind-specific authentication test completed
+733
View File
@@ -0,0 +1,733 @@
2026-04-02 11:34:15 - ==========================================
2026-04-02 11:34:15 - C-Relay-PG Comprehensive Test Suite Runner
2026-04-02 11:34:15 - ==========================================
2026-04-02 11:34:15 - Relay URL: ws://127.0.0.1:8888
2026-04-02 11:34:15 - Log file: test_results_20260402_113414.log
2026-04-02 11:34:15 - Report file: test_report_20260402_113415.html
2026-04-02 11:34:15 -
2026-04-02 11:34:15 - Checking relay status at ws://127.0.0.1:8888...
2026-04-02 11:34:15 - \033[0;32m✓ Relay HTTP endpoint is accessible\033[0m
2026-04-02 11:34:15 -
2026-04-02 11:34:15 - Starting comprehensive test execution...
2026-04-02 11:34:15 -
2026-04-02 11:34:15 - \033[0;34m=== SECURITY TEST SUITES ===\033[0m
2026-04-02 11:34:15 - ==========================================
2026-04-02 11:34:15 - Running Test Suite: SQL Injection Tests
2026-04-02 11:34:15 - Description: Comprehensive SQL injection vulnerability testing
2026-04-02 11:34:15 - ==========================================
==========================================
C-Relay-PG 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 (rejected with error)
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 (rejected with error)
Testing COUNT with authors payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: 0x53514C5F494E4A454354494F4E... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with authors payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (rejected with error)
Testing COUNT with search payload: J1NSTCBJTkpFQ1RJT04gLS0=... PASSED - SQL injection blocked (rejected with error)
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.
2026-04-02 11:34:24 - \033[0;32m✓ SQL Injection Tests PASSED\033[0m (Duration: 9s)
2026-04-02 11:34:24 - ==========================================
2026-04-02 11:34:24 - Running Test Suite: Filter Validation Tests
2026-04-02 11:34:24 - Description: Input validation for REQ and COUNT messages
2026-04-02 11:34:24 - ==========================================
=== C-Relay-PG 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!
2026-04-02 11:34:27 - \033[0;32m✓ Filter Validation Tests PASSED\033[0m (Duration: 3s)
2026-04-02 11:34:27 - ==========================================
2026-04-02 11:34:27 - Running Test Suite: Subscription Validation Tests
2026-04-02 11:34:27 - Description: Subscription ID and message validation
2026-04-02 11:34:27 - ==========================================
Testing subscription ID validation fixes...
Testing malformed subscription IDs...
Empty ID test: Connection failed (expected)
Long ID test: Connection failed (expected)
Invalid chars test: Connection failed (expected)
NULL ID test: Connection failed (expected)
Valid ID test: Failed
Testing CLOSE message validation...
CLOSE empty ID test: Connection failed (expected)
CLOSE valid ID test: Failed
Subscription validation tests completed.
2026-04-02 11:34:27 - \033[0;32m✓ Subscription Validation Tests PASSED\033[0m (Duration: 0s)
2026-04-02 11:34:27 - ==========================================
2026-04-02 11:34:27 - Running Test Suite: Memory Corruption Tests
2026-04-02 11:34:27 - Description: Buffer overflow and memory safety testing
2026-04-02 11:34:27 - ==========================================
==========================================
C-Relay-PG 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_1775144068210818169",{"kind":1,"id":"a36280322b87a9e9d6687e79d578e1059c9ee016d0e7a7b6e2676943b830955e","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1775142792,"tags":[],"content":"whoop ","sig":"7a570204107190614c6b70cb36b22c26138a7c45aa807fefb5c504f657b5335d68cf0e6ea8f0845155f4650434c6d52053f92ceaf85637f3a57277fcdad6ce3e"}]
["EVENT","concurrent_1775144068210818169",{"kind":1,"id":"a36280322b87a9e9d6687e79d578e1059c9ee016d0e7a7b6e2676943b830955e","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1775142792,"tags":[],"content":"whoop ","sig":"7a570204107190614c6b70cb36b22c26138a7c45aa807fefb5c504f657b5335d68cf0e6ea8f0845155f4650434c6d52053f92ceaf85637f3a57277fcdad6ce3e"}]
["EVENT","concurrent_1775144068210818169",{"kind":1,"id":"a36280322b87a9e9d6687e79d578e1059c9ee016d0e7a7b6e2676943b830955e","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1775142792,"tags":[],"content":"whoop ","sig":"7a570204107190614c6b70cb36b22c26138a7c45aa807fefb5c504f657b5335d68cf0e6ea8f0845155f4650434c6d52053f92ceaf85637f3a57277fcdad6ce3e"}]
["EVENT","concurrent_1775144068210818169",{"kind":1,"id":"a36280322b87a9e9d6687e79d578e1059c9ee016d0e7a7b6e2676943b830955e","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1775142792,"tags":[],"content":"whoop ","sig":"7a570204107190614c6b70cb36b22c26138a7c45aa807fefb5c504f657b5335d68cf0e6ea8f0845155f4650434c6d52053f92ceaf85637f3a57277fcdad6ce3e"}]
["EVENT","concurrent_1775144068210818169",{"kind":1,"id":"a36280322b87a9e9d6687e79d578e1059c9ee016d0e7a7b6e2676943b830955e","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1775142792,"tags":[],"content":"whoop ","sig":"7a570204107190614c6b70cb36b22c26138a7c45aa807fefb5c504f657b5335d68cf0e6ea8f0845155f4650434c6d52053f92ceaf85637f3a57277fcdad6ce3e"}]
["EVENT","concurrent_1775144068210818169",{"kind":1,"id":"a36280322b87a9e9d6687e79d578e1059c9ee016d0e7a7b6e2676943b830955e","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1775142792,"tags":[],"content":"whoop ","sig":"7a570204107190614c6b70cb36b22c26138a7c45aa807fefb5c504f657b5335d68cf0e6ea8f0845155f4650434c6d52053f92ceaf85637f3a57277fcdad6ce3e"}]
["EVENT","concurrent_1775144068210818169",{"kind":1,"id":"a36280322b87a9e9d6687e79d578e1059c9ee016d0e7a7b6e2676943b830955e","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1775142792,"tags":[],"content":"whoop ","sig":"7a570204107190614c6b70cb36b22c26138a7c45aa807fefb5c504f657b5335d68cf0e6ea8f0845155f4650434c6d52053f92ceaf85637f3a57277fcdad6ce3e"}]
["EVENT","concurrent_1775144068210818169",{"kind":1,"id":"a36280322b87a9e9d6687e79d578e1059c9ee016d0e7a7b6e2676943b830955e","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1775142792,"tags":[],"content":"whoop ","sig":"7a570204107190614c6b70cb36b22c26138a7c45aa807fefb5c504f657b5335d68cf0e6ea8f0845155f4650434c6d52053f92ceaf85637f3a57277fcdad6ce3e"}]
["EVENT","concurrent_1775144068210818169",{"kind":1,"id":"a36280322b87a9e9d6687e79d578e1059c9ee016d0e7a7b6e2676943b830955e","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1775142792,"tags":[],"content":"whoop ","sig":"7a570204107190614c6b70cb36b22c26138a7c45aa807fefb5c504f657b5335d68cf0e6ea8f0845155f4650434c6d52053f92ceaf85637f3a57277fcdad6ce3e"}]
["EVENT","concurrent_1775144068210818169",{"kind":1,"id":"a36280322b87a9e9d6687e79d578e1059c9ee016d0e7a7b6e2676943b830955e","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1775142792,"tags":[],"content":"whoop ","sig":"7a570204107190614c6b70cb36b22c26138a7c45aa807fefb5c504f657b5335d68cf0e6ea8f0845155f4650434c6d52053f92ceaf85637f3a57277fcdad6ce3e"}]
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.
2026-04-02 11:34:28 - \033[0;32m✓ Memory Corruption Tests PASSED\033[0m (Duration: 1s)
2026-04-02 11:34:28 - ==========================================
2026-04-02 11:34:28 - Running Test Suite: Input Validation Tests
2026-04-02 11:34:28 - Description: Comprehensive input boundary testing
2026-04-02 11:34:28 - ==========================================
==========================================
C-Relay-PG 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.
2026-04-02 11:34:30 - \033[0;32m✓ Input Validation Tests PASSED\033[0m (Duration: 2s)
2026-04-02 11:34:30 -
2026-04-02 11:34:30 - \033[0;34m=== PERFORMANCE TEST SUITES ===\033[0m
2026-04-02 11:34:30 - ==========================================
2026-04-02 11:34:30 - Running Test Suite: Subscription Limit Tests
2026-04-02 11:34:30 - Description: Subscription limit enforcement testing
2026-04-02 11:34:30 - ==========================================
=== 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 2
[PASS] Subscription limit enforcement working (limit hit after 1 subscriptions)
=== Test Complete ===
2026-04-02 11:34:30 - \033[0;32m✓ Subscription Limit Tests PASSED\033[0m (Duration: 0s)
2026-04-02 11:34:30 - ==========================================
2026-04-02 11:34:30 - Running Test Suite: Load Testing
2026-04-02 11:34:30 - Description: High concurrent connection testing
2026-04-02 11:34:30 - ==========================================
==========================================
C-Relay-PG 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: 350
✓ 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: 5s
Total connections attempted: 35
Successful connections: 25
Failed connections: 0
Connection success rate: 71%
Messages expected: 250
Messages sent: 250
Messages received: 1750
✗ 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: 15s
Total connections attempted: 85
Successful connections: 50
Failed connections: 0
Connection success rate: 58%
Messages expected: 1000
Messages sent: 1000
Messages received: 7000
✗ 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: 66s
Total connections attempted: 185
Successful connections: 100
Failed connections: 0
Connection success rate: 54%
Messages expected: 5000
Messages sent: 5000
Messages received: 20000
✗ 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.
2026-04-02 11:35:58 - \033[0;32m✓ Load Testing PASSED\033[0m (Duration: 88s)
2026-04-02 11:35:58 - ==========================================
2026-04-02 11:35:58 - Running Test Suite: Stress Testing
2026-04-02 11:35:58 - Description: Resource usage and stability testing
2026-04-02 11:35:58 - ==========================================
2026-04-02 11:35:58 - \033[0;31mERROR: Test script stress_tests.sh not found\033[0m