v2.1.10 - Fix NIP-50 PG search semantics and NIP-17 test compatibility
This commit is contained in:
+18
-6
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
```
|
||||
+17
-22
@@ -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());
|
||||
@@ -4724,27 +4731,15 @@ const char* get_config_value_hybrid(const char* key) {
|
||||
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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+24
-7
@@ -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);
|
||||
|
||||
+206
-22
@@ -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) {
|
||||
@@ -733,6 +775,13 @@ int init_database(const char* database_path_override) {
|
||||
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) {
|
||||
// Check config table row count immediately after database open
|
||||
@@ -818,10 +867,15 @@ 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");
|
||||
@@ -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
@@ -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
@@ -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
|
||||
@@ -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;
|
||||
@@ -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
@@ -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");
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
|
||||
[34m[1m[INFO][0m === Starting NIP-42 Authentication Tests ===
|
||||
[34m[1m[INFO][0m Checking dependencies...
|
||||
[33m[1m[WARNING][0m wscat not found. Some manual WebSocket tests will be skipped
|
||||
@@ -7,7 +7,7 @@
|
||||
[32m[1m[SUCCESS][0m Dependencies check complete
|
||||
[34m[1m[INFO][0m Test 1: Checking NIP-42 support in relay info
|
||||
[32m[1m[SUCCESS][0m 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
|
||||
[34m[1m[INFO][0m Test 2: Testing AUTH challenge generation
|
||||
[33m[1m[WARNING][0m Could not extract admin private key from relay.log - using manual test approach
|
||||
[34m[1m[INFO][0m Manual test: Connect to relay and send an event without auth to trigger challenge
|
||||
@@ -15,8 +15,8 @@
|
||||
[34m[1m[INFO][0m Generated test keypair: test_pubkey
|
||||
[34m[1m[INFO][0m Attempting to publish event without authentication...
|
||||
[34m[1m[INFO][0m 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.
|
||||
[32m[1m[SUCCESS][0m Relay requested authentication as expected
|
||||
[34m[1m[INFO][0m Test 4: Testing WebSocket AUTH message handling
|
||||
@@ -26,50 +26,50 @@ publishing to ws://localhost:8888... success.
|
||||
[33m[1m[WARNING][0m Could not retrieve configuration events
|
||||
[34m[1m[INFO][0m Test 6: Testing NIP-42 performance and stability
|
||||
[34m[1m[INFO][0m 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.
|
||||
[32m[1m[SUCCESS][0m Performance test completed: 5/5 successful responses
|
||||
[34m[1m[INFO][0m Test 7: Testing kind-specific NIP-42 authentication requirements
|
||||
[34m[1m[INFO][0m Generated test keypair for kind-specific tests: test_pubkey
|
||||
[34m[1m[INFO][0m 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.
|
||||
[32m[1m[SUCCESS][0m Kind 1 event accepted without authentication (correct behavior)
|
||||
[34m[1m[INFO][0m 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...
|
||||
[32m[1m[SUCCESS][0m Kind 4 event requested authentication (correct behavior for DMs)
|
||||
[34m[1m[INFO][0m 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...
|
||||
[32m[1m[SUCCESS][0m Kind 14 event requested authentication (correct behavior for DMs)
|
||||
[34m[1m[INFO][0m 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.
|
||||
[32m[1m[SUCCESS][0m 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.
|
||||
[32m[1m[SUCCESS][0m 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.
|
||||
[32m[1m[SUCCESS][0m Kind 7 event accepted without authentication (correct)
|
||||
[34m[1m[INFO][0m Kind-specific authentication test completed
|
||||
|
||||
@@ -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... [0;32mPASSED[0m - Valid query works
|
||||
|
||||
=== Authors Filter SQL Injection Tests ===
|
||||
Testing Authors filter with payload: '; DROP TABLE events; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Authors filter with payload: 1' OR '1'='1... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Authors filter with payload: 1; SELECT * FROM sqlite_master; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Authors filter with payload: ' OR 1=1 --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Authors filter with payload: admin'--... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Authors filter with payload: 1' UNION SELECT password FROM users --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Authors filter with payload: 1' AND SLEEP(5) --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Authors filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Authors filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Authors filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Authors filter with payload: ' UNION SELECT NULL,NULL,NULL --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Authors filter with payload: ' UNION SELECT 1,2,3 --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Authors filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Authors filter with payload: '; SELECT * FROM events; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Authors filter with payload: '; DELETE FROM events; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Authors filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Authors filter with payload: /*... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Authors filter with payload: */... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Authors filter with payload: /**/... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Authors filter with payload: --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Authors filter with payload: #... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Authors filter with payload: 0x53514C5F494E4A454354494F4E... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Authors filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Authors filter with payload: '))); DROP TABLE events; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Authors filter with payload: ')) UNION SELECT NULL; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Authors filter with payload: ' AND 1=1 --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Authors filter with payload: ' AND 1=2 --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Authors filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Authors filter with payload: '; EXEC master..xp_cmdshell 'net user' --... [0;32mPASSED[0m - 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'); --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
|
||||
=== IDs Filter SQL Injection Tests ===
|
||||
Testing IDs filter with payload: '; DROP TABLE events; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing IDs filter with payload: 1' OR '1'='1... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing IDs filter with payload: 1; SELECT * FROM sqlite_master; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing IDs filter with payload: ' OR 1=1 --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing IDs filter with payload: admin'--... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing IDs filter with payload: 1' UNION SELECT password FROM users --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing IDs filter with payload: 1' AND SLEEP(5) --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing IDs filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing IDs filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing IDs filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing IDs filter with payload: ' UNION SELECT NULL,NULL,NULL --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing IDs filter with payload: ' UNION SELECT 1,2,3 --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing IDs filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing IDs filter with payload: '; SELECT * FROM events; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing IDs filter with payload: '; DELETE FROM events; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing IDs filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing IDs filter with payload: /*... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing IDs filter with payload: */... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing IDs filter with payload: /**/... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing IDs filter with payload: --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing IDs filter with payload: #... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing IDs filter with payload: 0x53514C5F494E4A454354494F4E... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing IDs filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing IDs filter with payload: '))); DROP TABLE events; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing IDs filter with payload: ')) UNION SELECT NULL; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing IDs filter with payload: ' AND 1=1 --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing IDs filter with payload: ' AND 1=2 --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing IDs filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing IDs filter with payload: '; EXEC master..xp_cmdshell 'net user' --... [0;32mPASSED[0m - 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'); --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
|
||||
=== Kinds Filter SQL Injection Tests ===
|
||||
Testing Kinds filter with string injection... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Kinds filter with negative value... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Kinds filter with very large value... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
|
||||
=== Search Filter SQL Injection Tests ===
|
||||
Testing Search filter with payload: '; DROP TABLE events; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Search filter with payload: 1' OR '1'='1... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing Search filter with payload: 1; SELECT * FROM sqlite_master; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Search filter with payload: ' OR 1=1 --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Search filter with payload: admin'--... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Search filter with payload: 1' UNION SELECT password FROM users --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Search filter with payload: 1' AND SLEEP(5) --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Search filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Search filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Search filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Search filter with payload: ' UNION SELECT NULL,NULL,NULL --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Search filter with payload: ' UNION SELECT 1,2,3 --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Search filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Search filter with payload: '; SELECT * FROM events; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Search filter with payload: '; DELETE FROM events; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Search filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Search filter with payload: /*... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Search filter with payload: */... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Search filter with payload: /**/... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Search filter with payload: --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Search filter with payload: #... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing Search filter with payload: 0x53514C5F494E4A454354494F4E... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing Search filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing Search filter with payload: '))); DROP TABLE events; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Search filter with payload: ')) UNION SELECT NULL; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Search filter with payload: ' AND 1=1 --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Search filter with payload: ' AND 1=2 --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Search filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Search filter with payload: '; EXEC master..xp_cmdshell 'net user' --... [0;32mPASSED[0m - 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'); --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
|
||||
=== Tag Filter SQL Injection Tests ===
|
||||
Testing #e tag filter with payload: '; DROP TABLE events; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #e tag filter with payload: 1' OR '1'='1... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #e tag filter with payload: 1; SELECT * FROM sqlite_master; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #e tag filter with payload: ' OR 1=1 --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #e tag filter with payload: admin'--... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #e tag filter with payload: 1' UNION SELECT password FROM users --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #e tag filter with payload: 1' AND SLEEP(5) --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #e tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #e tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #e tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #e tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #e tag filter with payload: ' UNION SELECT 1,2,3 --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #e tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #e tag filter with payload: '; SELECT * FROM events; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #e tag filter with payload: '; DELETE FROM events; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #e tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #e tag filter with payload: /*... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #e tag filter with payload: */... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #e tag filter with payload: /**/... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #e tag filter with payload: --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #e tag filter with payload: #... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #e tag filter with payload: 0x53514C5F494E4A454354494F4E... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #e tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #e tag filter with payload: '))); DROP TABLE events; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #e tag filter with payload: ')) UNION SELECT NULL; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #e tag filter with payload: ' AND 1=1 --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #e tag filter with payload: ' AND 1=2 --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #e tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #e tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... [0;32mPASSED[0m - 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'); --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #p tag filter with payload: '; DROP TABLE events; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #p tag filter with payload: 1' OR '1'='1... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #p tag filter with payload: 1; SELECT * FROM sqlite_master; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #p tag filter with payload: ' OR 1=1 --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #p tag filter with payload: admin'--... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #p tag filter with payload: 1' UNION SELECT password FROM users --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #p tag filter with payload: 1' AND SLEEP(5) --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #p tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #p tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #p tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #p tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #p tag filter with payload: ' UNION SELECT 1,2,3 --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #p tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #p tag filter with payload: '; SELECT * FROM events; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #p tag filter with payload: '; DELETE FROM events; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #p tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #p tag filter with payload: /*... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #p tag filter with payload: */... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #p tag filter with payload: /**/... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #p tag filter with payload: --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #p tag filter with payload: #... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #p tag filter with payload: 0x53514C5F494E4A454354494F4E... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #p tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #p tag filter with payload: '))); DROP TABLE events; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #p tag filter with payload: ')) UNION SELECT NULL; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #p tag filter with payload: ' AND 1=1 --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #p tag filter with payload: ' AND 1=2 --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #p tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #p tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... [0;32mPASSED[0m - 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'); --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #t tag filter with payload: '; DROP TABLE events; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #t tag filter with payload: 1' OR '1'='1... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #t tag filter with payload: 1; SELECT * FROM sqlite_master; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #t tag filter with payload: ' OR 1=1 --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #t tag filter with payload: admin'--... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #t tag filter with payload: 1' UNION SELECT password FROM users --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #t tag filter with payload: 1' AND SLEEP(5) --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #t tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #t tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #t tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #t tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #t tag filter with payload: ' UNION SELECT 1,2,3 --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #t tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #t tag filter with payload: '; SELECT * FROM events; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #t tag filter with payload: '; DELETE FROM events; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #t tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #t tag filter with payload: /*... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #t tag filter with payload: */... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #t tag filter with payload: /**/... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #t tag filter with payload: --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #t tag filter with payload: #... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #t tag filter with payload: 0x53514C5F494E4A454354494F4E... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #t tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #t tag filter with payload: '))); DROP TABLE events; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #t tag filter with payload: ')) UNION SELECT NULL; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #t tag filter with payload: ' AND 1=1 --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #t tag filter with payload: ' AND 1=2 --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #t tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #t tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... [0;32mPASSED[0m - 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'); --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #r tag filter with payload: '; DROP TABLE events; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #r tag filter with payload: 1' OR '1'='1... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #r tag filter with payload: 1; SELECT * FROM sqlite_master; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #r tag filter with payload: ' OR 1=1 --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #r tag filter with payload: admin'--... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #r tag filter with payload: 1' UNION SELECT password FROM users --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #r tag filter with payload: 1' AND SLEEP(5) --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #r tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #r tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #r tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #r tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #r tag filter with payload: ' UNION SELECT 1,2,3 --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #r tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #r tag filter with payload: '; SELECT * FROM events; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #r tag filter with payload: '; DELETE FROM events; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #r tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #r tag filter with payload: /*... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #r tag filter with payload: */... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #r tag filter with payload: /**/... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #r tag filter with payload: --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #r tag filter with payload: #... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #r tag filter with payload: 0x53514C5F494E4A454354494F4E... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #r tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #r tag filter with payload: '))); DROP TABLE events; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #r tag filter with payload: ')) UNION SELECT NULL; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #r tag filter with payload: ' AND 1=1 --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #r tag filter with payload: ' AND 1=2 --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #r tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #r tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... [0;32mPASSED[0m - 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'); --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #d tag filter with payload: '; DROP TABLE events; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #d tag filter with payload: 1' OR '1'='1... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #d tag filter with payload: 1; SELECT * FROM sqlite_master; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #d tag filter with payload: ' OR 1=1 --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #d tag filter with payload: admin'--... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #d tag filter with payload: 1' UNION SELECT password FROM users --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #d tag filter with payload: 1' AND SLEEP(5) --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #d tag filter with payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #d tag filter with payload: 1' AND 1=CAST((SELECT version()) AS INT) --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #d tag filter with payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #d tag filter with payload: ' UNION SELECT NULL,NULL,NULL --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #d tag filter with payload: ' UNION SELECT 1,2,3 --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #d tag filter with payload: ' UNION ALL SELECT NULL,NULL,NULL --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #d tag filter with payload: '; SELECT * FROM events; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #d tag filter with payload: '; DELETE FROM events; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #d tag filter with payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #d tag filter with payload: /*... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #d tag filter with payload: */... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #d tag filter with payload: /**/... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #d tag filter with payload: --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #d tag filter with payload: #... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #d tag filter with payload: 0x53514C5F494E4A454354494F4E... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #d tag filter with payload: J1NSTCBJTkpFQ1RJT04gLS0=... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #d tag filter with payload: '))); DROP TABLE events; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #d tag filter with payload: ')) UNION SELECT NULL; --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #d tag filter with payload: ' AND 1=1 --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #d tag filter with payload: ' AND 1=2 --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #d tag filter with payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
Testing #d tag filter with payload: '; EXEC master..xp_cmdshell 'net user' --... [0;32mPASSED[0m - 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'); --... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
|
||||
=== Timestamp Filter SQL Injection Tests ===
|
||||
Testing Since parameter injection... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Until parameter injection... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
|
||||
=== Limit Parameter SQL Injection Tests ===
|
||||
Testing Limit parameter injection... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Limit with UNION... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
|
||||
=== Complex Multi-Filter SQL Injection Tests ===
|
||||
Testing Multi-filter with authors injection... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Multi-filter with search injection... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Multi-filter with tag injection... [0;32mPASSED[0m - SQL injection blocked (query sanitized)
|
||||
|
||||
=== COUNT Message SQL Injection Tests ===
|
||||
Testing COUNT with authors payload: '; DROP TABLE events; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with search payload: '; DROP TABLE events; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with authors payload: 1' OR '1'='1... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with search payload: 1' OR '1'='1... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with authors payload: 1; SELECT * FROM sqlite_master; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with search payload: 1; SELECT * FROM sqlite_master; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with authors payload: ' OR 1=1 --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with search payload: ' OR 1=1 --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with authors payload: admin'--... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with search payload: admin'--... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with authors payload: 1' UNION SELECT password FROM users --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with search payload: 1' UNION SELECT password FROM users --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with authors payload: 1' AND SLEEP(5) --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with search payload: 1' AND SLEEP(5) --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with authors payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with search payload: 1' AND 1=1 WAITFOR DELAY '0:0:5' --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with authors payload: 1' AND 1=CAST((SELECT version()) AS INT) --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with search payload: 1' AND 1=CAST((SELECT version()) AS INT) --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with authors payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with search payload: 1' AND 1=CONVERT(INT, (SELECT @@VERSION)) --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with authors payload: ' UNION SELECT NULL,NULL,NULL --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with search payload: ' UNION SELECT NULL,NULL,NULL --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with authors payload: ' UNION SELECT 1,2,3 --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with search payload: ' UNION SELECT 1,2,3 --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with authors payload: ' UNION ALL SELECT NULL,NULL,NULL --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with search payload: ' UNION ALL SELECT NULL,NULL,NULL --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with authors payload: '; SELECT * FROM events; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with search payload: '; SELECT * FROM events; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with authors payload: '; DELETE FROM events; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with search payload: '; DELETE FROM events; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with authors payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with search payload: '; UPDATE events SET content='hacked' WHERE 1=1; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with authors payload: /*... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with search payload: /*... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with authors payload: */... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with search payload: */... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with authors payload: /**/... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with search payload: /**/... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with authors payload: --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with search payload: --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with authors payload: #... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with search payload: #... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with authors payload: 0x53514C5F494E4A454354494F4E... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with search payload: 0x53514C5F494E4A454354494F4E... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with authors payload: J1NSTCBJTkpFQ1RJT04gLS0=... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with search payload: J1NSTCBJTkpFQ1RJT04gLS0=... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with authors payload: '))); DROP TABLE events; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with search payload: '))); DROP TABLE events; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with authors payload: ')) UNION SELECT NULL; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with search payload: ')) UNION SELECT NULL; --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with authors payload: ' AND 1=1 --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with search payload: ' AND 1=1 --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with authors payload: ' AND 1=2 --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with search payload: ' AND 1=2 --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with authors payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with search payload: ' AND (SELECT COUNT(*) FROM events) > 0 --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with authors payload: '; EXEC master..xp_cmdshell 'net user' --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing COUNT with search payload: '; EXEC master..xp_cmdshell 'net user' --... [0;32mPASSED[0m - 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'); --... [0;32mPASSED[0m - 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'); --... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
|
||||
=== Edge Case SQL Injection Tests ===
|
||||
Testing Empty string injection... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Null byte injection... [0;32mPASSED[0m - SQL injection blocked (silently rejected)
|
||||
Testing Unicode injection... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Very long injection payload... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
|
||||
=== Subscription ID SQL Injection Tests ===
|
||||
Testing Subscription ID injection... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
Testing Subscription ID with quotes... [0;32mPASSED[0m - SQL injection blocked (silently rejected)
|
||||
|
||||
=== CLOSE Message SQL Injection Tests ===
|
||||
Testing CLOSE with injection... [0;32mPASSED[0m - SQL injection blocked (rejected with error)
|
||||
|
||||
=== Test Results ===
|
||||
Total tests: 318
|
||||
Passed: [0;32m318[0m
|
||||
Failed: [0;31m0[0m
|
||||
[0;32m✓ All SQL injection tests passed![0m
|
||||
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... [0;32mPASSED[0m
|
||||
Testing Valid COUNT message... [0;32mPASSED[0m
|
||||
|
||||
=== Testing Filter Array Validation ===
|
||||
Testing Non-object filter... [0;32mPASSED[0m
|
||||
Testing Too many filters... [0;32mPASSED[0m
|
||||
|
||||
=== Testing Authors Validation ===
|
||||
Testing Invalid author type... [0;32mPASSED[0m
|
||||
Testing Invalid author hex... [0;32mPASSED[0m
|
||||
Testing Too many authors... [0;32mPASSED[0m
|
||||
|
||||
=== Testing IDs Validation ===
|
||||
Testing Invalid ID type... [0;32mPASSED[0m
|
||||
Testing Invalid ID hex... [0;32mPASSED[0m
|
||||
Testing Too many IDs... [0;32mPASSED[0m
|
||||
|
||||
=== Testing Kinds Validation ===
|
||||
Testing Invalid kind type... [0;32mPASSED[0m
|
||||
Testing Negative kind... [0;32mPASSED[0m
|
||||
Testing Too large kind... [0;32mPASSED[0m
|
||||
Testing Too many kinds... [0;32mPASSED[0m
|
||||
|
||||
=== Testing Timestamp Validation ===
|
||||
Testing Invalid since type... [0;32mPASSED[0m
|
||||
Testing Negative since... [0;32mPASSED[0m
|
||||
Testing Invalid until type... [0;32mPASSED[0m
|
||||
Testing Negative until... [0;32mPASSED[0m
|
||||
|
||||
=== Testing Limit Validation ===
|
||||
Testing Invalid limit type... [0;32mPASSED[0m
|
||||
Testing Negative limit... [0;32mPASSED[0m
|
||||
Testing Too large limit... [0;32mPASSED[0m
|
||||
|
||||
=== Testing Search Validation ===
|
||||
Testing Invalid search type... [0;32mPASSED[0m
|
||||
Testing Search too long... [0;32mPASSED[0m
|
||||
Testing Search SQL injection... [0;32mPASSED[0m
|
||||
|
||||
=== Testing Tag Filter Validation ===
|
||||
Testing Invalid tag filter type... [0;32mPASSED[0m
|
||||
Testing Too many tag values... [0;32mPASSED[0m
|
||||
Testing Tag value too long... [0;32mPASSED[0m
|
||||
|
||||
=== Testing Rate Limiting ===
|
||||
Testing rate limiting with malformed requests... [1;33mUNCERTAIN[0m - Rate limiting may not have triggered (this could be normal)
|
||||
|
||||
=== Test Results ===
|
||||
Total tests: 28
|
||||
Passed: [0;32m28[0m
|
||||
Failed: [0;31m0[0m
|
||||
[0;32mAll tests passed![0m
|
||||
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... [0;32mPASSED[0m - No memory corruption detected
|
||||
|
||||
=== Subscription ID Memory Corruption Tests ===
|
||||
Testing Empty subscription ID... [1;33mUNCERTAIN[0m - Expected error but got normal response
|
||||
Testing Very long subscription ID (1KB)... [1;33mUNCERTAIN[0m - Expected error but got normal response
|
||||
Testing Very long subscription ID (10KB)... [1;33mUNCERTAIN[0m - Expected error but got normal response
|
||||
Testing Subscription ID with null bytes... [1;33mUNCERTAIN[0m - Expected error but got normal response
|
||||
Testing Subscription ID with special chars... [1;33mUNCERTAIN[0m - Expected error but got normal response
|
||||
Testing Unicode subscription ID... [1;33mUNCERTAIN[0m - Expected error but got normal response
|
||||
Testing Subscription ID with path traversal... [1;33mUNCERTAIN[0m - Expected error but got normal response
|
||||
|
||||
=== Filter Array Memory Corruption Tests ===
|
||||
Testing Too many filters (50)... [1;33mUNCERTAIN[0m - 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"}]
|
||||
[0;32mPASSED[0m - Concurrent access handled safely
|
||||
Testing Concurrent CLOSE operations...
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[0;32mPASSED[0m - Concurrent access handled safely
|
||||
|
||||
=== Malformed JSON Memory Tests ===
|
||||
Testing Unclosed JSON object... [1;33mUNCERTAIN[0m - Expected error but got normal response
|
||||
Testing Mismatched brackets... [1;33mUNCERTAIN[0m - Expected error but got normal response
|
||||
Testing Extra closing brackets... [1;33mUNCERTAIN[0m - Expected error but got normal response
|
||||
Testing Null bytes in JSON... [1;33mUNCERTAIN[0m - Expected error but got normal response
|
||||
|
||||
=== Large Message Memory Tests ===
|
||||
Testing Very large filter array... [1;33mUNCERTAIN[0m - Expected error but got normal response
|
||||
Testing Very long search term... [1;33mUNCERTAIN[0m - Expected error but got normal response
|
||||
|
||||
=== Test Results ===
|
||||
Total tests: 17
|
||||
Passed: [0;32m17[0m
|
||||
Failed: [0;31m0[0m
|
||||
[0;32m✓ All memory corruption tests passed![0m
|
||||
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... [0;32mPASSED[0m - Input accepted correctly
|
||||
|
||||
=== Message Type Validation ===
|
||||
Testing Invalid message type - string... [0;32mPASSED[0m - Invalid input properly rejected
|
||||
Testing Invalid message type - number... [0;32mPASSED[0m - Invalid input properly rejected
|
||||
Testing Invalid message type - null... [0;32mPASSED[0m - Invalid input properly rejected
|
||||
Testing Invalid message type - object... [0;32mPASSED[0m - Invalid input properly rejected
|
||||
Testing Empty message type... [0;32mPASSED[0m - Invalid input properly rejected
|
||||
Testing Very long message type... [0;32mPASSED[0m - Invalid input properly rejected
|
||||
|
||||
=== Message Structure Validation ===
|
||||
Testing Too few arguments... [0;32mPASSED[0m - Invalid input properly rejected
|
||||
Testing Too many arguments... [0;32mPASSED[0m - Invalid input properly rejected
|
||||
Testing Non-array message... [0;32mPASSED[0m - Invalid input properly rejected
|
||||
Testing Empty array... [0;32mPASSED[0m - Invalid input properly rejected
|
||||
Testing Nested arrays incorrectly... [0;32mPASSED[0m - Invalid input properly rejected
|
||||
|
||||
=== Subscription ID Boundary Tests ===
|
||||
Testing Valid subscription ID... [0;32mPASSED[0m - Input accepted correctly
|
||||
Testing Empty subscription ID... [0;32mPASSED[0m - Invalid input properly rejected
|
||||
Testing Subscription ID with spaces... [0;32mPASSED[0m - Invalid input properly rejected
|
||||
Testing Subscription ID with newlines... [0;32mPASSED[0m - Invalid input properly rejected
|
||||
Testing Subscription ID with tabs... [0;32mPASSED[0m - Invalid input properly rejected
|
||||
Testing Subscription ID with control chars... [0;32mPASSED[0m - Invalid input properly rejected
|
||||
Testing Unicode subscription ID... [0;32mPASSED[0m - Invalid input properly rejected
|
||||
Testing Very long subscription ID... [0;32mPASSED[0m - Invalid input properly rejected
|
||||
|
||||
=== Filter Object Validation ===
|
||||
Testing Valid empty filter... [0;32mPASSED[0m - Input accepted correctly
|
||||
Testing Non-object filter... [0;32mPASSED[0m - Invalid input properly rejected
|
||||
Testing Null filter... [0;32mPASSED[0m - Invalid input properly rejected
|
||||
Testing Array filter... [0;32mPASSED[0m - Invalid input properly rejected
|
||||
Testing Filter with invalid keys... [0;32mPASSED[0m - Input accepted correctly
|
||||
|
||||
=== Authors Field Validation ===
|
||||
Testing Valid authors array... [0;32mPASSED[0m - Input accepted correctly
|
||||
Testing Empty authors array... [0;32mPASSED[0m - Input accepted correctly
|
||||
Testing Non-array authors... [0;32mPASSED[0m - Invalid input properly rejected
|
||||
Testing Invalid hex in authors... [0;32mPASSED[0m - Invalid input properly rejected
|
||||
Testing Short pubkey in authors... [0;32mPASSED[0m - Invalid input properly rejected
|
||||
|
||||
=== IDs Field Validation ===
|
||||
Testing Valid ids array... [0;32mPASSED[0m - Input accepted correctly
|
||||
Testing Empty ids array... [0;32mPASSED[0m - Input accepted correctly
|
||||
Testing Non-array ids... [0;32mPASSED[0m - Invalid input properly rejected
|
||||
|
||||
=== Kinds Field Validation ===
|
||||
Testing Valid kinds array... [0;32mPASSED[0m - Input accepted correctly
|
||||
Testing Empty kinds array... [0;32mPASSED[0m - Input accepted correctly
|
||||
Testing Non-array kinds... [0;32mPASSED[0m - Invalid input properly rejected
|
||||
Testing String in kinds... [0;32mPASSED[0m - Invalid input properly rejected
|
||||
|
||||
=== Timestamp Field Validation ===
|
||||
Testing Valid since timestamp... [0;32mPASSED[0m - Input accepted correctly
|
||||
Testing Valid until timestamp... [0;32mPASSED[0m - Input accepted correctly
|
||||
Testing String since timestamp... [0;32mPASSED[0m - Invalid input properly rejected
|
||||
Testing Negative timestamp... [0;32mPASSED[0m - Invalid input properly rejected
|
||||
|
||||
=== Limit Field Validation ===
|
||||
Testing Valid limit... [0;32mPASSED[0m - Input accepted correctly
|
||||
Testing Zero limit... [0;32mPASSED[0m - Input accepted correctly
|
||||
Testing String limit... [0;32mPASSED[0m - Invalid input properly rejected
|
||||
Testing Negative limit... [0;32mPASSED[0m - Invalid input properly rejected
|
||||
|
||||
=== Multiple Filters ===
|
||||
Testing Two valid filters... [0;32mPASSED[0m - Input accepted correctly
|
||||
Testing Many filters... [0;32mPASSED[0m - Input accepted correctly
|
||||
|
||||
=== Test Results ===
|
||||
Total tests: 47
|
||||
Passed: 47
|
||||
Failed: 0
|
||||
[0;32m✓ All input validation tests passed![0m
|
||||
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 ===
|
||||
[0;32m✓ Relay is accessible[0m
|
||||
|
||||
==========================================
|
||||
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
|
||||
[0;32m✓ EXCELLENT: High connection success rate[0m
|
||||
|
||||
Checking relay responsiveness... [0;32m✓ Relay is still responsive[0m
|
||||
|
||||
==========================================
|
||||
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
|
||||
[0;31m✗ POOR: Low connection success rate[0m
|
||||
|
||||
Checking relay responsiveness... [0;32m✓ Relay is still responsive[0m
|
||||
|
||||
==========================================
|
||||
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
|
||||
[0;31m✗ POOR: Low connection success rate[0m
|
||||
|
||||
Checking relay responsiveness... [0;32m✓ Relay is still responsive[0m
|
||||
|
||||
==========================================
|
||||
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
|
||||
[0;31m✗ POOR: Low connection success rate[0m
|
||||
|
||||
Checking relay responsiveness... [0;32m✓ Relay is still responsive[0m
|
||||
|
||||
==========================================
|
||||
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
|
||||
Reference in New Issue
Block a user